es Score Query优化查询

导读:本篇文章讲解 es Score Query优化查询,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

 

通过Function Score Query

 

function_score查询是处理分值计算过程的终极工具。它让你能够对所有匹配了主查询的每份文档调用一个函数来调整甚至是完全替换原来的_score。

实际上,你可以通过设置过滤器来将查询得到的结果分成若干个子集,然后对每个子集使用不同的函数。这样你就能够同时得益于:高效的分值计算以及可缓存的过滤器。

它拥有几种预先定义好了的函数:

weight

对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2 * _score。

field_value_factor

使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。

random_score

使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。

衰减函数(Decay Function) – linear,exp,gauss

将像publish_date,geo_location或者price这类浮动值考虑到_score中,偏好最近发布的文档,邻近于某个地理位置(译注:其中的某个字段)的文档或者价格(译注:其中的某个字段)靠近某一点的文档。

script_score

使用自定义的脚本来完全控制分值计算逻辑。如果你需要以上预定义函数之外的功能,可以根据需要通过脚本进行实现。

没有function_score查询的话,我们也许就不能将全文搜索得到分值和近因进行结合了。我们将不得不根据_score或者date进行排序;无论采用哪一种都会抹去另一种的影响。function_score查询让我们能够将两者融合在一起:仍然通过全文相关度排序,但是给新近发布的文档,或者流行的文档,或者符合用户价格期望的文档额外的权重。你可以想象,一个拥有所有这些功能的查询看起来会相当复杂。我们从一个简单的例子开始,循序渐进地对它进行介绍。

 

 

根据人气来提升(Boosting by Popularity)

 

假设我们有一个博客网站让用户投票选择他们喜欢的文章。我们希望让人气高的文章出现在结果列表的头部,但是主要的排序依据仍然是全文搜索分值。我们可以通过保存每篇文章的投票数量来实现:

PUT /blogposts/post/1
{
  "title":   "About popularity",
  "content": "In this post we will talk about...",
  "votes":   6
}

在搜索期间,使用带有field_value_factor函数的function_score查询将投票数和全文相关度分值结合起来:

GET /blogposts/post/_search
{
  "query": {
    "function_score": { 
      "query": { 
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": { 
        "field": "votes" 
      }
    }
  }
}

function_score查询会包含主查询(Main Query)和希望适用的函数。先会执行主查询,然后再为匹配的文档调用相应的函数。每份文档中都必须有一个votes字段用来保证function_score能够起作用。

在前面的例子中,每份文档的最终_score会通过下面的方式改变:

new_score = old_score * number_of_votes

它得到的结果并不好。全文搜索的_score通常会在0到10之间。而从下图我们可以发现,拥有10票的文章的分值大大超过了这个范围,而没有被投票的文章的分值会被重置为0。

es Score Query优化查询

modifier

为了让votes值对最终分值的影响更缓和,我们可以使用modifier。换言之,我们需要让头几票的效果更明显,其后的票的影响逐渐减小。0票和1票的区别应该比10票和11票的区别要大的多。

一个用于此场景的典型modifier是log1p,它将公式改成这样:

new_score = old_score * log(1 + number_of_votes)

log函数将votes字段的效果减缓了,其效果类似下面的曲线:

es Score Query优化查询

使用了modifier参数的请求如下:

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p" 
      }
    }
  }
}

可用的modifiers有:none(默认值),log,log1p,log2p,ln,ln1p,ln2p,square,sqrt以及reciprocal。它们的详细功能和用法可以参考field_value_factor文档

factor

可以通过将votes字段的值乘以某个数值来增加该字段的影响力,这个数值被称为factor:

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p",
        "factor":   2 
      }
    }
  }
}

添加了factor将公式修改成这样:

new_score = old_score * log(1 + factor * number_of_votes)

当factor大于1时,会增加其影响力,而小于1的factor则相应减小了其影响力,如下图所示:

es Score Query优化查询

boost_mode

将全文搜索的相关度分值乘以field_value_factor函数的结果,对最终分值的影响可能太大了。通过boost_mode参数,我们可以控制函数的结果应该如何与_score结合在一起,该参数接受下面的值:

  • multiply:_score乘以函数结果(默认情况)
  • sum:_score加上函数结果
  • min:_score和函数结果的较小值
  • max:_score和函数结果的较大值
  • replace:将_score替换成函数结果

如果我们是通过将函数结果累加来得到_score,其影响会小的多,特别是当我们使用了一个较低的factor时:

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p",
        "factor":   0.1
      },
      "boost_mode": "sum" 
    }
  }
}

上述请求的公式如下所示:

new_score = old_score + log(1 + 0.1 * number_of_votes)

es Score Query优化查询

max_boost

最后,我们能够通过制定max_boost参数来限制函数的最大影响:

GET /blogposts/post/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query":    "popularity",
          "fields": [ "title", "content" ]
        }
      },
      "field_value_factor": {
        "field":    "votes",
        "modifier": "log1p",
        "factor":   0.1
      },
      "boost_mode": "sum",
      "max_boost":  1.5 
    }
  }
}

