Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)

This commit is contained in:
2026-09-20 06:06:42 +00:00
committed by Claude
commit 8568c0ce6c
330 changed files with 23668 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
# 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/)