Boost Spring Boot 3 Search with Hibernate Search & Lucene: Full Guide

This article explains why traditional database search struggles with large data sets, introduces Lucene as a high‑performance full‑text engine, and shows step‑by‑step how to integrate it into Spring Boot 3 using Hibernate Search, custom analyzers, entity mapping, repository extensions, and test cases.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Boost Spring Boot 3 Search with Hibernate Search & Lucene: Full Guide

Traditional database queries such as MySQL LIKE or built‑in full‑text indexes become inefficient when data reaches millions of rows, causing full‑table scans and slow response times, while lacking support for synonym expansion or phonetic correction.

High‑concurrency scenarios demand features like tokenization, sorting, highlighting, and scalability that relational databases cannot provide; Lucene offers an inverted index and TF‑IDF algorithm for millisecond‑level responses and supports custom tokenizers like the Chinese IK analyzer.

In Spring Boot, Hibernate Search bridges the gap between Hibernate ORM and Lucene (or Elasticsearch), creating a "store‑index‑query" loop that can reduce database load by over 70%.

Environment

Spring Boot 3.4.2

1. Introduction

The article demonstrates using Hibernate Search as a bridge between Hibernate ORM and full‑text search engines (Lucene or Elasticsearch).

2. Practical Cases

2.1 Add Dependencies

<dependency>
  <groupId>org.hibernate.search</groupId>
  <artifactId>hibernate-search-mapper-orm</artifactId>
  <version>7.2.1.Final</version>
</dependency>
<dependency>
  <groupId>org.hibernate.search</groupId>
  <artifactId>hibernate-search-backend-lucene</artifactId>
  <version>7.2.1.Final</version>
</dependency>
<!-- IK Chinese Analyzer -->
<dependency>
  <groupId>com.jianggujin</groupId>
  <artifactId>IKAnalyzer-lucene</artifactId>
  <version>8.0.0</version>
  <exclusions>
    <exclusion>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-analyzers-common</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-core</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-queryparser</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.apache.lucene</groupId>
      <artifactId>lucene-queries</artifactId>
    </exclusion>
  </exclusions>
</dependency>

2.2 Configuration

spring:
  jpa:
    properties:
      hibernate:
        '[search.backend.type]': lucene
        '# Local index directory'
        '[search.backend.directory.root]': f:/indexes
        '# Use IK analyzer'
        '[search.backend.analysis.configurer]': com.pack.search.config.IKAnalysisConfigurer

2.3 Custom Analyzer

public class IKAnalysisConfigurer implements LuceneAnalysisConfigurer {
  public static final String IK = "ik";
  private final Analyzer ik = new IKAnalyzer();

  @Override
  public void configure(LuceneAnalysisConfigurationContext context) {
    context.analyzer(IK).instance(ik);
  }
}

2.4 Entity Definition

@Entity
@Table(name = "t_book")
@Indexed
public class Book {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @DocumentId
  private Long id;

  @Column(nullable = false)
  @FullTextField(name = "title")
  @KeywordField(name = "sort_title", sortable = Sortable.YES)
  private String title;

  @Column(nullable = false)
  @FullTextField(name = "author")
  @KeywordField(name = "sort_author", sortable = Sortable.YES)
  private String author;
}

2.5 Custom Repository Interface

public interface SearchRepository<T, ID extends Serializable> {
  List<T> fullTextSearch(String text, int offset, int limit, List<String> fields, String sortBy, SortOrder sortOrder);
  List<T> fuzzySearch(String text, int offset, int limit, List<String> fields, String sortBy, SortOrder sortOrder);
  List<T> wildcardSearch(String pattern, int offset, int limit, List<String> fields, String sortBy, SortOrder sortOrder);
}

2.6 Base Repository

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, SearchRepository<T, ID> {}

public class BaseRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements BaseRepository<T, ID> {
  private final EntityManager entityManager;

  public BaseRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
    super(domainClass, entityManager);
    this.entityManager = entityManager;
  }

  public BaseRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
    super(entityInformation, entityManager);
    this.entityManager = entityManager;
  }

  @Override
  public List<T> fullTextSearch(String text, int offset, int limit, List<String> fields, String sortBy, SortOrder sortOrder) {
    if (text == null || text.isEmpty()) return Collections.emptyList();
    return Search.session(entityManager)
        .search(getDomainClass())
        .where(f -> f.match().fields(fields.toArray(String[]::new)).matching(text))
        .sort(f -> f.field(sortBy).order(sortOrder))
        .fetchHits(offset, limit);
  }

  @Override
  public List<T> fuzzySearch(String text, int offset, int limit, List<String> fields, String sortBy, SortOrder sortOrder) {
    if (text == null || text.isEmpty()) return Collections.emptyList();
    return Search.session(entityManager)
        .search(getDomainClass())
        .where(f -> {
          BooleanPredicateClausesStep<?> steps = f.bool();
          for (String field : fields) {
            steps = steps.should(f.match().field(field).matching(text).fuzzy(0));
          }
          return steps;
        })
        .sort(s -> s.field(sortBy).order(sortOrder))
        .fetchHits(offset, limit);
  }

  @Override
  public List<T> wildcardSearch(String pattern, int offset, int limit, List<String> fields, String sortBy, SortOrder sortOrder) {
    return Search.session(entityManager)
        .search(getDomainClass())
        .where(f -> {
          BooleanPredicateClausesStep<?> steps = f.bool();
          for (String field : fields) {
            steps = steps.should(f.wildcard().field(field).matching(pattern));
          }
          return steps;
        })
        .sort(s -> s.field(sortBy).order(sortOrder))
        .fetchHits(offset, limit);
  }
}

2.7 Tests

@Test
public void testFullTextSearch() {
  bookService.saveBook(new Book("Spring Boot3实战案例200讲", "Pack"));
  bookService.saveBook(new Book("Spring全家桶实战案例", "Xg"));
  bookService.saveBook(new Book("MySQL从入门到精通", "Jack"));
  bookService.saveBook(new Book("MCP开发指南", "张三"));
  List<Book> books = bookRepository.fullTextSearch("案例", 0, 10, List.of("title"), "sort_title", SortOrder.DESC);
  System.err.println(books);
}

@Test
public void testFuzzySearchByTitleAndAuthor() {
  List<Book> books = bookRepository.fuzzySearch("案例", 0, 10, List.of("title", "author"), "sort_title", SortOrder.DESC);
  System.err.println(books);
}

Running the test creates index files for the Book entity under F:/indexes. The generated index can be inspected with the following screenshots.

Index directory
Index directory

Query results for full‑text search:

Full‑text search result
Full‑text search result

Query results for fuzzy search:

Fuzzy search result
Fuzzy search result
JavaLuceneSpring BootHibernate Search
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.