无论field_value_factor函数的结果是多少,它绝不会大于1.5。

NOTE

max_boost只是对函数的结果有所限制,并不是最终的_score。

ry优化Elasticsearch搜索结果

在使用 Elasticsearch 进行全文搜索时,搜索结果默认会以文档的相关度进行排序,如果想要改变默认的排序规则,也可以通过sort指定一个或多个排序字段。

但是使用sort排序过于绝对,它会直接忽略掉文档本身的相关度(根本不会去计算)。在很多时候这样做的效果并不好,这时候就需要对多个字段进行综合评估,得出一个最终的排序。

function_score

在 Elasticsearch 中function_score是用于处理文档分值的 DSL,它会在查询结束后对每一个匹配的文档进行一系列的重打分操作,最后以生成的最终分数进行排序。它提供了几种默认的计算分值的函数:

  • weight:设置权重
  • field_value_factor:将某个字段的值进行计算得出分数。
  • random_score:随机得到 0 到 1 分数
  • 衰减函数:同样以某个字段的值为标准,距离某个值越近得分越高
  • script_score:通过自定义脚本计算分值

    它还有一个属性boost_mode可以指定计算后的分数与原始的_score如何合并,有以下选项:

  • multiply:将结果乘以_score

  • sum:将结果加上_score
  • min:取结果与_score的较小值
  • max:取结果与_score的较大值
  • replace:使结果替换掉_score

    接下来本文将详细介绍这些函数的用法,以及它们的使用场景。

weight

weight 的用法最为简单,只需要设置一个数字作为权重,文档的分数就会乘以该权重。

他最大的用途应该就是和过滤器一起使用了,因为过滤器只会筛选出符合标准的文档,而不会去详细的计算每个文档的具体得分,所以只要满足条件的文档的分数都是 1,而 weight 可以将其更换为你想要的数值。

field\_value\_factor

field\_value\_factor 的目的是通过文档中某个字段的值计算出一个分数,它有以下属性:

  • field:指定字段名

  •  

factor:对字段值进行预处理,乘以指定的数值(默认为 1)

  • modifier将字段值进行加工,有以下的几个选项:

    • none:不处理
    • log:计算对数
    • log1p:先将字段值 +1,再计算对数
    • log2p:先将字段值 +2,再计算对数
    • ln:计算自然对数
    • ln1p:先将字段值 +1,再计算自然对数
    • ln2p:先将字段值 +2,再计算自然对数
    • square:计算平方
    • sqrt:计算平方根
    • reciprocal:计算倒数

    举一个简单的例子,假设有一个商品索引,搜索时希望在相关度排序的基础上,销量(sales)更高的商品能排在靠前的位置,那么这条查询 DSL 可以是这样的:

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "雨伞"
        }
      },
      "field_value_factor": {
        "field": "sales",
        "modifier": "log1p",
        "factor": 0.1
      },
      "boost_mode": "sum"
    }
  }
}

这条查询会将标题中带有雨伞的商品检索出来,然后对这些文档计算一个与库存相关的分数,并与之前相关度的分数相加,对应的公式为:

_score = _score + log (1 + 0.1 * sales)

random\_score

这个函数的使用相当简单,只需要调用一下就可以返回一个 0 到 1 的分数。

它有一个非常有用的特性是可以通过seed属性设置一个随机种子,该函数保证在随机种子相同时返回值也相同,这点使得它可以轻松地实现对于用户的个性化推荐。

衰减函数

衰减函数(Decay Function)提供了一个更为复杂的公式,它描述了这样一种情况:对于一个字段,它有一个理想的值,而字段实际的值越偏离这个理想值(无论是增大还是减小),就越不符合期望。这个函数可以很好的应用于数值、日期和地理位置类型,由以下属性组成:

  • 原点(origin):该字段最理想的值,这个值可以得到满分(1.0)
  • 偏移量(offset):与原点相差在偏移量之内的值也可以得到满分
  • 衰减规模(scale):当值超出了原点到偏移量这段范围,它所得的分数就开始进行衰减了,衰减规模决定了这个分数衰减速度的快慢
  • 衰减值(decay):该字段可以被接受的值(默认为 0.5),相当于一个分界点,具体的效果与衰减的模式有关

    例如我们想要买一样东西:

  • 它的理想价格是 50 元,这个值为原点

  • 但是我们不可能非 50 元就不买,而是会划定一个可接受的价格范围,例如 45-55 元,±5 就为偏移量
  • 当价格超出了可接受的范围,就会让人觉得越来越不值。如果价格是 70 元,评价可能是不太想买,而如果价格是 200 元,评价则会是不可能会买,这就是由衰减规模和衰减值所组成的一条衰减曲线

    或者如果我们想租一套房:

  • 它的理想位置是公司附近

  • 如果离公司在 5km 以内,是我们可以接受的范围,在这个范围内我们不去考虑距离,而是更偏向于其他信息
  • 当距离超过 5km 时,我们对这套房的评价就越来越低了,直到超出了某个范围就再也不会考虑了

    衰减函数还可以指定三种不同的模式:线性函数(linear)、以 e 为底的指数函数(Exp)和高斯函数(gauss),它们拥有不同的衰减曲线:

