ElasticSearch TF-IDF、BM25原理与打分策略优化

在做全文搜索的时候,我们经常会使用到es作为搜索引擎,用它来筛选出匹配度更高的文档。面对不同的场景,es默认的参数的排序结果可能并不能满足我们的需求,比如想降低词频或者文档长度的影响,那么我们如何根据实际场景调整参数呢?

作为铺垫,先让我们先来回顾一下es打分算法。

TF-IDF

在es 5.0 之前,使用的是TF-IDF算法(Term Frequency - Inverse Document Frequency)。TF-IDF理解起来并不困难,它由两部分组成,即TF(词频)-IDF(文档逆词频)。 简单点理解就是,TF代表单词在文档中出现的频率,频率越高证明这篇文档与搜索词越匹配。如果只使用TF可能会存在问题,比如有很多词他意义并不大,每段文章中都可能存在(比如"的"、"吗"等词),如果我们的搜索词内包含了这些词,那么可能文档越长评分就会越高,这显然是不合理的。IDF就是为了解决这个问题存在的,IDF代表了词的权重,搜索词越稀有,包含的文档可能就越匹配。 好了,我们了解基本原理,让我们看看具体计算公式是如何实现的

TF-IDF

简单的讲tf-idf 就是词频x词权重
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> TF-IDF ( t , d , D ) = TF ( t , d ) × IDF ( t , D ) \text{TF-IDF}(t, d, D) = \text{TF}(t, d) \times \text{IDF}(t, D) </math>TF-IDF(t,d,D)=TF(t,d)×IDF(t,D)

TF

让我们简单看一下公式
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> TF ( t , d ) = freq(t,d) totalWord(d) \text{TF}(t, d) = \frac{\text{freq(t,d)}}{\text{totalWord(d)}} </math>TF(t,d)=totalWord(d)freq(t,d)

t:term,就是搜索词。 d:document,某篇文档。 freq(t,d):词t在文档d中的出现频率 totalWord(d):文档d所有词的个数

IDF

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> IDF ( t , D ) = log ⁡ ( count(D) count(t,D) + 1 ) \text{IDF}(t, D) = \log\left(\frac{\text{count(D)}}{\text{count(t,D)} + 1}\right) </math>IDF(t,D)=log(count(t,D)+1count(D))

D:所有文档 count(D):所有文档数量 count(t,D):包含t的所有文档数量

缺点

在一个文档中如果一个词t的词频非常高,那么最终分数也会非常的高。例如一个文档中仅仅写了几十遍"中国",那么一旦我们搜索语句中带有"中国"这个term(ex:中国最大的城市),这篇文档会因为中国这个词的词频过高,最终影响整个查询问题的综合效果。而BM25正是一种TF-IDF 的优化算法算法。

BM25(best match)

我们首先来看一下BM25和TF-IDF的词频-分数曲线,可以看到当词频达到一定程度以后,BM25的分数会趋于固定。


<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> BM25 ( t , d ) = ∑ i = 1 n ( k 1 + 1 ) ⋅ f r e q ( t i , d ) k 1 ⋅ ( ( 1 − b ) + b ⋅ len ( d ) avg_len ) + f r e q ( t i , d ) ⋅ IDF ( t i , D ) \text{BM25}(t, d) = \sum_{i=1}^{n} \frac{{(k_1 + 1) \cdot freq(t_i, d)}}{{k_1 \cdot \left((1 - b) + b \cdot \frac{{\text{len}(d)}}{{\text{avg\_len}}}\right) + freq(t_i, d)}} \cdot \text{IDF}(t_i, D) </math>BM25(t,d)=i=1∑nk1⋅((1−b)+b⋅avg_lenlen(d))+freq(ti,d)(k1+1)⋅freq(ti,d)⋅IDF(ti,D)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> n 查询内容的总分词数, t i 第i个词, f r e q ( t i , d ) 第i个词在文档d中的频率 len d 文档d的长度 avg_len平均文档长度 k 1 词频调节参数 b 文档长度条件参数 \begin{align*} & n \text{ 查询内容的总分词数,} \\ & t_i \text{ 第i个词,} \\ & freq(t_i, d) \text{ 第i个词在文档d中的频率}\\ & \text{ len d} \text{ 文档d的长度 } \\ & \text{avg\_len} \text{平均文档长度} \\ & k_1 \text{ 词频调节参数 } \\ & b \text{ 文档长度条件参数 } \end{align*} </math>n 查询内容的总分词数,ti 第i个词,freq(ti,d) 第i个词在文档d中的频率 len d 文档d的长度 avg_len平均文档长度k1 词频调节参数 b 文档长度条件参数

这里与TF-IDF计算方式稍有不同,分子分母都有count(t,D)
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> IDF ( t , D ) = log ⁡ ( c o u n t ( D ) − c o u n t ( t , D ) + 0.5 c o u n t ( t , D ) + 0.5 ) \text{IDF}(t, D) = \log\left(\frac{count(D) - count(t,D) + 0.5}{count(t,D) + 0.5}\right) </math>IDF(t,D)=log(count(t,D)+0.5count(D)−count(t,D)+0.5)

