Hibernate.orgCommunity Documentation

Chapter 4. Mapping entities to the index structure

4.1. Mapping an entity
4.1.1. Basic mapping
4.1.2. Mapping properties multiple times
4.1.3. Embedded and associated objects
4.2. Boosting
4.2.1. Static index time boosting
4.2.2. Dynamic index time boosting
4.3. Analysis
4.3.1. Default analyzer and analyzer by class
4.3.2. Named analyzers
4.3.3. Dynamic analyzer selection
4.3.4. Retrieving an analyzer
4.4. Bridges
4.4.1. Built-in bridges
4.4.2. Tika bridge
4.4.3. Custom bridges
4.5. Conditional indexing: to index or not based on entity state
4.6. Providing your own id
4.6.1. The ProvidedId annotation
4.7. Programmatic API
4.7.1. Mapping an entity as indexable
4.7.2. Adding DocumentId to indexed entity
4.7.3. Defining analyzers
4.7.4. Defining full text filter definitions
4.7.5. Defining fields for indexing
4.7.6. Programmatically defining embedded entities
4.7.7. Contained In definition
4.7.8. Date/Calendar Bridge
4.7.9. Defining bridges
4.7.10. Mapping class bridge
4.7.11. Mapping dynamic boost

In Chapter 1, Getting started you have already learned that all the metadata information needed to index entities is described through annotations. There is no need for xml mapping files. You can still use Hibernate mapping files for the basic Hibernate configuration, but the Hibernate Search specific configuration has to be expressed via annotations.

Note

There is no XML configuration available for Hibernate Search but we provide a powerful programmatic mapping API that elegantly replaces this kind of deployment form (see Section 4.7, “Programmatic API” for more information).

If you want to contribute the XML mapping implementation, see HSEARCH-210.

Lets start with the most commonly used annotations when mapping an entity.

For each property (or attribute) of your entity, you have the ability to describe how it will be indexed. The default (no annotation present) means that the property is ignored by the indexing process. @Field does declare a property as indexed and allows you to configure several aspects of the indexing process by setting one or more of the following attributes:

  • name: describe under which name the property should be stored in the Lucene Document. The default value is the property name (following the JavaBeans convention)

  • store: describe whether or not the property is stored in the Lucene index. You can store the value Store.YES (consuming more space in the index but allowing projection, see Section 5.1.3.5, “Projection”), store it in a compressed way Store.COMPRESS (this does consume more CPU), or avoid any storage Store.NO (this is the default value). When a property is stored, you can retrieve its original value from the Lucene Document. Storing the property has no impact though on whether the value is searchable or not.

  • index: describe whether the property is indexed or not. The different values are Index.NO (no indexing, i.e. cannot be found by a query), Index.YES (the element gets indexed and is searchable). The default value is Index.YES. Index.NO can be useful for cases where a property is not required to be searchable, but should be available for projection.

    Tip

    Index.NO in combination with Analyze.YES or Norms.YES is not useful, since analyze and norms require the property to be indexed

  • analyze: determines whether the property is analyzed (Analyze.YES) or not (Analyze.NO). The default value is Analyze.YES.

    Tip

    Whether or not you want to analyze a property depends on whether you wish to search the element as is, or by the words it contains. It make sense to analyze a text field, but probably not a date field.

    Tip

    Fields used for sorting or faceting must not be analyzed.

  • norms: describes whether index time boosting information should be stored (Norms.YES) or not (Norms.NO). Not storing the norms can save a considerable amount of memory, but index time boosting will not be available in this case. The default value is Norms.YES.

  • termVector: describes collections of term-frequency pairs. This attribute enables the storing of the term vectors within the documents during indexing. The default value is TermVector.NO.

    The different values of this attribute are:

    ValueDefinition
    TermVector.YESStore the term vectors of each document. This produces two synchronized arrays, one contains document terms and the other contains the term's frequency.
    TermVector.NODo not store term vectors.
    TermVector.WITH_OFFSETSStore the term vector and token offset information. This is the same as TermVector.YES plus it contains the starting and ending offset position information for the terms.
    TermVector.WITH_POSITIONSStore the term vector and token position information. This is the same as TermVector.YES plus it contains the ordinal positions of each occurrence of a term in a document.
    TermVector.WITH_POSITION_OFFSETSStore the term vector, token position and offset information. This is a combination of the YES, WITH_OFFSETS and WITH_POSITIONS.
  • indexNullAs: Per default null values are ignored and not indexed. However, using indexNullAs you can specify a string which will be inserted as token for the null value. Per default this value is set to org.hibernate.search.annotations.Field.DO_NOT_INDEX_NULL indicating that null values should not be indexed. You can set this value to DEFAULT_NULL_TOKEN to indicate that a default null token should be used. This default null token can be specified in the configuration using hibernate.search.default_null_token. If this property is not set the string "_null_" will be used as default.

    Note

    When indexNullAs is used, it is important to use the chosen null token in search queries (see Querying) in order to find null values. It is also advisable to use this feature only with un-analyzed fields (analyze=Analyze.NO).

    Warning

    When implementing a custom FieldBridge or TwoWayFieldBridge it is up to the developer to handle the indexing of null values (see JavaDocs of LuceneOptions.indexNullAs()).

