相关度:从TF-IDF、BM25到对相关度的控制

什么是相关度

相关性描述的是⼀个⽂档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进⾏算分_score_score 的评分越高,相关度越高。

信息检索工具性能的3大指标

  1. 查准率 Precision:尽可能返回较少的无关文档;

  2. 查全率 Recall:尽可能返回较多的相关文档;

  3. 排序 Ranking:是否能按相关性排序。

    前两者更多与分词匹配相关,而后者则与相关性的判断与算分相关。

什么是TF-IDF 和 BM25

词频 TF(Term Frequency)

检索词在文档中出现的频度是多少?出现频率越高,相关性也越高。

关于TF的数学表达式,参考ES官网,如下:

tf(t in d) = √frequency 词 t 在文档 d 的词频( tf )是该词在文档中出现次数的平方根。

概念理解:比如说我们检索关键字“es”,“es”在文档A中出现了10次,在文档B中只出现了1次。我们不会认为文档B与“es”的相关性更高,而是文档A。

逆向文档频率 IDF(Inverse Document Frequency)

每个检索词在索引中出现的频率,频率越高,相关性越低。

关于 IDF 的数学表达式,参考ES官网,如下:

idf(t) = 1 + log ( numDocs / (docFreq + 1)) 词 t 的逆向文档频率( idf )是:索引中文档数量除以所有包含该词的文档数,然后求其对数。注意: 这里的log是指以e为底的对数,不是以10为底的对数。

概念理解:比如说检索词“学习ES”,按照Ik分词会得到两个Token【学习】【ES】,假设在当前索引下有100个文档包含Token “学习” ,只有10个文档包含Token “ES”。那么对于【学习】【ES】这两个Token来说,出现次数较少的 Token【ES】就可以帮助我们快速缩小范围找到我们想要的文档,所以说此时“ES”的权重就比“学习”的权重要高。

字段长度准则 field-length norm

字段的长度是多少?字段越短,字段的权重越高。检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大

关于 norm 的数学表达式,参考ES官网,如下:

norm(d) = 1 / √numTerms 字段长度归一值( norm )是字段中词数平方根的倒数。

以上三个因素——词频(term frequency)、逆向文档频率(inverse document frequency)和字段长度归一值(field-length norm)——是在索引时计算并存储的。最后将它们结合在一起计算单个词在特定文档中的权重。

Lucene 中的 TF-IDF 评分公式

该公式参考自官网:

score(q,d)  =
            queryNorm(q)
          · coord(q,d)
          · ∑ (
                tf(t in d)
              · idf(t)²
              · t.getBoost()
              · norm(t,d)
            ) (t in q)
  1. score(q,d) 文档d对查询q的相关性得分
  2. queryNorm(q) 查询的规范化因子
  3. coord(q,d) 协调因子
  4. 文档d的查询q中每个词t的权重之和
  5. tf(t in d) 文档d中t词的词频(出现次数)
  6. idf(t) t词的逆文档频率
  7. t.getBoost() 已应用于查询的boost
  8. norm(t,d) 是字段长度归一值,与检索时字段的Boost (如果存在)相结合。

BM25

整体而言BM25 就是对 TF-IDF 算法的改进,对于 TF-IDF 算法,TF(t) 部分的值越大,整个公式返回的值就会越大。

BM25 就针对这点进行来优化,随着TF(t) 的逐步加大,该算法的返回值会趋于一个数值。

BM25 有一个比较好的特性就是提供了两个可调参数:

k1、b、tf、L、tfScore 的关系如下图红框内所示(注:这里的 tf 即上式中的 f(qi,D))。

ES 中默认 k1=1.2 b=0.75

相关度控制

4种常用的相关度控制方式

  1. boost参数
  2. 改变查询方式
  3. rescore结果集重新评分
  4. 更改BM25的参数k1和b的值

boost 参数【常用】

我们检索博客时,我们一般会认为标题 title 的权重应该比内容 content 的权重大,那么这个时候我们就可以使用 boost 参数进行控制:

#测试数据
#创建index
PUT /blogs_index
{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 1
    }
  },
  "mappings": {
    "_doc": {
      "dynamic": false,
      "properties": {
        "id": {
          "type": "integer"
        },
        "author": {
          "type": "keyword"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_smart"
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word",
          "search_analyzer": "ik_smart"
        },
        "tag": {
          "type": "keyword"
        },
        "influence": {
          "type": "integer_range"
        },
        "createAt": {
          "type": "date",
          "format": "yyyy-MM-dd HH:mm"
        }
      }
    }
  }
}

# 导入数据
POST _bulk
{"index":{"_index":"blogs_index","_type":"_doc","_id":"1"}}
{"id":1,"author":"方才兄","title":"es的相关度","content":"这是关于es的相关度的文章","tag":[1,2,3],"influence":{"gte":10,"lte":12},"createAt":"2020-05-24 10:56"}
{"index":{"_index":"blogs_index","_type":"_doc","_id":"2"}}
{"id":2,"author":"方才兄","title":"相关度","content":"这是关于相关度的文章","tag":[2,3,4],"influence":{"gte":12,"lte":15},"createAt":"2020-05-23 10:56"}
{"index":{"_index":"blogs_index","_type":"_doc","_id":"3"}}
{"id":3,"author":"方才兄","title":"es","content":"这是关于关于es和编程的必看文章","tag":[2,3,4],"influence":{"gte":12,"lte":15},"createAt":"2020-05-22 10:56"}
{"index":{"_index":"blogs_index","_type":"_doc","_id":"4"}}
{"id":4,"author":"方才","title":"系统学习es","content":"这是关于es的文章,介绍了一点相关度的知识","tag":[1,2,3],"influence":{"gte":10,"lte":15},"createAt":"2020-05-24 10:56"}


GET /blogs_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": {
              "query": "es",
              "boost": 2
            }
          }
        },
        {
          "match": {
            "content": "es"
          }
        }
      ]
    }
  },
  "explain": true
}

boost 参数值范围:

  1. boost>1 相关度相对性提升
  2. 0<boost<1,相对性降低
  3. boost<0,贡献负分

注意:1)boost 可用于任何查询语句;2)这种提升或降低并不一定是线性的,新的评分 _score 会在应用权重提升之后被归一化 ,每种类型的查询都有自己的归一算法。

查询方式改变