k1 的作用与调节

在讲述TF-IDF缺点时,我们提到,过高的词频会较大程度影响查询效果,所以BM25这里设计了K1参数。那么k1是如何影响分数的呢? 我们来简化一下bm25算法,以方便观察。 假设文档某个文档d长度等于平均文档长度,即len(d)=avg_len。参数b(后面再说)设置为1,不考虑IDF,那么公式将会变为。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> BM25 ( t , d ) = ∑ i = 1 n ( k 1 + 1 ) ⋅ f r e q ( t i , d ) k 1 + f r e q ( t i , d ) \text{BM25}(t, d) = \sum_{i=1}^{n} \frac{{(k_1 + 1) \cdot freq(t_i, d)}}{{k_1 + freq(t_i, d)}} </math>BM25(t,d)=i=1∑nk1+freq(ti,d)(k1+1)⋅freq(ti,d)

简化坐标系上的公式就是
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> y = k 1 ∗ ( x ) ( k 1 + x ) y= \frac{{k1*(x)}}{{(k1+x)}} </math>y=(k1+x)k1∗(x)

当k1取es默认值1.25时,它的曲线时这样的。

如果我们希望词频的影响更小一些,那么我们就可以把k1调小,实现类似于 只要含有这个词,大家freq分数都差不多这样的效果。如果我们希望词频影响大一点,实现类似多次提到那么久可能含有更多相关信息的效果。一般情况下我们并不需要调整k1。

b 的作用与调节

b的主要作用是控制文档长度对分数的影响,比如在相同词频的情况下,文档越短,那么短的文档中term就越重要。

同样的,我们为b参数再简化一下公式,去掉IDF,k1我们取常数1。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> BM25 ( t , d ) = ∑ i = 1 n 1.75 ∗ f r e q ( t i , d ) ( 1 − b ) + b ⋅ len ( d ) avg_len + f r e q ( t i , d ) \text{BM25}(t, d) = \sum_{i=1}^{n} \frac{{ 1.75*freq(t_i, d)}}{{ (1 - b) + b \cdot \frac{{\text{len}(d)}}{{\text{avg\_len}}} + freq(t_i, d)}} </math>BM25(t,d)=i=1∑n(1−b)+b⋅avg_lenlen(d)+freq(ti,d)1.75∗freq(ti,d)

可以发现,b越大文档len(d)/avg_len 影响越大,也就是文档长度的影响越大,es中b的默认值为0.75。 在我的日常使用中发现,当我们在搜索标题这种短文本时,0.75会对结果产生比较大的影响,我们可以根据实际情况调小b的数值。

策略的查看与修改

如何查看打分过程

在查询时,增加"explain": true参数,可以在查询结果的details内看到分数是如何计算的。

js 复制代码
GET /scenic/_search
{
"explain": true, 
  "query": {
  }
}

结果:
{
  "details": [
    {
      "value": 3.814343,
      "description": "score(freq=1.0), computed as boost * idf * tf from:",
      "details": [
        {
          "value": 2.0,
          "description": "boost",
          "details": []
        },
        {
          "value": 4.4669995,
          "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
          "details": [
            {
              "value": 9750,
              "description": "n, number of documents containing term",
              "details": []
            },
            {
              "value": 849219,
              "description": "N, total number of documents with field",
              "details": []
            }
          ]
        },
        {
          "value": 0.42694688,
          "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
          "details": [
            {
              "value": 1.0,
              "description": "freq, occurrences of term within document",
              "details": []
            },
            {
              "value": 1.0,
              "description": "k1, term saturation parameter",
              "details": []
            },
            {
              "value": 0.3,
              "description": "b, length normalization parameter",
              "details": []
            },
            {
              "value": 6.0,
              "description": "dl, length of field",
              "details": []
            },
            {
              "value": 2.802814,
              "description": "avgdl, average length of field",
              "details": []
            }
          ]
        }
      ]
    }
  ]
}

修改打分参数

在es中,我们可以在索引settings中增加一个similarity方案。

javascript 复制代码
{
  "index": {
    "similarity": {
      "name_similarity": {
        "type": "BM25",
        "b": 0.3,
        "k1": 1.25
      }
    }
  }
}

然后在mapping里面指定使用这个方案。

js 复制代码
{
  "properties": {
    "name": {
      "type": "text",
      "analyzer": "ik_max_word",
      "similarity":"name_similarity",
      "fields": {
        "keyword": {
          "type": "keyword",
          "ignore_above": 256
        }
    }
    }
}
相关推荐
java小吕布29 分钟前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
XMYX-032 分钟前
Python 操作 Elasticsearch 全指南:从连接到数据查询与处理
python·elasticsearch·jenkins
Goboy1 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
李少兄1 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
代码小鑫2 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
Json____2 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
monkey_meng2 小时前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss2 小时前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
大鲤余3 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万3 小时前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc