1. Elasticsearch中的相关性计算

在正式进入算法解析阶段之前,先一步一步的补足相关的概念知识,这会帮助我们更好的学习和理解。

1.1 什么是相关性评分(relevance score)?

相关性评分(relevance score)是衡量每个文档与输入查询匹配的程度。默认情况下,Elasticsearch根据相关性评分对匹配的搜索结果进行排序。

相关性评分是一个正浮点数,在Search API的score元数据字段中返回。score越高,说明文档越相关。。。

一个简单的示例,首先通过bulk API 或者你熟悉的方式向索引里写入一些数据,这里以书名为例。

POST _bulk
{ "index" : { "_index" : "book_info", "_id" : "1" } }
{ "book_name" : "《大学》" }
{ "index" : { "_index" : "book_info", "_id" : "2" } }
{ "book_name" : "《中庸》" }
{ "index" : { "_index" : "book_info", "_id" : "3" } }
{ "book_name" : "《论语》" }
{ "index" : { "_index" : "book_info", "_id" : "4" } }
{ "book_name" : "《孟子》" }
{ "index" : { "_index" : "book_info", "_id" : "5" } }
{ "book_name" : "《道德经》" }
{ "index" : { "_index" : "book_info", "_id" : "6" } }
{ "book_name" : "《诗经》" }
{ "index" : { "_index" : "book_info", "_id" : "7" } }
{ "book_name" : "《春秋》" }

然后执行一个最简单的查询,搜索索引内书名匹配“诗经”这两个字的书籍信息

GET /book_info/_search
{
  "query": {
    "match": { "book_name": "诗经" }
  }
}

//得到以下响应
{
  "took" : 2,
........
    "max_score" : 2.916673,
    "hits" : [
      {
        "_index" : "book_info",
        "_type" : "_doc",
        "_id" : "6",
        "_score" : 2.916673,
        "_source" : {
          "book_name" : "《诗经》"
        }
      },
      {
        "_index" : "book_info",
        "_type" : "_doc",
        "_id" : "5",
        "_score" : 0.99958265,
        "_source" : {
          "book_name" : "《道德经》"
        }
      }
    ]
  }
}

可以看到结果如同预期的,《诗经》这本书的相关性评分为 2.916673 ,而第二本书因为只匹配了一个“经”字,所以得分较低。现在我们的目的就是彻底吃透这两个分数是如何计算的,以应对实际使用时的各种问题。

1.2 相关度评分是如何计算的?

Elasticsearch是基于 Lucene 之上的搜索引擎,这部分深入的内容以及概念留到后面补充,否则只会出现片面的描述或者陷入递归式的探究中。现在,先让我们囫囵吞枣的理解眼前的事物。

Elasticsearch中的相关性评分计算可以参考Elasticsearch文档相似模块的描述,传送门:Elasticsearch | Index Modules Similarity

在不做任何配置,默认的情况下我们可以使用以下三种相似度评分算法:

  • BM25:Okapi BM 25算法。在Elasticearch和Lucene中默认使用的算法。
  • classic: 在7.0.0中标记为过时。基于TF/IDF 算法,以前在Elasticearch和Lucene中的默认值。
  • boolean:一个简单的布尔相似度算法,当不需要全文排序时可以使用,并且分数应该只基于查询项是否匹配。布尔相似度给查询一个简单的分数,等价于设置的Query Boost。

通过以上描述我们可以了解到,Elasticsearch中默认的评分算法是BM25算法,且其他两个选项一个被标记过时,一个不适用于全文检索排序。现在实际尝试一下上面提到的三种算法,由于classic算法已经被标记过时,这里直接在Mapping中使用classic会直接抛出异常并提示我们可以使用脚本自定义实现原本的classic算法

{
        "type" : "illegal_argument_exception",
        "reason" : "The [classic] similarity may not be used anymore. Please use the [BM25] similarity or build a custom [scripted] similarity instead."
}

按照文档中给出的示例编写索引Mapping:

//删除之前创建的索引
DELETE book_info

