一、评分规则需求
按照用户画像(不同的标签分数)和用户省份在用户查询时,对查询结果进行自定义评分
二、ES自定义评分方式
参考:
博客:https://blog.csdn.net/W2044377578/article/details/128636611
官网:https://www.elastic.co/guide/en/elasticsearch/guide/master/function-score-query.html
重点仔细看官方文档,介绍的很详细,下面只是我的案例。
1.functions,weight权重形式
functions内部可以组合多种自定义评分函数+查询过滤函数
{
"explain":true,
"query": {
"function_score": {
//1.匹配:只有在通过这里的基本匹配后才有机会对结果进行自定义评分,即满足查询是基本要求
"query": { "match": {"policyTitle": "儿童教育"} },
//functions中可以放置多种评分规则,使用score_mode定义这些评分规则的总分模式
//评分规则:过滤label中是否为指定标签以及province是否为指定省份,如果是则返回指定权重分数*随机数评分,如果两者同时满足则评分求和
"functions": [
{
"filter": { "match": { "label": "教育" } },
//因为在特定查询上设置的boost提升值会被标准化,而对于此评分函数使用weight提升则不会
//可以为数组functions中的每个函数定义weight,使其与相应函数计算的分数相乘。如果在没有任何其他函数声明的情况下给出 weight,则仅返回weight
"random_score": {},
"weight": 10
},
{
"filter": { "match": { "province": "北京市" } },
"weight": 10
}
],
//max_boost表示自定义的函数的分数不能超过指定分数
"max_boost": 100,
//总评分的评分规则:score_mode为自定义的函数(functions)的计算规则,boost_mode为查询分数和函数分数的计算规则
//方法中分数的最低分为1(即即使设置权重为0,或者filter结果完全不匹配,仍然会返回结果1(即按理结果因当为0时)。其他结果则正常返回(小于1也正常返回))
"score_mode": "sum",
//boost_mode=replace表示仅使用函数分数,忽略查询分数
"boost_mode": "sum",
//min_score表示结果列表中会显示的最低分数(总分)
"min_score": 0
}
}
}
2.script_score脚本形式
{
"query": {
"function_score": {
//1.查询评分
"query": { "match": {"province": "湖北省"} },
//2.script_score评分函数
//在 Elasticsearch中,所有文档得分都是正的 32 位浮点数
//script_score函数允许包装另一个查询并自定义它的评分,而且可以使用脚本表达式对索引中数字类型的字段进行计算评分
"script_score": {
"script": {
"source": "Math.log(2 + doc['provinceNum'].value)"
}
},
//max_boost表示自定义的函数的分数不能超过指定分数
"max_boost": 42,
//总评分的评分规则:score_mode为自定义的函数(functions)的计算规则,boost_mode为查询分数和函数分数的计算规则
"score_mode": "max",
"boost_mode": "sum",
//min_score表示结果列表中会显示的最低分数(总分)
"min_score": 0
}
}
}
3.random_score随机数
{
"query": {
"function_score": {
"query": { "match": {"province": "湖北省"} },
//3.random_score随机评分函数
//生成0到1但不包括1的随机数评分,通过设置种子和字段的方式使随机数评分可以重现
"random_score": {
"seed": 10,
"field": "id"
},
"boost_mode": "sum"
}
}
}
4.field_value_factor影响因子形式
{
"query": {
"function_score": {
"query": { "match": {"province": "湖北省"} },
//4.field_value_factor函数允许您使用文档中的字段(数值型)来影响分数。
//它类似于使用script_score函数,但是它避免了编写脚本的开销。
"field_value_factor": {
"field": "labelNum",
"factor": 1.2,
"modifier": "sqrt",
//missing:如果文档该字段缺失值,则使用该值
"missing": 1
},
"boost_mode": "sum"
}
}
}
5.衰减函数
{
"query": {
"function_score": {
"query": { "match": {"province": "湖北省"} },
//5.衰减函数对文档进行评分,该函数根据文档的数字字段值与用户给定原点的距离而衰减。
//指定的字段必须是数字、日期或地理点字段。
//衰减的形状:linear(线性衰减)、exp(指数衰减)、gauss(正常衰减),结合图像理解
"linear": {
"pubTime": {
//原点:必须以数字字段的数字、日期字段的日期和地理字段的地理点的形式给出。地理和数字字段必填。
//对于日期字段,默认值为now,支持使用日期公式 (例如 now-1h)
"origin": "2021-01-01",
//与原点的距离:在距离范围内,文档分数按照规则从1开始衰减到decay
//对于地理字段:可以定义为数字+单位(1km,12m,...)。默认单位是米。
//对于日期字段:可以定义为数字+单位(“1h”、“10d”、… )。默认单位是毫秒。
//对于数字字段:任何数字。
"scale": "30d",
//偏移量:在(原点+-偏移量)内的文档分数=1,在(原点-scale-offset和原点+scale+offset)范围内的文档分数将按照规则进行衰减,直到达到decay的低点
//默认为0,即文档分数=1的点只有原点,呈峰状;设定值小则文档间区别较大,否则一定范围内的文档会难以区分
"offset": "10d",
"decay": 0.5
}
},
//这里就需要进行乘积评分了,因为gauss给出的是1以内的一个权重分数,如果字段对应为空函数返回为1
//改变为空字段返回0的方式:https://github.com/elastic/elasticsearch/issues/18892
"boost_mode": "multiply"
}
}
}
结合参数与下方的图像函数进行返回值的理解:
以上这些评分规则都可以综合起来写入functions中,于是思考后我得到了下面的请求来实现我的需求:
{
"explain": true,
"query": {
"function_score": {
"query": {
"match": {
"policyTitle": "政府"
}
},
//设定在
"functions": [
//在省份符合用户省份时:匹配省份id(仅此id)对应的得分为1
{
"linear": {
"provinceNum": {
"origin": 0,
"scale": 1,
"offset": 0,
"decay": 0.1
}
}
},
//在标签值符合用户标签时:返回用户在此标签上的权重
{
"script_score": {
"script": {
"source": "if(doc['labelNum'].value==13){return 1.0;}else if(doc['labelNum'].value==17){return 0.5;}else if(doc['labelNum'].value==18){return 0.3;}else if(doc['labelNum'].value==11){return 0.2;}",
"lang": "painless"
}
}
}
],
"score_mode": "sum",
//自定义评分结果与查询评分结果相乘
"boost_mode": "multiply"
}
}
}
三、Java实现自定义评分
参考:https://blog.csdn.net/xiaoll880214/article/details/86716393
代码:
functions内部构造,然后将得到的functions与查询语句一起放入functionScore,设定相应的mode计算方式就行(下面的是不可运行的,仅供参考,需要注意的是functions的层层包装和内部的构建函数使用方式)
public FunctionScoreQueryBuilder.FilterFunctionBuilder[] changeFunction(long userId,String province,Map<String,Float> face){
//userId==-1表示游客登录,不需要个性化,只用根据省份
double labelNumScore=faceService.labelNum;
double maxLabelScore= faceService.maxLabelScore;
double minLabelScore= faceService.minLabelScore;
List<String> labels=faceService.labels;
String[] provinces= PolicyService.chinaProvince;
List<String> provinceList = List.of(provinces);
StringBuilder scoreScript= new StringBuilder();
//记录function的数量
int functionNum=0;
FunctionScoreQueryBuilder.FilterFunctionBuilder[] filterFunctionBuilders;
//1.游客登录,仅记录省份影响,数组长度=1(设置过长会导致function==null产生错误)
if(userId==-1){
filterFunctionBuilders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[1];
}
//2.非游客登录:添加对应用户标签画像,标签score_script脚本自定义评分
else {
filterFunctionBuilders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[2];
//对省份和标签的自定义结果进行求和
Object[] label= face.keySet().toArray();
List<Short> labelNum=new ArrayList<>();
// 将字符串标签转换为数字编号形式,用于排序规则的编写
for (Object o : label) {
labelNum.add((short) labels.indexOf(o));
}
//label在用户占比超过25%,认为这个label是有利的,此时匹配省份=0.5,标签>1,score评分升高
//若匹配省份=0,标签>1,score评分升高也合理
//若占比小于25%,则此标签对用户没有明显影响,此时匹配省份=0.5,标签=1,score评分升高
//若匹配省份=0,标签=1,则score评分保持不变(也合理,如果查询评分非常高则足以超越前面的内容)
//对标签的影响进行一定限制,避免查询结果完全由标签控制
double labelScore=Math.min(face.get(label[0]) * labelNumScore,maxLabelScore);
labelScore=Math.max(labelScore,minLabelScore);
scoreScript.append("if(doc['labelNum'].value==").append(labelNum.get(0)).append("){return ").append(labelScore).append(";}");
for (int i=1;i<label.length;i++){
if(face.get(label[i]) * labelNumScore<minLabelScore){
continue;
}
labelScore=Math.min(face.get(label[i]) * labelNumScore,maxLabelScore);
scoreScript.append("else if(doc['labelNum'].value==").append(labelNum.get(i)).append("){return ").append(labelScore).append(";}");
}
scoreScript.append("else {return 1.0}");
//**层层包装填充放到functions中:https://blog.csdn.net/xiaoll880214/article/details/86716393
ScoreFunctionBuilder<ScriptScoreFunctionBuilder> labelScoreFunction = ScoreFunctionBuilders.scriptFunction(new Script(scoreScript.toString()));
FunctionScoreQueryBuilder.FilterFunctionBuilder labelFunction=new FunctionScoreQueryBuilder.FilterFunctionBuilder(labelScoreFunction);
filterFunctionBuilders[functionNum]=labelFunction;
functionNum++;
}
//2.省份num衰减评分
//利用衰减函数,设定在给定省份id(仅此id)对应的得分为0.5(以id+偏移量为原点,搜索偏移量范围得分为decay)
ScoreFunctionBuilder<LinearDecayFunctionBuilder> provinceScoreFunction = ScoreFunctionBuilders.linearDecayFunction("provinceNum", provinceList.indexOf(province)+0.1, 0.1, 0, 0.5);
FunctionScoreQueryBuilder.FilterFunctionBuilder provinceFunction=new FunctionScoreQueryBuilder.FilterFunctionBuilder(provinceScoreFunction);
filterFunctionBuilders[functionNum]=provinceFunction;
return filterFunctionBuilders;
}