There exists also a companion annotation to @Field, called @NumericField. It can be specified in the same scope as @Field or @DocumentId, but only on Integer, Long, Float or Double properties. When used, the annoated property will be indexed using a Trie structure. This enables efficient range queries and sorting, resulting in query response times being orders of magnitude faster than the same query with plain @Field. The @NumericField annotation accepts the following parameters:

ValueDefinition
forField(Optional) Specify the name of of the related @Field that will be indexed as numeric. It's only mandatory when the property contains more than a @Field declaration
precisionStep(Optional) Change the way that the Trie structure is stored in the index. Smaller precisionSteps lead to more disk space usage and faster range and sort queries. Larger values lead to less space used and range query performance more close to the range query in normal @Fields. Default value is 4.

Lucene supports the numeric types: Double, Long, Integer and Float. Other numeric types should use the default string encoding (via @Field), unless the application can deal with a potential loss in precision, in which case a custom NumericFieldBridge can be used. See Example 4.2, “Defining a custom NumericFieldBridge for BigDecimal.


You would use this custom bridge like seen in Example 4.3, “Use of BigDecimalNumericFieldBridge”. In this case three annotations are used - @Field, @NumericField and @FieldBridge. @Field is required to mark the property for being indexed (a standalone @NumericField is never allowed). @NumericField might be ommited in this specific case, because the used @FieldBridge annotation refers already to a NumericFieldBridge instance. However, the use of @NumericField is recommended to make the use of the property as numeric value explicit.


Associated objects as well as embedded objects can be indexed as part of the root entity index. This is useful if you expect to search a given entity based on properties of associated objects. In Example 4.6, “Indexing associations”t the aim is to return places where the associated city is Atlanta (In the Lucene query parser language, it would translate into address.city:Atlanta). The place fields will be indexed in the Place index. The Place index documents will also contain the fields address.id, address.street, and address.city which you will be able to query.


Be careful. Because the data is denormalized in the Lucene index when using the @IndexedEmbedded technique, Hibernate Search needs to be aware of any change in the Place object and any change in the Address object to keep the index up to date. To make sure the Place Lucene document is updated when it's Address changes, you need to mark the other side of the bidirectional relationship with @ContainedIn.

Tip

@ContainedIn is useful on both associations pointing to entities and on embedded (collection of) objects.

Let's make Example 4.6, “Indexing associations” a bit more complex by nesting @IndexedEmbedded as seen in Example 4.7, “Nested usage of @IndexedEmbedded and @ContainedIn.


As you can see, any @*ToMany, @*ToOne and @Embedded attribute can be annotated with @IndexedEmbedded. The attributes of the associated class will then be added to the main entity index. In Example 4.7, “Nested usage of @IndexedEmbedded and @ContainedIn the index will contain the following fields

  • id

  • name

  • address.street

  • address.city

  • address.ownedBy_name

The default prefix is propertyName., following the traditional object navigation convention. You can override it using the prefix attribute as it is shown on the ownedBy property.

Note

The prefix cannot be set to the empty string.

The depth property is necessary when the object graph contains a cyclic dependency of classes (not instances). For example, if Owner points to Place. Hibernate Search will stop including Indexed embedded attributes after reaching the expected depth (or the object graph boundaries are reached). A class having a self reference is an example of cyclic dependency. In our example, because depth is set to 1, any @IndexedEmbedded attribute in Owner (if any) will be ignored.

