首页 / 资讯中心 / 文章详情

ElasticSearch默认分词器详解:中文分词原理、配置与选型实践

ElasticSearch默认分词器详解:中文分词原理、配置与选型实践 ★ FEATURED ARTICLE
1. 从一次搜索翻车说起为什么默认分词器值得单独拎出来讲做过搜索相关项目的人大概都有过这种经历数据明明已经灌进了ElasticSearch字段也建好了可用户搜智能手机就是搜不到标题里写着智能手机壳的文档搜苹果手机却把苹果和手机拆得七零八落返回一堆风马牛不相及的结果。第一次遇到这种情况很多人会怀疑是查询语句写错了或者是数据没同步进去折腾半天才发现问题出在一个平时几乎不会注意的地方——默认分词器。ElasticSearch里分词器Analyzer决定了文本在写入倒排索引时被切成什么样也决定了查询词在检索时被切成什么样。它不是一个可选项而是每个text类型字段都必然要经历的一道工序。默认情况下如果你没有显式指定ES会使用standard分词器。这个分词器对英文还算友好但面对中文它的处理方式是按单字切分——智能手机会被切成智、能、手、机四个独立的词元。这意味着你搜手机时它匹配的是手和机这两个字而不是手机这个词召回率和准确率都会受到明显影响。这篇内容面向的是正在做搜索功能、日志分析、内容检索类项目的开发者尤其是那些第一次接触ES、被中文分词坑过的人。我会把默认分词器的机制、它和_analyze接口的关系、索引级别和字段级别的设置方式、以及实际项目里怎么选型和验证一层层拆开讲清楚。看完之后你至少能做到两件事第一知道为什么你的搜索看起来能用但就是不准第二能自己动手把默认分词器改成适合业务的那一个并且验证它确实生效了。需要提前说明的是分词器的选择没有绝对的最优解只有适不适合你的数据形态和查询习惯。下面所有内容都基于常见的工程实践来展开具体参数和版本行为可能因ES版本不同略有差异动手前建议先在自己的环境里验证一遍。2. 默认分词器到底默认了什么standard的切分逻辑与中文困境2.1 standard分词器的三段式处理流程要理解默认分词器的问题得先知道一个分词器内部由哪几部分组成。ES里的分词器并不是一个单一组件而是由三个环节串联而成字符过滤器Character Filter、分词器Tokenizer、词元过滤器Token Filter。数据从字段进来先过字符过滤器做预处理再交给分词器切成词元最后经过词元过滤器做二次加工才写入倒排索引。standard分词器的配置是这样的字符过滤器为空分词器用的是standardtokenizer词元过滤器是lowercase。standardtokenizer的切分规则基于Unicode文本分割算法它会按照词边界来切分对英文来说空格、标点、连字符都是边界所以hello-world会被切成hello和world对中文来说由于汉字之间没有空格算法会把每一个汉字当作一个独立的词元于是智能手机就变成了四个单字。这个行为本身没有错它是通用场景下的一个折中方案。问题在于中文的语义单位往往是双字词或多字词单字切分丢失了词与词之间的组合信息。你搜手机时ES实际是在找同时包含手和机的文档而手和机这两个字在很多不相关的词里也会出现比如手工、机会于是召回了一堆噪声。2.2 用_analyze接口亲眼看看切分结果光说理论不够直观ES提供了一个非常好用的调试接口_analyze可以直接看到一段文本被切成了什么。这是排查分词问题的第一把工具建议养成习惯改任何分词配置之前都先用它验证。POST _analyze { analyzer: standard, text: 创新实训ElasticSearch默认分词器 }返回结果里tokens数组会列出每个词元及其位置信息。你会看到创新实训被拆成了创、新、实、训四个tokenElasticSearch则被整体保留并转成小写elasticsearch。这个对比很能说明问题英文单词因为有大小写和空格边界standard处理得不错中文因为缺乏显式边界只能退化成单字切分。再试一个更贴近业务的例子POST _analyze { analyzer: standard, text: 苹果手机壳 }结果是苹、果、手、机、壳五个单字。如果用户搜苹果手机standard会把它切成苹、果、手、机然后去匹配包含这四个字的文档。理论上苹果手机壳能匹配上但苹果和手机分开匹配也会把苹果电脑、手机支架这类文档一起召回精准度就下来了。2.3 中文场景下默认分词器的三个典型症状把默认分词器的问题归纳一下实际项目里通常表现为三种症状。第一种是召回噪声大搜一个词返回大量只包含其中某个字的无关文档第二种是短语匹配失效用户搜数据分析结果把数据和分析分开匹配返回了数据存储和行为分析混在一起的列表第三种是相关性打分失真因为单字词元的区分度低TF-IDF或BM25算出来的分数拉不开差距排序结果看起来什么都沾一点什么都不突出。这三种症状的根源是同一个默认分词器不理解中文的词。要解决它就得换一个能识别中文词边界的分析器或者至少调整默认配置。接下来的章节会讲具体怎么改以及改的时候有哪些层级和优先级要注意。3. 改默认分词器的三条路径全局、索引、字段的优先级博弈3.1 全局默认改elasticsearch.yml的代价与适用场景最一劳永逸的方式是在集群配置文件elasticsearch.yml里指定index.analyzer.default这样所有新建索引在没有显式配置时都会用这个分析器。配置项大致长这样index.analyzer.default.type: standard不过说实话我不太推荐在生产环境里动这个全局配置。原因有两个一是它影响面太大集群里可能有日志索引、指标索引、业务索引它们对分词的需求完全不同用一个全局默认去覆盖所有场景很容易顾此失彼二是这个配置修改后通常需要重启节点才生效对线上服务不友好。全局默认更适合那种整个集群只服务单一业务、且团队对分词需求高度一致的场景比如一个纯粹的中文内容检索集群。3.2 索引级默认在settings里一次性定好基调更常见的做法是在创建索引时通过settings指定该索引的默认分析器。这样每个索引可以有自己的默认行为互不干扰。写法有两种一种是直接指定defaultPUT /my_index { settings: { analysis: { analyzer: { default: { type: custom, tokenizer: ik_max_word } } } } }另一种是分别指定default和default_search让写入和查询用不同的分析器。这个区分很关键后面会专门讲。索引级默认的好处是粒度适中一个业务一个索引配置清晰改起来也只影响这一个索引重建索引即可生效不需要重启集群。3.3 字段级覆盖mapping里最细粒度的控制如果同一个索引里不同字段需要不同的分词策略那就得在mapping里对具体字段指定analyzer。比如标题字段需要细粒度分词提高召回标签字段需要精确匹配正文字段需要粗粒度分词控制索引大小这时候字段级配置就是唯一的选择。PUT /my_index { mappings: { properties: { title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, tags: { type: keyword } } } }这里有个优先级要记牢字段级配置 索引级默认 全局默认。ES在决定一个字段用什么分析器时会从最细粒度往上找找到第一个就停。所以如果你在字段上写了analyzer索引级的default对它就不起作用了。理解这个优先级能帮你在排查为什么我改了默认配置却没生效这类问题时少走弯路。3.4 三种路径的对比与选型建议配置层级生效范围是否需重启适用场景主要风险全局默认整个集群通常需要单一业务集群影响面大难以兼顾多业务索引级默认单个索引不需要一业务一索引同索引内多字段需求冲突字段级覆盖单个字段不需要字段需求差异大配置分散维护成本高选型上我的经验是优先字段级其次索引级尽量别动全局。字段级虽然配置多但它最精确出问题也最容易定位。索引级适合那些字段需求比较统一的场景。全局默认除非你有非常明确的理由否则保持原样最稳妥。4. 写入与查询用不同分析器default_search这个坑必须单独讲4.1 为什么写入和查询的分析器可以不一样这是很多人第一次配分词器时容易忽略的点。ES允许你为写入index time和查询search time分别指定分析器。写入时用的分析器决定文档被切成什么词元存进倒排索引查询时用的分析器决定用户输入的查询词被切成什么词元去匹配。为什么要有这个区分因为写入和查询的目标不完全一样。写入时我们往往希望切得细一点让更多可能的查询词都能命中这叫提高召回查询时我们希望切得准一点避免把用户意图拆得太碎导致匹配到无关内容这叫提高精度。用一个分析器同时满足两个目标往往两头不讨好。4.2 default和default_search的配合方式在索引settings里可以这样配置PUT /my_index { settings: { analysis: { analyzer: { default: { type: custom, tokenizer: ik_max_word }, default_search: { type: custom, tokenizer: ik_smart } } } } }default作用于写入default_search作用于查询。以常见的中文分词插件为例ik_max_word会把文本切成尽可能多的词覆盖所有可能的组合ik_smart则只切出最可能的几个词粒度更粗。写入用ik_max_word查询用ik_smart是一个很经典的搭配文档里中华人民共和国会被切成中华、华人、人民、共和、共和国等多个词元用户搜共和国能命中而用户输入中华人民共和国时ik_smart只切出中华人民共和国这一个词匹配更精准。4.3 字段级如何分别指定analyzer和search_analyzer如果不想动索引级默认也可以在字段上分别指定PUT /my_index { mappings: { properties: { content: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart } } } }这里analyzer管写入search_analyzer管查询。注意如果你只写了analyzer没写search_analyzer那么查询时也会用analyzer指定的那个也就是写入和查询用同一个。所以想让两者不同必须显式写search_analyzer。提示改完search_analyzer后不需要重建索引因为它只影响查询阶段但改analyzer必须重建索引因为倒排索引里的词元已经按旧分析器生成好了不重建不会变。4.4 一个真实的排查案例为什么搜全称搜不到我遇到过这样一个情况文档标题是某跨平台系统开发指南用户搜完整标题却搜不到搜其中的片段反而能搜到。排查后发现写入用的是ik_max_word查询用的是默认的standard。用户输入完整标题时standard把它切成一个个单字而倒排索引里存的是ik_max_word切出来的词两边词元对不上自然匹配失败。把search_analyzer也改成ik_max_word或ik_smart后问题解决。这个案例说明写入和查询的分析器必须说同一种语言否则就会出现这种明明文档在就是搜不到的诡异现象。排查这类问题的标准动作就是对同一段文本分别用写入分析器和查询分析器跑一遍_analyze对比两边的词元列表看有没有交集。5. 中文分词器的选型从内置到插件怎么选不踩雷5.1 ES内置的中文相关分析器盘点ES自带的分析器里和中文沾边的其实不多。standard前面讲过了单字切分。simple只保留字母中文会被直接丢掉完全不能用。whitespace按空格切中文没空格等于整段保留也不合适。stop和keyword更不用考虑。所以结论很明确ES内置分析器没有能直接用于中文分词的想做好中文检索基本都得引入第三方分词插件。这一点新手容易误解以为ES作为成熟的搜索系统中文支持应该开箱即用。实际上ES的核心团队主要面向英文及拉丁语系场景中文分词一直依赖社区插件。知道这一点能帮你少花时间在是不是我配置错了上纠结。5.2 主流中文分词插件的特性对比社区里常见的中文分词插件有几个方向它们在词典、算法、粒度控制上各有侧重。下面这张表是选型时可以参考的维度对比维度侧重细粒度侧重粗粒度侧重自定义词典切分策略全切分覆盖多组合最短路取最优依赖词典命中索引体积较大较小取决于词典召回率高中高词典内准确率中高高词典内适用字段正文、描述标题、标签专业术语、品牌名选型的核心逻辑是看你的查询习惯。如果用户习惯搜短词、搜片段那写入侧要细粒度保证召回如果用户习惯搜完整短语那查询侧要粗粒度保证精度。如果业务里有大量专业术语、产品型号、人名地名那自定义词典就是刚需因为通用词典不可能覆盖这些。5.3 安装插件后必须做的三件事装完分词插件别急着改配置先做三件事验证。第一件用_analyze测试插件是否被正确加载POST _analyze { analyzer: ik_max_word, text: 创新实训项目 }如果返回报错说analyzer找不到说明插件没装好或者节点没重启。第二件测试自定义词典是否生效把你业务里的专有名词加进词典文件重启后看它是否被当成一个完整词元切出来。第三件对比新旧分析器在同一批样本上的切分结果评估召回和精度的变化别凭感觉就上线。注意自定义词典的更新通常需要重启节点或调用插件的热更新接口具体方式看插件文档。词典文件路径和格式也要严格按插件要求来格式错了插件可能静默忽略不报错但也不生效这个坑很隐蔽。5.4 词典维护的长期成本很多人选分词器时只看切分效果忽略了词典维护这件事。通用词典能覆盖大部分日常用语但业务特有的词——产品名、内部代号、行业术语——通用词典里没有不维护就会切错。比如一个内部系统叫星链平台通用分词可能切成星、链、平台用户搜星链就匹配不到。我的建议是项目初期就建立一个业务词典文件把已知的专有名词、高频查询词放进去后续根据搜索日志里搜不到的case持续补充。这个工作看起来琐碎但它是中文搜索效果的地基省不得。可以把它当成一个持续迭代的清单每次上线新功能、新增术语时顺手更新。6. 验证与调优让分词配置真正落地的实操清单6.1 用_analyze做写入与查询的双向验证配置改完后验证分两步。第一步验证写入侧取几条真实文档的文本用写入分析器跑_analyze看切出来的词元是否符合预期专有名词有没有被正确识别有没有出现大量无意义的单字。第二步验证查询侧取几个典型查询词用查询分析器跑_analyze看切出来的词元和写入侧有没有交集。POST /my_index/_analyze { field: title, text: 某跨平台系统开发指南 }注意这里用的是field参数而不是analyzer这样ES会直接使用该字段配置的分析器验证的是真实生效的配置而不是你手动指定的。这个细节很重要很多人用analyzer参数测了半天测的是自己指定的分析器而不是字段实际用的结果配置没生效也没发现。6.2 用真实查询回归测试召回与排序_analyze只能验证切分不能验证最终效果。真正靠谱的验证是拿一批真实查询词跑一遍搜索人工看召回结果和排序。建议准备一个小的测试集20到50个查询词每个词标注出应该命中的文档和不应该命中的文档然后对比改配置前后的命中情况。这个测试集不用很正式一个表格就行。重点看两类问题一是该召回的没召回通常是写入侧切分太粗或查询侧切分太细导致词元对不上二是不该召回的召回了通常是切分太细导致单字匹配泛滥。根据这两类问题的比例反过来调整分析器的粒度。6.3 索引重建的时机与注意事项只要改了写入侧的分析器analyzer或索引级default就必须重建索引因为已有的倒排索引是按旧分析器生成的不重建不会更新。重建的方式有几种可以新建一个索引用_reindex把数据搬过去验证无误后切换别名也可以直接用_reindex覆盖。生产环境推荐前者因为可以灰度验证出问题能快速回滚。重建时要注意数据量和时间窗口。大索引重建可能耗时较长期间要保证有足够的磁盘空间和集群负载余量。另外如果业务在重建期间有写入要考虑增量数据的同步别让新写入的数据落在旧索引里丢了。6.4 几个容易被忽略的配置细节最后列几个实操中容易踩的细节。第一keyword类型字段不分词如果你给一个字段设了keyword那它整段作为一个词元搜的时候必须完全匹配别指望它做模糊搜索。第二text字段如果同时需要精确匹配和分词匹配可以用fields做多字段一个子字段用keyword一个用text。第三search_analyzer不写时默认继承analyzer别以为不写就是standard。第四索引模板index template里的分析器配置会应用到匹配的新索引如果你用模板管理索引记得在模板里也配好别只改了单个索引。PUT /my_index { mappings: { properties: { name: { type: text, analyzer: ik_max_word, fields: { raw: { type: keyword } } } } } }这样name既能分词搜索又能通过name.raw做精确匹配和聚合是很实用的一个模式。7. 我在实际项目里踩过的几个分词坑说几个具体的经历都是文档里不太会写、但实际会遇到的。第一个坑是测试环境和生产环境分词器不一致。测试环境装了插件生产环境忘了装结果测试通过上线就报analyzer找不到。后来我们养成了习惯把插件版本和配置写进部署清单环境初始化时自动校验。第二个坑是自定义词典的编码问题。词典文件如果存成了带BOM的UTF-8某些插件解析时会出问题词条加载不全。排查了很久才发现是文件编码的锅。现在我们的词典文件统一用无BOM的UTF-8并且在CI里加了一步编码检查。第三个坑是过度依赖细粒度分词导致索引膨胀。有个项目为了追求召回写入侧用了最细的全切分结果索引体积涨了将近一倍查询延迟也上去了。后来改成正文用细粒度、标题用粗粒度索引体积回落效果反而更均衡。这让我意识到分词不是越细越好要在召回、精度、成本之间找平衡。第四个坑是改了配置忘了重建索引。这个最典型改完analyzer直接测发现没变化以为配置没生效其实是旧索引没重建。现在我们的流程是改写入分析器 → 重建索引 → 验证 → 切换别名一步都不能少。这些坑的共同点是它们都不在分词器怎么配的范畴里而在配完之后怎么保证它真的按你想的跑的范畴里。分词这件事配置只是开始验证和维护才是大头。把_analyze用熟把测试集建起来把重建流程固化下来中文搜索的效果才能稳定可控。
阅读完成 · 觉得有帮助?
咨询建站