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.1.4. Associated objects: building a dependency graph with @ContainedIn
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.4.4. BridgeProvider: associate a bridge to a given return type
4.5. Conditional indexing
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. Declaring bridges
4.7.10. Mapping class bridge
4.7.11. Mapping dynamic boost

In Chapter 1, Getting started you have already seen 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 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 of your entity, you have the ability to describe whether and how it will be indexed. Adding the @Field annotation declares a property as indexed and allows you to configure various aspects of the indexing process. Without @Field the property is ignored by the indexing process.

Hibernate Search tries to determine the best way to index your property. In most cases this will be as string, but for the types int, long, double and float (and their respective Java wrapper types) Lucene’s numeric field encoding (see Section 4.1.1.3, “@NumericField”) is used. This numeric encoding uses a so called Trie structure which allows for efficient range queries and sorting, resulting in query response times being orders of magnitude faster than with the plain string encoding. Byte and short properties will only be encoded in numeric fields if explicitly marked with the @NumericField annotation.

Caution

Prior to Search 5, numeric field encoding was only chosen if explicitly requested via @NumericField. As of Search 5 this encoding is automatically chosen for numeric types. To avoid numeric encoding you can explicitly specify a non numeric field bridge via @Field.bridge or @FieldBridge. The package org.hibernate.search.bridge.builtin contains a set of bridges which encode numbers as strings, for example org.hibernate.search.bridge.builtin.IntegerBridge.

The following attributes of the @Field annotation help you control the indexing outcome:

  • name: describes under which name the property should be stored in the Lucene Document. The default value is the property name (following the JavaBeans convention)
  • store: describes 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), 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 on whether the value is searchable or not.
  • index: describes whether the property is indexed or not. The different values are Index.NO (no indexing, meaning the value 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 needed 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.YES

    Store the term vectors of each document. This produces two synchronized arrays, one contains document terms and the other contains the term’s frequency.

    TermVector.NO

    Do not store term vectors.

    TermVector.WITH_OFFSETS

    Store 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_POSITIONS

    Store 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_OFFSETS

    Store 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 Chapter 5, Querying) in order to find null values. It is also advisable to use this feature only with un-analyzed fields (analyze=Analyze.NO).

    Note

    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()).

  • boost: Refer to section about boosting
  • bridge: Refer to section about field bridges

@NumericField is a companion annotation to @Field. It can be specified in the same scope as @Field, but only on properties of numeric type like byte, short, int, long, double and float (and their respective Java wrapper types). It allows to define a custom precisionStep for the numeric encoding of the property value.

@NumericField accepts the following parameters:

ValueDefinition

forField

(Optional) Specify the name of of the related @Field that will be indexed numerically. 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 using string encoding. Default value is 4.

Lucene supports the numeric types: Double, Long, Integer and Float. For properties of types Byte and Short, an Integer field will be used in the index. 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 omitted in this specific case, because the used @FieldBridge annotation refers already to a NumericFieldBridge instance. However, the use of @NumericField makes 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 the associated objects.

In the example Example 4.6, “Indexing associations” the aim is to return places where the associated city is Atlanta (in Lucene query parser language, it would translate into address.city:Atlanta). All place fields are added to the Place index, but also the address related fields address.street, and address.city will be added and made queryable. The embedded object id, address.id, is not added per default. To include it you need to also set @IndexedEmbedded(includeEmbeddedObjectId=true, …​).

Tip

Only actual indexed fields (properties annotated with @Field) are added to the root entity index when embedded objects are indexed. The embedded object identifiers are treated differently and need to be included explicitly.


Be careful. Because the data is de-normalized 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 or @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 with @ContainedIn. 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 in combination 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. Using includePaths with a undefined (default) value for depth 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 specified by includePaths. The first parents. is inferred from the field name, the remaining path is the attribute of includePaths

Tip

You can explicitly include the id of the embedded object using includePath, for example @IndexedEmbedded(includePaths = { "parents.id" }). This will work regardless of the includeEmbeddedObjectId attribute. However, it is recommended to just set includeEmbeddedObjectId=true.

Tip

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 use cases 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:

  • a name: the unique string used to refer to the definition
  • a list of char filters: each char filter is responsible to pre-process input characters before the tokenization. Char filters can add, change or remove characters; one common usage is for characters normalization
  • a tokenizer: responsible for tokenizing the input stream into individual words
  • a list of filters: each filter is responsible to remove, modify or sometimes even add words into the stream provided by the tokenizer

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 advanced analyzers provided by Lucene; this is often referred to as the 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 analyzers and filters have additional dependencies

