elasticsearch6.3.2最简单插件开发ScriptPlugin

需求背景

当不知道怎么去构造一个复杂到查文档都查不出所以然的query时,第一个想到的肯定是插件解决,插件终归是最灵活又最不灵活的解决方案。插件灵活在可以不受query语法的影响,通过java/python等来表达需求,最终可以形成非常复杂的query扩展,这个也是es一个开放性的好处。那么不灵活在哪呢?插件做为第三方扩展,需要通过额外安装的形式来加载到集群当中。当用单节点测试集群测试时可能并不会觉得困难,但是当到线上环境以后,面对的是多个节点集群的插件安装部署,需要做集群重启操作,会严重影响线上服务的提供,所以并不能频繁的迭代插件以达到更新召回调整。
这次我把需求概括到最简化的方式:

使用doc中的nested嵌套字段来计算_score以提供权重调整召回排序

即使最终这个需求经过讨论发现并不需要插件来实现,但这次插件开发也是很有意义的,为以后灵活定制es的query提供了新思路。我曾经介绍过关于es2.x版本similartity插件的开发,时过境迁我们的集群已经更新到了6.3.2版本,许多API已经完全不一样了。
一个小小的吐槽,es插件的API在几个小版本之间都发生了变化,更不要说一个大版本内能共用了,6.3.2版本的api和6.8.x的API就已经不一样了。

文档结构

简单点,把文档简化成以下的样子,直接关注最核心的部分。

{
    "_index": "tag_test_v1",
    "_type": "_doc",
    "_id": "2",
    "_version": 1,
    "_score": 1,
    "_source": {
        "tag": {
            "31": 1.1111,
            "32": 1.1111,
            "33": 1.1111,
            "34": 1.1111
        },
        "id": "2"
    }
}

上面的文档有2个字段,一个是唯一标识符id,另外一个就是nested字段tag,tag字段下有n组键值对,key为标签id,value为标签分数,而要做的事就是,在召回的时候根据query对于tag的命中,来调整对应的_score。
先不去想说能不能通过其他方式解决,其实是有的,但是这次我只是借这个例子来巩固一下关于去开发es插件来更灵活定制自己的需求。

字段规约

在说脚本之前首先要聊一下关于字段规约,也就是mapping,这里的mapping设计有两个点是需要注意的,一个是嵌套字段的mapping设计,另外一个有点特殊。仔细看tag下的键值对,并不是固定字段而是动态的,所以要使用动态模板。

    mapping = {
        "mappings": {
            "_doc": {
                "properties": {
                    "id":{
                        "type": "keyword"
                    }
                },
                "dynamic_templates": [
                    {
                        "tag":
                            {
                                "match":              "tag.*",
                                "match_mapping_type": "double",
                                "mapping":
                                    {
                                        "type":           "double"
                                    }
                            }
                    }
                ]
            }
        }
    }

插件开发

进入正题。

  1. 建立一个maven类型的项目
  2. pom文件引入依赖,我这里针对6.3.2版本做开发
    
        
            org.elasticsearch
            elasticsearch
            6.3.2
        
        
            com.alibaba
            fastjson
            1.2.39
        
    
  1. assembly配置文件,因为要压缩成符合es格式的zip包


    tag-plugin
    
        zip
    
    false
    
        
            ${project.basedir}/config
            config
        
    

    
        
            ${project.basedir}/src/main/resources/plugin-descriptor.properties
            
            true
        
    
    
        
            
            true
            true
            
                org.elasticsearch:elasticsearch
            
        
    

  1. 同时在pom文件下配置用assembly打包
            
                org.apache.maven.plugins
                maven-assembly-plugin
                2.6
                
                    false
                    
                    ${project.build.directory}/resource/
                    
                        ${basedir}/src/main/assemblies/plugin.xml
                    
                
                
                    
                        package
                        
                            single
                        
                    
                
            
  1. 外部配置基本完毕,接下来关注点都在代码本身上。首先明确目的,通过script方式自定义召回打分排序,所以我们需要继承ScriptPlugin这个类。建立一个类做为插件入口
package org.elasticsearch.plugin.analysis.tag;


import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;

import java.util.Collection;

public class TagScriptPlugin extends Plugin implements ScriptPlugin{

