ElasticSerach之分词器进阶-短语搜索不准确bug及修复实现
为了更清晰的描述问题,我们做个如下实验:
1.随机抽取包含“新能源” 且包含 "汽车"的数据M 条(本例中43条)
2.设置样本中包含 “新能源汽车” 的数据N条(N<M,本例中34条)
3.建立搜索引擎字段映射(字段采用ansj分词),将此批数据导入搜索引擎
1.搜索引擎中搜索关键词“新能源汽车”,可以看到结果数与数据库 中的记录相同
2.搜索短语“新能源汽车”时候发现搜索出来的条数仅有 18条,通过对比数据库,可找到若干包含”新能源汽车“但没有被搜索到的数据,例如数据:50993755
大家都知道lucene中的查询是通过倒排索引查询,而配置了分词器的字段是通过分词查找,短语查询内部实现也是分词查找,但其又与非短语查询存在一些差别,短语查询步骤如下:
首先,短语查询在语法解析的时候会解析提取到短语单元、关键词
其次,将短语单元分析成一个或多个关键词,使用生成的所有关键词到索引中查找并计算文档是否满足了输出逻辑条件
最后,lucene对文档进行短语检查,若文档包目标短语,则将文档加入结果集,若不满足则抛弃。
我们再来看看lucene检查文档是否存在短语的逻辑(如下为检查是否存在短语的lucene函数):
通过代码可以知道,lucene检查短语是否在文档中存在是通过索引中关键词的位置差与输入样本中的位置差的差值是否相等来进行判定的。
我们再来看看分词情况,由于建索引时候我们需要将词分的较细,故采用index的模式来进行分词。
以 "新能源汽车行业进入一个全新时代" 为例,采用ansj官方提供的分词插件分词后,lucene中的索引位置信息如下:
(新能源 , pos:0),(汽车行业 , pos:1),(进入 , pos:2),(一个 , pos:3),(全新 , pos:4),(的 , pos:5),(时代 , pos:6),(新 , pos:7),(新能 , pos:8),(能 , pos:9),(能源 , pos:10),(源 , pos:11),(源汽 , pos:12),(汽 , pos:13),(汽车 , pos:14),(车
,pos:15),(车行 , pos:16),(行 , pos:17),(行业 , pos:18),(业 , pos:19),(业进 , pos:20),(进 , pos:21),(入 , pos:22),(一 , pos:23),(个 , pos:24),(全 , pos:25),(新 , pos:26),(新的 , pos:27),(时 , pos:28),(代 , pos:29)
而查询时候采用基于向量的分词方式,以“新能源汽车”为例,系统将会才分为如下信息:
通过分词信息可以看到,输入样本中,关键词“汽车” 与关键词 “新能源” 距离差为 1 ,而在索引样本中,关键词“汽车” 与关键词 “新能源” 的距离为 14, 因此在lucene上会判定 该条文档中不存在“新能源汽车”的短语。
lucene判定是否存在短语的方式是为 索引关键词(n)位置 + 输入样本关键词(n+1)偏移 = 索引起始关键词(n+1)位置 ,故只要将分词阶段分析出的词的位置以及位置差满足该公式,该bug即可解决。目前在ansj方式常用的分词方式中,仅ToAnalysis 满足该关系式,为能将index与query方式均能满足该公式,我们需要将ansj输出的关键词统一设置一个位置输出信息。按照该关系式,我们只要将分词结果按照词的终止地址做排序输出,词的偏移量采用词的终止地址做位置差即可。
因在lucene中首个词位置增量值不能小于等于0,故可将所有词按照终止偏移地址(采用起始地址会导致首个曾量值为0)排序以后依次数据,输出过程的位置差即为位置增量。
修改点1:
ansj 源码修改,在分词器中进行修改:
修改 IndexAnalysis.java 中result 方法,
将原来的sort方法修改为(低版本没有sort函数,无sort函数的直接在setRealName(graph, result) 函数上方加上如下函数即可):
Collections.sort(result, new Comparator<Term>() {@Override
public int compare(Term o1, Term o2) {
int offset = o1.getOffe() - o2.getOffe() + o1.getName().length() - o2.getName().length();
if (0==offset) {
return o1.getName().length() - o2.getName().length();
} else {
return offset;
}
}
});
修改点2:
修改 AnsjTokenizer.java 类
a.添加成员变量 private int position = 0;
b.修改incrementToken 方法:
@Override
public final boolean incrementToken() throws IOException {
中间省略
if (obj instanceof Term) {
中间省略
offsetAtt.setOffset(term.getOffe(), term.getOffe() + term.getName().length());
typeAtt.setType(term.getNatureStr());
int incPosition = term.getOffe() - position;
position += incPosition;
positionAttr.setPositionIncrement(incPosition);
termAtt.setEmpty().append(rName);
} else {
positionAttr.setPositionIncrement(0);
termAtt.setEmpty().append(obj.toString());
}
return true;
}
c.修改parse方法:
private void parse() throws IOException {
Result parse = ta.parse();
if (synonyms != null) {
for (SynonymsRecgnition sr : synonyms) {
parse.recognition(sr);
}
}
this.position =0;
result = new LinkedList<Object>(parse.getTerms());
}
到此,所有修改已完成,我们同样以 "新能源汽车行业进入一个全新时代" 为例,采用ansj建索引后,lucene中的索引位置信息如下:
查询时候同样以“新能源汽车”为例,系统将会拆分为如下信息:
(新能源 , pos:3),(汽车 , pos:5)
可以看到,输入样本中词的距离为 2,而在lucene索引中,关键词" 汽车"与 关键词"新能源" 的距离同样为 2,代码修改达理论上已正确。
更新ansj相关的插件后重建索引,重新执行短语“新能源汽车”
搜索,可以看到结果已完全正确