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(词干提取)

📌 重点:lucenelucen, analyzersanalyz,这是词干化(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

配置步骤

  1. 定义字段与 Analyzer 的映射关系:
Map<String, Analyzer> analyzerMap = new HashMap<>();
analyzerMap.put("title", new MyCustomAnalyzer());     // 自定义处理标题
analyzerMap.put("body", new EnglishAnalyzer());      // 正常处理正文
  1. 创建 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 实现字段级精细化控制

📌 实战建议:

  • 英文场景优先考虑 StandardAnalyzerEnglishAnalyzer
  • ID 类字段用 KeywordAnalyzer
  • 多字段差异化需求务必用 PerFieldAnalyzerWrapper
  • 调试时用 analyze() 方法快速验证分词结果

完整示例代码可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/lucene


原始标题:Guide to Lucene Analyzers