Okapi BM25, TF-IDF, 以及 ElasticSearch/Lucene 搜索结果的分数
Okapi BM25
来源: https://en.wikipedia.org/wiki/Okapi_BM25
Okapi BM25 是搜索引擎用来对匹配文档进行排序的函数,依据是每个文档与搜索词的相关度。BM 是 Best Matching (最佳匹配) 的缩写。这个方法是上世纪七八十年代在概率搜索的框架下被提出的。Okapi 是第一个使用这种方法的信息获取系统的名称。
BM25 是基于词频的方法;也就是说,它不考虑多个搜索词在文档里是不是靠近,只考虑它们各自的出现次数。BM25 不是单个函数:许多函数都可以叫 BM25,彼此之间有些形式和参数个数的差异。最常用的形式之一是
\[ \text{score}(D,Q) = \sum_{i=1}^n\text{IDF}(q_i)\cdot\left[\frac{f(q_i,D)\cdot\left(k_1+1\right)}{f(q_i,D) + k_1\cdot\left(1-b+b\cdot\frac{|D|}{\text{avgdl}}\right)}\right], \] 其中各符号含义如下:
- \(D\): 文档
- \(Q\): 搜索词 (多个)
- \(f(q_i,D)\): \(q_i\) 这个词在文档 \(D\) 中的出现次数
- \(|D|\): \(D\) 的单词数
- \(\text{avgdl}\): 整个文档库中文档的平均长度
- \(k_1\), \(b\): 自由参数,一般取值范围是 \(k_1\in[1.2,2.0]\), \(b=0.75\)
- \(\text{IDF}(q_i)\): inverse document frequency,通常由下述公式计算
\[
\text{IDF}(q_i) = \log\left(\frac{N-n(q_i)+0.5}{n(q_i) + 0.5}\right),
\]
这里 \(N\) 是文档库里总的文档数,\(n(q_i)\) 是包含单词 \(q_i\) 的文档个数。一个单词的 \(\text{IDF}\) 大,意味着这个单词只在较少文档中出现,也就意味着这个单词比较独特。
这里 \(\text{IDF}\) 的定义有个问题,那就是,如果一个词在超过半数的文档里出现,则其 \(\text{IDF}\) 是负数,于是这个词对 BM25 分数的贡献是负的。一般不希望这样的特性,所以当 \(\text{IDF}\) 为负数时可强行改为 0,或者一个比较小的正数,或者改用一种平滑过渡到 0 的函数形式。
可以看到,如果搜索词中包含比较独特的词,则会提升分数;搜索词在一个文档中出现的次数越高,分数也会更高;但文档越长,分数会越低。
BM25 的一个较新升级版叫 BM25F,把文档结构和锚文本也考虑进来。另一个叫 BM25+,只是在上面公式中的方括号里加了一个 \(\delta\),用来弥补原来公式对超长文档的不公。
TF-IDF
来源: https://en.wikipedia.org/wiki/Tf%E2%80%93idf
思想类似,不过是针对单个搜索词的:
\[ \text{tfidf} = (\text{term frequency})\cdot\text{IDF}. \]
ElasticSearch/Lucene 的分数计算
来源: https://www.elastic.co/guide/en/elasticsearch/guide/current/practical-scoring-function.html, 以及 https://www.elastic.co/guide/en/elasticsearch/guide/current/scoring-theory.html 这个文档不一定最新的,不过这会儿我没找到最新的。
ElasticSearch 底层采用了 Lucene,而 Lucene 的分数计算综合了布尔模型 (Boolean model), TF-IDF, 以及矢量空间模型。
布尔模型
布尔模型使用 AND
, OR
, NOT
这三个逻辑运算符根据搜索词对文档进行筛选,只保留选中的文档,并不计算分数 (或者说选中的分数都是 1,没选中的都是 0)。
TF
TF-IDF 前面已经提过,不过按照 ElasticSearch 的文档,他们采用的 \(\text{tf}\) 函数形式是 \[\text{tf}(t, d) = \sqrt{\text{t 在 d 内的出现次数}}。\]
在索引阶段如果把一个字段的索引类型 (index_options
) 设为 docs
,则搜索阶段这个字段不会被用来计算出现频率,而只会被用来筛选是否在一个文档中出现。
IDF
他们使用的 \(\text{idf}\) 的函数形式是 \[ \text{idf}(t) = 1 + \log\left(\text{文档总个数}/(1+\text{包含 t 的文档个数})\right)。 \]
字段长度归一化
字段包含的内容越长,权重越低。比如,一般说来出现在标题里的词会比出现在正文内容里的词更重要 (不过考虑到现在流行的标题党,似乎这个说法不太对)。 计算字段长度归一化因子的公式是 \[ \text{norm}(d) = 1 / \sqrt{\text{字段包含的单词数}}。 \] 这种归一化可以通过设置去掉。
TF, IDF, 字段长度归一化因子,这三个东西都是在索引阶段计算好的。对于单个词语的搜索,只使用了这些因子。但对于多个词语的搜索,则还结合了下面的方法。
矢量空间模型
其实就是余弦相似度,根据两个矢量的内积来计算,与最开始计算 BM25 的公式类似。
Lucene 实际实用的公式
\[ \begin{split} \text{score}(q, d) =\;& \text{queryNorm}(q) \cdot \text{coord}(q,d) \cdot\\ & \sum_{t \in q} \text{tf}(t, d) \cdot \text{idf}(t)^2 \cdot t.\text{getBoost}() \cdot \text{norm}(t,d) \end{split} ,\] 其中,
- \(\text{queryNorm}(q,d)\) 是搜索归一化因子,目的是希望不同搜索的结果可以互相比较,但实际上在这点上做得不好,不要太看重这个。它的数值一般通过 \(1/\sqrt{\text{各搜索词的 IDF 的平方和}}\) 计算。
- \(\text{coord}(q,d)\) 用来奖励那些包含较多搜索词的文档;也就是说,在把匹配搜索词的权重加起来后,再乘以匹配的搜索词的个数,然后除以此次搜索词的总个数。
- \(t.\text{getBoost}()\) 是额外的提权因子。
- \(\text{norm}(t,d)\) 是字段长度归一化因子。