《Practical BM25》系列文章来自于elastic官方博客,共分为三部分,讲解了Elasticsearch的默认相似度算法BM25的原理。本篇为第一部分的中文翻译,原文链接 Practical BM25 - Part 1: How Shards Affect Relevance Scoring in Elasticsearch

背景

我们在Elasticsearch 5.0中,把默认的相似度算法换成了Okapi_BM25,用它来计算某条查询结果的得分。我不想在这篇文章中对BM25与其他的备选算法做深入的对比,如果你想学习BM25的理论,可以看看Elastic{ON} 2016上的BM25 Demystified这个演讲。在本文中,我希望能从实践的角度来介绍BM25,包括哪些参数是可用的,以及什么会影响评分。

记住,这篇文章主要关于对文本进行打分。也就是说,我们关注的是用搜索功能的用户。如果你只是索引一些日志或性能指标,并在查询结果时只是按照特定的元数据/数字顺序(如时间戳)排序,那么这篇文章大概只能用来满足你的好奇心。

理解分片如何影响评分

为了让你能跟上我的节奏,我们首先要理解的是,分片数大于1是如何影响打分的,因为Elasticsearch在每个索引上默认使用5个主分片。我们先创建一个叫做"people"的索引。我这里给出的settings其实是默认的情形(因此无需特意去定义),我这里这样定义了一下是为了进行演示。我将用我名字的变体(“Shane Connelly”) 来进行演示,你在尝试时,可以用任意的名字来进行替换。

PUT people
{
  "settings": {
    "number_of_shards": 5,
    "index" : {
        "similarity" : {
          "default" : {
            "type" : "BM25"
          }
        }
    }
  }
}

现在,让我添加一篇文档进去,然后进行搜索,首先,只把我的名字加进去。

PUT /people/_doc/1
{
  "title": "Shane"
}
GET /people/_doc/_search
{
    "query": {
        "match": {
             "title": "Shane"
         }
      }
}

你会获得一条结果,它的得分是0.2876821。我们稍后会研究这个得分是怎么来的。但我们先来看看,当我们添加一些我的名字的不同变体为内容的文档进去时会发生什么。

PUT /people/_doc/2
{
  "title": "Shane C"
}
PUT /people/_doc/3
{
  "title": "Shane Connelly"
}
PUT /people/_doc/4
{
  "title": "Shane P Connelly"
}

现在,再次执行和刚才相同的查询。

GET /people/_doc/_search
{
    "query": {
        "match": {
             "title": "Shane"
         }
      }
}

这时,你应该能得到4条结果,但如果你看看各条结果的得分,可能会一脸懵逼。文档1和3的分数都是0.2876821,但文档2的得分是0.19856805,文档4的得分是0.16853254。这简直是在劝退新人。我们能看到,文档2和3非常相似——它们都有2个term,并且都匹配了"shane",但是文档2的分数更低。你可能开始假设,"C"和"Connelly"的打分可能有什么区别,但事实上,这与文档们如何写入各个分片有关。

提醒一下,Elasticsearch把文档分别写入不同的分片中,每个分片保存了一部分数据(文档)。

GET /_cat/shards/people?v

如果你执行它,你会看到分片2中有2篇文档,而分片3和4都只有1篇文档。(分片0和1中还没有文档)这意味着,对于"shane"这个term,在这几个分片中的出现次数是不同的,而这就是造成得分差异的根本原因。默认情况下,Elasticsearch以每个分片为基准计算评分

有人刚把几篇文档写入索引中,就开始问“为什么文档A的得分比文档B的得分更高/低呢?”之类的问题,有些时候问题的答案就是用户的分片数与文档数比率相对较高,导致了不同分片之间的评分偏差严重。有几种方法可以让不同的分片评分更一致:

  1. 你写入索引的文档越多,分片的term统计就更一般化。当文档足够多时,你可能就难以察觉到不同分片term统计及得分之间细微的差异了。
  2. 你可以用更少的分片数来减少词频的统计偏差。例如,如果我们将索引settings中number_of_shards设置为1,我们就能得到完全不同的分数。我们会看到文档1的分数是0.13245322,文档2和3的分数都是0.105360515,文档4的分数是0.0874691。对于不同的主分片数量,我们有一些折中处理,这个问题在我们的quantitative cluster sizing webinar讨论过。
  3. 你可以在请求中添加?search_type=dfs_query_then_fetch参数,它会先收集分布词频(DFS = Distributed Frequency Search 分布频度搜索),然后用它们来计算得分。事实上,这样得出的结果与只有1个分片时是一样的。我们观察一下有和没有"search_type"参数时,结果有何不同:
GET /people/_doc/_search?search_type=dfs_query_then_fetch
{
    "query": {
        "match": {
             "title": "Shane"
         }
      }
}

它的结果与设置了number_of_shards=1时相同。你可能要问,“这样能生成更精确的评分,为什么它不是默认开启的呢?”答案是在收集所有统计信息的过程中,它增加了额外的一次请求往返,在某些场景中(即分数的精确性没有速度重要时),这样多一次请求是没有必要的。另外,当分片中有足够的数据是,统计数字将非常接近其他分片,进行多次请求就更无必要。如果你有足够多的数据,search_type=dfs_query_then_fetch这个参数只有在分片间数据持续性地不均匀分布时才有用,例如在一些自定义路由的场景中。

现在,我们知道了分片是如何影响评分的(也知道了如何调整它)。下一步,我们将会研究BM25算法,看看不同的变量是如何起作用的。