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分词器也不存在这个问题。
如果在使用中遇到这个问题,可以把源码下下来,去掉这部分代码,重新编译,安装插件就可以解决问题。
更多精彩内容,请关注公众号