基于Lucene的标签tag权重过滤和排序算法
前言
有兄弟在社区中提出这样一个需求,如下:
我想存储用户的标签 购物偏好:化妆品:0.2,零食:0.8 这种 怎么存储才能既能筛选出 零食偏好>0.5的用户
在业务系统对某条记录进行打标签,且标签会附带权重系数,在查询过滤时先基于标签过滤然后再针对权重系数来进行过滤。也可以基于权重系数进行排序这是业务系统非常普遍的需求。就这一需求,介绍一下如何通过扩展Lucene/Solr底层API的方式来实现这一需求
实现
主要思想是将权重值作为Lucene倒排索引的语汇单元(term)附带的paload值,查询过程中利用org.apache.lucene.search.spans.SpanQuery的子类取用payload信息来对文档进行过滤和排序操作。
笔者是使用Solr来作示例,如果用户是使用ES,理论上在ES中也有等效的扩展机制来扩展Lucene的
数据准备
首先要在Schema上定义一个字段类型,后续可以让Lucene在字段解析(parser)过程进去处理,默认字段格式为 “tag1_priority2,tag2_priority2[,tag_priority]" 这里的权重系数 priority取值需要在[0-1]之间
扩展org.apache.lucene.analysis.Tokenizer
扩展Tokenizer的目的是在构建索引过程中对tags列解析,分别将tag和priority存储到Lucene的>CharTermAttribute >PayloadAttribute 中,代码如下:
public class TagPayloadTokenizerFactory extends Tokenizer {
private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
private final PayloadAttribute payloadAtt = addAttribute(PayloadAttribute.class);
public static final Pattern KV_PAIR_LIST_PATTERN = Pattern.compile("([^,]+?)_(.+?)");
public final boolean incrementToken() throws IOException {
this.clearAttributes();
PayLoadTerm t = null;
if (payloadTerms == null) {
payloadTerms = new ArrayList<>();
Matcher kvPairMatcher = KV_PAIR_LIST_PATTERN.matcher(IOUtils.toString(this.input));
int start = 0;
float payload;
while (kvPairMatcher.find()) {
payload = getPayload(kvPairMatcher);
byte[] bytes = new byte[4];
// 将float序列化成bytes
NumericUtils.intToSortableBytes(NumericUtils.floatToSortableInt(payload), bytes, 0);
// builder.copyBytes(,0,4); // NumericUtils.floatToSortableInt(payload > 0 ? payload : 0f, 0, builder);
t = new PayLoadTerm(kvPairMatcher.group(1), new BytesRef(bytes));
payloadTerms.add(t);
start = start + kvPairMatcher.group(0).length() + 1;
}
termsIndex = 0;
termsLength = payloadTerms.size();
}
if (termsIndex >= termsLength) {
termsIndex = -1;
payloadTerms = null;
return false;
}
t = payloadTerms.get(termsIndex++);
termAtt.setEmpty().append(t.key);
payloadAtt.setPayload(t.payload);
return true;
}
}
需要将TagPayloadTokenizerFactory在Solr的配置文件中配置,需要一个工厂类进行包装:
public class TagPayloadTokenizerFactory extends TokenizerFactory {
@Override
public Tokenizer create(AttributeFactory factory) {
TagPayloadTokenizer tokenizer = new TagPayloadTokenizer(factory);
return tokenizer;
}
}
在schema.xml中配置:
<fieldType name="kv_boost_payload" class="solr.TextField">
<analyzer type="index">
<tokenizer class="com.qlangtech.tis.solrextend.fieldtype.TagPayloadTokenizerFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="solr.PatternTokenizerFactory" pattern=",\s*"/>
</analyzer>
</fieldType>
<field name="tags" type="kv_boost_payload" stored="true" indexed="true" required="false"/>
扩展QP(org.apache.solr.parser.QueryParser)
扩展QP是为了在进行查询过程中自定义Query对象,实现对语汇单元对应的payload信息的取用,最终实现过滤和排序操作。 该流程需要扩展Lucene的SpanQuery类(跨度查询)