最近工作中遇到一个搜索需求:同一条数据有多个关键字,每个关键字具有不同的权重,搜索时,要可以根据关键字的权重进行排序。例如,有如下两条数据,括号中为权重:
- 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 filter
数据查询过程相对复杂,官方并未提供一个现成的方案(可能是我没发现),需要自定义查询插件实现。起初打算看看网上是否有现成轮子,但是发现相关资料较少,只发现了一些基于较低版本实现的一些插件,由于版本之间改动较大,没法直接使用,只好参照这些插件并结合源码实现较高版本的插件。
这里附一个基于ElasticSearch 2.X开发的插件:
https://github.com/jprante/elasticsearch-payload我这边用的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
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对象的四个参数分别为:
- SpanTermQuery对象,
- PayloadFunction对象(包含多个Payload分数时的取分策略),
- PayloadDecoder对象(解析Payload中的内容,并产生一个分数)
- 算分时是否包含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