前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Elasticsearch:执行同样的查询语句多次结果不一致?!

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

原创
作者头像
bellen
发布2019-06-13 20:02:18
9.5K0
发布2019-06-13 20:02:18
举报
文章被收录于专栏:腾讯云Elasticsearch Service

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

背景

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

用户的查询语句如下:

代码语言:txt
复制
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

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

代码语言:txt
复制
POST cc/c/1
{
	"x":"ab abc abc"
}

2 随机delete或者update doc

代码语言:txt
复制
PUT cc/c/1
{
	"x":"abc abc abc abc"
}

	DELETE cc/c/5

3 执行forcemerge

代码语言:txt
复制
POST cc/_forcemerge?only_expunge_deletes=true

4 查看segment

代码语言:txt
复制
GET _cat/segments/cc

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

5 执行查询

指定preference只查询主分片

代码语言:txt
复制
GET cc/c/_search?preference=_primary

查询结果为:

代码语言:txt
复制
	{
  "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只查询副本分片

代码语言:txt
复制
	{
  "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的计算部分:

代码语言:txt
复制
	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()的说明如下:

代码语言:txt
复制
	/** 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,实际上把已经删除的文档也统计在内了;

代码语言:txt
复制
/** 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的所有文档数量,实际上也包含了已经删除的文档:

代码语言:txt
复制
/** 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或者其它自定义的值,保证同样的查询语句会请求到相同的分片。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Elasticsearch:执行同样的查询语句多次结果不一致?!
    • 背景
      • 原因分析
      相关产品与服务
      Elasticsearch Service
      腾讯云 Elasticsearch Service(ES)是云端全托管海量数据检索分析服务,拥有高性能自研内核,集成X-Pack。ES 支持通过自治索引、存算分离、集群巡检等特性轻松管理集群,也支持免运维、自动弹性、按需使用的 Serverless 模式。使用 ES 您可以高效构建信息检索、日志分析、运维监控等服务,它独特的向量检索还可助您构建基于语义、图像的AI深度应用。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档