衰减曲线

将上面提到的租房用 DSL 表示就是:

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "title": "公寓"
        }
      },
      "gauss": {
        "location": {
          "origin": { "lat": 40, "lon": 116 },
          "offset": "5km",
          "scale": "10km"
           }
         },
         "boost_mode": "sum"
    }
  }
}

我们希望租房的位置在40, 116坐标附近,5km以内是满意的距离,15km以内是可以接受的距离。

script\_score

虽然强大的 field\_value\_factor 和衰减函数已经可以解决大部分问题了,但是也可以看出它们还有一定的局限性:

  1. 这两种方式都只能针对一个字段计算分值
  2. 这两种方式应用的字段类型有限,field\_value\_factor 一般只用于数字类型,而衰减函数一般只用于数字、位置和时间类型

    这时候就需要 script\_score 了,它支持我们自己编写一个脚本运行,在该脚本中我们可以拿到当前文档的所有字段信息,并且只需要将计算的分数作为返回值传回 Elasticsearch 即可。

    注:使用脚本需要首先在配置文件中打开相关功能:

script.groovy.sandbox.enabled: true
script.inline: on
script.indexed: on
script.search: on
script.engine.groovy.inline.aggs: on

举一个之前做不到的例子,假如我们有一个位置索引,它有一个分类(category)属性,该属性是字符串枚举类型,例如商场、电影院或者餐厅等。现在由于我们有一个电影相关的活动,所以需要将电影院在搜索列表中的排位相对靠前。

之前的两种方式都无法给字符串打分,但是如果我们自己写脚本的话却很简单,使用 Groovy(Elasticsearch 的默认脚本语言)也就是一行的事:

return doc ['category'].value == '电影院' ? 1.1 : 1.0

接下来只要将这个脚本配置到查询语句中就可以了:

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "name": "天安门"
        }
      },
      "script_score": {
        "script": "return doc ['category'].value == '电影院' ? 1.1 : 1.0"
      }
    }
  }
}

或是将脚本放在elasticsearch/config/scripts下,然后在查询语句中引用它:

category-score.groovy:

return doc ['category'].value == '电影院' ? 1.1 : 1.0
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "name": "天安门"
        }
      },
      "script_score": {
        "script": {
         "file": "category-score"
        }
      }
    }
  }
}

script中还可以通过params属性向脚本传值,所以为了解除耦合,上面的 DSL 还能接着改写为:

category-score.groovy:

return doc ['category'].value == recommend_category ? 1.1 : 1.0
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "name": "天安门"
        }
      },
      "script_score": {
        "script": {
         "file": "category-score",
         "params": {
            "recommend_category": "电影院"
         }
        }
      }
    }
  }
}

这样就可以在不更改大部分查询语句和脚本的基础上动态修改推荐的位置类别了。

同时使用多个函数

上面的例子都只是调用某一个函数并与查询得到的_score进行合并处理,而在实际应用中肯定会出现在多个点上计算分值并合并,虽然脚本也许可以解决这个问题,但是应该没人愿意维护一个复杂的脚本吧。这时候通过多个函数将每个分值都计算出在合并才是更好的选择。

在 function\_score 中可以使用functions属性指定多个函数。它是一个数组,所以原有函数不需要发生改动。同时还可以通过score_mode指定各个函数分值之间的合并处理,值跟最开始提到的boost_mode相同。下面举两个例子介绍一些多个函数混用的场景。

第一个例子是类似于大众点评的餐厅应用。该应用希望向用户推荐一些不错的餐馆,特征是:范围要在当前位置的 5km 以内,有停车位是最重要的,有 Wi-Fi 更好,餐厅的评分(1 分到 5 分)越高越好,并且对不同用户最好展示不同的结果以增加随机性。

那么它的查询语句应该是这样的:

