Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | 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”,因此docCountdocFreq恰好相等。但是笔者通过insert1.pyinsert2.py 写入的数据一共就只有500条,这个559是什么鬼?

笔者安装了cerebro,通过它显示shard stats

image_1ddplf7bomspnj9o2h18m7tg09.png-95.1kB

在不同的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.pyinsert2.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 设置为你想要的数量。

参考资料

  1. ES-similarity
  2. ES-explain
  3. TF-IDF及其算法
  4. preference

请我喝瓶饮料

微信支付码

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据