125 lines
6.4 KiB
Markdown
125 lines
6.4 KiB
Markdown
# 25 — Hibernate Search
|
|
|
|
[← Previous: 24 — Interceptors](24-interceptors.md) | [Back to README →](../README.md)
|
|
|
|
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:
|
|
|
|
1. **Naming an analyzer that doesn't exist.** `@FullTextField(analyzer = "english")` fails
|
|
application startup outright with `HSEARCH000353: Unknown analyzer: 'english'` — the Lucene
|
|
backend ships no predefined analyzer under that name; a custom analyzer needs to be registered
|
|
via a `LuceneAnalysisConfigurer` bean first. Omitting the `analyzer` attribute uses Hibernate
|
|
Search's own built-in default, which is enough for straightforward full-text matching.
|
|
2. **`@IndexedEmbedded` on an association with no defined inverse side.** Fails bootstrap with
|
|
`HSEARCH700020: Unable to find the inverse side of the association` — Hibernate Search needs
|
|
to know how to find every `Movie` that embeds a given `Director` so it can reindex them when
|
|
the `Director` changes. Either add a `@OneToMany(mappedBy = ...)` back-reference, or, if
|
|
reindex-on-update isn't needed, opt out explicitly:
|
|
```java
|
|
@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`](../src/main/java/com/ankurm/hibernatedemo/search/Movie.java),
|
|
[`Director.java`](../src/main/java/com/ankurm/hibernatedemo/search/Director.java)
|
|
|
|
## Field types: full-text, keyword, and generic
|
|
|
|
```java
|
|
@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](../src/test/java/com/ankurm/hibernatedemo/search/HibernateSearchTest.java) —
|
|
[keyword output](output/25-keyword-exact-match.txt), [sortable output](output/25-sortable-generic-field.txt)
|
|
|
|
## Fuzzy full-text matching
|
|
|
|
```java
|
|
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](../src/test/java/com/ankurm/hibernatedemo/search/HibernateSearchTest.java) —
|
|
[output](output/25-fulltext-fuzzy.txt)
|
|
|
|
## `@IndexedEmbedded`: searching through an association
|
|
|
|
`Director` itself carries no `@Indexed` annotation — it only appears inside `Movie`'s index
|
|
because `Movie.director` is `@IndexedEmbedded`:
|
|
|
|
```java
|
|
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);
|
|
```
|
|
[Test](../src/test/java/com/ankurm/hibernatedemo/search/HibernateSearchTest.java) —
|
|
[output](output/25-indexed-embedded.txt)
|
|
|
|
## `MassIndexer`: rebuilding the index from the database
|
|
|
|
```java
|
|
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](../src/test/java/com/ankurm/hibernatedemo/search/HibernateSearchTest.java) —
|
|
[output](output/25-mass-indexer.txt)
|
|
|
|
## 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-elasticsearch` is a drop-in alternative to the Lucene backend used
|
|
here for a deployment that already runs Elasticsearch or OpenSearch — the annotations on
|
|
`Movie`/`Director` do not change; only the `pom.xml` dependency and backend properties do.
|
|
- [Hibernate Search 8.4 reference documentation](https://docs.jboss.org/hibernate/search/8.4/reference/en-US/html_single/)
|