ES搜索框架--自定义评分规则
一、评分规则需求
按照用户画像(不同的标签分数)和用户省份在用户查询时,对查询结果进行自定义评分
二、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; }