文章目录
- 前言
- 分片对 Elasticsearch 相关性评分的影响
- BM25 算法和它的变量
- 效果应用
- 总结
前言
上一章介绍了 Elasticsearch 的读写优化技巧。本章将深入探讨与 Elasticsearch 相关的 BM25 相关性评分公式。
我们将全面解析 BM25 如何在查询时影响文档评分。BM25 是 Elasticsearch 的核心内容之一,内容可能较为难懂,请耐心阅读。
分片对 Elasticsearch 相关性评分的影响
默认情况下,文档的评分是基于当前分片进行计算的。即,同一个 term 在不同的分片上评分会不一致。
我们来看下面这个例子。
创建一个 test_22
索引,并将分片数设置为 3
GET test_22/_search
{
"explain": true,
"query": {
"match": {
"name": "hello"
}
}
}
紧接着,我们写入 3 条数据
PUT test_22/_doc/1
{
"name": "hello es"
}
PUT test_22/_doc/2
{
"name": "hello php"
}
PUT test_22/_doc/4
{
"name": "hello world p"
}
OK,现在让我们来搜索 hello
GET test_22/_search
{
"query": {
"match": {
"name": "hello"
}
}
}
结果可能会让你感到意外。
文档1的得分为0.2876821,文档2得分为0.19856803,文档4得分为0.16853255。
是不是感到困惑?文档1和文档2的结构相似,为什么文档1的得分更高呢?而文档4仅仅多了一个 P,它的得分却是最低的?
文档1为什么比文档2得分更高?
因为文档1 在分片2上,文档2、文档4在分片1上。这就是我上述所说的,得分是基于文档所在分片计算的。这与逆文档词频有关,下文会介绍。
查询参数最外层,添加 "explain": true
,即可看到文档所在分片。
GET test_22/_search
{
"explain": true,
"query": {
"match": {
"name": "hello"
}
}
}
文档2为什么比文档4得分更高?
文档2和文档4在同一个分片中,但是文档2的平均长度小于文档4。因此,文档2的得分比文档4更高。
简单来说:在一句话中出现 hello
的权重要高于在一段话中出现 hello
的权重。
这里提到的平均长度是以
term
为维度进行计算的。例如,文档2的平均长度为2,而文档4的平均长度为3。
BM25 算法和它的变量
上面提到的例子根源于 BM25 公式。接下来,我将重点介绍 BM25 公式的相关内容。
ES 的评分使用以下公式:
接下来,我将逐一介绍公式中的每个参数。
-
q i q_i qi
表示当前查询的第几个term
。例如,我们搜索hello world
,它包含 2 个term
:
hello
对应 q 0 q_0 q0,world
对应 q 1 q_1 q1。 -
I D F ( q i ) IDF(q_i) IDF(qi)
逆文档词频 (Inverse Document Frequency),其计算公式为:
l n ( 1 + ( d o c C o u n t − f ( q i ) + 0.5 ) f ( q i ) + 0.5 ) ln(1+\frac{(docCount - f(q_i) + 0.5)}{f(q_i) + 0.5}) ln(1+f(qi)+0.5(docCount−f(qi)+0.5))
d o c C o u n t docCount docCount:当前分片文档数量
f ( q i ) f(q_i) f(qi):在当前分片中,命中该term
查询的文档数量
如果一个term
出现在大多数文档中,那么该term
的得分也就越低。就像在做毕业设计时,你们班50个人的论文都是XXX管理系统,而只有你的论文是关于人工智能,那么你的论文更有可能被评为优秀论文。
用上面的例子来说明:对于文档1的得分就是: l n ( 1 + ( 1 − 1 + 0.5 ) 1 + 0.5 ) ln(1+\frac{(1 - 1 + 0.5)}{1 + 0.5}) ln(1+1+0.5(1−1+0.5))。对于文档2得分就是: l n ( 1 + ( 2 − 2 + 0.5 ) 2 + 0.5 ) ln(1+\frac{(2 - 2 + 0.5)}{2 + 0.5}) ln(1+2+0.5(2−2+0.5))。 -
f i e l d L e n a v g F i e l d L e n \frac{fieldLen}{avgFieldLen} avgFieldLenfieldLen
这里的avgFieldLen
指的是所有文档term
的平均数量,fieldLen
则是指当前文档term
的数量。就比如,在一篇300多页的文档中提到term
,跟在微博短短一句话提到的term
,肯定是后者的权重会更高。 -
b b b
b b b 用于调整 文档长度( f i e l d L e n a v g F i e l d L e n \frac{fieldLen}{avgFieldLen} avgFieldLenfieldLen) 带来的影响。默认值为 0.75,取值范围为[0,1]
当 b b b = 0 时,BM25 不考虑文档长度,即所有文档被视为相同长度。
当 b b b = 1 时,文档长度的影响最大,较长的文档会受到更多惩罚。
一般来说,b 的推荐值在 0.5 到 0.75 之间,这样可以较好地平衡文档长度的影响。
例如,我们上面提到的文档2、文档4,他们都在同一个分片,仅因为文档4的长度大于文档2,文档4的得分就比文档2低。 -
f ( q i , D ) f(q_i,D) f(qi,D)、 k 1 k1 k1
f ( q i , D ) f(q_i,D) f(qi,D)、 k 1 k1 k1 同时存在于分子、分母因此需要放在一起看。
f ( q i , D ) f(q_i,D) f(qi,D) 表示term
出现在文档中的频率,频率越高,文档得分越高。
k 1 k1 k1 控制term
的饱和度,默认值为 1.2,该值通常在[0.5,2]
之间。
当 k 1 k1 k1 越大时,词频的影响会增强。不过当并不会无限增强,而是当达到某一临界值时,就会停止增长。
效果应用
可以看到,我们能调整的值只有 b b b、 k 1 k1 k1。下面我带大家写几个例子,通过调整 b b b、 k 1 k1 k1 直观的感受一下。
将 b b b 值设置为 0
通过将 b b b 值设置为0,文档的长度将不对算分产生影响。
PUT test_22_1
{
"settings": {
"number_of_shards": 3,
"index": {
"similarity": {
"default" : {
"type" : "BM25",
"b": 0,
"k1": 1.2
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text"
}
}
}
}
写入文档
PUT test_22_1/_doc/1
{
"name": "hello es"
}
PUT test_22_1/_doc/2
{
"name": "hello php"
}
PUT test_22_1/_doc/4
{
"name": "hello world p"
}
查询 hello
GET test_22_1/_search
{
"query": {
"match": {
"name": "hello"
}
}
}
可以看到,现在文档2、文档4的分数一样了。
将 k 1 k1 k1 设置为0
k 1 k1 k1 设置为0,文档长度、词频将不对算分产生影响。
PUT test_22_2
{
"settings": {
"number_of_shards": 3,
"index": {
"similarity": {
"default" : {
"type" : "BM25",
"b": 0.75,
"k1": 0
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text"
}
}
}
}
写入文档
PUT test_22_2/_doc/1
{
"name": "hello es"
}
PUT test_22_2/_doc/2
{
"name": "hello php"
}
PUT test_22_2/_doc/4
{
"name": "hello world hello"
}
查询文档
GET test_22_2/_search
{
"query": {
"match": {
"name": "hello"
}
}
}
可以看到文档2、文档4的分数还是一致。
总结
- 文档评分是基于文档所在的分片进行计算的。
- 在同一个分片中,1个 term 出现在大多数文档中,那么该 term 的得分就会低,反之则高。
- 一个文档中,如果 term 出现的频率越高,它的得分就会越高,频率达到某个阈值后,得分将不再增加。
- b b b 用于调整文档长度对评分的影响,默认为 0.75,取值范围 [0,1]。
- k 1 k1 k1 用于调整文档词频对评分的影响,默认为 1.2,该值通常在 [0.5,2] 之间。
- b b b、 k 1 k1 k1 大多数情况下,我们不需要修改它。