Lucene
文档模型 里,TextField 写入倒排前必须经过
Analyzer:同一篇中文正文,用
StandardAnalyzer 与用 ICU 或 IK 插件切出来的
term 集合不同,进而改变 FST
词典规模、postings 长度与召回行为。运维 ES 时改
analyzer 或 search_analyzer 往往要
reindex——根源就在分析链与段文件绑死,而非单纯改配置即生效。
本文是系列第 3 篇,回答:
- CharFilter、Tokenizer、TokenFilter 各改什么?
TokenStream契约为何约束索引与查询两侧必须一致?- 中文分词在引擎层只需掌握哪些接口与代价,而不必成为 NLP 专家?
版本锚定:Apache Lucene 9.x/10.x Analysis 包官方文档;Elasticsearch 8.x Analysis 与 Reindex 说明。不写 jieba / IK 内部词典算法;插件行为以对应发行版文档为准。
一、最小故事:同一标题,两种 analyzer,两个索引
字符串:「Elasticsearch倒排索引」(无空格中文
+ 英文粘连)
| 阶段 | Standard 路径(简化) | 中文插件路径(概念) |
|---|---|---|
| Tokenizer | 可能切出
elasticsearch、倒排、索引
或更碎 |
词典驱动:Elasticsearch、倒排索引
等 |
查询 倒排 |
能否命中取决于索引侧是否出现同 term | 同左——索引与查询 analyzer 必须对齐 |
| 词典 term 数 | 可能更多碎词 | 较少长词,单 term postings 变长 |
若索引用 ik_max_word、查询用
standard,会出现
搜不到已入库文档
的经典故障——不是倒排损坏,而是 term
空间不一致。第 17 篇排障会回链本节。
二、分析链三阶段
Lucene 官方 Analysis 文档将 Analyzer 拆为(A 级):
flowchart LR
raw["Raw text"]
cf["CharFilter chain"]
tok["Tokenizer"]
tf["TokenFilter chain"]
terms["Term stream to IndexWriter"]
raw --> cf --> tok --> tf --> terms
2.1 CharFilter:字符流预处理
在 字符流 上操作,可改长度(如折叠
fi→fi)。常见:
MappingCharFilter:字符映射表(简繁、符号)HTMLStripCharFilter:去标签
多个 CharFilter 串行;输出进入 Tokenizer。
2.2 Tokenizer:切分基准单元
产生 初始 token,带
offset、positionIncrement
等属性。例如:
StandardTokenizer:Unicode 文本规则分词KeywordTokenizer:整段一个 token(keyword字段)
Tokenizer 有且只有一个。
2.3 TokenFilter:token 级变换
在 token 流上过滤或变换,可删 token、改词形、注入同义词。常见:
LowerCaseFilterStopFilterSynonymGraphFilter(查询侧常用 Graph 展开)
多个 TokenFilter 串行;最终流交给
IndexWriter 写 postings。
flowchart TB
subgraph indexTime ["Index time analyzer"]
i1["CharFilter"]
i2["Tokenizer"]
i3["LowerCase + Stop"]
i1 --> i2 --> i3
end
subgraph searchTime ["Search time analyzer"]
s1["Same Tokenizer base"]
s2["Maybe SynonymGraph"]
s1 --> s2
end
indexTime --> inv["Inverted index"]
searchTime --> qry["Query parser terms"]
三、TokenStream 契约
TokenStream 继承
AttributeSource,核心属性包括(Lucene
TokenStream API):
| 属性 | 含义 |
|---|---|
CharTermAttribute |
词项文本(写入词典的 bytes) |
OffsetAttribute |
原文字符起止(高亮) |
PositionIncrementAttribute |
相对前一 token 的位置步进 |
PositionLengthAttribute |
Graph token 跨度(同义词) |
TypeAttribute |
可选类型标签 |
契约:
- 消费者调用
reset()→incrementToken()循环 →end()→close()。 - 索引与搜索对同一逻辑字段应使用
兼容 的分析结果(通常同一
Analyzer实例或显式search_analyzer)。 positionIncrement=0表示同位置多 token(同义词图);短语查询需正确消费 Graph(LuceneQueryParser/ ESmatch_phrase文档)。
违反契约的症状:能 single-term 搜到、phrase 搜不到;或 highlight 偏移错乱。
四、写放大与词典代价
每个 token 至少产生一个 (field, term)
倒排项。设文档 \(d\) 中字段
\(f\) 经分析得到 \(t_{d,f}\) 个 token,则单次
addDocument 对倒排的插入量约为:
\[ W_{\text{postings}} \approx \sum_{f \in \text{indexed}} t_{d,f} \]
再乘上 IndexOptions 决定的
freq/position/offset 倍数(第 5 篇)。因此:
- 过度分词(
max_word风格)膨胀词典与 postings,合并与 refresh 变慢。 - 同义词扩展在索引侧会使每个 doc 复制多份
term——通常更推荐查询侧
SynonymGraphFilter(Elastic 文档 Synonym token filter)。
| 策略 | 词典 | 召回 | 风险 |
|---|---|---|---|
| 细粒度分词 | 大 | 宽 | 噪声、写放大 |
| 粗粒度 / keyword | 小 | 窄 | 漏召回 |
| 索引细 + 搜索粗 | 需 custom 管道 | 折中 | 配置复杂 |
五、中文分词:引擎接口与运维,不是 NLP 教程
中文无空格分隔,Tokenizer 必须依赖 词典、统计或模型。Lucene 本体不提供生产级中文分词;常见做法:
- ICU Analysis 插件:基于 ICU 断词规则,偏通用。
- IK Analyzer、jieba 等第三方:词典可热更新,与 ES 版本绑定发布。
本系列只掌握引擎层四点(A 级边界 + 工程判断):
- 插件作为 Tokenizer/TokenFilter
接入,输出仍是标准
TokenStream。 - 词典更新不自动改写历史 Segment;要一致召回需 reindex 或新索引别名切换(ES Reindex)。
- 多义词与专有名词导致的切分歧义,体现为
term 集合不同,不是 SQL
COLLATE能修。 - 性能:分词在 flush 路径上 CPU 密集;bulk 写入时 analyzer 复杂度直接影响 indexing 吞吐——具体数字取决于硬件与插件,本站不在无实测环境下给出口径。
与 PG 对比:GIN
tsvector 使用内置 to_tsvector
词典配置;改配置同样涉及重建索引,语义与 Lucene analyzer
变更类似。
争论(工程边界):「通用中文分词应统一国标 / 统一模型」vs「业务定制词典不可少」——学术 NER/分词论文优化的是 F1,搜索引擎优化的是 term 一致性与写放大;生产以 可复现 pipeline + 版本化词典 为准,而非追逐单一 SOTA 模型(预印本算法不能单独支撑「业界已统一」断言)。
六、自定义 Analyzer 示例(Java)
与 Lucene 9.x 兼容的装配方式;本地需
lucene-analysis-common 等依赖。
import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.StandardTokenizer;
import org.apache.lucene.analysis.core.LowerCaseFilter;
import org.apache.lucene.analysis.core.StopFilter;
import org.apache.lucene.analysis.en.EnglishAnalyzer;
import org.apache.lucene.util.CharArraySet;
StandardTokenizerFactory tokFactory = new StandardTokenizerFactory(new HashMap<>());
Tokenizer tokenizer = tokFactory.create();
TokenStream stream = new LowerCaseFilter(tokenizer);
stream = new StopFilter(stream, EnglishAnalyzer.ENGLISH_STOP_WORDS_SET);
// IndexWriterConfig.setAnalyzer(new Analyzer() { ... wrap stream ... });Elasticsearch 侧等价物是 custom analyzer
定义(tokenizer + filter
列表),由节点加载插件提供 factory 类名。
6.1 Elasticsearch mapping 中的双 analyzer
生产常用 index 与 search 分离(ES 8.x Reference Analyzer):
{
"settings": {
"analysis": {
"analyzer": {
"idx_analyzer": {
"tokenizer": "standard",
"filter": ["lowercase"]
},
"srch_analyzer": {
"tokenizer": "standard",
"filter": ["lowercase", "synonym_graph_filter"]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "idx_analyzer",
"search_analyzer": "srch_analyzer"
}
}
}
}索引侧保持细粒度 term;查询侧在 Graph 上展开同义词,避免
postings 写放大。修改 analyzer 字段后,旧段
term 空间不变——必须 reindex
或建新索引别名切换,这与 Lucene 段不可变一致。
6.2 复现:对比两种 analyzer 的 term(步骤)
本站未粘贴本机输出;读者可在单节点 ES 8.x 或 Lucene
Analyzer 单元测试中复现:
# ES:查看某字段分析结果(需已创建索引)
curl -s -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d '{
"analyzer": "standard",
"text": "Elasticsearch倒排索引"
}'
# 将 analyzer 换为 icu_tokenizer / 已安装的中文插件,对比 tokens 数组Lucene 纯库路径:对同一 StringReader 分别
analyzer.tokenStream("f", reader),打印
CharTermAttribute,比较 token 列表差异。
七、与后续篇目的衔接
- 分析输出 term 进入 FST 词典,按 field 分离。
positions来自 TokenStream 顺序;短语查询在 postings codec 上解码。IndexWriter在 flush 时批量消费 TokenStream(第 7 篇)。
sequenceDiagram
participant IW as IndexWriter
participant An as Analyzer
participant TS as TokenStream
participant PF as PostingsConsumer
IW->>An: tokenStream(field, reader)
An->>TS: CharFilter/Tokenizer/Filters
loop incrementToken
IW->>TS: next token
TS-->>PF: term + pos + offsets
end
PF->>PF: flush block codec
八、常见误解
- 「换 analyzer 只影响新文档」。旧 Segment 仍用旧 term;除非 reindex,否则同一字段两套 term 空间共存,查询行为不可预测。
- 「同义词库越大越好」。索引侧同义词使 \(W_{\text{postings}}\) 线性膨胀,且干扰 BM25 -idf 统计(第 6 篇)。
- 「中文分词靠查询侧智能就行」。查询再智能,索引侧没切出 term 仍无法倒排 seek。
九、小结
三句话小结
- Analyzer 是 CharFilter → Tokenizer →
TokenFilter 的确定性管线,产出
TokenStream驱动倒排写入。 - 索引与搜索 analyzer 不一致是生产最常见「明明有数据却搜不到」的原因之一。
- 中文只需盯住 插件接口、词典版本、reindex 代价;算法细节留给 NLP 团队,引擎团队盯住写放大与 term 一致性。
参考资料
规范 / 文档
- Apache Lucene 9.x/10.x, Analysis
overview、
TokenStream、Analyzer。 - Elasticsearch 8.x Reference, Analysis、Synonym token filter、Reindex data。
- ICU Analysis integration(Lucene 官方模块文档)。
站内
上一篇:Lucene 文档模型 | 下一篇:词项词典 FST | 系列目录
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】全文引擎全景:架构叙事与 Lucene 内核之间的一层
定位 Lucene 9.x/10.x 库内核与 Elasticsearch 8.x 服务层相对 architecture/42、storage/29、向量引擎与 PG GIN 的分工;以最小索引故事建立坐标系,交代倒排索引学术谱系与 18 篇路线图。
【全文检索引擎】Lucene 文档模型:Field、docID 与正排/倒排
拆解 Lucene Index/Document/Field 与段内 docID 分配;说明 indexed、stored、docValues 三组正交开关;对照正排与倒排访问路径,为 Analyzer 与 postings 篇奠基。
【全文检索引擎】词项词典 FST:前缀压缩与 TermsEnum
说明 Lucene 段内 Terms 词典如何用 FST 做前缀共享与内存映射;介绍 TermsEnum seek 与字段迭代;与 PostgreSQL GIN Entry Tree 对照一句,为 postings 定位奠基。
【全文检索引擎】Postings 与 codec:freqs、positions、skip 与 impacts
拆解 Lucene PostingsFormat 中 freqs/positions/offsets/payloads 各层语义;说明块编码、skip list 与 impacts 如何服务相交剪枝;解释短语查询为何依赖 positions。