Using @IndexedEmbedded for object associations allows you to express queries (using Lucene's query syntax) such as:

  • Return places where name contains JBoss and where address city is Atlanta. In Lucene query this would be

    +name:jboss +address.city:atlanta
  • Return places where name contains JBoss and where owner's name contain Joe. In Lucene query this would be

    +name:jboss +address.ownedBy_name:joe

In a way it mimics the relational join operation in a more efficient way (at the cost of data duplication). Remember that, out of the box, Lucene indexes have no notion of association, the join operation is simply non-existent. It might help to keep the relational model normalized while benefiting from the full text index speed and feature richness.

Note

An associated object can itself (but does not have to) be @Indexed

When @IndexedEmbedded points to an entity, the association has to be directional and the other side has to be annotated @ContainedIn (as seen in the previous example). If not, Hibernate Search has no way to update the root index when the associated entity is updated (in our example, a Place index document has to be updated when the associated Address instance is updated).

Sometimes, the object type annotated by @IndexedEmbedded is not the object type targeted by Hibernate and Hibernate Search. This is especially the case when interfaces are used in lieu of their implementation. For this reason you can override the object type targeted by Hibernate Search using the targetElement parameter.


The @IndexedEmbedded annotation provides also an attribute includePaths which can be used as an alternative to depth, or be combined with it.

When using only depth all indexed fields of the embedded type will be added recursively at the same depth; this makes it harder to pick only a specific path without adding all other fields as well, which might not be needed.

To avoid unnecessarily loading and indexing entities you can specify exactly which paths are needed. A typical application might need different depths for different paths, or in other words it might need to specify paths explicitly, as shown in Example 4.9, “Using the includePaths property of @IndexedEmbedded


Using a mapping as in Example 4.9, “Using the includePaths property of @IndexedEmbedded, you would be able to search on a Person by name and/or surname, and/or the name of the parent. It will not index the surname of the parent, so searching on parent's surnames will not be possible but speeds up indexing, saves space and improve overall performance.

The @IndexedEmbedded includePaths will include the specified paths in addition to what you would index normally specifying a limited value for depth. When using includePaths, and leaving depth undefined, behavior is equivalent to setting depth=0: only the included paths are indexed.


In Example 4.10, “Using the includePaths property of @IndexedEmbedded, every human will have it's name and surname attributes indexed. The name and surname of parents will be indexed too, recursively up to second line because of the depth attribute. It will be possible to search by name or surname, of the person directly, his parents or of his grand parents. Beyond the second level, we will in addition index one more level but only the name, not the surname.

This results in the following fields in the index:

  • id - as primary key

  • _hibernate_class - stores entity type

  • name - as direct field

  • surname - as direct field

  • parents.name - as embedded field at depth 1

  • parents.surname - as embedded field at depth 1

  • parents.parents.name - as embedded field at depth 2

  • parents.parents.surname - as embedded field at depth 2

  • parents.parents.parents.name - as additional path as specifyed by includePaths. The first parents. is inferred from the field name, the remaining path is the attribute of includePaths

Having explicit control of the indexed paths might be easier if you're designing your application by defining the needed queries first, as at that point you might know exactly which fields you need, and which other fields are unnecessary to implement your use case.

Lucene has the notion of boosting which allows you to give certain documents or fields more or less importance than others. Lucene differentiates between index and search time boosting. The following sections show you how you can achieve index time boosting using Hibernate Search.

To define a static boost value for an indexed class or property you can use the @Boost annotation. You can use this annotation within @Field or specify it directly on method or class level.


In Example 4.11, “Different ways of using @Boost”, Essay's probability to reach the top of the search list will be multiplied by 1.7. The summary field will be 3.0 (2 * 1.5, because @Field.boost and @Boost on a property are cumulative) more important than the isbn field. The text field will be 1.2 times more important than the isbn field. Note that this explanation is wrong in strictest terms, but it is simple and close enough to reality for all practical purposes. Please check the Lucene documentation or the excellent Lucene In Action from Otis Gospodnetic and Erik Hatcher.

The @Boost annotation used in Section 4.2.1, “Static index time boosting” defines a static boost factor which is independent of the state of of the indexed entity at runtime. However, there are usecases in which the boost factor may depend on the actual state of the entity. In this case you can use the @DynamicBoost annotation together with an accompanying custom BoostStrategy.


In Example 4.12, “Dynamic boost example” a dynamic boost is defined on class level specifying VIPBoostStrategy as implementation of the BoostStrategy interface to be used at indexing time. You can place the @DynamicBoost either at class or field level. Depending on the placement of the annotation either the whole entity is passed to the defineBoost method or just the annotated field/property value. It's up to you to cast the passed object to the correct type. In the example all indexed values of a VIP person would be double as important as the values of a normal person.

Note

The specified BoostStrategy implementation must define a public no-arg constructor.

Of course you can mix and match @Boost and @DynamicBoost annotations in your entity. All defined boost factors are cumulative.

Analysis is the process of converting text into single terms (words) and can be considered as one of the key features of a fulltext search engine. Lucene uses the concept of Analyzers to control this process. In the following section we cover the multiple ways Hibernate Search offers to configure the analyzers.

Analyzers can become quite complex to deal with. For this reason introduces Hibernate Search the notion of analyzer definitions. An analyzer definition can be reused by many @Analyzer declarations and is composed of:

This separation of tasks - a list of char filters, and a tokenizer followed by a list of filters - allows for easy reuse of each individual component and let you build your customized analyzer in a very flexible way (just like Lego). Generally speaking the char filters do some pre-processing in the character input, then the Tokenizer starts the tokenizing process by turning the character input into tokens which are then further processed by the TokenFilters. Hibernate Search supports this infrastructure by utilizing the Solr analyzer framework.

Note

Some of the analyzers and filters will require additional dependencies. For example to use the snowball stemmer you have to also include the lucene-snowball jar and for the PhoneticFilterFactory you need the commons-codec jar. Your distribution of Hibernate Search provides these dependencies in its lib/optional directory. Have a look at Table 4.2, “Example of available tokenizers” and Table 4.3, “Examples of available filters” to see which anaylzers and filters have additional dependencies

Prior to Search version 3.3.0.Beta2 it was required to add the Solr dependency org.apache.solr:solr-core when you wanted to use the analyzer definition framework. In case you are using Maven this is no longer needed: all required Solr dependencies are now defined as dependencies of the artifact org.hibernate:hibernate-search-analyzers; just add the following dependency :

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-search-analyzers</artifactId>
   <version>4.5.3.Final</version>
<dependency>

Let's have a look at a concrete example now - Example 4.14, “@AnalyzerDef and the Solr framework”. First a char filter is defined by its factory. In our example, a mapping char filter is used, and will replace characters in the input based on the rules specified in the mapping file. Next a tokenizer is defined. This example uses the standard tokenizer. Last but not least, a list of filters is defined by their factories. In our example, the StopFilter filter is built reading the dedicated words property file. The filter is also expected to ignore case.


Tip

Filters and char filters are applied in the order they are defined in the @AnalyzerDef annotation. Order matters!

Some tokenizers, token filters or char filters load resources like a configuration or metadata file. This is the case for the stop filter and the synonym filter. If the resource charset is not using the VM default, you can explicitly specify it by adding a resource_charset parameter.


Once defined, an analyzer definition can be reused by an @Analyzer declaration as seen in Example 4.16, “Referencing an analyzer by name”.


Analyzer instances declared by @AnalyzerDef are also available by their name in the SearchFactory which is quite useful wen building queries.

Analyzer analyzer = fullTextSession.getSearchFactory().getAnalyzer("customanalyzer");

Fields in queries should be analyzed with the same analyzer used to index the field so that they speak a common "language": the same tokens are reused between the query and the indexing process. This rule has some exceptions but is true most of the time. Respect it unless you know what you are doing.

Solr and Lucene come with a lot of useful default char filters, tokenizers and filters. You can find a complete list of char filter factories, tokenizer factories and filter factories at http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters. Let's check a few of them.



Table 4.3. Examples of available filters

FactoryDescriptionParametersAdditional dependencies
StandardFilterFactoryRemove dots from acronyms and 's from wordsnonesolr-core
LowerCaseFilterFactoryLowercases all wordsnonesolr-core
StopFilterFactoryRemove words (tokens) matching a list of stop words

words: points to a resource file containing the stop words

ignoreCase: true if case should be ignore when comparing stop words, false otherwise

solr-core
SnowballPorterFilterFactoryReduces a word to it's root in a given language. (eg. protect, protects, protection share the same root). Using such a filter allows searches matching related words.language: Danish, Dutch, English, Finnish, French, German, Italian, Norwegian, Portuguese, Russian, Spanish, Swedish and a few moresolr-core
ISOLatin1AccentFilterFactoryRemove accents for languages like Frenchnonesolr-core
PhoneticFilterFactoryInserts phonetically similar tokens into the token stream

encoder: One of DoubleMetaphone, Metaphone, Soundex or RefinedSoundex

inject: true will add tokens to the stream, false will replace the existing token

maxCodeLength: sets the maximum length of the code to be generated. Supported only for Metaphone and DoubleMetaphone encodings

solr-core and commons-codec
CollationKeyFilterFactoryConverts each token into its java.text.CollationKey, and then encodes the CollationKey with IndexableBinaryStringTools, to allow it to be stored as an index term.custom, language, country, variant, strength, decomposition see Lucene's CollationKeyFilter javadocs for more infosolr-core and commons-io

We recommend to check all the implementations of org.apache.solr.analysis.TokenizerFactory and org.apache.solr.analysis.TokenFilterFactory in your IDE to see the implementations available.

So far all the introduced ways to specify an analyzer were static. However, there are use cases where it is useful to select an analyzer depending on the current state of the entity to be indexed, for example in a multilingual applications. For an BlogEntry class for example the analyzer could depend on the language property of the entry. Depending on this property the correct language specific stemmer should be chosen to index the actual text.

To enable this dynamic analyzer selection Hibernate Search introduces the AnalyzerDiscriminator annotation. Example 4.17, “Usage of @AnalyzerDiscriminator” demonstrates the usage of this annotation.


The prerequisite for using @AnalyzerDiscriminator is that all analyzers which are going to be used dynamically are predefined via @AnalyzerDef definitions. If this is the case, one can place the @AnalyzerDiscriminator annotation either on the class or on a specific property of the entity for which to dynamically select an analyzer. Via the impl parameter of the AnalyzerDiscriminator you specify a concrete implementation of the Discriminator interface. It is up to you to provide an implementation for this interface. The only method you have to implement is getAnalyzerDefinitionName() which gets called for each field added to the Lucene document. The entity which is getting indexed is also passed to the interface method. The value parameter is only set if the AnalyzerDiscriminator is placed on property level instead of class level. In this case the value represents the current value of this property.

An implemention of the Discriminator interface has to return the name of an existing analyzer definition or null if the default analyzer should not be overridden. Example 4.17, “Usage of @AnalyzerDiscriminator” assumes that the language parameter is either 'de' or 'en' which matches the specified names in the @AnalyzerDefs.

In some situations retrieving analyzers can be handy. For example, if your domain model makes use of multiple analyzers (maybe to benefit from stemming, use phonetic approximation and so on), you need to make sure to use the same analyzers when you build your query.

Whether you are using the Lucene programmatic API or the Lucene query parser, you can retrieve the scoped analyzer for a given entity. A scoped analyzer is an analyzer which applies the right analyzers depending on the field indexed. Remember, multiple analyzers can be defined on a given entity each one working on an individual field. A scoped analyzer unifies all these analyzers into a context-aware analyzer. While the theory seems a bit complex, using the right analyzer in a query is very easy.


In the example above, the song title is indexed in two fields: the standard analyzer is used in the field title and a stemming analyzer is used in the field title_stemmed. By using the analyzer provided by the search factory, the query uses the appropriate analyzer depending on the field targeted.

Tip

You can also retrieve analyzers defined via @AnalyzerDef by their definition name using searchFactory.getAnalyzer(String).

When discussing the basic mapping for an entity one important fact was so far disregarded. In Lucene all index fields have to be represented as strings. All entity properties annotated with @Field have to be converted to strings to be indexed. The reason we have not mentioned it so far is, that for most of your properties Hibernate Search does the translation job for you thanks to a set of built-in bridges. However, in some cases you need a more fine grained control over the translation process.

Hibernate Search comes bundled with a set of built-in bridges between a Java property type and its full text representation.

null

Per default null elements are not indexed. Lucene does not support null elements. However, in some situation it can be useful to insert a custom token representing the null value. See Section 4.1.1.2, “@Field” for more information.

java.lang.String

Strings are indexed as are

short, Short, integer, Integer, long, Long, float, Float, double, Double, BigInteger, BigDecimal

Numbers are converted into their string representation. Note that numbers cannot be compared by Lucene (ie used in ranged queries) out of the box: they have to be padded

Note

Using a Range query has drawbacks; an alternative approach is to use a Filter query which will filter the result query to the appropriate range.

Hibernate Search will support a padding mechanism

java.util.Date

Dates are stored as yyyyMMddHHmmssSSS in GMT time (200611072203012 for Nov 7th of 2006 4:03PM and 12ms EST). You shouldn't really bother with the internal format. What is important is that when using a TermRangeQuery, you should know that the dates have to be expressed in GMT time.

Usually, storing the date up to the millisecond is not necessary. @DateBridge defines the appropriate resolution you are willing to store in the index (@DateBridge(resolution=Resolution.DAY)). The date pattern will then be truncated accordingly.

@Entity 

@Indexed
public class Meeting {
    @Field(analyze=Analyze.NO)
    @DateBridge(resolution=Resolution.MINUTE)
    private Date date;
    ...                 

Warning

A Date whose resolution is lower than MILLISECOND cannot be a @DocumentId

Important

The default Date bridge uses Lucene's DateTools to convert from and to String. This means that all dates are expressed in GMT time. If your requirements are to store dates in a fixed time zone you have to implement a custom date bridge. Make sure you understand the requirements of your applications regarding to date indexing and searching.

java.net.URI, java.net.URL

URI and URL are converted to their string representation

java.lang.Class

Class are converted to their fully qualified class name. The thread context classloader is used when the class is rehydrated

Hibernate Search allows you to extract text from various document types using the built-in TikaBridge which utilizes Apache Tika to extract text and metadata from the provided documents. The TikaBridge annotation can be used with String, URI, byte[] or java.sql.Blob properties. In the case of String and URI the bridge interprets the values are file paths and tries to open a file to parse the document. In the case of byte[] and Blob the values are directly passed to Tika for parsing.

Tika uses metadata as in- and output of the parsing process and it also allows to provide additional context information. This process is described in Parser interface. The Hibernate Search Tika bridge allows you to make use of these additional configuration options by providing two interfaces in conjunction with TikaBridge. The first interface is the TikaParseContextProvider. It allows you to create a custom ParseContext for the document parsing. The second interface is TikaMetadataProcessor which has two methods - prepareMetadata() and set(String, Object, Document, LuceneOptions, Metadata metadata). The former allows to add additional metadata to the parsing process (for example the file name) and the latter allows you to index metadata discovered during the parsing process.

TikaParseContextProvider as well as TikaMetadataProcessor implementation classes can both be specified as parameters on the TikaBridge annotation.


In the Example 4.19, “Example mapping with Apache Tika” the property mp3FileName represents a path to an MP3 file; the headers of this file will be indexed and so the performed query will be able to match the MP3 metadata.

Warning

TikaBridge does not implement TwoWayFieldBridge: queries built using the DSL (as in the Example 4.19, “Example mapping with Apache Tika”) need to explicitly enable the option ignoreFieldBridge().

Sometimes, the built-in bridges of Hibernate Search do not cover some of your property types, or the String representation used by the bridge does not meet your requirements. The following paragraphs describe several solutions to this problem.

The simplest custom solution is to give Hibernate Search an implementation of your expected Object to String bridge. To do so you need to implement the org.hibernate.search.bridge.StringBridge interface. All implementations have to be thread-safe as they are used concurrently.


Given the string bridge defined in Example 4.20, “Custom StringBridge implementation”, any property or field can use this bridge thanks to the @FieldBridge annotation:

@FieldBridge(impl = PaddedIntegerBridge.class)

private Integer length;                

Parameters can also be passed to the bridge implementation making it more flexible. Example 4.21, “Passing parameters to your bridge implementation” implements a ParameterizedBridge interface and parameters are passed through the @FieldBridge annotation.


The ParameterizedBridge interface can be implemented by StringBridge, TwoWayStringBridge, FieldBridge implementations.

All implementations have to be thread-safe, but the parameters are set during initialization and no special care is required at this stage.

If you expect to use your bridge implementation on an id property (ie annotated with @DocumentId ), you need to use a slightly extended version of StringBridge named TwoWayStringBridge. Hibernate Search needs to read the string representation of the identifier and generate the object out of it. There is no difference in the way the @FieldBridge annotation is used.


Important

It is important for the two-way process to be idempotent (ie object = stringToObject( objectToString( object ) ) ).

Some use cases require more than a simple object to string translation when mapping a property to a Lucene index. To give you the greatest possible flexibility you can also implement a bridge as a FieldBridge. This interface gives you a property value and let you map it the way you want in your Lucene Document. You can for example store a property in two different document fields. The interface is very similar in its concept to the Hibernate UserTypes.

Example 4.23. Implementing the FieldBridge interface

/**

 * Store the date in 3 different fields - year, month, day - to ease Range Query per
 * year, month or day (eg get all the elements of December for the last 5 years).
 * @author Emmanuel Bernard
 */
public class DateSplitBridge implements FieldBridge {
    private final static TimeZone GMT = TimeZone.getTimeZone("GMT");
    public void set(String name, Object value, Document document, LuceneOptions luceneOptions) {
        Date date = (Date) value;
        Calendar cal = GregorianCalendar.getInstance(GMT);
        cal.setTime(date);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH) + 1;
        int day = cal.get(Calendar.DAY_OF_MONTH);
  
        // set year
        luceneOptions.addFieldToDocument(
            name + ".year",
            String.valueOf( year ),
            document );
  
        // set month and pad it if needed
        luceneOptions.addFieldToDocument(
            name + ".month",
            month < 10 ? "0" : "" + String.valueOf( month ),
            document );
  
        // set day and pad it if needed
        luceneOptions.addFieldToDocument(
            name + ".day",
            day < 10 ? "0" : "" + String.valueOf( day ),
            document );
    }
}
//property @FieldBridge(impl = DateSplitBridge.class)
private Date date;                

In Example 4.23, “Implementing the FieldBridge interface” the fields are not added directly to Document. Instead the addition is delegated to the LuceneOptions helper; this helper will apply the options you have selected on @Field, like Store or TermVector, or apply the choosen @Boost value. It is especially useful to encapsulate the complexity of COMPRESS implementations. Even though it is recommended to delegate to LuceneOptions to add fields to the Document, nothing stops you from editing the Document directly and ignore the LuceneOptions in case you need to.

Tip

Classes like LuceneOptions are created to shield your application from changes in Lucene API and simplify your code. Use them if you can, but if you need more flexibility you're not required to.

It is sometimes useful to combine more than one property of a given entity and index this combination in a specific way into the Lucene index. The @ClassBridge respectively @ClassBridges annotations can be defined at class level (as opposed to the property level). In this case the custom field bridge implementation receives the entity instance as the value parameter instead of a particular property. Though not shown in Example 4.24, “Implementing a class bridge”, @ClassBridge supports the termVector attribute discussed in section Section 4.1.1, “Basic mapping”.

Example 4.24. Implementing a class bridge

@Entity

@Indexed @ClassBridge(name="branchnetwork",
             store=Store.YES,
             impl = CatFieldsClassBridge.class,
             params = @Parameter( name="sepChar", value=" " ) )
public class Department {
    private int id;
    private String network;
    private String branchHead;
    private String branch;
    private Integer maxEmployees
    ...
}
public class CatFieldsClassBridge implements FieldBridge, ParameterizedBridge {
    private String sepChar;
    public void setParameterValues(Map parameters) {
        this.sepChar = (String) parameters.get( "sepChar" );
    }
    public void set( String name, Object value, Document document, LuceneOptions luceneOptions) {
        // In this particular class the name of the new field was passed
        // from the name field of the ClassBridge Annotation. This is not
        // a requirement. It just works that way in this instance. The
        // actual name could be supplied by hard coding it below.
        Department dep = (Department) value;
        String fieldValue1 = dep.getBranch();
        if ( fieldValue1 == null ) {
            fieldValue1 = "";
        }
        String fieldValue2 = dep.getNetwork();
        if ( fieldValue2 == null ) {
            fieldValue2 = "";
        }
        String fieldValue = fieldValue1 + sepChar + fieldValue2;
        Field field = new Field( name, fieldValue, luceneOptions.getStore(),
            luceneOptions.getIndex(), luceneOptions.getTermVector() );
        field.setBoost( luceneOptions.getBoost() );
        document.add( field );
   }
}

In this example, the particular CatFieldsClassBridge is applied to the department instance, the field bridge then concatenate both branch and network and index the concatenation.

In some situations, you want to index an entity only when it is in a given state, for example:

This serves both functional and technical needs. You don't want your blog readers to find your draft entries and filtering them off the query is a bit annoying. Very few of your entities are actually required to be indexed and you want to limit indexing overhead and keep indexes small and fast.

Hibernate Search lets you intercept entity indexing operations and override them. It is quite simple:

Let's look at the blog example at Example 4.25, “Index blog entries only when they are published and remove them when they are in a different state”

Example 4.25. Index blog entries only when they are published and remove them when they are in a different state

/**

 * Only index blog when it is in published state
 *
 * @author Emmanuel Bernard <emmanuel@hibernate.org>
 */
public class IndexWhenPublishedInterceptor implements EntityIndexingInterceptor<Blog> {
    @Override
    public IndexingOverride onAdd(Blog entity) {
        if (entity.getStatus() == BlogStatus.PUBLISHED) {
            return IndexingOverride.APPLY_DEFAULT;
        }
        return IndexingOverride.SKIP;
    }
    @Override
    public IndexingOverride onUpdate(Blog entity) {
        if (entity.getStatus() == BlogStatus.PUBLISHED) {
            return IndexingOverride.UPDATE;
        }
        return IndexingOverride.REMOVE;
    }
    @Override
    public IndexingOverride onDelete(Blog entity) {
        return IndexingOverride.APPLY_DEFAULT;
    }
    @Override
    public IndexingOverride onCollectionUpdate(Blog entity) {
        return onUpdate(entity);
    }
}
@Entity

@Indexed(interceptor=IndexWhenPublishedInterceptor.class)
public class Blog {
    @Id
    @GeneratedValue
    public Integer getId() { return id; }
    public void setId(Integer id) {  this.id = id; }
    private Integer id;
    @Field
    public String getTitle() { return title; }
    public void setTitle(String title) {  this.title = title; }
    private String title;
    public BlogStatus getStatus() { return status; }
    public void setStatus(BlogStatus status) {  this.status = status; }
    private BlogStatus status;
    [...]
}

We mark the Blog entity with @Indexed.interceptor. As you can see, IndexWhenPublishedInterceptor implements EntityIndexingInterceptor and accepts Blog entities (it could have accepted superclasses as well - for example Object if you create a generic interceptor.

You can react to several planned indexing events:

  • when an entity is added to your datastore

  • when an entity is updated in your datastore

  • when an entity is deleted from your datastore

  • when a collection own by this entity is updated in your datastore

For each occurring event you can respond with one of the following actions:

  • APPLY_DEFAULT: that's the basic operation that lets Hibernate Search update the index as expected - creating, updating or removing the document

  • SKIP: ask Hibernate Search to not do anything to the index for this event - data will not be created, updated or removed from the index in any way

  • REMOVE: ask Hibernate Search to remove indexing data about this entity - you can safely ask for REMOVE even if the entity has not yet been indexed

  • UPDATE: ask Hibernate Search to either index or update the index for this entity - it is safe to ask for UPDATE even if the entity has never been indexed

Note

Be careful, not every combination makes sense: for example, asking to UPDATE the index upon onDelete. Note that you could ask for SKIP in this situation if saving indexing time is critical for you. That's rarely the case though.

By default, no interceptor is applied on an entity. You have to explicitly define an interceptor via the @Indexed annotation (see Section 4.1.1.1, “@Indexed”) or programmatically (see Section 4.7, “Programmatic API”). This class and all its subclasses will then be intercepted. You can stop or change the interceptor used in a subclass by overriding @Indexed.interceptor. Hibernate Search provides DontInterceptEntityInterceptor which will explicitly not intercept any call. This is useful to reset interception within a class hierarchy.

Note

Dirty checking optimization is disabled when interceptors are used. Dirty checking optimization does check what has changed in an entity and only triggers an index update if indexed properties are changed. The reason is simple, your interceptor might depend on a non indexed property which would be ignored by this optimization.

Warning

An EntityIndexingInterceptor can never override an explicit indexing operation such as index(T), purge(T, id) or purgeAll(class).

You can provide your own id for Hibernate Search if you are extending the internals. You will have to generate a unique value so it can be given to Lucene to be indexed. This will have to be given to Hibernate Search when you create an org.hibernate.search.Work object - the document id is required in the constructor.

Although the recommended approach for mapping indexed entities is to use annotations, it is sometimes more convenient to use a different approach:

While it has been a popular demand in the past, the Hibernate team never found the idea of an XML alternative to annotations appealing due to it's heavy duplication, lack of code refactoring safety, because it did not cover all the use case spectrum and because we are in the 21st century :)

The idea of a programmatic API was much more appealing and has now become a reality. You can programmatically define your mapping using a programmatic API: you define entities and fields as indexable by using mapping classes which effectively mirror the annotation concepts in Hibernate Search. Note that fan(s) of XML approach can design their own schema and use the programmatic API to create the mapping while parsing the XML stream.

In order to use the programmatic model you must first construct a SearchMapping object which you can do in two ways:

You can pass the SearchMapping object directly via the property key hibernate.search.model_mapping or the constant Environment.MODEL_MAPPING. Use the Configuration API or the Map passed to the JPA Persistence bootstrap methods.



Alternatively, you can create a factory class (ie. hosting a method annotated with @Factory) whose factory method returns the SearchMapping object. The factory class must have a no-arg constructor and its fully qualified class name is passed to the property key hibernate.search.model_mapping or its type-safe representation Environment.MODEL_MAPPING. This approach is useful when you do not necessarily control the bootstrap process like in a Java EE, CDI or Spring Framework container.


The SearchMapping is the root object which contains all the necessary indexable entities and fields. From there, the SearchMapping object exposes a fluent (and thus intuitive) API to express your mappings: it contextually exposes the relevant mapping options in a type-safe way. Just let your IDE autocompletion feature guide you through.

Today, the programmatic API cannot be used on a class annotated with Hibernate Search annotations, chose one approach or the other. Also note that the same default values apply in annotations and the programmatic API. For example, the @Field.name is defaulted to the property name and does not have to be set.

Each core concept of the programmatic API has a corresponding example to depict how the same definition would look using annotation. Therefore seeing an annotation example of the programmatic approach should give you a clear picture of what Hibernate Search will build with the marked entities and associated properties.

Analyzers can be programmatically defined using the analyzerDef(String analyzerDef, Class<? extends TokenizerFactory> tokenizerFactory) method. This method also enables you to define filters for the analyzer definition. Each filter that you define can optionally take in parameters as seen in the following example :


The analyzer mapping defined above is equivalent to the annotation model using @AnalyzerDef in conjunction with @AnalyzerDefs:


The programmatic API provides easy mechanism for defining full text filter definitions which is available via @FullTextFilterDef and @FullTextFilterDefs (see Section 5.3, “Filters”). The next example depicts the creation of full text filter definition using the fullTextFilterDef method.


The previous example can effectively been seen as annotating your entity with @FullTextFilterDef like below:


When defining fields for indexing using the programmatic API, call field() on the property(String propertyName, ElementType elementType) method. From field() you can specify the name, index, store, bridge and analyzer definitions.


The above example of marking fields as indexable is equivalent to defining fields using @Field as seen below:


Note

When using a programmatic mapping for a given type X, you can only refer to fields defined on X. Fields or methods inherited from a super type are not configurable. In case you need to configure a super class property, you need to either override the property in X or create a programmatic mapping for the super class. This mimics the usage of annotations where you cannot annotate a field or method of a super class either, unless it is redefined in the given type.

In this section you will see how to programmatically define entities to be embedded into the indexed entity similar to using the @IndexedEmbedded model. In order to define this you must mark the property as indexEmbedded.There is the option to add a prefix to the embedded entity definition which can be done by calling prefix as seen in the example below:


The next example shows the same definition using annotation (@IndexedEmbedded):


@ContainedIn can be define as seen in the example below:


This is equivalent to defining @ContainedIn in your entity:


It is possible to associate bridges to programmatically defined fields. When you define a field() programmatically you can use the bridge(Class<?> impl) to associate a FieldBridge implementation class. The bridge method also provides optional methods to include any parameters required for the bridge class. The below shows an example of programmatically defining a bridge:


The above can equally be defined using annotations, as seen in the next example.