{
  "query": {
    "function_score": {
      "filter": {
        "geo_distance": {
          "distance": "5km",
          "location": {
            "lat": $lat,
            "lon": $lng
          }
        }
      },
      "functions": [
        {
          "filter": {
            "term": {
              "features": "wifi"
            }
          },
          "weight": 1
        },
        {
          "filter": {
            "term": {
              "features": "停车位"
            }
          },
          "weight": 2
        },
        {
            "field_value_factor": {
               "field": "score",
               "factor": 1.2
             }
        },
        {
          "random_score": {
            "seed": "$id"
          }
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}

注:其中所有以$开头的都是变量。

这样一个饭馆的最高得分应该是 2 分(有停车位)+ 1 分(有 wifi)+ 6 分(评分 5 分 \* 1.2)+ 1 分(随机评分)。

另一个例子是类似于新浪微博的社交网站。现在要优化搜索功能,使其以文本相关度排序为主,但是越新的微博会排在相对靠前的位置,点赞(忽略相同计算方式的转发和评论)数较高的微博也会排在较前面。如果这篇微博购买了推广并且是创建不到 24 小时(同时满足),它的位置会非常靠前。

{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "content": "$text"
        }
      },
      "functions": [
        {
          "gauss": {
            "createDate": {
              "origin": "$now",
              "scale": "6d",
              "offset": "1d"
            }
          }
        },
        {
          "field_value_factor": {
            "field": "like_count",
            "modifier": "log1p",
            "factor": 0.1
          }
        },
        {
          "script_score": {
            "script": "return doc ['is_recommend'].value && doc ['create_date'] > time ? 1.5 : 1.0",
            params: {
                "time": $time
            }
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

它的公式为:

_score * gauss (create_date, $now, "1d", "6d") * log (1 + 0.1 * like_count) * is_recommend ? 1.5 : 1.0

通过Function Score Query优化Elasticsearch搜索结果:function_score 是这种形式的DSL,单个函数模式

1

2

3

4

5

6

"function_score": {

    "query": {},

    "boost": "boost for the whole query",

    "FUNCTION": {},

    "boost_mode":"(multiply|replace|...)"

}

也可以同时使用多个函数, 通过filter筛选出来的文档会应用上对应的函数
多函数模式

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

"function_score": {

    "query": {},

    "boost": "boost for the whole query",

    "functions": [

        {

            "filter": {},

            "FUNCTION": {},

            "weight": number

        },

        {

            "FUNCTION": {}

        },

        {

            "filter": {},

            "weight": number

        }

    ],

    "max_boost": number,

    "score_mode": "(multiply|max|...)",

    "boost_mode": "(multiply|replace|...)",

    "min_score" : number

}

score_mode 指定 如何将 函数算出的分数 和 原来分数 _score 合并起来

1

2

3

4

5

6

7

8

9

10

11

multiply

 

sum scores are summed

 

avg scores are averaged

 

first the first function that has a matching filter is applied

 

max maximum score is used

 

min minimum score is used

score_mode 表示多个函数之间的关系,
boost_mode 表示单个函数 应用 weight的计算关系
weight是权重,每个函数可以有一个权重weight,默认是相乘的关系。

举个例子,
在score_mode 是 avg 的情况下,如果两个函数返回的分数是1和2,它们的权重分别是3和4,那么最后的得分是 (1*3+2*4)/(3+4) 而不是(1*3+2*4)/2.

函数算出的新分数 被限定不能超过 max_boost, max_boost 的默认值是FLT_MAX

boost_mode的取值有

1

2

3

4

5

6

multiply

replace

sum

avg

max

min

min_score 这个字段没有理解。

 

我们可以让score不走默认的,让他与我们某个field进行计算(加减乘除自己定义)得到一个结果作为score值

数据准备
follower_num:帖子阅读量

POST /forum/article/_bulk
{ "update": { "_id": "1"} }
{ "doc" : {"follower_num" : 5} }
{ "update": { "_id": "2"} }
{ "doc" : {"follower_num" : 10} }
{ "update": { "_id": "3"} }
{ "doc" : {"follower_num" : 25} }
{ "update": { "_id": "4"} }
{ "doc" : {"follower_num" : 3} }
{ "update": { "_id": "5"} }
{ "doc" : {"follower_num" : 60} }

将对帖子搜索得到的分数,跟follower_num进行运算,由follower_num在一定程度上增强帖子的分数
看帖子的人越多,那么帖子的分数就越高

GET /forum/article/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "java spark",
          "fields": ["title", "content"]
        }
      },
      "field_value_factor": {
        "field": "follower_num"
      }
    }
  }
}

结果:

{
  "took": 124,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 40.994698,
    "hits": [
      {
        "_index": "forum",
        "_type": "article",
        "_id": "5",
        "_score": 40.994698,
        "_source": {
          "articleID": "DHJK-B-1395-#Ky5",
          "userID": 3,
          "hidden": false,
          "postDate": "2017-03-01",
          "tag": [
            "elasticsearch"
          ],
          "tag_cnt": 1,
          "view_cnt": 10,
          "title": "this is spark blog",
          "content": "spark is best big data solution based on scala ,an programming language similar to java spark",
          "sub_title": "haha, hello world",
          "author_first_name": "Tonny",
          "author_last_name": "Peter Smith",
          "new_author_last_name": "Peter Smith",
          "new_author_first_name": "Tonny",
          "follower_num": 60
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "2",
        "_score": 6.8640785,
        "_source": {
          "articleID": "KDKE-B-9947-#kL5",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-02",
          "tag": [
            "java"
          ],
          "tag_cnt": 1,
          "view_cnt": 50,
          "title": "this is java blog",
          "content": "i think java is the best programming language",
          "sub_title": "learned a lot of course",
          "author_first_name": "Smith",
          "author_last_name": "Williams",
          "new_author_last_name": "Williams",
          "new_author_first_name": "Smith",
          "follower_num": 10
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "1",
        "_score": 1.3371139,
        "_source": {
          "articleID": "XHDK-A-1293-#fJ3",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-01",
          "tag": [
            "java",
            "hadoop"
          ],
          "tag_cnt": 2,
          "view_cnt": 30,
          "title": "this is java and elasticsearch blog",
          "content": "i like to write best elasticsearch article",
          "sub_title": "learning more courses",
          "author_first_name": "Peter",
          "author_last_name": "Smith",
          "new_author_last_name": "Smith",
          "new_author_first_name": "Peter",
          "follower_num": 5
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "4",
        "_score": 0.46640402,
        "_source": {
          "articleID": "QQPX-R-3956-#aD8",
          "userID": 2,
          "hidden": true,
          "postDate": "2017-01-02",
          "tag": [
            "java",
            "elasticsearch"
          ],
          "tag_cnt": 2,
          "view_cnt": 80,
          "title": "this is java, elasticsearch, hadoop blog",
          "content": "elasticsearch and hadoop are all very good solution, i am a beginner",
          "sub_title": "both of them are good",
          "author_first_name": "Robbin",
          "author_last_name": "Li",
          "new_author_last_name": "Li",
          "new_author_first_name": "Robbin",
          "follower_num": 3
        }
      }
    ]
  }
}

可以发现分数都特别的大,都在好几十。是因为它与我们的follower_num进行了乘法运算。

我们想让分数不要相差的这么离谱怎么办? modifier

GET /forum/article/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "java spark",
          "fields": ["title", "content"]
        }
      },
      "field_value_factor": {
        "field": "follower_num",
        "modifier": "log1p"
      }
    }
  }
}

