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:
@@ -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/)
|
||||
Reference in New Issue
Block a user