//创建自定义的索引并制定字段类型、相关度评分算法
PUT book_info
{
  "mappings": {
    "properties": {
      //默认字段依旧采用BM25,并且对该字段赋值时自动复制到下面两个字段
      "book_name": {
        "type": "text",
        "similarity": "BM25",
        "copy_to": [
          "book_name_classic",
          "book_name_boolean"
        ]
      },
      //这个字段使用classic相关度算法
      "book_name_classic": {
        "type": "text",
        "similarity": "my_classic"
      },
      //这个字段使用boolean相关度算法
      "book_name_boolean": {
        "type": "text",
        "similarity": "boolean"
      }
    }
  },
  "settings": {
    "number_of_shards": 1,
    "similarity": {
      "my_classic": {
        "type": "scripted",
        "script": {
          "source": "double tf = Math.sqrt(doc.freq); double idf = Math.log((field.docCount+1.0)/(term.docFreq+1.0)) + 1.0; double norm = 1/Math.sqrt(doc.length); return query.boost * tf * idf * norm;"
        }
      }
    }
  }
}

之后用与上面相同的Bulk请求填充一下数据,即可观察相关性算法配置的结果,在这里不对结果进行解析。

GET /book_info/_search
{
  "query": {
    "match": {
      "book_name": {
        "query":  "诗经"
      }
    }
  }
}
GET /book_info/_search
{
  "query": {
    "match": {
      "book_name_classic": {
        "query":  "诗经"
      }
    }
  }
}
GET /book_info/_search
{
  "query": {
    "match": {
      "book_name_boolean": {
        "query":  "诗经"
      }
    }
  }
}

2. BM 25 算法

通过第一章的描述,我们知道了现在在Elasticsearch中的相关性评分默认采用BM25相似度算法,下面正式进入算法的学习阶段。

BM25全称Okapi BM25。Okapi 是使用它的第一个系统的名称,即Okapi信息检索系统,BM则是best matching的缩写。

BM25是基于TF-IDF算法并做了改进,基于概率模型的文档检索算法,目前BM25及其较新的变体(例如BM25F)代表了文档检索中使用的最先进的TF/IDF类检索功能。

现在,抛开中文分词器、同义词、停词等一切可能的干扰项,我们就使用最基本的Standard分词器,准备一点英文文档数据:

PUT _bulk
{ "index" : { "_index" : "people", "_id" : "1" } }
{ "title": "Shane" }
{ "index" : { "_index" : "people", "_id" : "2" } }
{ "title": "Shane C" }
{ "index" : { "_index" : "people", "_id" : "3" } }
{ "title": "Shane Connelly" }
{ "index" : { "_index" : "people", "_id" : "4" } }
{ "title": "Shane P Connelly" }

下面是BM25算法的标准公式,表示给定一个查询Q,包含关键字 q{1},...,q{n},文档D的BM 25分数计算公式为

Elasticsearch BM25相关度算法超详细解释-LMLPHP

现在详细解释等式每个部分的含义

2.1 搜索项 qi及词频TF

\(q_i\) : 查询项,例如我搜索“Shane”,只有一个查询项,所以 \(q_0\) 是“Shane”。

如果我用英语搜索“Shane Connelly”,Elasticsearch将看到空格并将其标记为两个Term:

  • \(q_0\) :Shane
  • \(q_1\): Connelly

\(f(q_i,D)\) : D 是文档, Term Frequency (TF)是指Term在文档中出现的频率,即词频。Term在文档中出现的权重与频率成正比。用最通俗易懂的话来说就是查询的词语在文档中出现了多少次。

这很有直觉意义,例如我正在搜索Elasticsearch,一篇文章内提到了一次可能只是简单的引用,如果文章中出现了很多次Elasticsearch那就更有可能与我们的搜索内容相关。

2.2 逆文档频率 IDF

\(IDF(q_i)\): 查询项的逆文档频率,衡量这个词提供了多少信息,也就是说它在所有文档中是常见的还是罕见的。这

其中的含义是如果一个搜索词是非常罕见的词语(例如专业术语)且在某个文档中匹配了,则分数会提高;反之对于非常常见的匹配词降低分数。

TF-IDF中原本的逆文档频率公式是这样的:

\[IDF(q_i,D) = log{N \over |{d\in D}:t\in d|}\]
08-24 05:02