深入 Elasticsearch(04):从原始文本到可搜索词项
上一篇解决了文档的结构定义——mapping 把 JSON 字段翻译成 Lucene 数据结构。这一篇进入 text 类型字段怎样从原始字符串变成可搜索的词项。
mapping 决定一个字段"要不要做全文索引",analysis 决定"怎样做"。一个 text 字段在写入时经过 analysis pipeline,产生一组标准化的词项(terms),这些词项被写入倒排索引。搜索时,查询文本也经过 analysis,产生同样标准化的词项,然后在倒排索引中匹配。
本文只抓一个问题:analysis pipeline 的三个阶段怎样工作,以及索引时分析和搜索时分析为什么可能用不同的 analyzer。
Analysis 三阶段
一个 analyzer 由三个组件按顺序组成:
1 | |
三个阶段的职责边界和数据流向如下图。左侧是输入的原始文本,右侧是写入倒排索引的词项列表。中间每个阶段只做一类变换:字符级替换、切分、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 | |
这个自定义 analyzer 的处理流程:
1 | |
上面的 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 | |
通过 search_analyzer 字段配置搜索时使用的 analyzer:
1 | |
实验:用 _analyze API 观察分词
_analyze API 可以直接测试 analyzer 的分词效果,不需要创建 index:
1 | |
返回:
1 | |
对比 simple analyzer(去掉数字):
1 | |
返回的 tokens 中不包含 “2”,因为 simple analyzer 按非字母字符切分,数字被丢弃。
对比 whitespace analyzer(保留大小写和标点):
1 | |
返回 [“The”, “Quick”, “Brown”, “Fox”, “jumped”, “over”, “2”, “lazy”, “dogs!”]——注意 “The” 保留了大写,“dogs!” 保留了感叹号。
用自定义的 token filter 组合测试:
1 | |
返回 [“dog”, “run”, “quickli”, “through”, “forest”]——停用词 “the”、“were” 被去除,“dogs” → “dog”(词干化),“running” → “run”(词干化)。
模式提炼:管道处理模式
1 | |
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,直接以原始值索引。
练习
-
用
_analyzeAPI 测试同一段英文文本在standard、english、whitespace三个 analyzer 下的分词差异。注意englishanalyzer 的词干化效果。 -
创建一个 index,定义包含
lowercase+stop+stemmer三个 token filter 的 custom analyzer。写入文档后,验证搜索 “running” 能否匹配到包含 “run” 的文档。 -
定义一个使用
search_analyzer的字段,配置搜索时同义词扩展。验证搜索同义词能否命中原始文档。
系列导航
| 上一篇 | 下一篇 |
|---|---|
| Mapping 与字段类型:给文档定义结构 | 写入路径:近实时、Translog 与 Refresh/Flush |