结果

{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1.0189654,
    "hits": [
      {
        "_index": "forum",
        "_type": "article",
        "_id": "5",
        "_score": 1.0189654,
        "_source": {
          "articleID": "DHJK-B-1395-#Ky5",
          "userID": 3,
          "hidden": false,
          "postDate": "2017-03-01",
          "tag": [
            "elasticsearch"
          ],
          "tag_cnt": 1,
          "view_cnt": 10,
          "title": "this is spark blog",
          "content": "spark is best big data solution based on scala ,an programming language similar to java spark",
          "sub_title": "haha, hello world",
          "author_first_name": "Tonny",
          "author_last_name": "Peter Smith",
          "new_author_last_name": "Peter Smith",
          "new_author_first_name": "Tonny",
          "follower_num": 60
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "2",
        "_score": 0.53412914,
        "_source": {
          "articleID": "KDKE-B-9947-#kL5",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-02",
          "tag": [
            "java"
          ],
          "tag_cnt": 1,
          "view_cnt": 50,
          "title": "this is java blog",
          "content": "i think java is the best programming language",
          "sub_title": "learned a lot of course",
          "author_first_name": "Smith",
          "author_last_name": "Williams",
          "new_author_last_name": "Williams",
          "new_author_first_name": "Smith",
          "follower_num": 10
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "1",
        "_score": 0.14549617,
        "_source": {
          "articleID": "XHDK-A-1293-#fJ3",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-01",
          "tag": [
            "java",
            "hadoop"
          ],
          "tag_cnt": 2,
          "view_cnt": 30,
          "title": "this is java and elasticsearch blog",
          "content": "i like to write best elasticsearch article",
          "sub_title": "learning more courses",
          "author_first_name": "Peter",
          "author_last_name": "Smith",
          "new_author_last_name": "Smith",
          "new_author_first_name": "Peter",
          "follower_num": 5
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "4",
        "_score": 0.06186694,
        "_source": {
          "articleID": "QQPX-R-3956-#aD8",
          "userID": 2,
          "hidden": true,
          "postDate": "2017-01-02",
          "tag": [
            "java",
            "elasticsearch"
          ],
          "tag_cnt": 2,
          "view_cnt": 80,
          "title": "this is java, elasticsearch, hadoop blog",
          "content": "elasticsearch and hadoop are all very good solution, i am a beginner",
          "sub_title": "both of them are good",
          "author_first_name": "Robbin",
          "author_last_name": "Li",
          "new_author_last_name": "Li",
          "new_author_first_name": "Robbin",
          "follower_num": 3
        }
      }
    ]
  }
}

公式会变为,new_score = old_score * log(1 + number_of_votes),这样出来的分数会比较合理

再加个factor,可以进一步影响分数,new_score = old_score * log(1 + factor * number_of_votes)

GET /forum/article/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "java spark",
          "fields": ["title", "content"]
        }
      },
      "field_value_factor": {
        "field": "follower_num",
        "modifier": "log1p",
        "factor": 0.5
      }
    }
  }
}

结果

