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文件

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)的索引文件。


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




收藏了,感谢分享