ES 分词器彻底搞懂
ES 分词器彻底搞懂(analyzer / tokenizer / filter)
"为什么搜 '南京市长江大桥' 会匹配到 '南京市长'?"
"为什么搜 'java' 能找到 'JavaScript'?"
"为什么搜同一个词,中文能搜到英文搜不到?"
答案都在一个组件里:分词器(Analyzer)。它是 ES 全文搜索最底层的基础设施——索引时怎么切词,搜索时就怎么匹配。本篇把它拆开揉碎讲清楚。
Analyzer 的三个零件
Analyzer = Character Filter → Tokenizer → Token Filter
↓ ↓ ↓
字符预处理 切割成词 对词做后处理
Character Filter:还没切,先洗
在分词之前对原始字符串做字符级别的转换。ES 内置三种:
// HTML Strip:去掉 HTML 标签
"<p>iPhone 15</p>" → "iPhone 15"
// Mapping:字符替换(比如把表情符号转成文字)
"iPhone 🙂" → "iPhone smile"
// Pattern Replace:正则替换
"call 139-1234-5678" → "call 13912345678"
Character Filter 可以串联,按顺序执行。
Tokenizer:动刀切词
把字符串切成一个个 token(词元)。这是分词的核心步骤——选错 tokenizer,后面全白干。
| Tokenizer | 行为 | 示例 |
|---|---|---|
standard |
按词边界切,Unicode 标准算法 | "iPhone 15 Pro" → ["iPhone", "15", "Pro"] |
whitespace |
按空白字符切 | "iPhone 15 Pro" → ["iPhone", "15", "Pro"] |
keyword |
不切,原样保留 | "iPhone 15 Pro" → ["iPhone 15 Pro"] |
ngram |
按字符 n-gram 切 | "abc" min=2,max=2 → ["ab", "bc"] |
edge_ngram |
前 n-gram | "abc" min=2,max=3 → ["ab", "abc"] |
pattern |
正则切分 | "a,b;c" pattern=[,;] → ["a", "b", "c"] |
ik_smart(IK 插件) |
中文粗粒度切 | "南京市长江大桥" → ["南京市", "长江大桥"] |
ik_max_word(IK 插件) |
中文细粒度切 | "南京市长江大桥" → ["南京市", "南京", "市长", "长江大桥", "长江", "大桥"] |
Token Filter:切完后加工
对 token 做增、删、改。
// lowercase:转小写
["iPhone", "15", "Pro"] → ["iphone", "15", "pro"]
// stop:去掉停用词
["the", "iphone", "15", "is"] → ["iphone", "15"]
// stemmer:词干提取(英文)
["running", "runs", "ran"] → ["run", "run", "ran"]
// synonym:同义词扩展
["iphone"] → ["iphone", "苹果手机"]
// unique:去重
["iphone", "15", "iphone"] → ["iphone", "15"]
组装一个完整的中文 Analyzer
PUT /products
{
"settings": {
"analysis": {
"char_filter": {
"my_char_filter": {
"type": "mapping",
"mappings": ["=> => ", "& => and"]
}
},
"filter": {
"my_stop": {
"type": "stop",
"stopwords": ["的", "了", "在", "是", "我", "有", "和", "就"]
}
},
"analyzer": {
"my_chinese_analyzer": {
"type": "custom",
"char_filter": ["html_strip", "my_char_filter"],
"tokenizer": "ik_max_word",
"filter": ["lowercase", "my_stop"]
}
}
}
}
}
处理流程:
输入:"iPhone 15 Pro Max 在售,有折扣"
1. char_filter html_strip:不变(无 HTML)
2. char_filter my_char_filter:不变
3. tokenizer ik_max_word:["iPhone", "15", "Pro", "Max", "在售", "有", "折扣"]
4. filter lowercase:["iphone", "15", "pro", "max", "在售", "有", "折扣"]
5. filter my_stop:去掉"有" → ["iphone", "15", "pro", "max", "在售", "折扣"]
输出:["iphone", "15", "pro", "max", "在售", "折扣"]
索引时 vs 搜索时 Analyzer
最重要也是最容易踩坑的原则:索引和搜索必须用语义上一致的 analyzer,否则匹配不上。
PUT /products
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word", // 索引时:细粒度切
"search_analyzer": "ik_smart" // 搜索时:粗粒度切
}
}
}
}
为什么这样做?
- 索引时细切:
"南京市长江大桥"→ 尽可能多地产生 term,覆盖更多搜索可能性 - 搜索时粗切:用户输入
"南京市长江大桥"→ 只提取最核心的 term,减少噪音匹配
这是中英文搜索的标准优化手段——索引求全,搜索求精。
用 Analyze API 调试
不知道某个字段的 analyzer 效果如何?直接调 API 看:
GET /products/_analyze
{
"field": "title",
"text": "iPhone 15 Pro Max 在售"
}
返回:
{
"tokens": [
{ "token": "iphone", "start_offset": 0, "end_offset": 6, "position": 0 },
{ "token": "15", "start_offset": 7, "end_offset": 9, "position": 1 },
{ "token": "pro", "start_offset": 10, "end_offset": 13, "position": 2 },
{ "token": "max", "start_offset": 14, "end_offset": 17, "position": 3 },
{ "token": "在售", "start_offset": 18, "end_offset": 20, "position": 4 }
]
}
每个 token 的位置、起止偏移一目了然。上线前养成用 _analyze 验证的习惯——搜索不准,从分词开始排查。
三种常见的中文分词器
| 分词器 | 内置 | 效果 | 推荐 |
|---|---|---|---|
| IK | 插件 | 专业中文词典 + 自定义词典 | ⭐ 首选 |
| jieba | 插件 | 统计分词,处理新词能力强 | 备选 |
| standard | 内置 | 单字切分:"南京市"→["南","京","市"] | ❌ 中文不推荐 |
| NGram | 内置 | 按字符 n-gram 切分 | 只用于自动补全 |
IK 是目前 ES 中文生态的事实标准。安装后配置自定义词典能解决大量分词不准的问题:
<!-- IKAnalyzer.cfg.xml -->
<properties>
<entry key="ext_dict">custom/mydict.dic</entry>
</properties>
<!-- custom/mydict.dic -->
长江大桥
南京市
Pro Max
iPhone 15
总结
分词器是 ES 全文搜索的地基。三个零件分工明确:
Character Filter → 洗字符(去 HTML、统一符号)
Tokenizer → 动刀切(选对切法最关键)
Token Filter → 后处理(转小写、去停用词、同义词)
记住一句话:索引和搜索的 analyzer 不一致,是所有"为什么搜不到"问题的第一嫌疑人。