Prior to Hibernate Search 5 it was required to add the Apache Solr dependency to your project as well; this is no longer required.

Let’s have a look at a concrete example now - Example 4.14, “@AnalyzerDef and the Analyzer 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.


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.

Apache Lucene comes 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

StandardFilterFactory

Remove dots from acronyms and 's from words

none

lucene-analyzers-common

LowerCaseFilterFactory

Lowercases all words

none

lucene-analyzers-common

StopFilterFactory

Remove 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

lucene-analyzers-common

SnowballPorterFilterFactory

Reduces 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 more

lucene-analyzers-common

ASCIIFoldingFilterFactory

Remove accents for languages like French

none

lucene-analyzers-common

PhoneticFilterFactory

Inserts 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

lucene-analyzers-phonetic and commons-codec

CollationKeyFilterFactory

Converts 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 info

lucene-analyzers-common and commons-io


We recommend to check out the implementations of org.apache.lucene.analysis.util.TokenizerFactory and org.apache.lucene.analysis.util.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 implementation 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
Are per default indexed numerically using a Trie structure. You need to use a NumericRangeQuery to search for values. See also Section 4.1.1.2, “@Field” and Section 4.1.1.3, “@NumericField”
BigInteger, BigDecimal
BigInteger and BigDecimal are converted into their string representation and indexed. Note that in this form the values cannot be compared by Lucene using for example a TermRangeQuery. For that the string representation would need to be padded. An alternative using numeric encoding with a potential loss in precision can be seen in Example 4.2, “Defining a custom NumericFieldBridge for BigDecimal”.
java.util.Date, java.util.Calendar

Dates are indexed as long value representing the number of milliseconds since January 1, 1970, 00:00:00 GMT. You shouldn’t really bother with the internal format. It is important, however, to query a numerically indexed date via a NumericRangeQuery.

Usually, storing the date up to the millisecond is not necessary. @DateBridge defines the appropriate resolution you are willing to store in the index.

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

You can also choose to encode the date as string using the encoding=EncodingType.STRING of DateBridge. In this case the dates are stored in the format yyyyMMddHHmmssSSS (using GMT time).

Important

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

Important

The default date bridge uses Lucene’s DateTools to convert from Date or Calendar to its indexed value. 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
Classes 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 ORM UserTypes.


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 chosen @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”.


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.

Custom field bridges are very flexible, but it can be tedious and error prone to apply the same custom @FieldBridge annotation every time a property of a given type is present in your domain model. That is what BridgeProviders are for.

Let’s imagine that you have a type Currency in your application and that you want to apply your very own CurrencyFieldBridge every time an indexed property returns Currency. You can do it the hard way:


Or you can write your own BridgeProvider implementation for Currency.


You need to implement BridgeProvider and create a service file named META-INF/services/org.hibernate.search.bridge.spi.BridgeProvider. This file must contain the fully qualified class name(s) of the BridgeProvider implementations. This is the classic Service Loader discovery mechanism.

Now, any indexed property of type Currency will use CurrencyFieldBridge automatically.


A few more things you need to know:

  • a BridgeProvider must have a no-arg constructor
  • if a BridgeProvider only returns FieldBridge instances if it is meaningful for the calling context. Null otherwise. In our example, the return type must be Currency to be meaningful to our provider.
  • if two or more bridge providers return a FieldBridge instance for a given return type, an exception will be raised.

What is a calling context

A calling context is represented by the BridgeContext object and represents the environment for which we are looking for a bridge. BridgeContext gives access to the return type of the indexed property as well as the ServiceManager which gives access to the ClassLoaderService for everything class loader related.

ClassLoaderService classLoaderService = serviceManager.requestService( ClassLoaderService.class );
//use the classLoaderService
serviceManager.releaseService( ClassLoaderService.class );

Important

This feature is considered experimental. More operation types might be added in the future depending on user feedback.

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

  • only index blog entries marked as published
  • no longer index invoices when they are marked archived

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:

  • Write an EntityIndexingInterceptor class with your entity state based logic
  • Mark the entity as intercepted by this implementation

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


We mark the Blog entity with @Indexed.interceptor. As you can see, IndexWhenPublishedInterceptor implements EntityIndexingInterceptor and accepts Blog entities (it could have accepted super classes 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:

  • the same entity is mapped differently depending on deployment needs (customization for clients)
  • some automation process requires the dynamic mapping of many entities sharing common traits

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 its 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:

  • directly
  • via a factory

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 auto-completion 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.