上一篇解决了文档的结构定义——mapping 把 JSON 字段翻译成 Lucene 数据结构。这一篇进入 text 类型字段怎样从原始字符串变成可搜索的词项。

mapping 决定一个字段"要不要做全文索引",analysis 决定"怎样做"。一个 text 字段在写入时经过 analysis pipeline,产生一组标准化的词项(terms),这些词项被写入倒排索引。搜索时,查询文本也经过 analysis,产生同样标准化的词项,然后在倒排索引中匹配。

本文只抓一个问题:analysis pipeline 的三个阶段怎样工作,以及索引时分析和搜索时分析为什么可能用不同的 analyzer。

Analysis 三阶段

一个 analyzer 由三个组件按顺序组成:

1
2
3
4
5
原始文本
→ Character Filter(s) 字符级预处理
→ Tokenizer 切分为 token
→ Token Filter(s) token 级后处理
→ [term1, term2, ...] 写入倒排索引

三个阶段的职责边界和数据流向如下图。左侧是输入的原始文本,右侧是写入倒排索引的词项列表。中间每个阶段只做一类变换:字符级替换、切分、token 级修饰。

flowchart LR
    raw["原始文本"] --> CF["Character Filter<br/>0..N 个<br/>字符级替换"]
    CF --> TK["Tokenizer<br/>有且仅 1 个<br/>切分为 token"]
    TK --> TF["Token Filter<br/>0..N 个<br/>token 级后处理"]
    TF --> terms["词项列表<br/>写入倒排索引"]

    style CF fill:#f0f4ff,stroke:#6688cc
    style TK fill:#fff4e0,stroke:#cc9944
    style TF fill:#f0fff0,stroke:#66aa66

Character Filter 在分词之前处理原始字符流。典型用途包括:去除 HTML 标签(html_strip)、字符映射替换(mapping,如把 & 替换为 and)、正则替换(pattern_replace)。一个 analyzer 可以有零个或多个 character filter。

Tokenizer 把经过 character filter 处理后的字符流切分为独立的 token。每个 analyzer 有且只有一个 tokenizer。常见的 tokenizer:

Tokenizer 切分规则 示例输入 → 输出
standard Unicode 文本分词规则 “The quick-fox” → [“The”, “quick”, “fox”]
whitespace 按空白符切分 “The quick-fox” → [“The”, “quick-fox”]
keyword 不切分,整体作为一个 token “The quick-fox” → [“The quick-fox”]
pattern 按正则表达式切分 可自定义分隔符
ik_smart / ik_max_word 中文分词(需安装 IK 插件) “搜索引擎” → [“搜索引擎”] 或 [“搜索”, “引擎”]

Token Filter 对 tokenizer 产生的 token 做后处理。一个 analyzer 可以有零个或多个 token filter,按顺序执行。常见的 token filter:

Token Filter 作用
lowercase 转小写
stop 去除停用词(the, a, is 等)
stemmer 词干化(running → run, dogs → dog)
synonym 同义词扩展(quick → fast)
asciifolding 去除变音符号(café → cafe)
edge_ngram 前缀 n-gram(search → [s, se, sea, sear, …])

内置 Analyzer

ES 预定义了几个常用的 analyzer,它们是 character filter + tokenizer + token filter 的固定组合:

Analyzer 组成 效果
standard standard tokenizer + lowercase filter 按 Unicode 规则分词,转小写
simple 按非字母字符切分 + lowercase 去掉数字和标点
whitespace 按空白符切分 保留大小写和标点
keyword 不切分 整个输入作为单个词项
english standard tokenizer + 英语停用词 + 英语词干 针对英文优化

standard analyzer 是默认 analyzer。大多数场景下足够使用,但对中文等不以空格分词的语言,需要安装专门的分词插件(如 IK Analyzer、jieba)。

Custom Analyzer

当内置 analyzer 不满足需求时,可以定义 custom analyzer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
PUT /custom-analysis-demo
{
"settings": {
"analysis": {
"char_filter": {
"ampersand_mapping": {
"type": "mapping",
"mappings": ["& => and"]
}
},
"filter": {
"english_stop": {
"type": "stop",
"stopwords": "_english_"
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["ampersand_mapping"],
"tokenizer": "standard",
"filter": ["lowercase", "english_stop"]
}
}
}
}
}

这个自定义 analyzer 的处理流程:

1
2
3
4
5
"Search & Retrieval is Important"
→ char filter: "Search and Retrieval is Important"
→ tokenizer: ["Search", "and", "Retrieval", "is", "Important"]
→ lowercase: ["search", "and", "retrieval", "is", "important"]
→ stop: ["search", "retrieval", "important"]

上面的 JSON 配置和文本示例对应到组件拓扑如下图。my_analyzer 把一个自定义 char_filter、内置 tokenizer、两个 token filter 串成一条管道。内置 analyzer(如 standard)也是同样的结构,只是组件选型由 ES 预设。

flowchart LR
    subgraph my_analyzer
        direction LR
        cf["ampersand_mapping<br/>(char_filter)<br/>& → and"] --> tk["standard<br/>(tokenizer)"]
        tk --> f1["lowercase<br/>(token filter)"]
        f1 --> f2["english_stop<br/>(token filter)"]
    end
    input["原始文本"] --> cf
    f2 --> output["词项列表"]

索引时 Analysis vs 搜索时 Analysis

ES 在两个时机执行 analysis:写入时(index time)和搜索时(search time)。