{
  "took": 10,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1.0189654,
    "hits": [
      {
        "_index": "forum",
        "_type": "article",
        "_id": "5",
        "_score": 1.0189654,
        "_source": {
          "articleID": "DHJK-B-1395-#Ky5",
          "userID": 3,
          "hidden": false,
          "postDate": "2017-03-01",
          "tag": [
            "elasticsearch"
          ],
          "tag_cnt": 1,
          "view_cnt": 10,
          "title": "this is spark blog",
          "content": "spark is best big data solution based on scala ,an programming language similar to java spark",
          "sub_title": "haha, hello world",
          "author_first_name": "Tonny",
          "author_last_name": "Peter Smith",
          "new_author_last_name": "Peter Smith",
          "new_author_first_name": "Tonny",
          "follower_num": 60
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "2",
        "_score": 0.53412914,
        "_source": {
          "articleID": "KDKE-B-9947-#kL5",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-02",
          "tag": [
            "java"
          ],
          "tag_cnt": 1,
          "view_cnt": 50,
          "title": "this is java blog",
          "content": "i think java is the best programming language",
          "sub_title": "learned a lot of course",
          "author_first_name": "Smith",
          "author_last_name": "Williams",
          "new_author_last_name": "Williams",
          "new_author_first_name": "Smith",
          "follower_num": 10
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "1",
        "_score": 0.14549617,
        "_source": {
          "articleID": "XHDK-A-1293-#fJ3",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-01",
          "tag": [
            "java",
            "hadoop"
          ],
          "tag_cnt": 2,
          "view_cnt": 30,
          "title": "this is java and elasticsearch blog",
          "content": "i like to write best elasticsearch article",
          "sub_title": "learning more courses",
          "author_first_name": "Peter",
          "author_last_name": "Smith",
          "new_author_last_name": "Smith",
          "new_author_first_name": "Peter",
          "follower_num": 5
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "4",
        "_score": 0.06186694,
        "_source": {
          "articleID": "QQPX-R-3956-#aD8",
          "userID": 2,
          "hidden": true,
          "postDate": "2017-01-02",
          "tag": [
            "java",
            "elasticsearch"
          ],
          "tag_cnt": 2,
          "view_cnt": 80,
          "title": "this is java, elasticsearch, hadoop blog",
          "content": "elasticsearch and hadoop are all very good solution, i am a beginner",
          "sub_title": "both of them are good",
          "author_first_name": "Robbin",
          "author_last_name": "Li",
          "new_author_last_name": "Li",
          "new_author_first_name": "Robbin",
          "follower_num": 3
        }
      }
    ]
  }
}

我们不想乘法,想加上follower_num

GET /forum/article/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "java spark",
          "fields": ["title", "content"]
        }
      },
      "field_value_factor": {
        "field": "follower_num",
        "modifier": "log1p",
        "factor": 0.5
      },
      "boost_mode": "sum"
    }
  }
}

结果

