给 IK 分词结果加上业务类型:一次源码改造记录

IK 能把一段中文切成词,但在业务里,知道“切出了什么”有时还不够。面对“施耐德继电器”这样的文本,我还希望分词结果能告诉下游:施耐德 是品牌,继电器 是品类;遇到物料号时,也应该保留对应的业务类型。

普通扩展词典可以让 IK 识别新词,却不会自动区分品牌、品类和物料号。为了补上这部分信息,我顺着 IK 的词典加载与词元输出流程做了一次改造。这篇记录只整理其中最关键的路径,以及二开时实际改动的地方。

扩展词典能识别新词,却分不清它们是什么

IK 原本已经支持主词典、扩展词典和停用词词典。把“施耐德”加入扩展词典后,它可以作为完整词元出现在分词结果中,避免被拆成没有业务意义的片段。

问题是,所有扩展词最终都会合并进主词典。对分词器来说,“施耐德”“继电器”和某个物料号都只是命中的词条,下游拿到结果后仍然不知道哪个是品牌、哪个是品类。

如果只想改善切词结果,使用扩展词典已经足够。这次改造还需要保留词条所属的业务分类,因此不能把所有词继续放进同一棵词典树。我分别增加了品牌、品类和物料号词典,让每类词拥有独立的匹配入口。

先找到词典是怎样进内存的

IK 的源码不少,但和这次改造直接相关的主要是配置、分词核心与词典管理三部分。继续往下追,入口最终落在 DictionaryDictSegment 上。

Dictionary 负责读取配置并初始化词典。分词器第一次使用时,主词典、量词词典和停用词词典会被加载到内存;配置文件里声明的扩展词典随后也会写入主词典。真正保存词条的不是一张字符串列表,而是由 DictSegment 组成的树。

image-20220508220918804

以“施耐德继电器”为例,匹配从字符“施”开始:

1
2
3

└── 耐
└── 德 ← 完整词条

只要沿着字符节点依次找到“施”“耐”“德”,并且最后一个节点被标记为词尾,就可以确认“施耐德”是一个完整词条。如果走到一半没有后续节点,这次匹配便结束。

这里有个实现细节很值得保留。一个节点的子节点较少时,IK 使用数组存储;数量超过阈值后,再迁移到 Map。数组避免了每个小分支都维护 HashMap 的额外开销,分支变多后改用 Map,又能避免数组查找随着节点数量增加而持续变慢。

理解这条加载路径后,新增业务词典就不需要改动分词算法本身。我沿用原有方式,为品牌、品类和物料号分别创建 DictSegment,启动时从对应的词典文件读取内容,再通过 fillSegment 逐词写入各自的树中。

自定义词典文件

词典分开以后,在哪里给词元标类型

业务词典加载完成,只解决了“数据放在哪里”的问题。接下来还要在分词结果返回之前,用当前词元去这些词典中做匹配。

IKSegmenter.next() 会逐个返回词元,同时在这里过滤停用词。这个位置已经拿到了词元的字符范围,也处在结果交给调用方之前,因此我把业务类型判断放在了这条路径上:先检查品牌词典,命中后将类型标为 brand;没有命中再继续检查品类、物料号等词典。

一次词典匹配会得到三类状态:

  • UNMATCH:当前字符路径不存在;
  • MATCH:已经命中一个完整词条;
  • PREFIX:当前内容是某个更长词条的前缀,还可以继续向下匹配。

这三个状态来自同一棵词典树。匹配首字符后,如果后面还有字符,就沿当前节点继续查找;处理到最后一个字符时,再根据节点是否为词尾、是否还有子节点,设置完整匹配或前缀匹配状态。业务类型判断只关心最终是否完整命中。

整个改造可以简化成下面这段流程:

1
2
3
4
5
6
7
8
9
IK 输出词元

过滤停用词

依次匹配品牌、品类、物料号词典

命中后写入业务类型

返回词元

这样处理以后,“施耐德继电器”的分词结果不再只有词元文本,还能携带 brandcategory 等信息。后续做索引、聚合或业务规则时,不必再根据字符串重新猜测词元属于哪一类。

独立词典也带来了新的取舍

把不同业务词混进主词典,可以让它们被完整切出,但分类信息会在加载时丢失。这次实现将业务词分开保存,IK 原来的主词典继续负责分词,各业务词典只在词元输出阶段补充分类。

代价是每个词元可能需要依次检查多棵树。业务词典数量继续增加时,匹配顺序和额外开销都需要重新评估。另一种做法是在主词典节点上增加类型字段,但同一个词可能属于多个分类,词典的加载和更新也要跟着改变。

词典之间还要提前约定优先级。一个词如果同时出现在品牌和品类词典中,是命中第一类后立即返回,还是允许一个词元携带多个类型,会直接影响后续数据结构。当前实现采用依次匹配的方式,因此词典顺序本身就是业务规则的一部分。

词典更新比首次加载更麻烦

词典文件随程序启动加载,只适合变化不频繁的场景。如果品牌、品类和物料号每次变化都要靠重启服务生效,维护会很麻烦,因此这次二开还处理了词典热更新。

热更新需要考虑的不只是重新读取文件。加载过程中旧词典是否还能继续提供查询,新词典何时替换,多个词典怎样保持一致,以及更新失败后是否保留旧数据,都会影响线上分词结果。这里涉及当时项目的数字资产保密条款,具体实现不展示,只保留这个边界。

这次改造最终没有改变 IK 原本的切词职责。主词典仍然决定一个词能否被识别,独立的业务词典则给已经产生的词元补上分类。后续如果业务类型继续增多,下一步需要关注的就是多词典匹配的开销,以及类型冲突应当如何表达。