Spring Boot + Lucene 构建全文检索

1. 简介

传统数据库查询的局限

业务系统初期多依赖 MySQL 的 LIKE 语句或内置全文索引实现搜索,但数据量达百万级后性能骤降:LIKE 模糊匹配需全表扫描,响应时间从毫秒级升至秒级;数据库全文索引功能单一,无法支持同义词扩展(如“手机”匹配“智能手机”)或拼音纠错,导致搜索结果不精准。

高并发与复杂查询的挑战

系统面临日均百万级搜索请求时,传统数据库难以兼顾性能与功能:需同时满足分词搜索、结果排序、高亮显示等需求,但数据库扩展性弱,复杂查询易拖垮主库。Lucene 作为成熟的全文检索引擎,通过倒排索引和 TF-IDF 算法实现毫秒级响应,支持自定义分词(如中文 IK 分词器)和语义分析,成为高并发场景下的高效解决方案。

全文检索与业务系统的整合

在 Spring Boot 中,Lucene 可通过 Hibernate Search 等框架无缝集成,构建“存储-索引-查询”闭环。该方案降低数据库负载 70% 以上,成为智能搜索服务的核心基础设施。

注意:如果你的应用是一个规模较小(数据量不大)或者资源受限的应用,并且不需要分布式搜索等功能,那么使用Lucene可能是更轻量级的选择。其它情况下提议使用ES。

本篇文章我们将通过 Hibernate Search 模块充当 Hibernate ORM 与全文检索引擎(如 Lucene 或 Elasticsearch)之间的桥梁。

2.实战案例

2.1 引入依赖

<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中文分词器-->
<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 配置文件

spring:
  jpa:
    properties:
      hibernate:
        '[search.backend.type]': lucene
        # 本地索引文件位置
        '[search.backend.directory.root]': f:/indexes
        # 我们要使用IK中文分词器,需要自定义配置类
        '[search.backend.analysis.configurer]': com.pack.search.config.IKAnalysisConfigurer

自定义分词器配置类

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) ;
  }
}

ik分词器配置文件(扩展自定义分词字典)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IKAnalyzer扩展配置</comment>
    <!--用户的扩展字典 -->
    <entry key="ext_dict">extend.dic</entry>
    <!--用户扩展停止词字典 -->
    <entry key="ext_stopwords">stop.dic</entry>
</properties>

同时在src/main/resources目录下新建extend.dic和stop.dic文件

Spring Boot + Lucene 构建全文检索

2.3 实体定义

@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;
}

注解说明:

  • @Indexed: 将实体标记为由 Hibernate Search 索引,并使其可被检索
  • @DocumentId: 将实体属性映射为索引中文档的标识符(ID)
  • @FullTextField: 将字段标记为需进行全文检索索引(并应用文本分析处理)
  • @KeywordField: 将字段标记为以关键字形式索引(不进行分词处理,仅支持准确匹配)

2.4 自定义Repository

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);
}

定义通用的搜索接口。我们可以在定义具体的Repository时都继承该接口。

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

该Repository集成了Jpa及上面自定义查询方法。

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.5 测试

@Test
public void testFullTextSearch() {
  bookService.saveBook(new Book("Spring Boot案例", "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);
}

运行上面代码后,第一会在F:/indexes目录下生成对应实体对象(Book)的索引文件。

Spring Boot + Lucene 构建全文检索

Spring Boot + Lucene 构建全文检索

通过Lucene获取到对应文档的ID后,再通过ID查询数据库。

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

Spring Boot + Lucene 构建全文检索

© 版权声明

相关文章

1 条评论

您必须登录才能参与评论!
立即登录
  • 头像
    桃思君 读者

    收藏了,感谢分享

    无记录