中文搜索这块,只要数据量一上来,分词就是绕不过去的坎。默认的 standard 分词器把“中华人民共和国”切成一个个单字,搜“中国”命中率惨不忍睹;换成 ik 之后好一些,但遇到“南京市长江大桥”这种经典歧义句,照样给你切得七零八落。我前后在几个日志检索和商品搜索的项目里折腾过分词方案,最后稳定跑在生产上的是 elasticsearch-analysis-hanlp 这个插件。它把 HanLP 那套自然语言处理能力塞进了 Elasticsearch,支持自定义词典、词性标注、命名实体识别,还能按业务场景切换不同的分词模式。这篇就把我从选型、安装、配置到踩坑排查的完整过程摊开讲一遍,适合正在做中文搜索、日志分析、知识库检索的开发和运维同学参考,新手照着做也能跑通,老手可以重点看后面的调优和避坑部分。
1. 为什么中文分词值得单独折腾
1.1 中文分词的难点到底在哪
英文天然有空格做分隔符,tokenizer 只要按空格和标点切就行。中文是一串连续的字符,词与词之间没有显式边界,这就导致分词本质上是一个“歧义消解”问题。举个最经典的例子:“乒乓球拍卖完了”,可以切成“乒乓球 / 拍卖 / 完 / 了”,也可以切成“乒乓球拍 / 卖 / 完 / 了”。两种切法语义完全不同,靠规则很难穷举。
再比如“南京市长江大桥”,正确切分应该是“南京市 / 长江大桥”,但机械切分很容易变成“南京 / 市长 / 江大桥”,直接闹笑话。这类歧义在搜索场景里会直接导致召回率下降——用户搜“长江大桥”搜不到,因为索引里存的是“江大桥”这个莫名其妙的词。
除了歧义,还有几个现实问题:新词不断涌现(网络热词、产品型号、人名地名),未登录词识别困难;专业领域术语(医疗、法律、金融)通用词典覆盖不到;繁简混用、全半角混用、中英夹杂的情况在真实数据里比比皆是。这些问题叠加起来,就是为什么中文搜索必须认真选分词器。
1.2 常见分词方案横向对比
在选 hanlp 之前,我把市面上主流的几个方案都试了一遍,这里做个横向对比,方便你按自己的场景选。
| 方案 | 分词算法 | 自定义词典 | 词性标注 | 实体识别 | 部署复杂度 | 适用场景 |
|---|---|---|---|---|---|---|
| standard | 单字切分 | 不支持 | 无 | 无 | 内置零成本 | 几乎不适用于中文搜索 |
| ik | 词典+歧义消解 | 支持 | 无 | 无 | 低 | 通用中文搜索,社区活跃 |
| jieba | 前缀词典+动态规划 | 支持 | 支持 | 部分 | 中 | 需要 Python 生态配合 |
| hanlp | 词典+CRF+感知机 | 支持 | 支持 | 支持 | 中高 | 复杂 NLP 场景、实体抽取 |
| pinyin | 拼音转换 | 不涉及 | 无 | 无 | 低 | 拼音搜索辅助 |
ik 的优势是轻量、上手快、社区文档多,大部分电商搜索用它就够了。但 ik 的短板在于没有词性标注和命名实体识别,遇到需要按人名、地名、机构名做过滤或聚合的场景就力不从心。hanlp 的核心竞争力在于它内置了多种分词模式(标准、索引、NLP、极速),并且支持词性标注和 NER,这在构建知识图谱、做智能问答、日志实体抽取时非常关键。
我最终选 hanlp 的原因很直接:项目里既要全文检索,又要从日志里抽人名、IP、机构名做聚合分析,ik 做不到,hanlp 一个插件全包了。
1.3 hanlp 插件的核心能力拆解
elasticsearch-analysis-hanlp 本质上是把 HanLP 的 Java 版能力封装成 ES 的 Analysis 插件。它对外暴露的核心能力有这么几块:
- 多种分词模式:hanlp 标准模式、hanlp 索引模式、hanlp NLP 模式、hanlp 极速模式,不同模式对应不同的切分粒度和性能开销。
- 自定义词典:支持通过配置文件加载扩展词典、停用词典,还能配置远程词典实现热更新。
- 词性标注:每个 token 可以带上词性标签(名词 n、动词 v、地名 ns 等),方便后续按词性过滤。
- 命名实体识别:能识别人名、地名、机构名、时间、数字等实体,输出对应的实体类型。
- 简繁转换:内置简繁转换能力,搜索时可以把繁体查询转成简体再匹配。
这些能力组合起来,让 hanlp 不只是一个分词器,更像是一个轻量级的 NLP 处理管道。理解这一点,后面的配置思路就顺了。
2. 环境准备与插件安装实操
2.1 版本匹配是第一步,别踩这个坑
hanlp 插件和 Elasticsearch 的版本必须严格对应,这是最容易翻车的地方。插件内部调用了 ES 的 Analysis 接口,不同大版本的接口签名可能变化,版本不匹配轻则插件加载失败,重则 ES 起不来。
我整理了一份常见的版本对应关系,供参考:
| Elasticsearch 版本 | hanlp 插件版本 | JDK 要求 |
|---|---|---|
| 7.10.x | 7.10.x | JDK 11 |
| 7.17.x | 7.17.x | JDK 11 |
| 8.5.x | 8.5.x | JDK 17 |
| 8.11.x | 8.11.x | JDK 17 |
注意:插件版本号的前两段必须和 ES 完全一致,第三段可以有小差异。比如 ES 7.17.3 可以配插件 7.17.1,但绝不能配 7.16.x。
查看当前 ES 版本用这条命令:
curl -X GET "localhost:9200/?pretty"返回的number字段就是版本号。确认版本后再去下载对应插件包,别图省事随便下个最新的。
2.2 安装插件的两种方式
安装方式有两种,在线安装和离线安装,生产环境我强烈建议离线安装,因为在线安装依赖网络,而且很多内网环境根本连不上插件仓库。
在线安装(仅限能访问外网的测试环境):
bin/elasticsearch-plugin install https://github.com/.../elasticsearch-analysis-hanlp-7.17.1.zip离线安装(生产推荐):
# 先下载 zip 包到本地 # 然后执行 bin/elasticsearch-plugin install file:///opt/plugins/elasticsearch-analysis-hanlp-7.17.1.zip安装过程中会提示是否继续,输入y确认。安装完成后,插件会被解压到plugins/elasticsearch-analysis-hanlp目录下。
2.3 安装后的目录结构与数据文件
安装完成后,插件目录下会有这么几个关键部分:
plugins/elasticsearch-analysis-hanlp/ ├── elasticsearch-analysis-hanlp-7.17.1.jar ├── config/ │ ├── hanlp.properties │ ├── stopwords.txt │ └── custom_dict.txt └── data/ ├── dictionary/ └── model/config目录放配置文件,data目录放词典和模型文件。hanlp 的词典和模型文件比较大,尤其是 NER 模型,加起来可能上百 MB,所以首次安装后启动 ES 会慢一些,这是正常的,别以为卡死了。
实操心得:如果你的服务器内存紧张,可以在
hanlp.properties里关掉不需要的模型加载,比如不做 NER 就注释掉相关配置,能省不少内存。
2.4 重启与验证插件是否生效
安装完插件必须重启 ES 节点,插件才会加载。重启后验证插件是否生效:
curl -X GET "localhost:9200/_cat/plugins?v"如果输出里能看到analysis-hanlp,说明插件加载成功。再用_analyze接口做个快速测试:
curl -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d' { "analyzer": "hanlp", "text": "南京市长江大桥" } '如果返回的 tokens 里能看到“南京市”和“长江大桥”,说明分词正常工作。如果报analyzer [hanlp] not found,那就是插件没加载成功,回去检查版本和目录。
3. 分词模式与核心配置详解
3.1 四种分词模式的区别与选择
hanlp 插件提供了四种分词模式,很多人装完就用默认的,其实不同模式差别很大,选错了要么效果差要么性能崩。
| 模式 | 切分粒度 | 性能 | 典型用途 |
|---|---|---|---|
| hanlp | 标准粒度 | 中等 | 通用搜索 |
| hanlp_index | 细粒度 | 较慢 | 索引构建,追求召回 |
| hanlp_nlp | 最细+词性 | 最慢 | NLP 分析、实体抽取 |
| hanlp_speed | 粗粒度 | 最快 | 高并发、低延迟场景 |
hanlp 标准模式:按标准分词切,适合大多数搜索场景,粒度和性能平衡得比较好。
hanlp_index 索引模式:切得更细,比如“中华人民共和国”会切成“中华”“人民”“共和国”甚至更细,召回率高但索引体积大。适合对召回率要求极高的场景。
hanlp_nlp 模式:在分词基础上做词性标注和 NER,输出最丰富但最慢。适合离线分析、日志实体抽取。
hanlp_speed 模式:用最短路径快速切分,牺牲部分准确率换性能。适合 QPS 很高的在线服务。
我的经验是:索引阶段用hanlp_index保证召回,查询阶段用hanlp保证精度,这是搜索领域经典的“索引细、查询粗”策略。
3.2 自定义词典的配置方法
通用词典再全也覆盖不了你的业务术语。比如你做的是手机搜索,“骁龙8Gen3”这种词通用词典肯定没有,必须自己加。
自定义词典文件放在config/custom_dict.txt,格式是每行一个词,可以带词性和词频:
骁龙8Gen3 nz 100 鸿蒙系统 nz 100 某品牌旗舰机 nz 100然后在hanlp.properties里指定词典路径:
CustomDictionaryPath=data/dictionary/custom/CustomDictionary.txt;custom_dict.txt改完配置要重启 ES 才生效。如果不想重启,可以用远程词典方案,后面会讲。
注意:自定义词典的词频字段很关键。词频越高,分词时越倾向于把这个词当成一个整体。如果你发现某个词还是被切开了,先把词频调大试试。
3.3 停用词与同义词处理
停用词是那些对搜索没意义的高频词,比如“的”“了”“是”“在”。把它们过滤掉能减小索引体积、提升查询效率。
停用词文件config/stopwords.txt,每行一个词:
的 了 是 在 和同义词处理稍微复杂一点,hanlp 插件本身不直接提供同义词过滤器,但可以配合 ES 的synonymfilter 使用。在自定义 analyzer 里串一个同义词过滤器:
{ "settings": { "analysis": { "filter": { "my_synonym": { "type": "synonym", "synonyms_path": "analysis/synonym.txt" } }, "analyzer": { "my_hanlp": { "type": "custom", "tokenizer": "hanlp", "filter": ["my_synonym", "lowercase"] } } } } }同义词文件格式:
手机,移动电话,手提电话 电脑,计算机,PC这样用户搜“移动电话”也能命中“手机”的文档。
3.4 远程词典实现热更新
生产环境最怕改个词典就要重启 ES。hanlp 支持远程词典,把词典放到一个 HTTP 服务上,配置好 URL 和刷新间隔,插件会定期拉取更新。
配置示例:
RemoteDictionaryUrl=http://your-dict-server/dict/ RemoteDictionaryRefreshInterval=60000RemoteDictionaryRefreshInterval单位是毫秒,60000 就是每分钟刷新一次。这样你更新词典文件后,最多等一分钟就生效,不用重启。
实操心得:远程词典服务建议用 Nginx 挂静态文件,简单可靠。词典文件更新时先传新文件再改软链,避免拉取到写了一半的文件。
4. 索引与查询的完整实操流程
4.1 创建带 hanlp 分析器的索引
先定义一个自定义 analyzer,把 hanlp 分词器和停用词、同义词串起来:
PUT /product_index { "settings": { "analysis": { "analyzer": { "hanlp_index_analyzer": { "type": "custom", "tokenizer": "hanlp_index", "filter": ["lowercase", "my_stop"] }, "hanlp_search_analyzer": { "type": "custom", "tokenizer": "hanlp", "filter": ["lowercase", "my_stop"] } }, "filter": { "my_stop": { "type": "stop", "stopwords_path": "stopwords.txt" } } } }, "mappings": { "properties": { "title": { "type": "text", "analyzer": "hanlp_index_analyzer", "search_analyzer": "hanlp_search_analyzer" }, "description": { "type": "text", "analyzer": "hanlp_index_analyzer", "search_analyzer": "hanlp_search_analyzer" } } } }这里的关键是analyzer和search_analyzer分开设置。索引时用细粒度的hanlp_index,查询时用标准粒度的hanlp,这就是前面说的“索引细、查询粗”策略的落地。
4.2 写入数据并验证分词效果
写入几条测试数据:
POST /product_index/_doc/1 { "title": "某品牌旗舰手机骁龙8Gen3处理器", "description": "搭载鸿蒙系统,支持5G网络" }然后用_analyze看看实际切成了什么:
curl -X POST "localhost:9200/product_index/_analyze" -H 'Content-Type: application/json' -d' { "field": "title", "text": "某品牌旗舰手机骁龙8Gen3处理器" } '如果“骁龙8Gen3”被完整保留,说明自定义词典生效了。如果被切成“骁龙”“8”“Gen3”,回去检查词典配置和词频。
4.3 查询阶段的匹配策略
查询时用match查询就能自动走 search_analyzer:
GET /product_index/_search { "query": { "match": { "title": "骁龙处理器手机" } } }如果想控制匹配的严格程度,可以用match_phrase做短语匹配,或者用minimum_should_match控制最少匹配词数:
{ "query": { "match": { "title": { "query": "骁龙处理器手机", "minimum_should_match": "75%" } } } }minimum_should_match设成 75% 意味着至少匹配 75% 的分词,能有效过滤掉只匹配一两个词的弱相关结果。
4.4 性能调优的几个关键参数
分词是 CPU 密集型操作,高并发下容易成为瓶颈。几个调优方向:
控制分词模式:在线查询用hanlp或hanlp_speed,别用hanlp_nlp,后者慢一个数量级。
合理设置分片数:分片不是越多越好,分片过多会导致查询时合并结果的开销增大。一般按数据量和节点数来定,单分片控制在 30-50GB。
开启查询缓存:ES 的request_cache对重复查询有效,在查询 URL 上加?request_cache=true即可。
预热常用查询:用_search接口提前跑一遍高频查询,让缓存热起来。
实操心得:我遇到过一次分词导致 CPU 飙高的问题,排查发现是某个字段用了
hanlp_nlp模式,每次查询都触发 NER 模型推理。改成hanlp模式后 CPU 直接降了一半。所以模式选择一定要按场景来,别为了功能全就全用最重的模式。
5. 常见问题排查与避坑实录
5.1 插件加载失败的排查思路
插件加载失败是最常见的问题,表现是 ES 启动报错或者_cat/plugins里看不到插件。排查顺序:
- 查版本:确认插件版本和 ES 版本前两段一致。
- 查日志:看
logs/elasticsearch.log里的报错信息,通常是ClassNotFoundException或NoSuchMethodError,指向版本不匹配。 - 查权限:插件目录的属主必须是运行 ES 的用户,权限不对会加载失败。
- 查 JDK:ES 8.x 要求 JDK 17,用 JDK 11 跑会报错。
5.2 分词结果不符合预期的原因
分词结果和预期不一致,通常有这几个原因:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 自定义词被切开 | 词典未加载或词频太低 | 检查词典路径,调高词频 |
| 停用词没过滤 | 停用词文件路径错 | 检查 stopwords_path |
| 简繁没转换 | 未开启简繁转换 | 配置中开启对应选项 |
| 英文被切碎 | 未配置英文处理 | 加 lowercase 和英文分词 filter |
5.3 内存与性能问题的处理
hanlp 加载词典和模型会占用不少内存,尤其是 NER 模型。如果 ES 堆内存设置得比较小(比如 2GB),可能出现 OOM。
处理办法:
- 增大 ES 堆内存,建议至少 4GB,生产环境 8GB 起步。
- 在
hanlp.properties里关掉不需要的模型,比如不做 NER 就注释掉 NER 相关配置。 - 用
hanlp_speed模式替代hanlp_nlp模式。
5.4 词典热更新的注意事项
远程词典热更新虽然方便,但有几个坑:
- 刷新间隔别设太短:设成 1 秒会导致频繁 HTTP 请求,给词典服务器压力。60 秒是个合理值。
- 词典文件要原子更新:先传临时文件再 mv 覆盖,避免拉取到不完整文件。
- 更新后要验证:用
_analyze确认新词生效,别假设一定成功。
踩过的坑:有次更新词典后忘了验证,结果新词一直没生效,排查半天发现是词典服务器返回了 304 缓存响应,插件没重新拉取。后来在 URL 上加了时间戳参数才解决。
6. 进阶玩法与场景扩展
6.1 结合词性标注做精准过滤
hanlp_nlp 模式会给每个 token 打上词性标签,利用这个可以做精准过滤。比如只搜地名:
{ "query": { "match": { "content": { "query": "北京", "analyzer": "hanlp_nlp" } } } }配合term查询过滤词性字段,能实现“只匹配人名”或“只匹配机构名”这类需求。这在构建人物关系图谱时特别有用。
6.2 用 NER 做日志实体抽取
日志分析场景里,从日志文本里抽 IP、人名、机构名是刚需。用 hanlp_nlp 模式建索引,然后按实体类型聚合:
{ "aggs": { "entities": { "terms": { "field": "content.ner", "size": 20 } } } }这样能快速统计出日志里出现最多的实体,对安全分析和运维监控都有价值。
6.3 多字段不同分析器的组合策略
一个文档里不同字段可以用不同分析器。比如标题用hanlp_index追求召回,标签用keyword精确匹配,正文用hanlp平衡性能:
{ "properties": { "title": { "type": "text", "analyzer": "hanlp_index_analyzer" }, "tags": { "type": "keyword" }, "content": { "type": "text", "analyzer": "hanlp_analyzer" } } }这种组合策略能让每个字段都发挥最优效果,是生产环境的常见做法。
6.4 与拼音搜索的配合
中文搜索经常需要拼音辅助,比如用户输入“shouji”要能搜到“手机”。hanlp 本身不做拼音,但可以配合 pinyin 插件,在 analyzer 里串一个 pinyin filter:
{ "filter": { "my_pinyin": { "type": "pinyin", "keep_first_letter": true, "keep_full_pinyin": true } }, "analyzer": { "hanlp_pinyin": { "tokenizer": "hanlp", "filter": ["my_pinyin", "lowercase"] } } }这样索引里同时存了中文词和拼音,用户用拼音也能搜到。
我在几个项目里用下来,hanlp 插件的稳定性是够的,关键是配置要对、模式要选对、词典要维护好。分词这东西没有一劳永逸的方案,业务在变、热词在变,词典就得跟着更新。把远程词典热更新配好,日常维护成本其实很低。最后再分享一个小技巧:上线前一定要用真实数据做一轮分词效果评估,把 top 100 高频查询词跑一遍,看看有没有被切错的,这比任何理论分析都管用。