ElasticSearch学习(二):相关性评分计算原理简介

2023-11-06

我们前面已经发现搜索后,每条匹配结果都会有一个_score字段,以以下结果为例:
搜索条件为:

{
  "query": {
    "match": {
      "title": "马鲁斯"
    }
  }
}

得到一条结果集为

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 5.61129,
    "hits": [
      {
        "_index": "indexA",
        "_type": "question_type",
        "_id": "254",
        "_score": 5.61129,
        "_routing": "761002",
        "_source": {
          "title": "马鲁斯英雄转职攻略"
        }
      }
  }
}

_score字段的值是5.61129,即代表着ES计算出来此条记录与搜索条件的相关性得分。得分越高,一般认为越符合搜索预期。

实例分析

第一篇文章里也简单提过,ES使用了一种名为词频/逆向文档频率(TD/IDF)的评分原则。
由于ES的索引原理是基于词的倒排索引,因此相关性评分也是基于词生成,简单来说,就是计算搜索条件内,每个词汇的相关性得分,然后进行相加后得到最终整个搜索条件的相关性得分。

我们再来回顾一下ES的评分原则:

  1. 搜索的词在文档中出现的次数越多,相关性越高
    词频原则这个比较好理解,如果一篇文本反复提到某个词,那么文本想表达的含义就与这个词关联性越大

  2. 搜索的词在所有文档中出现的次数越少,相关性越高
    这个逆向文档频率原则其实也可以用一句古话来形容:物以稀为贵。 其他的文档很少有描述你这个词,这个文档就很有可能是搜索者想看到的结果,

  3. 搜索的词占文档总长度比例越大,相关性越高
    这个原则没有体现在总的评分原则的命名上,但是仔细一想确实是比较合理的。文字数量越多,其表达的意思越会出现变数,虽然有关键词出现,但是如果总文档很长的话,可能并不是我们想要的结果。相对的,如果我们想要搜索的文本就已经占了文档的相当一部分比例,那么其相关性得分有所增加是非常合情合理的。

我们可以通过在查询请求体后面增加?explain参数来查看详细的相关性得分计算过程:

GET indexA/_search?explain
{
  "query": {
    "match": {
      "title": "马鲁斯"
    }
  }
}
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "hits": {
    "total": 12,
    "max_score": 5.61129,
    "hits": [
      {
        "_shard": "[test][0]",
        "_node": "blBEsWJQSAiQt2mHUQ9AJg",
        "_index": "test",
        "_type": "question_type",
        "_id": "254",
        "_score": 5.61129,
        "_routing": "761002",
        "_source": {
          "title": "马鲁斯英雄转职攻略"
        },
        "_explanation": {
          "value": 5.61129,
          "description": "sum of:",
          "details": [
            {
              "value": 5.61129,
              "description": "weight(title:马鲁斯 in 188) [PerFieldSimilarity], result of:",
              "details": [
                {
                  "value": 5.61129,
                  "description": "score(doc=188,freq=1.0 = termFreq=1.0\n), product of:",
                  "details": [
                    {
                      "value": 4.513055,
                      "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
                      "details": [
                        {
                          "value": 12,
                          "description": "docFreq",
                          "details": []
                        },
                        {
                          "value": 1139,
                          "description": "docCount",
                          "details": []
                        }
                      ]
                    },
                    {
                      "value": 1.2433463,
                      "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
                      "details": [
                        {
                          "value": 1,
                          "description": "termFreq=1.0",
                          "details": []
                        },
                        {
                          "value": 1.2,
                          "description": "parameter k1",
                          "details": []
                        },
                        {
                          "value": 0.75,
                          "description": "parameter b",
                          "details": []
                        },
                        {
                          "value": 13.633889,
                          "description": "avgFieldLength",
                          "details": []
                        },
                        {
                          "value": 7.111111,
                          "description": "fieldLength",
                          "details": []
                        }
                      ]
                    }
                  ]
                }
              ]
            },
            {
              "value": 0,
              "description": "match on required clause, product of:",
              "details": [
                {
                  "value": 0,
                  "description": "# clause",
                  "details": []
                },
                {
                  "value": 1,
                  "description": "*:*, product of:",
                  "details": [
                    {
                      "value": 1,
                      "description": "boost",
                      "details": []
                    },
                    {
                      "value": 1,
                      "description": "queryNorm",
                      "details": []
                    }
                  ]
                }
              ]
            }
          ]
        }
      }
    ]
  }
}

可以看到,每条记录的返回结果多了一个_explanation字段,里面详细描述了,本文档的相关性得分是如何计算出来的。

                    {
                      "value": 4.513055,
                      "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
                      "details": [
                        {
                          "value": 12,
                          "description": "docFreq",
                          "details": []
                        },
                        {
                          "value": 1139,
                          "description": "docCount",
                          "details": []
                        }
                      ]
                    },

