1. 概述
Lucene 的 Analyzer(分析器)在文档索引和搜索过程中负责对文本进行处理,将其拆解为可被检索的词汇单元(token)。
我们在之前的 Lucene 入门教程 中简单提过 Analyzer。本文将深入介绍:
- 常见的内置 Analyzer 及其行为差异
- 如何构建自定义 Analyzer
- 如何为不同字段配置不同的 Analyzer
✅ 这些是实际项目中经常踩坑的地方,掌握后能显著提升搜索准确性和性能。
2. Maven 依赖
使用 Lucene Analyzer 需要引入以下核心依赖:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>7.4.0</version>
</dependency>
📌 最新版本可参考 Maven Central。
⚠️ 注意:lucene-analyzers-common
包含了大多数常用语言的 Analyzer 实现,不要遗漏。
3. Lucene Analyzer 基本原理
Analyzer 的核心作用是将原始文本切分为一系列 token,这个过程称为“分词”。
核心组成
Analyzer 主要由两部分构成:
- ✅ Tokenizer(分词器):负责将文本按规则切分成 token
- ✅ Filter(过滤器):对 token 流进行加工,如转小写、去停用词、词干提取等
不同 Analyzer 实际上就是 Tokenizer 和多个 Filter 的不同组合。
分词结果查看工具
为了直观对比不同 Analyzer 的行为,我们定义一个通用方法来查看分词结果:
public List<String> analyze(String text, Analyzer analyzer) throws IOException {
List<String> result = new ArrayList<>();
TokenStream tokenStream = analyzer.tokenStream("content", text);
CharTermAttribute attr = tokenStream.addAttribute(CharTermAttribute.class);
tokenStream.reset();
while (tokenStream.incrementToken()) {
result.add(attr.toString());
}
tokenStream.end();
tokenStream.close();
return result;
}
📌 这个方法非常实用,建议集合。它能帮助你在调试时快速验证分词效果。
4. 常见 Lucene Analyzer
下面通过示例对比几种常用 Analyzer 的行为差异。
4.1 StandardAnalyzer
最常用的默认 Analyzer,适用于大多数英文场景。
private static final String SAMPLE_TEXT = "This is baeldung.com Lucene Analyzers test";
@Test
public void whenUseStandardAnalyzer_thenAnalyzed() throws IOException {
List<String> result = analyze(SAMPLE_TEXT, new StandardAnalyzer());
assertThat(result,
contains("baeldung.com", "lucene", "analyzers", "test"));
}
✅ 特点总结:
- 能识别 URL 和邮箱地址(如
baeldung.com
不会被拆开) - 自动转小写
- 移除英文停用词(如 "this", "is")
- 使用
StandardTokenizer
按 Unicode 标准切词
⚠️ 不适用于中文,需配合其他方案。
4.2 StopAnalyzer
基于字母切分,并去除停用词。
@Test
public void whenUseStopAnalyzer_thenAnalyzed() throws IOException {
List<String> result = analyze(SAMPLE_TEXT, new StopAnalyzer());
assertThat(result,
contains("baeldung", "com", "lucene", "analyzers", "test"));
}
✅ 组成结构:
LetterTokenizer
:遇到非字母字符就切分LowerCaseFilter
:转小写StopFilter
:移除停用词
❌ 缺点:无法识别 URL,baeldung.com
被拆成 "baeldung"
和 "com"
。
4.3 SimpleAnalyzer
仅做字母切分 + 小写转换,无其他处理。
@Test
public void whenUseSimpleAnalyzer_thenAnalyzed() throws IOException {
List<String> result = analyze(SAMPLE_TEXT, new SimpleAnalyzer());
assertThat(result,
contains("this", "is", "baeldung", "com", "lucene", "analyzers", "test"));
}
✅ 组成:
LetterTokenizer
LowerCaseFilter
❌ 不去停用词,也不识别 URL。
适合简单场景或作为自定义基础。
4.4 WhitespaceAnalyzer
仅按空白字符(空格、换行等)切分,不做其他处理。
@Test
public void whenUseWhiteSpaceAnalyzer_thenAnalyzed() throws IOException {
List<String> result = analyze(SAMPLE_TEXT, new WhitespaceAnalyzer());
assertThat(result,
contains("This", "is", "baeldung.com", "Lucene", "Analyzers", "test"));
}
✅ 保留原始大小写
✅ 保留完整单词(包括带点的 URL)
适合用于不需要复杂分析的字段,比如日志行、原始语句等。
4.5 KeywordAnalyzer
将整个输入视为一个 token,不分词。
@Test
public void whenUseKeywordAnalyzer_thenAnalyzed() throws IOException {
List<String> result = analyze(SAMPLE_TEXT, new KeywordAnalyzer());
assertThat(result, contains("This is baeldung.com Lucene Analyzers test"));
}
✅ 典型应用场景:
- ID 字段(如用户 ID、订单号)
- ZIP Code
- 精确匹配的标签字段
⚠️ 常被误用。如果你需要模糊搜索或全文检索,别用它。
4.6 语言专用 Analyzer
Lucene 提供了针对特定语言优化的 Analyzer,例如:
EnglishAnalyzer
FrenchAnalyzer
SpanishAnalyzer
以 EnglishAnalyzer
为例:
@Test
public void whenUseEnglishAnalyzer_thenAnalyzed() throws IOException {
List<String> result = analyze(SAMPLE_TEXT, new EnglishAnalyzer());
assertThat(result, contains("baeldung.com", "lucen", "analyz", "test"));
}
✅ 内部组件链:
StandardTokenizer
StandardFilter
EnglishPossessiveFilter
(去掉所有格's
)LowerCaseFilter
StopFilter
PorterStemFilter
(词干提取)
📌 重点:lucene
→ lucen
, analyzers
→ analyz
,这是词干化(stemming)的结果。
适合英文全文检索,提升召回率。
5. 自定义 Analyzer
有时候内置 Analyzer 无法满足需求,比如我们想要:
保留词干化 + 去停用词,但结果 token 首字母大写
方法一:使用 CustomAnalyzer.builder(推荐)
简单粗暴,声明式构建:
@Test
public void whenUseCustomAnalyzerBuilder_thenAnalyzed() throws IOException {
Analyzer analyzer = CustomAnalyzer.builder()
.withTokenizer("standard")
.addTokenFilter("lowercase")
.addTokenFilter("stop")
.addTokenFilter("porterstem")
.addTokenFilter("capitalization") // 首字母大写
.build();
List<String> result = analyze(SAMPLE_TEXT, analyzer);
assertThat(result, contains("Baeldung.com", "Lucen", "Analyz", "Test"));
}
✅ 优点:无需写类,配置灵活,适合动态场景。
⚠️ 注意:capitalization
filter 并非 Lucene 内建,此处为示意。实际可用 PatternReplaceCharFilter
或自定义 Filter 实现类似功能。
方法二:继承 Analyzer 抽象类
更灵活,适合复杂逻辑:
public class MyCustomAnalyzer extends Analyzer {
@Override
protected TokenStreamComponents createComponents(String fieldName) {
StandardTokenizer src = new StandardTokenizer();
TokenStream result = new StandardFilter(src);
result = new LowerCaseFilter(result);
result = new StopFilter(result, StandardAnalyzer.STOP_WORDS_SET);
result = new PorterStemFilter(result);
result = new CapitalizationFilter(result); // 假设已实现
return new TokenStreamComponents(src, result);
}
}
📌 关键点:
- 必须重写
createComponents()
方法 - 返回
TokenStreamComponents(src, result)
,其中result
是最终的 token 流
实际测试
使用一个内存中的 Lucene 索引进行验证:
@Test
public void givenTermQuery_whenUseCustomAnalyzer_thenCorrect() {
InMemoryLuceneIndex luceneIndex = new InMemoryLuceneIndex(
new RAMDirectory(), new MyCustomAnalyzer());
luceneIndex.indexDocument("introduction", "introduction to lucene");
luceneIndex.indexDocument("analyzers", "guide to lucene analyzers");
Query query = new TermQuery(new Term("body", "Introduct"));
List<Document> documents = luceneIndex.searchIndex(query);
assertEquals(1, documents.size());
}
✅ 成功匹配到 Introduct
(词干化后),说明自定义 Analyzer 生效。
6. PerFieldAnalyzerWrapper:字段级 Analyzer 控制
实际业务中,不同字段可能需要不同的分析策略。例如:
title
字段:希望保留格式、做轻量分析body
字段:深度分析,去停用词 + 词干化
这时就需要 PerFieldAnalyzerWrapper
。
配置步骤
- 定义字段与 Analyzer 的映射关系:
Map<String, Analyzer> analyzerMap = new HashMap<>();
analyzerMap.put("title", new MyCustomAnalyzer()); // 自定义处理标题
analyzerMap.put("body", new EnglishAnalyzer()); // 正常处理正文
- 创建 wrapper,指定默认 fallback Analyzer:
PerFieldAnalyzerWrapper wrapper = new PerFieldAnalyzerWrapper(
new StandardAnalyzer(), analyzerMap);
📌 未明确指定的字段将使用 StandardAnalyzer
。
测试验证
@Test
public void givenTermQuery_whenUsePerFieldAnalyzerWrapper_thenCorrect() {
InMemoryLuceneIndex luceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), wrapper);
luceneIndex.indexDocument("introduction", "introduction to lucene");
luceneIndex.indexDocument("analyzers", "guide to lucene analyzers");
// 搜索 body 字段(使用 EnglishAnalyzer)
Query query = new TermQuery(new Term("body", "introduct"));
List<Document> documents = luceneIndex.searchIndex(query);
assertEquals(1, documents.size());
// 搜索 title 字段(使用 MyCustomAnalyzer)
query = new TermQuery(new Term("title", "Introduct"));
documents = luceneIndex.searchIndex(query);
assertEquals(1, documents.size());
}
✅ 两个查询都命中,说明字段级 Analyzer 正确生效。
7. 总结
本文系统梳理了 Lucene Analyzer 的核心知识点:
- ✅ 常见 Analyzer 的行为差异(Standard、Stop、Simple、Whitespace、Keyword、语言专用)
- ✅ 自定义 Analyzer 的两种方式:
CustomAnalyzer.builder()
和继承Analyzer
- ✅ 使用
PerFieldAnalyzerWrapper
实现字段级精细化控制
📌 实战建议:
- 英文场景优先考虑
StandardAnalyzer
或EnglishAnalyzer
- ID 类字段用
KeywordAnalyzer
- 多字段差异化需求务必用
PerFieldAnalyzerWrapper
- 调试时用
analyze()
方法快速验证分词结果
完整示例代码可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/lucene