ElasticSearch自定义PayloadSearchPlugin

最近工作中遇到一个搜索需求:同一条数据有多个关键字,每个关键字具有不同的权重,搜索时,要可以根据关键字的权重进行排序。例如,有如下两条数据,括号中为权重:

  • id:0,商品:迪奥999口红,关键字:男性青睐(0.1) 女性青睐(0.9)
  • id:1,商品:玩家国度2080TI,关键字:男性青睐(0.9) 女性青睐(0.1)

搜索关键字男性青睐时,搜索时要以 1 0 的顺序返回;搜索女性青睐时,排序应该是 0 1。

当时考虑了两种方案解决这种需求:

方案一:父子文档,以上述数据为例,父文档为商品本身属性,子文档为每个关键字及其权重。

方案二:ElasticSearch提供了Delimited Payload Token Filter,利用Luncen的Payload机制来解决这类问题。

基于性能和索引结构简单性的考虑,决定采用第二种方案。

使用Delimited Payload Token Filter主要包括两个过程:数据索引过程、数据查询过程。其中数据索引过程,官方提供了详细文档表述,按部就班操作即可。详见:

Delimited payload token filterwww.elastic.co图标

数据查询过程相对复杂,官方并未提供一个现成的方案(可能是我没发现),需要自定义查询插件实现。起初打算看看网上是否有现成轮子,但是发现相关资料较少,只发现了一些基于较低版本实现的一些插件,由于版本之间改动较大,没法直接使用,只好参照这些插件并结合源码实现较高版本的插件。

这里附一个基于ElasticSearch 2.X开发的插件:

https://github.com/jprante/elasticsearch-payloadgithub.com

我这边用的ElasticSearch版本为6.8.1,因此下述自定义PayloadSearchPlugin基于6.8.1开发


基于ElasticSearch良好的插件扩展性,代码实现非常简单,首先,实现一个继承Plugin的插件并实现SearchPlugin接口,SearchPlugin接口提供了多个方法,其中 getQueries() 方法就是用来提供自定义查询Query的方法,这里只需要重写该方法。

public class PayloadSearchPlugin extends Plugin implements SearchPlugin {
    @Override
    public List<QuerySpec> getQueries() {
        return Collections.singletonList(
            new QuerySpec<>(PayloadSearchQueryBuilder.NAME, PayloadSearchQueryBuilder::new, PayloadSearchQueryBuilder::fromXContent)
        );
    }
}

SearchPlugin会在节点启动时,在SearchModule中被注册,详见SearchModule.registerQueryParsers(List plugins)方法。

PayloadSearchPlugin插件类主要逻辑是提供了一个解析请求中查询体并生成新的Query对象的PayloadSearchQueryBuilder类。

public class PayloadSearchQueryBuilder extends BaseTermQueryBuilder {
    public static final String NAME = "payload_term";

    /** 省略多个构造函数 **/

    public PayloadSearchQueryBuilder(String fieldName, Object value) {
        super(fieldName, value);
    }

    public PayloadSearchQueryBuilder(StreamInput in) throws IOException {
        super(in);
    }

    public static QueryBuilder fromXContent(XContentParser parser) throws IOException {
        XContentParser.Token token = parser.currentToken();
        if (token == XContentParser.Token.START_OBJECT) {
            token = parser.nextToken();
        }
        assert token == XContentParser.Token.FIELD_NAME;
        String fieldName = parser.currentName();
        token = parser.nextToken();
        Object value;
        if (token == XContentParser.Token.START_OBJECT) {
            throw new RuntimeException();
        } else {
            value = parser.objectBytes();
            parser.nextToken();
        }
        return new PayloadSearchQueryBuilder(fieldName, value);
    }

    @Override
    protected Query doToQuery(QueryShardContext context) throws IOException {
        return new PayloadScoreQuery(new SpanTermQuery(new Term(fieldName, (BytesRef) value)), new AveragePayloadFunction(), new FloatPayloadDecoder(), false);
    }

    @Override
    public String getWriteableName() {
        return NAME;
    }
}

PayloadSearchQueryBuilder继承自BaseTermQueryBuilder类,基本继承了term查询的特性(即不再进行分词)。内部提供了一个关键词的声明和两个主要的方法:

  • public static final String NAME = "payload_term",注册到es时用的字段,表明该QueryBuilder负责解析请求体中的"payload_term"查询。
  • public static QueryBuilder fromXContent(XContentParser parser) 主要用于解析请求体中的内容,并将其生成PayloadSearchQueryBuilder,上述代码只支持解析格式为 {"payload_term":{"field":"value"}}的请求。
  • protected Query doToQuery(QueryShardContext context) 生成Query对象的具体实现,这里用到的是Lucene的PayloadScoreQuery,构造该Query对象的四个参数分别为:
  1. SpanTermQuery对象,
  2. PayloadFunction对象(包含多个Payload分数时的取分策略),
  3. PayloadDecoder对象(解析Payload中的内容,并产生一个分数)
  4. 算分时是否包含SpanTermQuery的得分。

自定义FloatPayloadDecoder对象,逻辑比较简单,从payload中解析出权重值,并将该值作为分数返回。

public class FloatPayloadDecoder implements PayloadDecoder {
    @Override
    public float computePayloadFactor(BytesRef payload) {
        return Objects.isNull(payload) ? 0.0f : PayloadHelper.decodeFloat(payload.bytes, payload.offset);
    }
}

具体代码地址:

//TODO

请使用浏览器的分享功能分享到微信等