IK_MAX_WORD分词问题分析

2,934 阅读2分钟

1.问题现象

使用ES6.8.1(对应的elasticsearch-analysis-ik也为6.8.1)在进行doc索引的时候,出现错误 : startOffset must be non-negative, and endOffset must be >= startOffset, and offsets must not go backwards startOffset=3,endOffset=4,lastStartOffset=4 for field

在kibina上模拟,索引请求

POST /test_ratings/doc/10086
{
  "title":"得饶人处且饶人"
}

出现错误

{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "startOffset must be non-negative, and endOffset must be >= startOffset, and offsets must not go backwards startOffset=3,endOffset=4,lastStartOffset=4 for field 'title'"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "startOffset must be non-negative, and endOffset must be >= startOffset, and offsets must not go backwards startOffset=3,endOffset=4,lastStartOffset=4 for field 'title'"
  },
  "status": 400
}

这是由于ik_max_word分词引起的,我们看一下ik_max_word的分词结果

{
  "tokens" : [
    {
      "token" : "得饶人处且饶人",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "得饶人处",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "饶人",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "且",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "CN_CHAR",
      "position" : 3
    },
    {
      "token" : "处",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "CN_CHAR",
      "position" : 4
    },
    {
      "token" : "且",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "CN_CHAR",
      "position" : 5
    },
    {
      "token" : "饶人",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 6
    }
  ]
}

我们看这里的且这个token,被分出来的两次,而且两次的start_offset和end_offset是一样的,这是报错的主要原因。

2.问题原因

为什么ik分词器会出现这种现象呢,这需要从ik分词的原理讲起。ik分词器的原理可以参考文章 IK分词器原理。在这片文章中也提到了,用6.8.1这个版本的ik分词器会存在重复输出的问题。

主要问题在AnalyzeContext#outputToResult这个方法内

void outputToResult(){
   int index = 0;
   for( ; index <= this.cursor ;){
      //跳过非CJK字符
      if(CharacterUtil.CHAR_USELESS == this.charTypes[index]){
         index++;
         continue;
      }
      //从pathMap找出对应index位置的LexemePath
      LexemePath path = this.pathMap.get(index);
      if(path != null){
         //输出LexemePath中的lexeme到results集合
         Lexeme l = path.pollFirst();
         while(l != null){
            this.results.add(l);
             
            //(1)
            //字典中无单字,但是词元冲突了,切分出相交词元的前一个词元中的单字
            int innerIndex = index + 1;
            for (; innerIndex < index + l.getLength(); innerIndex++) {
               Lexeme innerL = path.peekFirst();
               if (innerL != null && innerIndex == innerL.getBegin()) {
                  this.outputSingleCJK(innerIndex - 1);
               }
            }
             
            //将index移至lexeme后
            index = l.getBegin() + l.getLength();             
            l = path.pollFirst();
            if(l != null){
               //输出path内部,词元间遗漏的单字
               for(;index < l.getBegin();index++){
                  this.outputSingleCJK(index);
               }
            }
         }
      }else{//pathMap中找不到index对应的LexemePath
         //单字输出
         this.outputSingleCJK(index);
         index++;
      }
   }
   //清空当前的Map
   this.pathMap.clear();
}

我们来看(1)的这部分代码,是由github.com/medcl/elast… 这个commit提交的代码。

主要是为了解决 金力泰合同审批 这个分词,原先的结果是 金 力 泰合 合同 审批, 加了这个后结果为 金 力 泰合 泰 合同 审批 强行分出泰来。、

对于得饶人处且饶人的这个场景,输出 得饶人处且饶人、得饶人处、饶人、且、处、且、饶人。这个且字出现了两次,而且是同一位置,并且并没有像commint说的那样,相交词元冲突,切分出相交词元的前一个词元的单字。

3.结论

master的最新分支已经把这块代码注释了,低版本的ik分词器也不存在这个问题。

如果在使用中遇到这个问题,可以把源码下下来,去掉这部分代码,重新编译,安装插件就可以解决问题。

更多精彩内容,请关注公众号