通过使用不同的组合查询来实现对相关度的控制

  1. constant_scorequery嵌套一个 filter 查询,为任意一个匹配的文档指定一个常量评分,常量值为 boost 的参数值【默认值为1】 ,忽略 TF-IDF 信息。

    GET /blogs_index/_search
    {
        "query": {
            "constant_score" : {
                "filter" : {
                    "term" : { "title": "es"}
                },
                "boost" : 1.2
            }
        }
    }
    
  2. function_score queryFunction Score Query 允许我们修改通过 query 检索出来的文档的分数。在使用时,我们必须定义一个查询一个或多个函数,这些函数为查询返回的每个文档计算一个新分数。

    GET /blogs_index/_search
    {
      "query": {
        "function_score": {
          "query": {
            "match_all": {}
          },
          "boost": "5",
          "functions": [
            {
              "filter": {
                "match": {
                  "title": "es"
                }
              },
              "random_score": {},
              "weight": 23
            },
            {
              "filter": {
                "match": {
                  "title": "相关度"
                }
              },
              "weight": 42
            }
          ],
          "max_boost": 42,
          "score_mode": "max",
          "boost_mode": "multiply",
          "min_score": 10
        }
      },
      "explain": true
    }
    

    function_score query 的用法非常多,适用场景也比较广,比如说:

    1)通过文档中的字段值影响相关度,比如可以让博客的点赞数越多,相关度越高;

    2)随机分数【可应用于千人千面】;

    3)根据距离参考值的衰减函数计算相关度,比如说地理位置查询,距离参考点越远的,相关性越低;

    4)更复杂的场景也可以用自定义脚本完全控制评分计算,实现所需逻辑。

  3. dis_max query使用单个最佳匹配查询子句的分数。同时,也可以通过参数 tie_breaker 【默认值为0】控制其他查询子句的分数对 _score 的影响。dis_max query 有一个非常好的使用场景就是,利用参数 tie_breaker 能够确保满足多个条件的文档的相关性得分一定比只满足单个条件的文档的得分要高。

    #queries 下的查询子句间的布尔关系是OR。
    GET /blogs_index/_search
    {
      "query": {
        "dis_max": {
          "tie_breaker": 0.5,
          "boost": 1.2,
          "queries": [
            {
              "term": {
                "content": "es"
              }
            },
            {
              "match": {
                  
                "content": "相关度"
              }
            }
          ]
        }
      },
      "explain": true
    }
    
  4. boosting query【常用】boosting query 可用于有效降级与给定查询匹配的结果。与布尔查询中的“ NOT”子句不同的是,它仍会选择包含不良词的文档,但会降低其总体得分。

    positive:用于获取返回结果
    negative:对上述结果的相关性打分进行调整
    negative_boost:调整参数:升权(>1), 降权(>0 and <1)

    #希望检索title 包含“es”“相关性”的文章,同时认为如果content包含“编程”,那我们认为这个文档的相关性应该被降低
    GET /blogs_index/_search
    {
      "query": {
        "boosting": {
          "positive": {
            "bool": {
              "should": [
                {
                  "term": {
                    "title": "es"
                  }
                },
                {
                  "term": {
                    "title": "相关性"
                  }
                }
              ]
            }
          },
          "negative": {
            "term": {
              "content": "编程"
            }
          },
          "negative_boost": 0.2
        }
      },
      "explain": true
    }
    

    1)根据 positive 下的查询语句检索,得到结果集;
    2)在上述的结果集中,对于那些同时还匹配 negative 查询的文档,将通过文档的原始 _score 与 negative_boost 相乘的方式重新计算相关性得分。

    negative_boost 的值>1,是正向评分,增加匹配 negative 查询的文档的权重。

rescore 结果集重新评分

先query,再在结果集基础上 rescore。query 目前唯一支持的重新打分算法。参数 window_size 是每一分片进行重新评分的顶部文档数量。

#先检索content包含“es的相关度”或者 title 包含“es”的文档,再此基础上,对于前3个文档,利用match_phrase 重新计算相关度。
GET /blogs_index/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "content": {
              "query": "es的相关度",
              "minimum_should_match": "30%"
            }
          }
        },
        {
          "match": {
            "title": {
              "query": "es"
            }
          }
        }
      ]
    }
  },
  "rescore": {
    "window_size": 3,
    "query": {
      "rescore_query": {
        "match_phrase": {
          "content": {
            "query": "es的相关度",
            "slop": 50
          }
        }
      }
    }
  }
}

更改BM25 参数 k1 和 b 的值

k1 参数【默认值1.2】控制着词频结果在词频饱和度中的上升速度。b 参数【默认值0.75】控制着字段长归一值所起的作用。手动定义这两个参数的值从而去改变相关性算分。注意:一般情况不建议更改这两个参数值。

PUT /my_index
{
  "settings": {
    "similarity": {
      "my_bm25": {
        "type": "BM25",
        "b": 0.8,
        "k1": 1.5
      }
    }
  },
  "mappings": {
    "doc": {
      "properties": {
        "title": {
          "type": "text",
          "similarity": "my_bm25"
        }
      }
    }
  }
}

参考文档

BM25 调参调研

ES系列13:彻底掌握相关度:从TF-IDF、BM25到对相关度的控制

Pluggable Similarity Algorithms

posted @ 2021-06-08 16:02  知白守黑,和光同尘  阅读(436)  评论(0编辑  收藏  举报