Model or dataset
hibernate/hibernate-search avatar
hibernate/hibernate-search

Hibernate Search keeps the Lucene index in step with the ORM session

Hibernate Search: full-text search for domain model

537 stars247 forksJavaApache-2.0

At a glance

What is it?
Hibernate Search is a Hibernate ORM extension that maps entity properties to Lucene, Elasticsearch or OpenSearch index fields and keeps them synchronised as entities change. It suits Java teams that want full-text search returning managed entities, and it asks you to design an index mapping and to think about index latency.
Who is it for?
Hibernate Search is the right tool for a Java application that already runs Hibernate ORM and needs full-text search whose results come back as ordinary managed entities, because the index mapping is declared next to the entity mapping and the DSL returns entities rather than documents.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Java, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The index is derived from the ORM model, not maintained beside it

The core mechanism is that Hibernate Search automatically extracts data from Hibernate ORM entities and pushes it to index fields, and the mapping is declarative. A property of an entity becomes a document field through annotations or through a programmatic API, and the example shows it in a form that is easy to read. An entity marked with `@Indexed` is mapped to an index, its identifier becomes the document ID, a property marked with `@FullTextField` becomes a document field, and a collection marked with `@IndexedEmbedded` is embedded into the document:

java
@Entity
// This entity is mapped to an index
@Indexed
public class Book {

    // The entity ID is the document ID
    @Id
    @GeneratedValue
    private Integer id;

