在 ElasticSearch 的世界中,分词器(Analyzer) 是决定搜索质量的第一道关口。很多团队折腾了半天"为什么搜不出来",最终发现根因就是分词器配置不当。本章从原理到实战,带你彻底吃透分词器。
一、分词器工作原理:字符过滤 → 分词 → 词汇过滤
1.1 一条文本的"变身"之旅
假设你往 ES 写入一条文档:
"The 2 QUICK Brown-Foxes jumped over the lazy dog's bone! 🦊"
全文检索时,这条文本会依次经过三个处理阶段:
原始文本
│
▼
┌─────────────────────────────────┐
│ ① 字符过滤器 (Character Filter) │ ← 预处理:去掉 HTML 标签、转换字符
└─────────────────────────────────┘
│ "The 2 QUICK Brown-Foxes..."
▼
┌─────────────────────────────────┐
│ ② 分词器 (Tokenizer) │ ← 核心:把字符串切成一个个词项
└─────────────────────────────────┘
│ ["The", "2", "QUICK", "Brown", "Foxes"...]
▼
┌─────────────────────────────────┐
│ ③ 词汇过滤器 (Token Filter) │ ← 后处理:大小写、停用词、同义词
└─────────────────────────────────┘
│ ["quick", "brown", "fox", "jump", "lazy", "dog", "bone"]
1.2 三个阶段详解
① 字符过滤器 (Character Filter)
作用于原始文本,在分词之前对字符进行清洗和转换。ES 内置三种:
| 字符过滤器 | 功能 | 示例 |
|---|---|---|
html_strip |
去掉 HTML 标签 | <p>Hello</p> → Hello |
mapping |
自定义字符映射 | & → and,:) → _happy_ |
pattern_replace |
正则替换 | \d{3}-\d{4} → 脱敏 |
配置示例:
json
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"my_char_filter": {
"type": "mapping",
"mappings": ["& => and", ":) => _happy_"]
}
},
"analyzer": {
"my_analyzer": {
"char_filter": ["html_strip", "my_char_filter"],
"tokenizer": "standard"
}
}
}
}
}
② 分词器 (Tokenizer)
这是整个分析过程的核心,负责将字符串切分成独立的词项 (Term)。不同分词器策略差异巨大:
"hello world" → standard 分词器 → ["hello", "world"]
"hello world" → keyword 分词器 → ["hello world"]
"hello-world" → letter 分词器 → ["hello", "world"]
③ 词汇过滤器 (Token Filter)
对切分后的词项做二次加工:
| 词汇过滤器 | 功能 | 示例 |
|---|---|---|
lowercase |
转小写 | "HELLO" → "hello" |
stop |
移除停用词 | "the quick fox" → "quick", "fox" |
stemmer |
词干提取 | "running" → "run" |
synonym |
同义词扩展 | "happy" → "happy", "joyful" |
shingle |
N-gram 组合 | "hello world" → "hello", "hello world", "world" |
1.3 完整自定义分析器
json
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"remove_at": {
"type": "mapping",
"mappings": ["@ => "]
}
},
"filter": {
"english_stop": {
"type": "stop",
"stopwords": "_english_"
},
"english_stemmer": {
"type": "stemmer",
"language": "english"
}
},
"analyzer": {
"my_english_analyzer": {
"type": "custom",
"char_filter": ["html_strip", "remove_at"],
"tokenizer": "standard",
"filter": ["lowercase", "english_stop", "english_stemmer"]
}
}
}
}
}
验证分析效果:
json
GET /my_index/_analyze
{
"analyzer": "my_english_analyzer",
"text": "The 2 QUICK Brown-Foxes jumped @hello over the lazy dog's bone!"
}
输出:
json
{
"tokens": [
{ "token": "2", "position": 0 },
{ "token": "quick", "position": 1 },
{ "token": "brown", "position": 2 },
{ "token": "fox", "position": 3 },
{ "token": "jump", "position": 4 },
{ "token": "hello", "position": 5 },
{ "token": "lazi", "position": 6 },
{ "token": "dog", "position": 7 },
{ "token": "bone", "position": 8 }
]
}
二、ES 内置分词器:standard、simple、keyword 适用场景
ES 内置了 8 种分词器,其中最常用的三种足以覆盖 70% 的英文场景。
2.1 内置分词器速览
| 分词器 | 切分策略 | 输出示例(输入 "Hello World! 你好") |
|---|---|---|
| standard | Unicode 标准切分,按词边界 | ["hello", "world", "你", "好"] |
| simple | 遇到非字母即切开,全小写 | ["hello", "world", "你", "好"] |
| whitespace | 按空白字符切开 | ["Hello", "World!", "你好"] |
| keyword | 不分词,整体作为一个词 | ["Hello World! 你好"] |
| letter | 遇到非字母即切开 | ["Hello", "World", "你", "好"] |
| lowercase | letter + 自动小写 | ["hello", "world", "你", "好"] |
| stop | letter + 去掉停用词 | ["hello", "world"] |
| pattern | 正则表达式切分 | 自定义 |
2.2 三大核心分词器深度对比
standard --- 默认分词器,通用首选
分词算法:Unicode 标准文本分割算法 (UAX #29)
特点:按词边界切分,保留 email、URL,去掉标点
json
GET /_analyze
{
"tokenizer": "standard",
"text": "user@example.com is at http://example.com/a?b=1&c=2"
}
切分结果:
["user@example.com", "is", "at", "http://example.com/a?b=1&c=2"]
适用场景:
- 通用英文全文搜索
- 包含 email / URL 的文本
- 精细化场景的底层 base
simple --- 极简主义
分词算法:遇到非字母字符就切开,全部转小写
特点:最"暴力"的分词器,但也是最可控的
json
GET /_analyze
{
"tokenizer": "simple",
"text": "user@example.com Product-ID: ABC-123"
}
切分结果:
["user", "example", "com", "product", "id", "abc"]
keyword --- 精确匹配的守护者
分词算法:不分词。整条文本原样作为一个词项。
特点:用于结构化的、需要精确匹配的字段。
json
GET /_analyze
{
"tokenizer": "keyword",
"text": "Order-2024-001"
}
切分结果:
["Order-2024-001"] ← 原样保留
适用场景:
- 订单号、身份证号、手机号
- 邮箱地址、用户名
- 标签、状态码
最佳实践: 在 mapping 中对这类字段同时设置
keyword类型和text类型(multi-field),既支持精确匹配又支持全文搜索。
json
PUT /orders
{
"mappings": {
"properties": {
"order_id": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"status": {
"type": "keyword" // 状态值不需要分词
}
}
}
}
三、主流中文分词器:IK、Jieba、HanLP 安装与配置
中文和英文最大的不同------词与词之间没有空格分隔。所以必须使用专用的中文分词器。
3.1 三大中文分词器对比
| 分词器 | 特色 | 分词准确性 | 性能 | 维护 | 推荐度 |
|---|---|---|---|---|---|
| IK Analyzer | ES 社区首选,词典丰富 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 活跃 | ★★★★★ |
| Jieba | Python 社区主流,自带词性标注 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 活跃 | ★★★★ |
| HanLP | NLP 级别,语义理解最强 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 活跃 | ★★★★ |
3.2 IK 分词器 --- 统治 ES 中文分词
安装
bash
# ES 8.x 版本安装(版本号需与 ES 匹配)
./bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.12.0
# 或手动安装
wget https://github.com/infinilabs/analysis-ik/releases/download/v8.12.0/elasticsearch-analysis-ik-8.12.0.zip
./bin/elasticsearch-plugin install file:///path/to/elasticsearch-analysis-ik-8.12.0.zip
安装后重启 ES:
bash
# 验证是否安装成功
./bin/elasticsearch-plugin list
# 输出应包含:analysis-ik
两种核心模式
json
# ik_smart --- 最细粒度切分(粗粒度)
GET /_analyze
{
"analyzer": "ik_smart",
"text": "中华人民共和国国歌"
}
// 结果:["中华人民共和国", "国歌"]
# ik_max_word --- 最细粒度切分(穷举所有可能)
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "中华人民共和国国歌"
}
// 结果:["中华人民共和国", "中华人民", "中华", "华人", "人民共和国", "人民", "共和国", "共和", "国", "国歌"]
3.3 Jieba 分词器
Jieba 自带词性标注,对短语理解有天然优势。
bash
# 安装
./bin/elasticsearch-plugin install https://github.com/sing1ee/elasticsearch-jieba-plugin/releases/download/v8.12.0/elasticsearch-jieba-plugin-8.12.0.zip
json
GET /_analyze
{
"analyzer": "jieba_index",
"text": "他来到了南京市长江大桥"
}
// 结果:["他", "来到", "了", "南京市", "南京", "市长", "长江大桥", "长江", "大桥"]
3.4 HanLP 分词器 --- NLP 级别分词
支持命名实体识别、词性标注、依存句法分析,适合对搜索质量有极致要求的场景。
json
GET /_analyze
{
"analyzer": "hanlp",
"text": "刘德华在台北小巨蛋举办了演唱会"
}
// 结果:["刘德华/nr", "台北/ns", "小巨蛋/nz", "举办/v", "演唱会/n"]
// nr=人名, ns=地名, nz=专用名, v=动词, n=名词
四、IK 分词器精准模式 & 最大词长模式实战
4.1 两种模式的本质区别
ik_smart (智能模式) ik_max_word (最大词长模式)
─────────────────────── ─────────────────────────
只切分出有意义的词 尽可能切分出所有可能的词
召回精度高,误召回少 召回率高,覆盖率好
适合: 精确搜索 适合: 召回优先的场景
4.2 实战:电商商品搜索
场景: 用户搜索 "苹果手机壳"
json
# 注册自定义索引分析器
PUT /shop
{
"settings": {
"analysis": {
"analyzer": {
"ik_smart_analyzer": { "type": "custom", "tokenizer": "ik_smart" },
"ik_max_word_analyzer": { "type": "custom", "tokenizer": "ik_max_word" }
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word", // 索引时:穷举分词,保证召回
"search_analyzer": "ik_smart" // 搜索时:智能分词,保证精度
}
}
}
}
核心技巧: 索引时用
ik_max_word,搜索时用ik_smart------这是 IK 分词器的最优配置范式。
- 索引时穷举 → 不漏掉任何可能的匹配
- 搜索时精确 → 不引入无关噪音
4.3 验证分词效果
json
# 验证索引时分词
GET /shop/_analyze
{
"analyzer": "ik_max_word",
"text": "苹果手机壳"
}
// 结果:["苹果", "手机", "手机壳", "机壳"]
# 验证搜索时分词
GET /shop/_analyze
{
"analyzer": "ik_smart",
"text": "苹果手机壳"
}
// 结果:["苹果", "手机", "壳"]
4.4 实战:复杂场景分词对比
json
POST /_analyze
{
"analyzer": "ik_max_word",
"text": "深度学习自然语言处理框架"
}
| 分词器 | 结果 | 分析 |
|---|---|---|
ik_smart |
["深度学习", "自然语言处理", "框架"] |
精准,3 个词 |
ik_max_word |
["深度学习", "深度", "学习", "自然语言处理", "自然", "自然语言", "语言", "处理", "框架"] |
全面,9 个词 |
五、自定义词库、停用词配置(解决搜索不准确问题)
5.1 为什么需要自定义词库?
默认词库无法覆盖业务专有名词、新兴词汇、品牌名,导致分词错误:
"麒麟芯片性能测试" → 默认分词:["麒麟", "芯片", "性能", "测试"] ✅
"鸿蒙Next版本更新" → 默认分词:["鸿", "蒙", "Next", "版本", "更新"] ❌ 鸿蒙被拆开了!
5.2 IK 自定义词库配置
配置文件位置
IK 插件的配置目录在 ES 安装路径下:
{ES_HOME}/config/analysis-ik/
├── IKAnalyzer.cfg.xml ← 主配置文件
├── main.dic ← 系统主词典(1.1W+ 词条)
├── ext.dic ← 自定义扩展词典 ← 你在这里加词
├── stopword.dic ← 系统停用词
├── extra_stopword.dic ← 自定义停用词典
└── custom/
├── my_tech.dic ← 可以分文件管理
└── my_brand.dic
修改主配置
xml
<!-- IKAnalyzer.cfg.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!-- 用户扩展字典 -->
<entry key="ext_dict">ext.dic;custom/my_tech.dic;custom/my_brand.dic</entry>
<!-- 用户扩展停用词 -->
<entry key="ext_stopwords">extra_stopword.dic</entry>
</properties>
编写自定义词典
ext.dic(扩展词典):
# 技术名词
鸿蒙
麒麟芯片
鲲鹏
昇腾
微服务
容器化
云原生
大模型
Prompt工程
RAG检索增强生成
# 品牌/产品名
蔡司镜头
索尼微单
戴森吸尘器
苹果MacBookPro
# 网络热词
躺平
内卷
摆烂
AI数字人
extra_stopword.dic(停用词词典):
的
了
呢
吧
啊
嘛
哦
哈
咦
# 以及"吗、呀、哇、唉、嘿、呵"等语气词
重要提醒: 每次修改词典文件后,必须重启 ES 才能生效。IK 分词器在启动时加载词典到内存。
动态热更新(推荐)
IK 支持从远程 URL 加载词典,无需重启:
xml
<!-- IKAnalyzer.cfg.xml -->
<properties>
<entry key="ext_dict">http://your-server.com/ik/custom_dict.dic</entry>
<entry key="ext_stopwords">http://your-server.com/ik/stopwords.dic</entry>
<!-- 远程词典更新检测间隔,单位:毫秒 -->
<entry key="remote_ext_dict_check_interval">60000</entry>
<entry key="remote_ext_stopwords_check_interval">60000</entry>
</properties>
只需更新远端文件,IK 会在 60 秒内自动拉取新词典。
5.3 停用词实战
场景: 客服系统中,"请问"、"您好"、"麻烦问一下" 这类高频客套话不应参与检索。
json
PUT /customer_service
{
"settings": {
"analysis": {
"filter": {
"cs_stop": {
"type": "stop",
"stopwords": ["请问", "您好", "谢谢", "麻烦问一下", "我想咨询"]
}
},
"analyzer": {
"cs_analyzer": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": ["cs_stop"]
}
}
}
}
}
验证效果:
json
GET /customer_service/_analyze
{
"analyzer": "cs_analyzer",
"text": "请问如何修改密码?谢谢"
}
// 结果:["如何", "修改", "密码"] ← "请问"和"谢谢"被过滤
5.4 同义词配置
场景: 用户搜 "笔记本",也应该召回含有 "笔记本电脑" 的文档。
json
PUT /shop
{
"settings": {
"analysis": {
"filter": {
"my_synonym": {
"type": "synonym",
"synonyms_path": "analysis/synonyms.txt"
}
},
"analyzer": {
"ik_synonym": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["my_synonym"]
}
}
}
}
}
config/analysis/synonyms.txt 内容:
笔记本, 笔记本电脑, laptop, 本本
手机, 电话, 移动电话, cellphone, mobile
优惠券, 优惠卷, 折扣券, 券
六、分词优化:解决分词不全、分词错误、搜索匹配失败问题
6.1 问题诊断三板斧
当搜索不准确时,第一步永远是------先看分词结果:
json
# 三板斧第一斧:_analyze API
GET /_analyze
{
"field": "title", // 用实际字段名
"text": "你搜不到的那个词"
}
# 三板斧第二斧:_termvectors API(查看已索引的词项)
GET /my_index/_termvectors/1?fields=title
# 三板斧第三斧:Profile API(查看搜索执行细节)
GET /my_index/_search
{
"profile": true,
"query": {
"match": { "title": "你搜不到的词" }
}
}
6.2 问题场景与解决方案
问题①:分词不全 --- 新词/专有名词被切开
现象: 搜 "GPT-4o" 搜不到,因为被切成 ["gpt", "4o"]
解决方案A:加自定义词典
# ext.dic 添加
GPT-4o
GPT-4
ChatGPT
Claude
解决方案B:使用 keyword 子字段兜底
json
{
"properties": {
"model_name": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": { "type": "keyword" } // 兜底精确匹配
}
}
}
}
问题②:分词错误 --- 歧义切分
经典歧义: "南京市长江大桥"
错误切分:["南京", "市长", "江大桥"] ← 把"市长"当成了一个词
正确切分:["南京市", "长江大桥"]
解决方案:自定义词典 + 词性标注
json
# 1. 在 ext.dic 中添加
南京市
长江大桥
# 2. 如果对准确度要求极高,考虑升级到 HanLP
# HanLP 利用词性标注和语义理解,能正确识别"南京市"为地名
问题③:搜索匹配失败 --- 索引/搜索分词器不一致
陷阱: 如果索引和搜索使用不同的分词器,可能出现索引了的词搜索时切不出来的情况。
json
# ❌ 错误配置
{
"title": {
"type": "text",
"analyzer": "ik_smart" // 索引
// search_analyzer 未指定 → 默认也用 ik_smart
}
}
// 索引"苹果手机壳" → ["苹果", "手机", "壳"]
// 用户搜"手机壳" → ["手机壳"]
// 索引里没有独立的"手机壳"词项 → 搜不到!❌
json
# ✅ 正确配置
{
"title": {
"type": "text",
"analyzer": "ik_max_word", // 索引:穷举
"search_analyzer": "ik_smart" // 搜索:精确
}
}
// 索引"苹果手机壳" → ["苹果", "手机", "手机壳", "机壳", "壳"]
// 用户搜"手机壳" → ["手机壳"]
// 命中!✅
6.3 分词语义度调优:相似度算法
搜索结果的排序由相似度算法 决定。ES 默认使用 BM25:
json
PUT /my_index
{
"settings": {
"similarity": {
"my_bm25": {
"type": "BM25",
"k1": 1.2, // 词频饱和度参数(默认1.2,调高→词频影响更大)
"b": 0.75 // 文档长度归一化(默认0.75,调低→短文档更占优)
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"similarity": "my_bm25",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}
6.4 搜索精准度优化清单
| 优化项 | 具体操作 | 效果 |
|---|---|---|
| 索引/搜索分词器分离 | analyzer: ik_max_word + search_analyzer: ik_smart |
兼顾召回和精度 |
| 自定义业务词库 | ext.dic 添加行业术语、产品名、品牌名 | 消除专有名词被切开 |
| 停用词过滤 | 过滤无语义价值的语气词、客套话 | 减少噪音匹配 |
| 同义词扩展 | synonym filter 配置同义词表 | 提升召回率 |
| ngram 兜底 | 对标题等短文本加 ngram 子字段 | 防止短词搜不到 |
| multi-field | keyword + text 双字段 | 精确匹配和全文搜索兼得 |
| 近实时更新词典 | 远程词典 + 自动热更新 | 无需重启即可更新词库 |
6.5 ngram 兜底方案
对于标题等短文本,增加 ngram 子字段作为最后的兜底:
json
PUT /shop
{
"settings": {
"analysis": {
"analyzer": {
"title_ngram": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": ["ngram_filter"]
}
},
"filter": {
"ngram_filter": {
"type": "ngram",
"min_gram": 2,
"max_gram": 3
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"ngram": {
"type": "text",
"analyzer": "title_ngram"
},
"keyword": {
"type": "keyword"
}
}
}
}
}
}
搜索时使用 multi_match 多字段检索 + bool query 加权:
json
GET /shop/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": { "query": "手机壳", "boost": 10 } } },
{ "match": { "title.ngram": { "query": "手机壳", "boost": 2 } } },
{ "term": { "title.keyword": { "value": "手机壳", "boost": 100 } } }
]
}
}
}
本章总结
┌───────────────────────────────────────────────────────────┐
│ 分词器 → 搜索精准度的命门 │
│ │
│ ① 原理:char_filter → tokenizer → token_filter │
│ ② 英文:standard/simple/keyword 覆盖主流场景 │
│ ③ 中文:IK (首选) / Jieba / HanLP (NLP级) │
│ ④ 范式:索引 ik_max_word + 搜索 ik_smart │
│ ⑤ 词库:ext.dic + 停用词 + 同义词 + 远程热更新 │
│ ⑥ 诊断:_analyze → _termvectors → Profile API │
│ ⑦ 兜底:keyword + ngram + multi_match 加权 │
│ │
│ "分词对了,搜索就对了一半。" │
└───────────────────────────────────────────────────────────┘
下一章预告: 第7章 搜索排序与相关性调优 ------ BM25 算法深度解析、Function Score 实战、搜索点击反馈与 Learning to Rank 入门。