{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 2.1746066,
    "hits": [
      {
        "_index": "forum",
        "_type": "article",
        "_id": "5",
        "_score": 2.1746066,
        "_source": {
          "articleID": "DHJK-B-1395-#Ky5",
          "userID": 3,
          "hidden": false,
          "postDate": "2017-03-01",
          "tag": [
            "elasticsearch"
          ],
          "tag_cnt": 1,
          "view_cnt": 10,
          "title": "this is spark blog",
          "content": "spark is best big data solution based on scala ,an programming language similar to java spark",
          "sub_title": "haha, hello world",
          "author_first_name": "Tonny",
          "author_last_name": "Peter Smith",
          "new_author_last_name": "Peter Smith",
          "new_author_first_name": "Tonny",
          "follower_num": 60
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "2",
        "_score": 1.4645591,
        "_source": {
          "articleID": "KDKE-B-9947-#kL5",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-02",
          "tag": [
            "java"
          ],
          "tag_cnt": 1,
          "view_cnt": 50,
          "title": "this is java blog",
          "content": "i think java is the best programming language",
          "sub_title": "learned a lot of course",
          "author_first_name": "Smith",
          "author_last_name": "Williams",
          "new_author_last_name": "Williams",
          "new_author_first_name": "Smith",
          "follower_num": 10
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "1",
        "_score": 0.81149083,
        "_source": {
          "articleID": "XHDK-A-1293-#fJ3",
          "userID": 1,
          "hidden": false,
          "postDate": "2017-01-01",
          "tag": [
            "java",
            "hadoop"
          ],
          "tag_cnt": 2,
          "view_cnt": 30,
          "title": "this is java and elasticsearch blog",
          "content": "i like to write best elasticsearch article",
          "sub_title": "learning more courses",
          "author_first_name": "Peter",
          "author_last_name": "Smith",
          "new_author_last_name": "Smith",
          "new_author_first_name": "Peter",
          "follower_num": 5
        }
      },
      {
        "_index": "forum",
        "_type": "article",
        "_id": "4",
        "_score": 0.553408,
        "_source": {
          "articleID": "QQPX-R-3956-#aD8",
          "userID": 2,
          "hidden": true,
          "postDate": "2017-01-02",
          "tag": [
            "java",
            "elasticsearch"
          ],
          "tag_cnt": 2,
          "view_cnt": 80,
          "title": "this is java, elasticsearch, hadoop blog",
          "content": "elasticsearch and hadoop are all very good solution, i am a beginner",
          "sub_title": "both of them are good",
          "author_first_name": "Robbin",
          "author_last_name": "Li",
          "new_author_last_name": "Li",
          "new_author_first_name": "Robbin",
          "follower_num": 3
        }
      }
    ]
  }
}

boost_mode,可以决定分数与指定字段的值如何计算,multiply,sum,min,max,replace

max_boost,限制计算出来的分数不要超过max_boost指定的值

GET /forum/article/_search
{
  "query": {
    "function_score": {
      "query": {
        "multi_match": {
          "query": "java spark",
          "fields": ["title", "content"]
        }
      },
      "field_value_factor": {
        "field": "follower_num",
        "modifier": "log1p",
        "factor": 0.5
      },
      "boost_mode": "sum",
      "max_boost": 2
    }
  }
}

原文地址

上次写了关于 Elasticsearch 如何分词索引, 接着继续写 Elasticsearch 怎么计算搜索结果的得分(_score).

Elasticsearch 默认是按照文档与查询的相关度(匹配度)的得分倒序返回结果的. 得分 (_score) 就越大, 表示相关性越高.

所以, 相关度是啥? 分数又是怎么计算出来的? (全文检索和结构化的 SQL 查询不太一样, 虽然看起来结果比较’飘忽’, 但也是可以追根问底的)


在 Elasticsearch 中, 标准的算法是 Term Frequency/Inverse Document Frequency, 简写为 TF/IDF, (刚刚发布的 5.0 版本, 改为了据说更先进的 BM25 算法)

Term Frequency

某单个关键词(term) 在某文档的某字段中出现的频率次数, 显然, 出现频率越高意味着该文档与搜索的相关度也越高

具体计算公式是 tf(q in d) = sqrt(termFreq)

另外, 索引的时候可以做一些设置, “index_options”: “docs” 的情况下, 只考虑 term 是否出现(命中), 不考虑出现的次数.

 
  1. PUT /my_index

  2. {

  3. "mappings": {

  4. "doc": {

  5. "properties": {

  6. "text": {

  7. "type": "string",

  8. "index_options": "docs"

  9. }

  10. }

  11. }

  12. }

  13. }

  14.  

Inverse document frequency

某个关键词(term) 在索引(单个分片)之中出现的频次. 出现频次越高, 这个词的相关度越低. 相对的, 当某个关键词(term)在一大票的文档下面都有出现, 那么这个词在计算得分时候所占的比重就要比那些只在少部分文档出现的词所占的得分比重要低. 说的那么长一句话, 用人话来描述就是 “物以稀为贵”, 比如, ‘的’, ‘得’, ‘the’ 这些一般在一些文档中出现的频次都是非常高的, 因此, 这些词占的得分比重远比特殊一些的词(如’Solr’, ‘Docker’, ‘哈苏’)占比要低,

具体计算公式是 idf = 1 + ln(maxDocs/(docFreq + 1))

Field-length Norm

字段长度, 这个字段长度越短, 那么字段里的每个词的相关度也就越大. 某个关键词(term) 在一个短的句子出现, 其得分比重比在一个长句子中出现要来的高.

具体计算公式是 norm = 1/sqrt(numFieldTerms)

默认每个 analyzed 的 string 都有一个 norm 值, 用来存储该字段的长度,

用 “norms”: { “enabled”: false } 关闭以后, 评分时, 不管文档的该字段长短如何, 得分都一样.

 
  1. PUT /my_index

  2. {

  3. "mappings": {

  4. "doc": {

  5. "properties": {

  6. "text": {

  7. "type": "string",

  8. "norms": { "enabled": false }

  9. }

  10. }

  11. }

  12. }

  13. }

  14.  

最后的得分是三者的乘积 tf * idf * norm

以上描述的是最原始的针对单个关键字(term)的搜索. 如果是有多个搜索关键词(terms)的时候, 还要用到的 Vector Space Model

如果查询复杂些, 或者用到一些修改了分数的查询, 或者索引时候修改了字段的权重, 比如 function_score 之类的,计算方式也就又更复杂一些.

Explain

看上去 TF/IDF 的算法已经一脸懵逼吓跑人了, 不过其实, 用 Explain 跑一跑也没啥, 虽然各种开方, 自然对数的, Google一个科学计算器就是了.

举个例子

 
  1. /*先删掉索引, 如果有的话*/

  2. curl -XDELETE 'http://localhost:9200/blog'

  3.  
  4. curl -XPUT 'http://localhost:9200/blog/' -d '

  5. {

  6. "mappings": {

  7. "post": {

  8. "properties": {

  9. "title": {

  10. "type": "string",

  11. "analyzer": "standard",

  12. "term_vector": "yes"

  13. }

  14. }

  15. }

  16. }

  17. }'

  18.  

存入一些文档 (Water 随手加进去测试的.)

 
  1. curl -s -XPOST localhost:9200/_bulk -d '

  2. { "create": { "_index": "blog", "_type": "post", "_id": "1" }}

  3. { "title": "What is the best water temperature, Mr Water" }

  4. { "create": { "_index": "blog", "_type": "post", "_id": "2" }}

  5. { "title": "Water no symptoms" }

  6. { "create": { "_index": "blog", "_type": "post", "_id": "3" }}

  7. { "title": "Did Vitamin B6 alone work for you? Water?" }

  8. { "create": { "_index": "blog", "_type": "post", "_id": "4" }}

  9. { "title": "The ball drifted on the water." }

  10. { "create": { "_index": "blog", "_type": "post", "_id": "5" }}

  11. { "title": "No water no food no air" }

  12. '

  13.  

bulk insert 以后先用 Kopf 插件输出看一下, 5 个文档并不是平均分配在 5 个分片的, 其中, 编号为 2 的这个分片里边有两个文档, 其中编号为 0 的那个分片是没有分配文档在里面的.

 

es Score Query优化查询

 

接下来, 搜索的同时 explain

原本输出的 json 即使加了 pretty 也很难看, 换成 yaml 会好不少

  1. curl -XGET "http://127.0.0.1:9200/blog/post/_search?explain&format=yaml" -d '

  2. {

  3. "query": {

  4. "term": {

  5. "title": "water"

  6. }

  7. }

  8. }'

  9.  

输出如图(json)

 

es Score Query优化查询

 

可以看到五个文档都命中了这个查询, 注意看每个文档的 _shard

整个输出 yml 太长了, 丢到最后面, 只截取了其中一部分, 如图,

es Score Query优化查询

 

返回排名第一的分数是 _score: 0.2972674, _shard(2),

“weight(title:water in 0) [PerFieldSimilarity], result of:” 这里的 0 不是 _id, 只是 Lucene 的一个内部文档 ID, 可以忽略.

排名第一和第二的两个文档刚好是在同一个分片的, 所以跟另外三个的返回结果有些许不一样, 主要就是多了一个 queryWeight, 里面的 queryNorm 只要在同一分片下, 都是一样的, 总而言之, 这个可以忽略(至少目前这个例子可以忽略)

只关注 fieldWeight, 排名第一和第二的的 tf 都是 1,

在 idf(docFreq=2, maxDocs=2) 中, docFreq 和 maxDocs 都是针对单个分片而言, 2号分片一共有 2个文档(maxDocs), 然后命中的文档也是两个(docFreq).

所以 idf 的得分, 根据公式, 1 + ln(maxDocs/(docFreq + 1)) 是 0.59453489189

最后 fieldNorm, 这个 field 有三个词, 所以是 1/sqrt(3), 但是按官方给的这个公式怎么算都不对, 不管哪个文档. 后来查了一下, 说是 Lucene 存这个 lengthNorm 数据时候都是用的 1 byte来存, 所以不管怎么着都会丢掉一些精度. 呵呵哒了 = . =

最后的最后, 总得分 = 1 * 0.5945349 * 0.5 = 0.2972674.

同理其他的几个文档也可以算出这个得分, 只是都要被 fieldNorm 的精度问题蛋疼一把.

完整结果太长了, 贴个 gist
https://gist.github.com/xguox/077e18afe24f52f6e2b45efb0b4e304f


Elasticsearch 5 (Lucene 6) 的 BM25 算法

Elasticsearch 前不久发布了 5.0 版本, 基于 Lucene 6, 默认使用了 BM25 评分算法.

BM25 的 BM 是缩写自 Best Match, 25 貌似是经过 25 次迭代调整之后得出的算法. 它也是基于 TF/IDF 进化来的. Wikipedia 那个公式看起来很吓唬人, 尤其是那个求和符号, 不过分解开来也是比较好理解的.

总体而言, 主要还是分三部分, TF – IDF – Document Length

 

es Score Query优化查询

 

IDF 还是和之前的一样. 公式 IDF(q) = 1 + ln(maxDocs/(docFreq + 1))

f(q, D) 是 tf(term frequency)

|d| 是文档的长度, avgdl 是平均文档长度.

先不看 IDF 和 Document Length 的部分, 变成 tf * (k + 1) / (tf + k),

相比传统的 TF/IDF (tf(q in d) = sqrt(termFreq)) 而言, BM25 抑制了 tf 对整体评分的影响程度, 虽然同样都是增函数, 但是, BM25 中, tf 越大, 带来的影响无限趋近于 (k + 1), 这里 k 值通常取 [1.2, 2], 而传统的 TF/IDF 则会没有临界点的无限增长.

而文档长度的影响, 同样的, 可以看到, 命中搜索词的情况下, 文档越短, 相关性越高, 具体影响程度又可以由公式中的 b 来调整, 当设值为 0 的时候, 就跟之前 ‘TF/IDF‘ 那篇提到的 "norms": { "enabled": false } 一样, 忽略文档长度的影响.

综合起来,

k = 1.2

b = 0.75

idf * (tf * (k + 1)) / (tf + k * (1 – b + b * (|d|/avgdl)))

最后再对所有的 terms 求和. 就是 Elasticsearch 5 中一般查询的得分了.


Related:

http://opensourceconnections.com/blog/2015/10/16/bm25-the-next-generation-of-lucene-relevation/

What Is Relevance?

From:

Elasticsearch 的分数(_score)是怎么计算得出
Elasticsearch 5.X(Lucene 6) 的 BM25 相关度算法

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/14183.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!