需求背景
当不知道怎么去构造一个复杂到查文档都查不出所以然的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"
}
}
}
]
}
}
}
插件开发
进入正题。
- 建立一个maven类型的项目
- pom文件引入依赖,我这里针对6.3.2版本做开发
org.elasticsearch
elasticsearch
6.3.2
com.alibaba
fastjson
1.2.39
- assembly配置文件,因为要压缩成符合es格式的zip包
tag-plugin
zip
false
${project.basedir}/config
config
true
true
true
org.elasticsearch:elasticsearch
- 同时在pom文件下配置用assembly打包
org.apache.maven.plugins
maven-assembly-plugin
2.6
false
${project.build.directory}/resource/
${basedir}/src/main/assemblies/plugin.xml
package
single
- 外部配置基本完毕,接下来关注点都在代码本身上。首先明确目的,通过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();
}
}
- 接着实现插件打分的逻辑
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。
- 打包上传运行
通过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插件开发