Elasticsearch:执行同样的查询语句多次结果不一致?!

您所在的位置:网站首页 点聚合每次聚合的结果不一样怎么办 Elasticsearch:执行同样的查询语句多次结果不一致?!

Elasticsearch:执行同样的查询语句多次结果不一致?!

2023-09-19 11:31| 来源: 网络整理| 查看: 265

Elasticsearch:执行同样的查询语句多次结果不一致?!背景

最近有用户让帮忙看一下一个诡异的问题,同样的一个查询语句,执行多次查询结果竟然不一致,查询结果中hits.total一会是30,一会为15,这是为什么呢?

用户的查询语句如下:

GET test/_search { "query": { "match": { "title": "中国" } }, "min_score": 2.0 }原因分析

关于这个问题,官方文档中有解释:https://www.elastic.co/guide/en/elasticsearch/reference/6.4/consistent-scoring.html, 主要的原因是因为有副本(replica)的存在,主分片和副本分片可能不一致,导致最终在主分片和副本分片上计算得到的得分不同,而导致最终的查询结果不一致。用户的查询dsl中指定了min_score,限定文档最低得分为2.0,不同的查询请求落到不同的分片上,获取到的得分大于2.0的文档集就可能不一致,最终才会出现hits.total一会是30,一会为15这种情况。

但是是如何造成主分片和副本分片不一致的情况,可能是因为用户删除了部分文档,之后主分片进行了merge, 而副本分片没有进行merge。 这种情况下主分片和副本分片上的总文档数量就会不同,打分时计算出的IDF的值不同,最终得到了不同的得分。

下面通过示例复现上述过程,更加直观的了解问题出现的原因:

1 index doc

批量插入文档,文档数量越多越好

POST cc/c/1 { "x":"ab abc abc" }

2 随机delete或者update doc

PUT cc/c/1 { "x":"abc abc abc abc" } DELETE cc/c/5

3 执行forcemerge

POST cc/_forcemerge?only_expunge_deletes=true

4 查看segment

GET _cat/segments/cc

上图中,经过第3步的forcemerge, 分片1的主分片进行了merge,但是副本分片并没有进行merge,副本分片的segments_a中包含了一个标记为删除的文档,主分片因为进行了merge,没有包含标记未删除的文档。

5 执行查询

指定preference只查询主分片

GET cc/c/_search?preference=_primary

查询结果为:

{ "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 3, "max_score": 4.205637, "hits": [ { "_index": "cc", "_type": "c", "_id": "1", "_score": 4.205637, "_source": { "x": "abc" } }, { "_index": "cc", "_type": "c", "_id": "5", "_score": 1.7646677, "_source": { "x": "abc a c" } }, { "_index": "cc", "_type": "c", "_id": "8", "_score": 1.7646677, "_source": { "x": "abc ax c" } } ] } }

指定preference只查询副本分片

{ "took": 0, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 3, "max_score": 4.205637, "hits": [ { "_index": "cc", "_type": "c", "_id": "1", "_score": 4.205637, "_source": { "x": "abc" } }, { "_index": "cc", "_type": "c", "_id": "5", "_score": 1.8076806, "_source": { "x": "abc a c" } }, { "_index": "cc", "_type": "c", "_id": "8", "_score": 1.8076806, "_source": { "x": "abc ax c" } } ] } }

比较两个查询结果可以看到, hits中的第2条和第3条文档在两个查询结果中的得分不同,即便他们是同一个文档。

通过在查询时增加explain参数,查看打分明细:

当preference=_primary时计算idf时的docCount为22:

当preference=_primary时计算idf时的docCount为23,包含了标记为删除的文档:

翻阅lucene源码(7.6.0),org.apache.lucene.search.similarities.BM25Similarity类中,idf的计算部分:

public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats) { final long df = termStats.docFreq(); final long docCount = collectionStats.docCount() == -1 ? collectionStats.maxDoc() : collectionStats.docCount(); final float idf = idf(df, docCount); return Explanation.match(idf, "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:", Explanation.match(df, "docFreq"), Explanation.match(docCount, "docCount")); }

其中docCount的值,先判断collectionStats.docCount是否为-1,如果是则赋值为collectionStats.maxDoc(),否则为collectionStats.docCount(), collectionStats.maxDoc()和collectionStats.docCount()的说明如下:

/** returns the total number of documents, regardless of * whether they all contain values for this field. * @see IndexReader#maxDoc() */ public final long maxDoc() { return maxDoc; } /** returns the total number of documents that * have at least one term for this field. * @see Terms#getDocCount() */ public final long docCount() { return docCount; }

collectionStats.maxDoc()实际上是indexReader.maxDoc(), 该值是shard级别的最大的lucene docId,实际上把已经删除的文档也统计在内了;

/** Returns one greater than the largest possible document number. * This may be used to, e.g., determine how big to allocate an array which * will have an element for every document number in an index. */ public abstract int maxDoc();

而collectionStats.docCount()则是terms.getDocCount(),代码中的注释比较让人困惑,经过实测, terms.getDocCount()意思是包含要查询的field的所有文档数量,实际上也包含了已经删除的文档:

/** Returns the number of documents that have at least one * term for this field, or -1 if this measure isn't * stored by the codec. Note that, just like other term * measures, this measure does not take deleted documents * into account. */ public abstract int getDocCount() throws IOException;

最终取值实际上为后者也就是collectionStats.docCount()

(8.x之后的lucene直接把docCount赋值为collectionStats.docCount(), 取消了三元表达式,因为这个三元表达式实际上是无用的),最终计算idf时的docCount值为包含要查询field字段的总文档数量,并且标记为删除的文档也统计在内。所以,本例中,在指定preference为_primay时,docCount=22;指定preference为_replica时,docCount=23,因为副本分片中包含了一个标记为删除的文档。

实际应用中,为了保证每次查询都得到相同的结果,可以通过指定preference参数(可以自定义)让每次查询都请求到相同的分片上解决。

但是,怎么样得到准确的docCount值呢,常规的方法是可以通过执行_forcemerge?only_expunge_deletes把标记为删除的文档物理删除,但是实际上forcemerge也不能保证主分片和副本分片同时merge, 比如在本例中,主分片进行了merge, 副本分片没有merge,所以才会造成最终查询结果不一致。至于为什么主分片和副本分片不能同时merge, 这里涉及到forcemerge的逻辑了,需要进一步查看源码研究。

以上实战验证了如果主分片和副本分片不一致的情况下,文档的分值会不同,最终影响到查询结果。解决方式就是在查询时指定preference, 可以指定为_primary、_replica或者其它自定义的值,保证同样的查询语句会请求到相同的分片。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3