2020-03-21

Hibernate Search - Mapping associated elements with @IndexedEmbedded

Using only @Indexed combined with @*Field annotations allows indexing an entity and its direct properties, which is nice but simplistic. A real-world model will include multiple object types holding references to one another, like the authors association in the example below.

A multi-entity model with associations
This mapping will declare the following fields in the Book index:

title

…​ and nothing else.

@Entity
@Indexed 
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english") 
    private String title;

    @ManyToMany
    private List<Author> authors = new ArrayList<>(); 

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    private String name;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}
The Book entity is indexed.
The title of the book is mapped to an index field.
But how to index the Author name into the Book index?
When searching for a book, users will likely need to search by author name. In the world of high-performance indexes, cross-index joins are costly and usually not an option. The best way to address such use cases is generally to copy data: when indexing a Book, just copy the name of all its authors into the Book document.

That’s what @IndexedEmbedded does: it instructs Hibernate Search to embed the fields of an associated object into the main object. In the example below, it will instruct Hibernate Search to embed the name field defined in Author into Book, creating the field authors.name.

@IndexedEmbedded can be used on Hibernate ORM’s @Embedded properties as well as associations (@OneToOne, @OneToMany, @ManyToMany, …​).

Using @IndexedEmbedded to index associated elements

This mapping will declare the following fields in the Book index:

title

authors.name

@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String title;

    @ManyToMany
    @IndexedEmbedded 
    private List<Author> authors = new ArrayList<>();

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name") 
    private String name;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}
Add an @IndexedEmbedded to the authors property.
Map Author.name to an index field, even though Author is not directly mapped to an index (no @Indexed).
Document identifiers are not index fields. Consequently, they will be ignored by @IndexedEmbedded.

To embed another entity’s identifier with @IndexedEmbedded, map that identifier to a field explicitly using @GenericField or another @*Field annotation.

When @IndexedEmbedded is applied to an association, i.e. to a property that refers to entities (like the example above), the association must be bi-directional. Otherwise, Hibernate Search will throw an exception on startup.

See Reindexing when embedded elements change for the reasons behind this restriction and ways to circumvent it.

Index-embedding can be nested on multiple levels; for example you can decide to index-embed the place of birth of authors, so as to be able to search for books written by Russian authors exclusively:

Nesting multiple @IndexedEmbedded

This mapping will declare the following fields in the Book index:

title

authors.name

authors.placeOfBirth.country

@Entity
@Indexed
public class Book {

    @Id
    private Integer id;

    @FullTextField(analyzer = "english")
    private String title;

    @ManyToMany
    @IndexedEmbedded
    private List<Author> authors = new ArrayList<>();

    public Book() {
    }

    // Getters and setters
    // ...

}
@Entity
public class Author {

    @Id
    private Integer id;

    @FullTextField(analyzer = "name")
    private String name;

    @Embedded
    @IndexedEmbedded
    private Address placeOfBirth;

    @ManyToMany(mappedBy = "authors")
    private List<Book> books = new ArrayList<>();

    public Author() {
    }

    // Getters and setters
    // ...

}
@Embeddable
public class Address {

    @FullTextField(analyzer = "name")
    private String country;

    private String city;

    private String street;

    public Address() {
    }

    // Getters and setters
    // ...

}
Add an @IndexedEmbedded to the authors property.
Map Author.name to an index field, even though Author is not directly mapped to an index (no @Indexed).
Add an @IndexedEmbedded to the placeOfBirth property.
Map Address.country to an index field, even though Address is not directly mapped to an index (no @Indexed).
By default, @IndexedEmbedded will nest other @IndexedEmbedded encountered in the indexed-embedded type recursively, without any sort of limit, which can cause infinite recursion.

To address this, see Filtering embedded fields and breaking @IndexedEmbedded cycles.

@IndexedEmbedded and null values

When properties targeted by an @IndexedEmbedded contain null elements, these elements are simply not indexed.

On contrary to Mapping a property to an index field with @GenericField, @FullTextField, …​, there is no indexNullAs feature to index a specific value for null objects, but you can take advantage of the exists predicate in search queries to look for documents where a given @IndexedEmbedded has or doesn’t have a value: simply pass the name of the object field to the exists predicate, for example authors in the example above.

@IndexedEmbedded on container types

When properties targeted by an @IndexedEmbedded have a container type (List, Optional, Map, …​), the innermost elements will be embedded. For example for a property of type List<MyEntity>, elements of type MyEntity will be embedded.

This default behavior and ways to override it are described in the section Mapping container types with container extractors.

No comments:

Post a Comment