现在 ElasticSearch 大量应用在搜索领域,开发者可以通过其提供的多样的查询api达到希望的搜索效果,而且Elasticsearch版本也一直在不断迭代,以满足开发者的需要。但是,实际开发过程中,可能需要将搜索和自己的业务场景进行结合,来达到自定义的排序、搜索规则。Elasticsearch针对这种情况,提供了插件的功能,可以这么说,如果能够学会使用插件,那我们就有了自由扩充ELasticsearch功能的手段,对搜索的掌控力就能提升一个档次。
插件作为ES的架构中的重要一环,ES为其开放了足够多的接口使开发者可以实现自定义的功能需求,其共支持下面十种插件,AnalysisPlugin,ScriptPlugin,SearchPlugin这三个常用插件我们在后面会更详细的讲解
PluginService 加载插件的元信息会从该文件中进行读取,所有插件都需要这个文件,下面两个配置比较重要,如果es版本不一致会加载失败。
name=${project.name}
description=${project.description}
version=${project.version}
classname=${elasticsearch.plugin.classname} 插件入口
java.version=1.8
elasticsearch.version=${elasticsearch.version} 插件对应es版本
这里我们就使用十分流行的ik分词来解释,ik是一款十分流行的中文分词器,其能支持粗细力度的中文分词,其就是一款基于AnalysisPlugin实现的插件
我们可以看到ik分词主要实现了接口中的getTokenizers()和getAnalyzers(),其调用流程如下
1.其在Node初始化时就会将pluginService的中的AnalysisPlugin插件加载到AnalysisModule中。
2.在AnalysisModule中进行分词器和分词器的注册。
3.在注册的过程其实就是将AnalysisIkPlugin的getTokenizers和getAnalyzers返回的分析器和分词器放入key是名称,value是工厂类的map中。
我们这里只看分词器,实际上被注册到分词组中的是一个工厂类,其返回一个继承自Tokenizer的IK Tokenizer,这里最核心的就是incrementToken(),其会进行循环词语切分,最终将词语切分完毕,如果自定义分词器,此处就是决定分词的方法。
如果我们要实现我们自己的分词器的话其实只要进行如下几步
1.继承AnalysisPlugin接口和Plugin接口,实现其中获得工厂类的getTokenizers。
2.实现自定义分词的工厂类方法,其要继承自AbstractTokenizerFactory,实现create来返回自定义测分词类。
3.实现自定义分词类其继承自Lucene的Tokenizer抽象类,将实现incrementToken方法。
4.最终将程序中加入plugin-descriptor.properties组价描述文件,打包放入plugin文件中即可。
容错在搜索中十分常见,但我们经过对搜索无结果日志分析发现对于有很大一部分错误都发生在拼音相同但字写错的了情况。
无结果日志
事件 总数 平均 20190624 20190625 20190626
A->总次数,马徐俊 8 1.1429 0 0 0
搜索词日志
搜索关键词 搜索次数 搜索人数
逻辑思维 168 137
所以我们希望能够实现拼音级别的容错,然后又不希望字错的字太多,就使用如下DSL
{
"query":{
"bool":{
"filter":[
{
"multi_match":{
"query":"{{.Query}}",
"analyzer":"standard",
"fields":[
"title.standard",
"author.standard"
],
"minimum_should_match":"50%"
}
},
{
"bool":{
"should":[
{
"match_phrase":{
"author.pinyin":{
"query":"{{.Query}}",
"analyzer":"pinyin"
}
}
},
{
"match_phrase":{
"title.pinyin":{
"query":"{{.Query}}",
"analyzer":"pinyin"
}
}
}
]
}
}
]
}
}
}
但如此便存在一个问题,其匹配到了dujia的拼音,又匹配到了其中一半的字家,所以其能被命中返回,如下所示
所以我们新增了一个组件用以限制查询词的长度,太短的词不应进行容错,而且在词变长就不会出现上述问题。其继承自ScriptPlugin,并且实现了自定义的打分逻辑,如果限制的查询语句超过少于限制的长度则直接返回-1分,否则根据配置返回固定的分或者ES打出的分。
主要代码逻辑
public SearchScript newInstance(LeafReaderContext context) throws IOException {
return new SearchScript(p, lookup, context) {
public double runAsDouble() {
if(query.length()<length){
return -1d;
}
return Integer.MIN_VALUE==constant_score?getScore():constant_score;
}
};
}
新增语句
{
"script_score": {
"script": {
"source": "limit_query",
"lang": "limit_query_length",
"params": {
"query": "{{.Query}}",
"length": "3",
"constant_score": "1"
}
}
}
}
其实这里我们可以做的更多
我们在实现长句搜索的时候可以使用 more-like-this,其原理大体就是将like的语句进行分词后然后依照BM25 选出在该字段中得分最高的n个词语,然后将原本查询的长语句变成了多个重要词的查询。
从morelike中提取出来的词相距距离太长依旧可以召回,相信熟悉Es的同学都知道ES有match_phrase的语法,其中的slop可以限制词的距离,所以我们希望能够实现一个增加词距离的morelike语句,我们称其为more_like_this_phrase,要使es能够识别我们的组件实现SearchPlugin接口,并返回build的类和解析查询的方法就可以了。
public class MoreLikeThisPharseSearchPlugin extends Plugin implements SearchPlugin {
@Override
public List<QuerySpec<?>> getQueries() {
return singletonList(new QuerySpec<>(MoreLikeThisPharseQueryBuilder.NAME, MoreLikeThisPharseQueryBuilder::new, MoreLikeThisPharseQueryBuilder::fromXContent));
}
}
由于我们是基于more_like_this进行的修改,所以主要修改的解析体和创建lucene query的逻辑
加入解析slop的方法,将slop存到MoreLikeThisPharseQuery对象中
more_like_this_phrase 在原基础上进行了修改,从多个term query抽取出最少需要匹配到的个数(如果minishouldmatch有配置则使用minishouldmatch的个数,只需匹配任意一个即可),将所抽出的m个 数的词中任意挑选 n个词进行match_phrase+slop的查询
原lucene 查询结构
插件是解决复杂自定义打分排序逻辑的利器,后面我们会依赖插件实现更多的打分召回策略,为用户提供更好的搜索服务。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。