1. 一个错误结果的例子
上一篇文章,我们介绍了地图兴趣检索的基本流程,以及如何用elasticsearch+ik搭建一个简单的demo。在运行demo时我们用“通州区万达广场“去搜索,结果排第一位的结果竟然是位于朝阳区的”建国路万达广场“。为了弄清楚为什么会得到这个结果,这篇文章我们对ES的相关性打分原理进行探索。
再次查询并输出一下结果,结果内会给出每个结果的得分
http://localhost:9200/idx_default/_search
{
"query": {
"match": {
"address": {
"query": "通州区万达广场"
}
}
}
}
结果如下(只摘出前两名)
{
"took": 28,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 10000,
"relation": "gte"
},
"max_score": 17.299044,
"hits": [
{
"_index": "idx_default",
"_type": "_doc",
"_id": "138069",
"_score": 17.299044,
"_source": {
"address": "建国路万达广场",
"name": "恒大山水城",
"location": "39.90867476611688,116.46468505121267"
}
},
{
"_index": "idx_default",
"_type": "_doc",
"_id": "28730",
"_score": 16.216942,
"_source": {
"address": "北京市通州区新华西街58号万达广场F2",
"name": "手寓工坊(万达广场店)",
"location": "39.904175142894765,116.63712318703388"
}
}
]
}
}
从结果可见**”建国路万达广场“得分为17.299044,而”北京市通州区新华西街58号万达广场F2“**只有16.216942。这个结果的计算细节,可以通过explain=true来查看,但是在分析之前,我们需要了解ES打分机制的原理。
2. score模型
了解ES的同学应该知道,它是基于Lucene建立的一套完整的全文索引框架,所以在相关性评分方面ES直接继承了Lucene的基于布尔模型来进行文档匹配的方法。这是一套名为practical scoring function的公式,这个公式借鉴了TF/IDF(term frequency/inverse document frequency)即词频/逆文档词频理论。我们先来看一下TF/IDF是如何计算的
2.1 TF/IDF(词频/逆文档词频)
TF/IDF的思想是,如果某个词在一篇文章中出现的频率越高,并且在其他文章中很少出现,这个词的分数越高。它倾向于过滤掉常见的词语,保留重要的词语。
比如,有一个文档内容为“中国足球是冠军”(心真大),我们假设得到几个分词后的词条“中国”,“足球”,“冠军”,这个例子把“是”这类无意义词语去掉了,一般分词器都会这样处理。TF(词频)的意思就是词在文档中出现的次数。假如我们的计算模型只有TF,那可以认为用这三个词任意一个去匹配这个文档,得分是相同的。但是,还有IDF的计算。
假设,我们的文档库里还有其他几个文档,分别为“中国篮球是冠军”、“中国羽毛球是亚军”、“美国网球是季军”。那么,再次搜索上面三个词语的得分还会一样吗。IDF(逆文档词频)表达的是词语在整个词库里面的罕见程度,出现越少,说明这个词语越能代表这个文档,词语得分就越高。这里,“足球”、“篮球”、“羽毛球”、“网球”都只出现一次,说明他们在这个词库里更能代表对应的文档,从常理分析确实有点道理。我们具体看看TF/IDF是怎么计算的。
T F 公式如下: t f i , j 表示第 i 个词在第 j 个文档中出现的词频,其中 n i , j 是该词在文档中出现的次数,分母 则表示在文档 j 中所有词条的个数 \begin{array}{l} TF公式如下:tf_{i,j}表示第i个词在第j个文档中出现的词频,其中n_{i,j}是该词在文档中出现的次数,分母\\ 则表示在文档j中所有词条的个数 \end{array} TF公式如下:tfi,j表示第i个词在第j个文档中出现的词频,其中ni,j是该词在文档中出现的次数,分母则表示在文档j中所有词条的个数
t f i , j = n i , j ∑ k n k , j tf_{i,j}=\frac{n_{i,j}}{\sum_{k}n_{k,j}} tfi,j=∑knk,jni,j
简单来讲:
词频
(
T
F
)
=
某词在文章中出现的次数
文章中的总词数
词频(TF)=\frac{某词在文章中出现的次数}{文章中的总词数}
词频(TF)=文章中的总词数某词在文章中出现的次数
I
D
F
公式如下:
∣
D
∣
表示文档库中总文档数,
∣
{
j
:
t
i
∈
d
j
}
∣
表示包含词语
t
i
的文件个数。如果文档库无
该词,会使分母为
0
,一般要加
1
\begin{array}{l} IDF公式如下:|D|表示文档库中总文档数,|\{j:t_i\in d_j\}|表示包含词语t_i的文件个数。如果文档库无\\ 该词,会使分母为0,一般要加1 \end{array}
IDF公式如下:∣D∣表示文档库中总文档数,∣{j:ti∈dj}∣表示包含词语ti的文件个数。如果文档库无该词,会使分母为0,一般要加1
i d f i = l o g ∣ D ∣ ∣ { j : t i ∈ d j } ∣ idf_i=log\frac{|D|}{|\{j:t_i\in{d_j}\}|} idfi=log∣{j:ti∈dj}∣∣D∣
即:
逆文档词频
(
I
D
F
)
=
l
o
g
(
文档库中总文档数
包含该词的文档数
+
1
)
逆文档词频(IDF)=log(\frac{文档库中总文档数}{包含该词的文档数+1})
逆文档词频(IDF)=log(包含该词的文档数+1文档库中总文档数)
最终:
T
F
/
I
D
F
=
T
F
∗
I
D
F
TF/IDF=TF*IDF
TF/IDF=TF∗IDF
我们基于上述例子来计算一遍,假设有下面词库,左边是文档编号和文档内对应内容,右边是倒排索引后的词语与文档关系:
假设我们以**“篮球冠军的新闻”**为查询条件进行搜索,会有怎样的结果?
首先,引擎会对检索词进行分词,得到“篮球”、“冠军”、“新闻“三个词,然后分别计算这三个词在每个文档中的TF/IDF得分,将三个词的得分相加,就是查询语句与每个文档的相关性得分。
文档d1:
文档d2:
文档d3、d4:
最终可见d2得分最高,即与我们搜索词最匹配,这与我们直观感受相同。TF/IDF的优点是逻辑简单,计算速度快,很好理解。但是并非所有情况,它都能表现的与我们期待的一致。比如,我们把上面例子简单修改一下。
假设,文档d1的内容做了扩充,变为**“中国足球是冠军,这是难得的冠军,梦寐以求的冠军,百年一遇的冠军!”。我们再用“篮球冠军的新闻”**来搜索,结果还与我们想的一样吗。按理说,d1扩充的内容和我们要搜索的“篮球冠军”没啥关系,d2应该还是最匹配的文档。下面看看新的得分表。
文档d1:
文档d2不变:
文档d1的得分反超了,原因是冠军这个词的TF得分暴涨。这正是TF/IDF算法一个弊端,词频的得分不可控,一些非关键词的增加会严重影响文档得分。为了让相关性计算更合理,BM25计算模型被提出,它也是目前ElasticSearch内部默认的计算方法。
2.2 BM25
BM25可以理解为TF/IDF的改进版本,因此它也是一种词频与逆文档词频的模型。
BM25的逆文档词频和TF-IDF类似,也是对词语罕见程度的表示,某个词在词库里越多文档中出现,说明它的代表性越低。公式如下
q
i
表示查询
Q
中第
i
个词,
N
表示词库中文档个数,
n
(
q
i
)
则是包括词
q
i
的文档个数。
q_i表示查询Q中第i个词,N表示词库中文档个数,n(q_i)则是包括词q_i的文档个数。
qi表示查询Q中第i个词,N表示词库中文档个数,n(qi)则是包括词qi的文档个数。
I D F ( q i ) = l o g N − n ( q i ) + 0.5 n ( q i ) + 0.5 IDF(q_i)=log\frac{N-n(q_i)+0.5}{n(q_i)+0.5} IDF(qi)=logn(qi)+0.5N−n(qi)+0.5
和传统TF-IDF的逆文档词频计算相比,BM25没有做太多修改,从他们的图像也可以发现,两条曲线变化趋势是一致的
BM25更多工作在于对TF词频计算的优化。上节末尾的例子表明,有些情况下词频对于结果的影响过大。而BM25则将这种过分影响进行了抑制。它仍然允许词频对于得分的正向贡献,但是这种贡献是有限的。即当词频增大到到一定水平之后,它的影响会趋近于一个常量。如下图所示
上面这个图对应的公式为
T
F
=
(
k
+
1
)
⋅
f
i
k
+
f
i
TF=\frac{(k+1)\cdot f_i}{k+f_i}
TF=k+fi(k+1)⋅fi
通过常量k的引入,使得TF永远都不会超过k+1(此处k=1.2),随着词频的增加只能越来越接近它。k值的变化是非常重要的模型调节手段,找到一个适合应用场景的k值,可以让词频的影响达到最合适的水平。ES内部k的默认值也是1.2。
BM25对TF的优化还远不止于此,它还将文档的长度考虑进来。具体做法是将上面TF公式中分母上的k值做了扩展,得到下面的公式
T
F
=
(
k
+
1
)
⋅
f
i
k
⋅
(
1
−
b
+
b
⋅
d
l
a
v
g
(
d
l
)
)
+
f
i
TF=\frac{(k+1)\cdot f_i}{k\cdot(1-b+b\cdot\frac{dl}{avg(dl)})+f_i}
TF=k⋅(1−b+b⋅avg(dl)dl)+fi(k+1)⋅fi
其中dl为当前文档的长度,avg(dl)为文档库中文档的平均长度。下图可见,文档越短,则逼近上限的速度越快,反之则越慢。比如,一个标题可能只有几个词,则只需要匹配很少的词就可以确定相关性,而一个大篇幅的文章需要有更多词的匹配才可能与它的主要内容相吻合。
上面公式里还有一个常量b,它是BM25让我们调节文档长度影响程度的因子,当b=0时,分母变为k+fi,完全消除了文档长度影响。当b值更高时,长度因素则会对TF得分有更大的影响。最终BM25的公式如下:
S c o r e ( Q , d ) = ∑ i n I D F ( q i ) ⋅ ( k 1 + 1 ) ⋅ f i k 1 ⋅ ( 1 − b + b ⋅ d l a v g ( d l ) ) + f i Score(Q,d)=\sum_{i}^{n}IDF(q_i)\cdot\frac{(k_1+1)\cdot f_i}{k_1\cdot(1-b+b\cdot\frac{dl}{avg(dl)})+f_i} Score(Q,d)=i∑nIDF(qi)⋅k1⋅(1−b+b⋅avg(dl)dl)+fi(k1+1)⋅fi
BM25相比传统TF/IDF,考虑的因素更多,且各因子的影响更加平滑。最重要的是,它为我们提供了各种调节因子,方便我们针对不同场景控制各种因素的影响程度。后面的文章,我们将通过ES提供的接口来尝试调整这些因子,从而让搜索结果更加符合我们的期望。