上一篇解决了文档的结构定义——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 级修饰。

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 预设。

索引时 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。keyword、integer、date 等字段类型不经过 analysis,直接以原始值索引。

练习

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

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

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

系列导航

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

参考资料