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,198 @@
|
||||
package com.ankurm.hibernatedemo.search;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.HibernateDemoApplication;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import java.util.List;
|
||||
import org.hibernate.search.mapper.orm.Search;
|
||||
import org.hibernate.search.mapper.orm.session.SearchSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Proves chapter 25's Hibernate Search claims: automatic indexing on persist, full-text search
|
||||
* with fuzzy matching, exact keyword filtering, sortable range fields, searching through an
|
||||
* {@code @IndexedEmbedded} association, and rebuilding the index with a {@code MassIndexer}.
|
||||
*
|
||||
* <p>All Movie titles in this class carry a unique per-test prefix, and every search query
|
||||
* filters by that prefix -- the Lucene index, like the H2 database, is shared across every test
|
||||
* method (and every OTHER test class in this repo that boots the same Spring context), so an
|
||||
* unscoped query would also match movies indexed by earlier test runs still on disk under
|
||||
* {@code target/lucene-indexes}.
|
||||
*
|
||||
* <p>Docs: docs/25-hibernate-search.md.
|
||||
*/
|
||||
@SpringBootTest(classes = HibernateDemoApplication.class)
|
||||
class HibernateSearchTest {
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void fullTextSearch_withFuzzyMatching_findsATypo() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Director coppola = new Director("Fts Francis Ford Coppola");
|
||||
em.persist(coppola);
|
||||
em.persist(new Movie("Fts The Godfather", "Drama", 1972, coppola));
|
||||
em.persist(new Movie("Fts The Godfather Part II", "Drama", 1974, coppola));
|
||||
em.getTransaction().commit();
|
||||
|
||||
SearchSession searchSession = Search.session(em);
|
||||
// Deliberate typo: "Godfaher" instead of "Godfather" -- fuzzy(1) tolerates a
|
||||
// one-character edit distance.
|
||||
List<Movie> hits = searchSession.search(Movie.class)
|
||||
.where(f -> f.match().field("title").matching("Godfaher").fuzzy(1))
|
||||
.fetchHits(20);
|
||||
em.close();
|
||||
|
||||
List<String> titles = hits.stream().map(Movie::getTitle).filter(t -> t.startsWith("Fts")).toList();
|
||||
System.out.println("RESULT[search-fulltext-fuzzy]: searching title for 'Godfaher' "
|
||||
+ "(a one-character typo of 'Godfather') with .fuzzy(1) matched: " + titles
|
||||
+ " -- a plain SQL LIKE '%Godfaher%' would have matched nothing.");
|
||||
|
||||
assertThat(titles).containsExactlyInAnyOrder("Fts The Godfather", "Fts The Godfather Part II");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keywordField_exactMatchOnly_noPartialOrCaseInsensitiveMatch() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Director lucas = new Director("Kwf George Lucas");
|
||||
em.persist(lucas);
|
||||
em.persist(new Movie("Kwf A New Hope", "Science Fiction", 1977, lucas));
|
||||
em.persist(new Movie("Kwf Annie Hall", "Comedy", 1977, lucas));
|
||||
em.getTransaction().commit();
|
||||
|
||||
SearchSession searchSession = Search.session(em);
|
||||
List<Movie> exactMatch = searchSession.search(Movie.class)
|
||||
.where(f -> f.bool()
|
||||
.must(f.match().field("genre").matching("Science Fiction"))
|
||||
.must(f.match().field("title").matching("Kwf")))
|
||||
.fetchHits(20);
|
||||
// A KeywordField compares the WHOLE stored value -- "science" alone (lowercase, partial)
|
||||
// does not match "Science Fiction" the way a FullTextField's tokenized/lower-cased terms
|
||||
// would.
|
||||
List<Movie> partialLowercaseAttempt = searchSession.search(Movie.class)
|
||||
.where(f -> f.bool()
|
||||
.must(f.match().field("genre").matching("science"))
|
||||
.must(f.match().field("title").matching("Kwf")))
|
||||
.fetchHits(20);
|
||||
em.close();
|
||||
|
||||
System.out.println("RESULT[search-keyword-exact-match]: @KeywordField genre matched by "
|
||||
+ "the exact stored value 'Science Fiction' -> " + exactMatch.size() + " hit(s) | "
|
||||
+ "the same field searched with the partial, lowercase 'science' -> "
|
||||
+ partialLowercaseAttempt.size() + " hit(s) -- a KeywordField is compared whole,"
|
||||
+ " unlike a FullTextField's tokenized and lower-cased terms.");
|
||||
|
||||
assertThat(exactMatch).hasSize(1);
|
||||
assertThat(partialLowercaseAttempt).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortableGenericField_ordersByReleaseYear() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Director spielberg = new Director("Sgf Steven Spielberg");
|
||||
em.persist(spielberg);
|
||||
em.persist(new Movie("Sgf Jaws", "Thriller", 1975, spielberg));
|
||||
em.persist(new Movie("Sgf E.T.", "Family", 1982, spielberg));
|
||||
em.persist(new Movie("Sgf Jurassic Park", "Adventure", 1993, spielberg));
|
||||
em.getTransaction().commit();
|
||||
|
||||
SearchSession searchSession = Search.session(em);
|
||||
List<Movie> sortedNewestFirst = searchSession.search(Movie.class)
|
||||
.where(f -> f.match().field("title").matching("Sgf"))
|
||||
.sort(f -> f.field("releaseYear").desc())
|
||||
.fetchHits(20);
|
||||
em.close();
|
||||
|
||||
List<Integer> years = sortedNewestFirst.stream().map(Movie::getReleaseYear).toList();
|
||||
System.out.println("RESULT[search-sortable-generic-field]: sort(f -> f.field("
|
||||
+ "\"releaseYear\").desc()) over the 3 Sgf-prefixed movies -- years in the order "
|
||||
+ "returned: " + years + " -- @GenericField(sortable = Sortable.YES) is what "
|
||||
+ "makes this sort possible; the default is NOT sortable.");
|
||||
|
||||
assertThat(years).containsExactly(1993, 1982, 1975);
|
||||
}
|
||||
|
||||
@Test
|
||||
void indexedEmbedded_searchesThroughTheAssociation() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Director nolan = new Director("Iea Christopher Nolan");
|
||||
Director tarantino = new Director("Iea Quentin Tarantino");
|
||||
em.persist(nolan);
|
||||
em.persist(tarantino);
|
||||
em.persist(new Movie("Iea Inception", "Science Fiction", 2010, nolan));
|
||||
em.persist(new Movie("Iea Pulp Fiction", "Crime", 1994, tarantino));
|
||||
em.getTransaction().commit();
|
||||
|
||||
// director.name is a @KeywordField -- compared as one whole value, same as the genre
|
||||
// field in the previous test -- so the query has to match the FULL stored director name,
|
||||
// not a substring of it. That's why this searches "Iea Christopher Nolan" whole, not
|
||||
// just "Nolan".
|
||||
SearchSession searchSession = Search.session(em);
|
||||
List<Movie> hits = 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);
|
||||
em.close();
|
||||
|
||||
List<String> titles = hits.stream().map(Movie::getTitle).toList();
|
||||
System.out.println("RESULT[search-indexed-embedded]: field 'director.name' matched the "
|
||||
+ "full keyword value 'Iea Christopher Nolan' -- " + titles + " -- Director "
|
||||
+ "itself carries no @Indexed annotation at all; its @KeywordField only exists "
|
||||
+ "inside Movie's index because of @IndexedEmbedded on the director association.");
|
||||
|
||||
assertThat(titles).containsExactly("Iea Inception");
|
||||
}
|
||||
|
||||
@Test
|
||||
void massIndexer_rebuildsTheIndexFromTheDatabase() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Director scorsese = new Director("Mix Martin Scorsese");
|
||||
em.persist(scorsese);
|
||||
em.persist(new Movie("Mix Goodfellas", "Crime", 1990, scorsese));
|
||||
em.getTransaction().commit();
|
||||
|
||||
SearchSession searchSession = Search.session(em);
|
||||
long beforePurge = searchSession.search(Movie.class)
|
||||
.where(f -> f.match().field("title").matching("Mix"))
|
||||
.fetchTotalHitCount();
|
||||
|
||||
// Purge the index (NOT the database) to simulate an index that has gone stale or was
|
||||
// never built -- the row is still in H2, only the Lucene index forgot about it.
|
||||
searchSession.workspace().purge();
|
||||
long afterPurgeBeforeReindex = searchSession.search(Movie.class)
|
||||
.where(f -> f.match().field("title").matching("Mix"))
|
||||
.fetchTotalHitCount();
|
||||
|
||||
try {
|
||||
searchSession.massIndexer(Movie.class).startAndWait();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
long afterMassIndexer = searchSession.search(Movie.class)
|
||||
.where(f -> f.match().field("title").matching("Mix"))
|
||||
.fetchTotalHitCount();
|
||||
em.close();
|
||||
|
||||
System.out.println("RESULT[search-mass-indexer]: hits before purge=" + beforePurge
|
||||
+ " | hits after workspace().purge() (row still in H2, index emptied)="
|
||||
+ afterPurgeBeforeReindex + " | hits after massIndexer(Movie.class)."
|
||||
+ "startAndWait() (index rebuilt straight from the database, no re-persisting)="
|
||||
+ afterMassIndexer + ".");
|
||||
|
||||
assertThat(beforePurge).isEqualTo(1);
|
||||
assertThat(afterPurgeBeforeReindex).isZero();
|
||||
assertThat(afterMassIndexer).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user