    @Override
    public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) {
        return new TagScriptEngine();
    }

}

  1. 接着实现插件打分的逻辑
package org.elasticsearch.plugin.analysis.tag;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.PostingsEnum;
import org.apache.lucene.index.Term;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SourceLookup;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.Map;

public class TagScriptEngine implements ScriptEngine{

    private static final String SCRIPT_NAME = "tag_weight";

    private static final String SCRIPT_SOURCE = "test";

    protected final Logger logger =  Loggers.getLogger(getClass());



    @Override
    public String getType() {
        return SCRIPT_NAME;
    }

    @Override
    public  FactoryType compile(String name, String code, ScriptContext context, Map params) {
        if (!context.equals(SearchScript.CONTEXT)) {
            throw new IllegalArgumentException(getType()
                    + " scripts cannot be used for context ["
                    + context.name + "]");
        }
        if (!code.equals(SCRIPT_SOURCE)){
            throw new IllegalArgumentException("Unknown script name " + code);
        }
        SearchScript.Factory factory = (p, lookup) -> new SearchScript.LeafFactory() {

            final String tag_id;
            final String[] tag_ids;
            {
                if (p.containsKey("tag_id") == false) {
                    throw new IllegalArgumentException("Missing parameter [tag_id]");
                }

                tag_id = p.get("tag_id").toString();
                tag_ids = tag_id.split(" ");
            }

            @Override
            public SearchScript newInstance(LeafReaderContext context) throws IOException {
                return new SearchScript(p, lookup, context) {
                    @Override
                    public double runAsDouble() {
                        SourceLookup source = lookup.source();
                        Double score = 0d;
                        logger.info("========" + source.get("tag").getClass().toString() + "=========");
                        HashMap tag = (HashMap) source.get("tag");
                        for (String tag_id : tag_ids) {
//                            如果没获取标签直接跳过
                            if (!tag.containsKey(tag_id)) continue;
                            score += (Double)(tag.get(tag_id));
                        }
                        return score;
                    }

                };
            }

            @Override
            public boolean needs_score() {
                return false;
            }
        };

        return context.factoryClazz.cast(factory);
    }

    @Override
    public void close() throws IOException {

    }
}

我主要重写的方法就是runAsDouble,其余的方法还没有研究明白可以定制那些东西。修改runAsDouble就可以随意定制每个doc的_score。

  1. 打包上传运行
    通过maven打包,执行以下命令
    mvn clean package
    打成zip包后,上传到es集群所在的服务器。来到es的bin目录下
    安装插件命令
    elasticsearch-plugin install file:///home/tag-plugin-1.0-SNAPSHOT.zip
    卸载插件命令
    elasticsearch-plugin remove DemoPlugin
    查看集群插接件列表
    curl http://10.14.12.32:9400/_cat/plugins
    注意一个点,安装插件后需要重启集群才能加载,插件安装可以离线进行,也就是说在关闭集群的状态下也能安装插件
    8.最后来到使用查询环节
{
    "explain": true,
    "from": 0,
    "size": 20,
    "query": {
        "bool": {
            "should": [{
                "match": {
                    "id": "21507"
                }
            }],
            "adjust_pure_negative": true,
            "minimum_should_match": "1",
            "boost": 1
        }
    },
    "rescore": [{
        "window_size": 1500,
        "query": {
            "rescore_query": {
                "function_score": {
                    "query": {
                        "match_all": {
                            "boost": 1
                        }
                    },
                    "functions": [{
                        "script_score": {
                            "script": {
                                "source": "test",
                                "lang": "tag_weight",
                                "params": {
                                    "tag_id": "32 33"
                                }
                            }
                        }
                    }],
                    "score_mode": "multiply",
                    "boost_mode": "multiply",
                    "max_boost": 3.4028235e+38,
                    "boost": 1
                }
            },
            "query_weight": 1,
            "rescore_query_weight": 1,
            "score_mode": "multiply"
        }
    }]
}

这里关键的参数是source和lang,都是我们在插件里定义的参数。这样一来doc的打分逻辑便会跟随插件中定义的逻辑。我这个插件的逻辑很简单,输入参数为32 33,doc的分数就是32的值加上33的值,也就是2.2222。
源代码我上传到了这里
es插件开发

你可能感兴趣的:(elasticsearch6.3.2最简单插件开发ScriptPlugin)