ES搜索框架--自定义评分规则

发布时间 2023-04-10 20:42:43作者: 脑袋凉凉

一、评分规则需求

按照用户画像(不同的标签分数)和用户省份在用户查询时,对查询结果进行自定义评分


二、ES自定义评分方式

参考:

博客:https://blog.csdn.net/W2044377578/article/details/128636611

官网:https://www.elastic.co/guide/en/elasticsearch/guide/master/function-score-query.html

重点仔细看官方文档,介绍的很详细,下面只是我的案例。

image

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"
    }
  }
}

结合参数与下方的图像函数进行返回值的理解:

image


以上这些评分规则都可以综合起来写入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;
    }