6.4 KiB
25 — Hibernate Search
← Previous: 24 — Interceptors | Back to README →
Backs the rewrite of ankurm.com post 4892 (Hibernate Search).
The version this repo's own earlier post got wrong
The previous version of this post claimed 7.3.2.Final. Checked against
repo1.maven.org/maven2/org/hibernate/search/hibernate-search-mapper-orm/maven-metadata.xml,
the current GA line is 8.4.0.Final — its own pom.xml depends on
hibernate-core:7.4.0.Final, compatible with this repo's pinned 7.4.5.Final (same
major.minor line). Hibernate Search's own version numbering does not track Hibernate ORM's —
Search 8.x pairs with ORM 7.x, not because "8" follows "7" but because that's simply what its own
POM resolves.
Two configuration traps that break the whole application, not just this chapter
Both of these were found by actually wiring this chapter up against the real jars, not by reading about the API:
- Naming an analyzer that doesn't exist.
@FullTextField(analyzer = "english")fails application startup outright withHSEARCH000353: Unknown analyzer: 'english'— the Lucene backend ships no predefined analyzer under that name; a custom analyzer needs to be registered via aLuceneAnalysisConfigurerbean first. Omitting theanalyzerattribute uses Hibernate Search's own built-in default, which is enough for straightforward full-text matching. @IndexedEmbeddedon an association with no defined inverse side. Fails bootstrap withHSEARCH700020: Unable to find the inverse side of the association— Hibernate Search needs to know how to find everyMoviethat embeds a givenDirectorso it can reindex them when theDirectorchanges. Either add a@OneToMany(mappedBy = ...)back-reference, or, if reindex-on-update isn't needed, opt out explicitly:@ManyToOne @IndexedEmbedded @IndexingDependency(reindexOnUpdate = ReindexOnUpdate.SHALLOW) private Director director;
Because com.ankurm.hibernatedemo.search.Movie is @Indexed and lives in this repository's
normally-scanned package tree, every test in this repo that boots the shared Spring context
now bootstraps Hibernate Search too — confirmed the hard way, by watching an unrelated,
already-passing aggregate-function test fail until the backend was configured correctly. See
src/main/resources/application.yml's hibernate.search.* block and its comment.
Movie.java,
Director.java
Field types: full-text, keyword, and generic
@FullTextField
private String title; // analyzed, tokenized, fuzzy-matchable
@KeywordField
private String genre; // stored and compared as ONE whole value, never tokenized
@GenericField(sortable = Sortable.YES)
private int releaseYear; // plain value, explicitly opted into sorting
A @KeywordField search has to match the entire stored value — "science" does not match a
stored "Science Fiction" the way a tokenized @FullTextField term would.
Test —
keyword output, sortable output
Fuzzy full-text matching
searchSession.search(Movie.class)
.where(f -> f.match().field("title").matching("Godfaher").fuzzy(1))
.fetchHits(20);
.fuzzy(1) tolerates a one-character edit distance — a typo a plain SQL LIKE '%Godfaher%'
would never match.
Test —
output
@IndexedEmbedded: searching through an association
Director itself carries no @Indexed annotation — it only appears inside Movie's index
because Movie.director is @IndexedEmbedded:
searchSession.search(Movie.class)
.where(f -> f.bool()
.must(f.match().field("title").matching("Iea"))
.must(f.match().field("director.name").matching("Iea Christopher Nolan")))
.fetchHits(20);
MassIndexer: rebuilding the index from the database
searchSession.workspace().purge(); // empty the index; the database row is untouched
searchSession.massIndexer(Movie.class).startAndWait();
After purge(), a search for a row that's still in the database returns zero hits — the index
and the database are two separate stores, and nothing keeps them in sync automatically once the
index falls behind. MassIndexer rebuilds the index straight from what's in the database,
without re-persisting anything — the fix for "the index went stale" or "this table existed
before Hibernate Search was added to the project."
Test —
output
Coordination strategies, briefly
This chapter uses Hibernate Search's default coordination: indexing happens synchronously, in
the same thread and transaction as the entity change. For a JTA or distributed deployment,
hibernate-search-mapper-orm-coordination-outbox-polling (confirmed to exist at the same
8.4.0.Final line on Maven Central) writes index updates to an outbox table first and applies
them asynchronously — trading immediate search-index consistency for not blocking the write
transaction on indexing work. This repo's tests rely on synchronous indexing specifically so a
search immediately after a commit() is guaranteed to see the new data; that guarantee does not
hold under the outbox strategy without an explicit wait.
Going deeper
- Automatic indexing tracks entity changes through Hibernate's own event system — a bulk HQL/SQL
mutation bypasses it exactly the same way it bypasses interceptor callbacks (chapter 24);
reindex explicitly (or via
MassIndexer) after any bulk write. hibernate-search-backend-elasticsearchis a drop-in alternative to the Lucene backend used here for a deployment that already runs Elasticsearch or OpenSearch — the annotations onMovie/Directordo not change; only thepom.xmldependency and backend properties do.- Hibernate Search 8.4 reference documentation