Function score查询的应用及源码解析

2021-02-03 12:04:21 浏览数 (1)

概念介绍

function_score查询可以在原有的查询结果算分的基础上,对每个文档计算一个新的算分,而计算的规则取决于应用的具体的funcion以及相关的一些选项。

看一个例子,

代码语言:javascript复制
GET kibana_sample_data_ecommerce/_search?
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "manufacturer": "Elitelligence"
        }
      },
      "boost": "5",
      "weight": 2,
      "boost_mode": "multiply"
    }
  }
}

上面这个查询,它的执行过程是这样的,首先执行查询

代码语言:javascript复制
GET kibana_sample_data_ecommerce/_search?
{
  "query": {
    "match": {
      "manufacturer": "Elitelligence"
    }
  }
}

比如某个文档查询处理算分是1.6829565,function指定的函数是weight,ES默认会用这个值乘以当前的算分,然后因为boost的值是5,并且boost_mode指定的计算方法是multiply,所以最终的算分是:

代码语言:javascript复制
1.6829565 * 2 * 5 = 16.829565

boost_mode : 决定 old_score 和 加强score 如何合併。score_mode : 决定functions裡面的加强score们怎麽合併,会先合併加强score们成一个总加强score,再使用总加强score去和old_score做合併,换言之就是会先执行score_mode,再执行boost_mode。

boost_mode除了multiply还有一些其它的选项,比如max,sum等,具体可以看官方文档。而function除了weight之外,还有script_score, random_scorefield_value_factordecay functions等,下个章节讲应用场景会有一些涉及到。

一些应用的场景

比如CSDN博客网站,在站内搜索博客时,产品经理可能希望除了基本的相关度之外,把博客点赞数较高的相对排名较高。这就是希望点赞数影响进本的搜索算分。就是一个function sorce典型的应用场景。

插入一些测试数据,

代码语言:javascript复制
PUT /blogs/_doc/2
{
  "content": "We like elasticsearch",
  "votes":   2
}


GET blogs/_search
{
  "query": {
    "match": {
      "content": "elasticsearch"
    }
  }
}

正常情况下,如果我们执行下面这个查询语句

代码语言:javascript复制
GET blogs/_search
{
  "query": {
    "match": {
      "content": "elasticsearch"
    }
  }
}

显然文档2的评分会更高,因为文档2更短,如果我们使用function score查询,

代码语言:javascript复制
GET blogs/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "content": "elasticsearch"
        }
      },
      "field_value_factor": {
        "field": "likes",
        "factor": 2,
        "modifier": "sqrt",
        "missing": 1
      }
    }
  }
}

结果是文档1的评分更高了,原因在于field_value_factor这个funcion会执行下面的规则计算一个得分,然后乘以原始的算分。

代码语言:javascript复制
sqrt(2 * doc['likes'].value)

其中的modifier还可以是其它的数学公式,比如log,详细的可以参考官方文档。missing的意思是说,如果某个文档的likes字段是空的,就默认它的值是1。

同样的例子,比如有一个商品索引,搜索时希望在相关度排序的基础上,销量(sales)更高的商品能排在靠前的位置,也是类似的方案实现。

再说一个应用场景,比如网站的广告,为了提高展示率,希望广告能随机展示出来,不然就总是展示某几个算分高的。但是可能又希望同一个用户看到的广告排名是一致的。这种场景,random_score就排上用场了,我们可以使用用户id作为随机的seed。

查询语句类似如下:

代码语言:javascript复制

GET kibana_sample_data_ecommerce/_search?
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "manufacturer": "优衣库"
        }
      },
      "random_score": {
        "seed": "userid"
      }
    }
  }
}

源码层面的分析

源码版本基于ES7.10

function score的核心处理类是AFunctionScoreQuery类,这个类的生成是在FunctionScoreQueryBuilder,如下图所示:

FunctionScoreQueryBuilder.java

我们基于一个具体的查询示例来分析源码:

代码语言:javascript复制
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "content": "elasticsearch"
        }
      },
      "field_value_factor": {
        "field": "likes",
        "factor": 2,
        "modifier": "sqrt",
        "missing": 1
      }
    }
  }
}

基于我们上面的示例,这里调用第2个构造函数,因为我们这里只有一个function,boostMode没有指定,用的是默认的multiply。minScore是设置一个最低算分的阈值,就是结果不能低于这个,没有指定默认是null。maxBoost是最高算分的阈值,没有指定的话默认是FLT_MAX。

代码语言:javascript复制
FLT_MAX的定义则是
#define FLT_MAX 3.402823466e 38F

Luncene的算分(score),Weight类是Search过程中很重要的类,它负责生成Scorer,这是一个所有命中文档的迭代器,IndexSearch类调用的createWeight最终会调用到实际的Query(这里就是FunctionScoreQuery)中的方法,

FunctionScoreQuery.java

拿到Weight类之后,上层通用的流程会创建Scorer(本示例中是FunctionFactorScorer),然后遍历符合条件的文档计算算分(score方法)。

FunctionScoreQuery.java

上面中,factor是最终要和原始算分相乘(默认是相乘)的计算因子。这个因子是怎么算出来的呢?计算规则是调用了FieldValueFactorFunction类的score方法。

FieldValueFactorFunction.java

看到这里,你应该已经看出来了,这里最终的算分就是

代码语言:javascript复制
sqrt(2 * doc['likes'].value) * 原始算分

0 人点赞