默认情况下,两个时机使用同一个 analyzer。但在某些场景下需要不同的 analyzer。典型场景是搜索时的同义词扩展:

1
2
3
4
5
索引时:按原词索引
"quick brown fox" → ["quick", "brown", "fox"]

搜索时:扩展同义词
搜索 "fast" → analyzer 扩展为 ["fast", "quick"] → 匹配到包含 "quick" 的文档

通过 search_analyzer 字段配置搜索时使用的 analyzer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
PUT /synonym-demo
{
"settings": {
"analysis": {
"filter": {
"my_synonyms": {
"type": "synonym",
"synonyms": ["quick,fast", "big,large"]
}
},
"analyzer": {
"search_with_synonyms": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "my_synonyms"]
}
}
}
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "standard",
"search_analyzer": "search_with_synonyms"
}
}
}
}

实验:用 _analyze API 观察分词

_analyze API 可以直接测试 analyzer 的分词效果,不需要创建 index:

1
2
3
4
5
6
# 默认 standard analyzer
POST /_analyze
{
"text": "The Quick Brown Fox jumped over 2 lazy dogs!",
"analyzer": "standard"
}

返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"tokens": [
{"token": "the", "position": 0},
{"token": "quick", "position": 1},
{"token": "brown", "position": 2},
{"token": "fox", "position": 3},
{"token": "jumped", "position": 4},
{"token": "over", "position": 5},
{"token": "2", "position": 6},
{"token": "lazy", "position": 7},
{"token": "dogs", "position": 8}
]
}

对比 simple analyzer(去掉数字):

1
2
3
4
5
POST /_analyze
{
"text": "The Quick Brown Fox jumped over 2 lazy dogs!",
"analyzer": "simple"
}

返回的 tokens 中不包含 “2”,因为 simple analyzer 按非字母字符切分,数字被丢弃。

对比 whitespace analyzer(保留大小写和标点):

1
2
3
4
5
POST /_analyze
{
"text": "The Quick Brown Fox jumped over 2 lazy dogs!",
"analyzer": "whitespace"
}

返回 [“The”, “Quick”, “Brown”, “Fox”, “jumped”, “over”, “2”, “lazy”, “dogs!”]——注意 “The” 保留了大写,“dogs!” 保留了感叹号。

用自定义的 token filter 组合测试:

1
2
3
4
5
6
POST /_analyze
{
"tokenizer": "standard",
"filter": ["lowercase", "stop", "stemmer"],
"text": "The dogs were running quickly through the forest"
}

返回 [“dog”, “run”, “quickli”, “through”, “forest”]——停用词 “the”、“were” 被去除,“dogs” → “dog”(词干化),“running” → “run”(词干化)。

模式提炼:管道处理模式

1
input → stage1 → stage2 → stage3 → output

Analysis pipeline 是管道处理模式(pipeline pattern)的典型实现。每个阶段接收上一阶段的输出,做一种特定的变换,传给下一阶段。这种模式在软件工程中反复出现:

系统 管道 阶段
ES Analysis text → terms char filter → tokenizer → token filter
Unix Shell data stream cmd1 | cmd2 | cmd3
Logstash event → output input → filter → output
ES Ingest document → document processor pipeline
Java Stream Collection → result map → filter → collect
Compiler source → binary lexer → parser → codegen

管道模式的优势在于每个阶段职责单一、可独立替换和组合。ES 的 analysis 系统正是通过组合不同的 char filter、tokenizer 和 token filter 来适配不同的语言和业务需求。

工程迁移表

概念 Elasticsearch Apache Solr Lucene RDBMS
分析器 Analyzer Analyzer (FieldType) Analyzer (Java class) 全文索引(有限)
字符预处理 Character Filter CharFilter CharFilter
分词器 Tokenizer Tokenizer Tokenizer 内置(不可配置)
词项后处理 Token Filter TokenFilter TokenFilter
配置方式 JSON (settings.analysis) XML (schema.xml) Java API SQL 参数
中文分词 IK / jieba 插件 SmartCN / jieba CJKAnalyzer 有限支持
同义词 synonym filter SynonymFilterFactory SynonymFilter

常见误解

误解一:analyzer 只在写入时起作用。 搜索时查询文本也会经过 analysis。如果索引时用 standard analyzer 转小写,搜索时用 keyword analyzer 不转小写,大写查询词就匹配不到小写词项。

误解二:中文可以用默认 standard analyzer。 standard analyzer 按 Unicode 规则分词,对中文会把每个字拆成独立的 token(逐字切分)。搜索"搜索引擎"会被拆成"搜"“索”“引”"擎"四个词项,召回大量不相关文档。中文需要安装专门的分词插件。

误解三:analysis 只影响 text 字段。 是的,只有 text 类型的字段会经过 analysis。keywordintegerdate 等字段类型不经过 analysis,直接以原始值索引。

练习

  1. _analyze API 测试同一段英文文本在 standardenglishwhitespace 三个 analyzer 下的分词差异。注意 english analyzer 的词干化效果。

  2. 创建一个 index,定义包含 lowercase + stop + stemmer 三个 token filter 的 custom analyzer。写入文档后,验证搜索 “running” 能否匹配到包含 “run” 的文档。

  3. 定义一个使用 search_analyzer 的字段,配置搜索时同义词扩展。验证搜索同义词能否命中原始文档。

系列导航

上一篇 下一篇
Mapping 与字段类型:给文档定义结构 写入路径:近实时、Translog 与 Refresh/Flush

参考资料