聊聊关于es打分的有趣现象
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc
1. 引子
公司内部有简单的搜索引擎,使用ES搭建。
前两天测试人员问我,为什么同一个查询条件,同一条数据,多次查询。score会发生变化。经过验证,确实存在这种问题,那么这种情况到底怎么产生的呢?
2. 例子
来造个例子
2.1 创建index
curl -XPUT -H "Content-Type: application/json" dev1:9200/test -d '
{
"settings": {
"index.number_of_replicas": "2",
"index.number_of_shards": "1"
},
"mappings": {
"_default_": {
"dynamic_templates": [],
"properties": {
"brand": {
"type": "keyword"
}
}
}
}
}
2.2 写入数据
第1次执行
insert1.py
import requests
for i in range(500):
url = "http://dev1:9200/test/car/%d" % (i)
res = requests.put(url, json={"brand":"buick", "age":i})
print(i, res.status_code)
第2次执行
insert2.py
import requests
for i in range(200):
url = "http://dev1:9200/test/car/%d" % (i)
res = requests.put(url, json={"brand":"buick", "age":i})
print(i, res.status_code)
3.验证
多次执行(可阅读参考资料2)
curl -XPOST -H "Content-Type: application/json" dev1:9200/test/car/1/_explain -d '
{
"query": {
"term": {
"brand": "buick"
}
}
}'
可以观察到分值确实有发生微小的变动
{
"_index": "test",
"_type": "car",
"_id": "1",
"matched": true,
"explanation": {
"value": 0.000893256,
"description": "weight(brand:buick in 429) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.000893256,
"description": "score(doc=429,freq=1.0 = termFreq=1.0\n), product of:",
"details": [
{
"value": 0.000893256,
"description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
"details": [
{
"value": 559, # 变化的部分
"description": "docFreq",
"details": []
},
{
"value": 559, # 变化的部分
"description": "docCount",
"details": []
}
]
},
... ...
}
docCount
/docFreq
多次执行值都不同。
第1次是 559
第2次是 558
第3次是 560
其实ES中默认的相似度计算打分算法是BM25
,BM25
类似TD/IDF
先来说docCount
/docFreq
的含义
docCount
是指 shard中的文档个数
docFreq
是指 包含有查询关键词的文档个数。
在此处是指品牌是"buick"的文档个数,由于数据完全是笔者臆造的,每个doc的品牌都是"buick",因此docCount
和docFreq
恰好相等。但是笔者通过insert1.py
和insert2.py
写入的数据一共就只有500条,这个559是什么鬼?
笔者安装了cerebro,通过它显示shard stats
在不同的shard上
{
"routing": {
"state": "STARTED",
"primary": false,
"node": "uf_-1wJHSEqgQmgPxanZJA",
"relocating_node": null
},
"docs": {
"count": 500,
"deleted": 58
}
...
}
{
"routing": {
"state": "STARTED",
"primary": true,
"node": "3bo9Z0krShyAK-t_F5Fo8A",
"relocating_node": null
},
"docs": {
"count": 500,
"deleted": 59
}
}
{
"routing": {
"state": "STARTED",
"primary": false,
"node": "XnajLSWPS2O2n5iO57pdjw",
"relocating_node": null
},
"docs": {
"count": 500,
"deleted": 60
}
}
3. 解释
结合上面的线索,答案已经明了了。
1. 在ES中,相同ID数据的写入是先删除,再插入。(insert1.py
和insert2.py
部分数据ID重合)
2. ES中数据的删除通过标记实现的伪删除,只有在segment merge时,才会真正的执行删除,移除被删除的数据。(见我的文章LUCENE索引结构漫谈)
shard中 docCount
实际等于 count
+ deleted
那么为什么shard 0的不同副本, deleted
不相同?
笔者猜测,这是由于多个副本中,数据分布在了不同的segment中,而segment的合并是有一定的随机性的。所以deleted
并不相同。实时上如果执行force merge
curl -XPOST http://dev1:9200/test/_forcemerge?max_num_segments=1
可以将deleted
变为0
提示
对于设置多个shard的index。即使查询条件相同,但由于不同的shard之间docCount
不同,即使2条数据完全一样(_id不同),也有可能导致分值不同。
后记
2023年8月14日
如果业务场景的类似于周期性的任务,比如对于以天为单位统计广告的曝光情况,还可以采取一个取巧的办法来避免这种情况
比如 凌晨 00:00 ~ 02:00 写入数据, 02:00 之后开放给用户读取,那么可以在写入阶段,将index.number_of_replicas
, 设置为0, 在读取阶段再将 index.number_of_replicas
设置为你想要的数量。