    // This property is mapped to a document field
    @FullTextField
    private String title;

The reason this design matters is that the search index becomes a projection of the domain model rather than a second schema you maintain by hand. A field added to the entity is added to the index by changing one annotation. A relationship that is embedded in the document is declared once, and both the index and the query DSL agree on the field path, which is why a query can name `authors.name` without anyone maintaining a list of index field names in a second place.

Two indexing paths: mass indexer for history, listeners for the future

Keeping an index correct involves two separate problems, and the library gives you a tool for each. The first is getting the data that already exists into the index, which is what on-demand mass indexing of all entities in the database is for, described as the way to initialise the indexes with pre-existing data. The code is three lines:

java
SearchSession searchSession = Search.session( entityManager );
MassIndexer indexer = searchSession.massIndexer( Book.class );
indexer.startAndWait();

The second is keeping it up to date, and the key property here is that on-the-fly listener-triggered indexing of entities modified through a Hibernate ORM session requires no change to code based on JPA or Hibernate ORM. You persist as you always did:

java
Author author = new Author();
author.setName( "Isaac Asimov" );

Book book = new Book();
book.setTitle( "The Caves Of Steel" );
book.getAuthors().add( author );
author.getBooks().add( book );

entityManager.persist( author );
entityManager.persist( book );

The session flush is the trigger. That is the whole integration cost, and it is worth understanding that it also means the index update is tied to the transaction boundary rather than to your code, which is what makes the behaviour predictable but also what makes it eventually consistent rather than immediate.

The Search DSL returns entities and lets you name fields across relationships

Querying is done through a Search DSL, and the result type is the important design decision. The example searches for `Book` and retrieves the hits as Hibernate ORM entities rather than as raw documents:

java
SearchResult<Book> result = Search.session( entityManager )
        .search( Book.class )
        .where( f -> f.match()
                .fields( "title", "authors.name" )
                .matching( "Isaac" ) )
        .fetch( 20 );

List<Book> hits = result.hits();
long totalHitCount = result.total().hitCount();

Two things are visible in that snippet. The field paths are written as `title` and `authors.name`, so a query can reach into an embedded relationship using the same naming the mapping declared, and the fetch size is a method argument on the query rather than a property of the session, which means two differently sized queries can share a session. The result exposes both the hits and the total hit count separately, since a search interface usually needs to show a total number alongside a page of results. The DSL also carries many different predicates and sorts, configuration of analyzers, and the ability to return projections instead of entities, plus aggregations and spatial support, so a search page that needs facets or a map does not need a second API.

The index can be local Lucene or a remote Elasticsearch or OpenSearch cluster

The backend choice is a deployment decision rather than a code decision, and the documentation lists both ends of it. Hibernate Search pushes data to local Apache Lucene indexes or to remote Elasticsearch or OpenSearch indexes. The topic list on the repository records the same set, with elasticsearch, lucene and opensearch all present, alongside hibernate and orm. The practical difference is where the search runs and who operates it. A local Lucene index lives in the application's own process or on its own disk, so there is no network hop and no extra service to run, but the index is per instance and a cluster of application nodes needs either a shared directory or a search node that proxies. A remote Elasticsearch or OpenSearch cluster centralises the index, which is what you want when the search must span several application instances or when non-Java services need to query the same data, and it brings an operational dependency on a service that has to be sized, versioned and kept available. The ORM integration is the same in both cases, which is the point of the abstraction.

The repository is a multi-module build, not a single library

The layout of the repository tells you how large the project really is. At the root there is a `pom.xml`, a `Jenkinsfile`, a `mvnw` and `mvnw.cmd` pair for the Maven wrapper, a `bom/` directory for the bill of materials, and a `changelog.md`. The functional modules are separate directories: `backend/` for the search backends, `engine/` for the query engine, `mapper/` for the mapping layer, `metamodel/` for the metamodel integration, and `util/` for shared utilities. Then there is the part that tells you the project takes its own quality seriously. `integrationtest/` holds cross-module tests, `ci/` holds continuous integration configuration, `distribution/` handles packaging, `documentation/` holds the docs sources, and `lucene-next/` is a separate area for the next Lucene generation. There is also a `sonar-project.properties` file and a `.sonar/` directory, and the project page carries badges for SonarCloud coverage and a quality gate, which means coverage and the quality gate are enforced in the build rather than reported for information. The project page also links a reproducible builds badge, so the published artifacts are checked for byte-level reproducibility.

Versioning is the thing to check before you add the dependency

The release list shows two lines in flight at once, which is a normal state for a project with a long support history. There is a 9.0.0.Beta1 from 2026-09-28, and there are final releases 8.4.0.Final and 8.3.2.Final from 2026-05-27 and 2026-05-24. The project publishes a compatibility matrix and available versions page, and the project page points you at both that and the reference documentation for all versions, current and past, which is a sign that a long-lived library is expected to be consulted rather than memorised. The licence history is worth a sentence too, because it changed. The software and its documentation are distributed under the Apache License version 2.0, with the licence text included verbatim in the `LICENSE.txt` file at the repository root, but the project page carries a note that Hibernate Search 7.2.0.Alpha1 and lower are distributed under a different licence, with a pointer to the licensing information provided alongside published binaries and to the corresponding branch in the source repository. So if you are still on a 7.x line, the licence terms you inherited are not necessarily the ones on the current line.

Where the search index and the database disagree

The structural limitation of this approach is that you now have two copies of the data and the library keeps them in step asynchronously. The listener-triggered indexing happens when the session flushes, so between a commit and the index update there is a window in which a search returns the old state. For most applications that is fine and invisible. For a workflow where a user searches immediately after a write and must see their own change, or where a background job reads through the search index to decide what to do next, the window becomes a correctness problem rather than a latency detail. There is a second, subtler issue: the index mapping is not the same shape as the table, so a mapping that embeds a collection has to decide what happens when that collection is large, and the answer affects both index size and query latency in ways the ORM developer is not expected to guess. The alternative to weigh against is not another library but a different arrangement: keeping the search index strictly as a read-side cache that you rebuild from scratch, or running queries against the database with a full-text extension and no index at all. Hibernate Search wins when the ORM already exists and the search must return managed entities; it loses when the consistency requirement is strict or the domain has no ORM to begin with.

Editorial conclusion

Hibernate Search is the right tool for a Java application that already runs Hibernate ORM and needs full-text search whose results come back as ordinary managed entities, because the index mapping is declared next to the entity mapping and the DSL returns entities rather than documents. It is a poor fit for an application that does not use Hibernate ORM, since the whole value is the coupling between the two, and a poor fit when the search must reflect a write instantly, because the listener-triggered indexing is asynchronous by nature. Before adopting it, read the compatibility matrix for the version you are on, because the project ships an 8.x line alongside a 9.0.0.Beta1, check whether the search results need to be consistent with the database the instant a write happens, and note that the last push to the repository was on 2026-10-22 while the most recent stable release is 8.4.0.Final from 2026-05-27.

Frequently asked questions

What does Hibernate Search do?

Hibernate Search automatically extracts data from Hibernate ORM entities and pushes it to local Apache Lucene indexes or to remote Elasticsearch or OpenSearch indexes. It offers declarative mapping of entity properties to index fields, on-demand mass indexing, on-the-fly listener-triggered indexing, and a Search DSL for building full-text queries that return hits as Hibernate ORM entities.

How do I index existing data with Hibernate Search?

Open a search session on the entity manager, ask it for a mass indexer for the entity type and start it. The documented sequence is Search.session( entityManager ), then searchSession.massIndexer( Book.class ), then indexer.startAndWait(), which blocks until the existing data has been written to the index.

How does Hibernate Search keep the index up to date?

Through on-the-fly listener-triggered indexing of entities modified through a Hibernate ORM session, which requires no change to code based on JPA or Hibernate ORM. The session flush triggers the update, so the index follows the ORM lifecycle rather than explicit calls in your own code.

Can I search across relationships in Hibernate Search?

Yes, using the field paths from the mapping. The documented example matches on fields title and authors.name, where the authors collection is declared with @IndexedEmbedded and therefore embedded into the Book document, and the result exposes both the hits and the total hit count.

Official sources

  1. hibernate/hibernate-search on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hibernate-hibernate-search.svg)](https://hysenlabs.com/projects/hibernate-hibernate-search)

Was this useful?