这一段说明,是搜索词的逆向文档频率得分,通过details字段发现,一共收录了有1139篇内容,其中有12篇包含了我们要搜索的词,算是比较罕有的词汇。
因此逆向文档频率原则的得分为:4.513055description字段则是描述计算得分的公式。

{
                      "value": 1.2433463,
                      "description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
                      "details": [
                        {
                          "value": 1,
                          "description": "termFreq=1.0",
                          "details": []
                        },
                        {
                          "value": 1.2,
                          "description": "parameter k1",
                          "details": []
                        },
                        {
                          "value": 0.75,
                          "description": "parameter b",
                          "details": []
                        },
                        {
                          "value": 13.633889,
                          "description": "avgFieldLength",
                          "details": []
                        },
                        {
                          "value": 7.111111,
                          "description": "fieldLength",
                          "details": []
                        }
                      ]
                    }

这一段则表明了,文档词频的计算得分(已包含原则三的文档长度占比计算),其得分为1.2433463, 与逆向文档频率的得分相乘,得到我们最终的_score得分。

具体计算公式

实际上知道了ES默认相关性得分的计算原则以及explain结果分析已经可以应付日常大部分问题,下面简单介绍一些具体的计算公式,不算复杂,个人觉得记不住详细的公式问题也不大,准确理解这个公式制定背后的原理,在实际场景问题中能分辨对搜索结果计算影响较大的是什么就足够。

词频的计算公式如下:

tf(t in d) = √frequency

即,词t在文档d中的词频得分为:词语出现次数的平方根。 这样可以避免某个词反复出现而显著地提升词频得分。

逆向词频的得分计算公式为:

idf(t) = 1 + log ( numDocs / (docFreq + 1))

docFreq为所有文档中出现词语的次数,numDocs为索引文档总数, 观察公式可以发现,出现次数与得分是负相关的关系,而且和词频一样,取对数避免显著影响评分

字段长度归一值

norm(d) = 1 / √numTerms

其含义为,搜索的字段中,含有词数平方根的倒数。相当于一个因数,含有词语越多的字段,整体评分度会有所下降。
所基于的还是词,不是字符串,说实话和初始理解的有些不一样,之前还以为只是一个字符串长度的占比。
这里可以看出,ES的整体匹配得分策略都是基于词的。

以上三点为ES所依赖的词频/逆向文档频率的三个原则的具体公式计算,但实际的得分计算还没有这么简单。
ES使用了Lucene 的实用评分函数,具体公式如下:

score(q,d)  =  
            queryNorm(q)  
          · coord(q,d)    
          · ∑ (           
                tf(t in d)   
              · idf(t)²      
              · t.getBoost() 
              · norm(t,d)    
            ) (t in q)    

对于一个查询q,文档d的相关性得分计算公式为:
查询归一化因子 * 协调因子 * (每个搜索词得分之和)

一个个简单介绍:
queryNorm(q)-查询归一化因子:这个值设置的目的是为了方便对比不同的查询结果得分,在实际应用的场景比较少用到,一般大家注意点都是同一次查询里不同文档的得分比较。
其计算公式为:查询里每个词的 IDF(逆向词频)的平方和。

coord(q,d)-协调因子:协调因子主要是为匹配度高的文档提供额外的得分加成。
其计算公式为:匹配词的数量/所有查询词的数量

没有协调因子的时候,搜索三个关键字,匹配一个词得分是1,匹配两个词得分是2,匹配三个词得分是3(假设每个词单独的得分都为1),
有了协调因子,匹配一个词的得分是1 * (1/3) = 0.333, 匹配两个词得分是 2 *(2/3)= 1.333, 三个词是 3 * (3/3) = 3
可能上面的描述不够准确,实际上不是为匹配度高的文档提供得分加成,而是削减了匹配度低的得分。只有全部匹配的文档才能获得100%的得分。

搜索词得分的实际计算原则有四个数据:
**tf(词频),idf(逆向词频),norm(字段长度归一值),boost(权重) **。前面三个上面已经介绍了,boost权重默认为1,查询时可自定义某个词语的权重,就可在计算得分时享受相关性得分的加成。

总结

本文总结了ES计算文档相关性得分的默认策略和公式,这是一个不算复杂且有效的策略,相信已经能解决大部分应用的问题。
总结起来评分有如下四个原则:

  1. 搜索词出现次数越多,得分越高
  2. 搜索词在整个索引中出现次数越少,得分越高
  3. 搜索字段中所有词语数量越少,得分越高
  4. 匹配字段越多,得分越高。
    前面三个都是针对单个词搜索得分,且都是对数级的增加,第四个是针对整体搜索而言。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ElasticSearch学习(二):相关性评分计算原理简介 的相关文章

随机推荐