es分页查询

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMoyMRCL-1650002020707)(d:\user\01412349\Application Data\Typora\typora-user-images\image-20211112163819046.png)]

1、page+size

GET test_dev/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "age": 28
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 20
}

和mysql类似,查询深分页时性能较差。当page*size过大时,会出现效率急剧下降的问题,同时其性能下滑相较于mysql会更加严重。

ES为什么深分页效率低?

在这里插入图片描述

这是由于es集群是分布式架构,对于一个查询实际上有两个阶段。即Query阶段和Fetch阶段。

Query阶段比较轻量级,通过查询倒排索引,获取满足查询结果的文档ID列表和其评分。 而Fetch阶段比较耗费资源,需要将每个shard的结果取回,在协调结点进行全局排序,最后通过doc_id获取全量数据。

举个例子,当page=1000,size=100时,请求会发送到所有的shard上去,每个Shard都会在本地存储一个大小为1000*100+100的priority queue,然后每个shard又会将priority queue发送给协调节点进行合并,最后将结果返回。也就是说,ES的深分页问题将会大大耗费每个节点的带宽、cpu和内存

ES的排序方式

在query阶段就要进行排序,不全量查询的情况下怎么排序的?

  • filter 查询 为 doc_id(Lucene 文件结构的当时索引时的先后顺序)
  • 按照相关性得分排序( _score)
  • 按照指定的字段排序 (term index中的顺序)

2、search_after

search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。

其思想类似于mysql常用的方法:记录上一次访问的最后位置

select * from table where id > #max_id# order by id limit n; 

这样实际上就避免了page过大导致的深分页问题。

唯一值id查询

为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。 比如下面我们就使用了timestamp和 _id组成的唯一值。(分布式带来的问题)

GET test_dev/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "age": 28
          }
        }
      ]
    }
  },
  "size": 10,
  "from": 0,
  "search_after": [
    1541495312521,
    "d0xH6GYBBtbwbQSP0j1A"
  ],
  "sort": [
    {
      "timestamp": {
        "order": "desc"
      },
      "_id": {
        "order": "desc"
      }
    }
  ]
}

3、scroll游标查询

from+size查询在10000-50000条数据(1000到5000页)以内的时候还是可以的,但是如果数据过多的话,就会出现深分页问题。

为了解决上面的问题,elasticsearch提出了一个scroll滚动的方式。

GET test_dev/_search?scroll=5m
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "age": 28
          }
        }
      ]
    }
  },
  "size": 100
}
scroll如何进行查询的?

Query阶段:每个shard将命中的结果( doc_id和_score) 按照 _score 顺序在上下文中创建一个优先队列快照,并通过scroll_id指向它,lastEmittedDoc指向上次访问的位置,最后将TOP(size)的doc id返回给协调节点。

Fetch阶段:协调节点将各个shard返回的结果再进行合并排序,最后通过doc_id查找返回结果的全量数据。之后更新各个分片上的上下文。

GET _search/scroll
{
  "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAKP_RVFmFRbGp0Vm1pU01hM2M0eXlyUGVxRkEAAAAACeMUaxZSV1JjSnFtQVFDT094RUNsQjFFTF9BAAAAAAo_9FQWYVFsanRWbWlTTWEzYzR5eXJQZXFGQQAAAAAKP_RTFmFRbGp0Vm1pU01hM2M0eXlyUGVxRkEAAAAACeMUahZSV1JjSnFtQVFDT094RUNsQjFFTF9B",
  "scroll": "5m"
}

如果对数据不要求排序,可以直接指定**_doc**来排序,不计算_score。

scroll_id如何根据上次查询找到对应的数据?

在Query阶段通过scroll_id找到对应的快照,然后用lastEmittedDoc将原来的查询语句添加bool查询条件**( >=lastEmitted.doc + 1)**,在快照中找数据。

scroll_id为什么有时候会变化

scroll_id其中保留了shard信息,假如scroll查询语句需要路由到100个shard上查。scroll_id会比较长,记录这100个shard。有可能从开始到完成都需要路由到这100个shard,shard_id就不会变化。也有可能随着不断进行scroll,需要路由到的shard越来越少,shard_id也会越来越短。

scroll查询为什么要生成快照

1.加速。

  1. 由于ES是分布式项目,数据通过 page+sizesearch_after(无唯一ID) 方法无法保证每次分页后返回的数据是上一页没有出现过的(想一想每个分片之间都有打分相同的数据,合并阶段会发生什么,每个分片内也有打分相同的数据)。
scroll的缺点

scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。

4、track_total_hits

在不进行集群配置的情况下,Elasticsearch 限制了最多的数值为10000,如果想要访问命中的数据中超过10000条,需要配置max_result_window

从 Elasticsearch 7.0之后,为了提高搜索的性能,在 hits 字段中返回的文档数有时不是最精确的数值。

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10000,
      "relation" : "gte" //表示命中了一部分
    },
  ...
}

可以通过将track_total_hits设置为true来显示全量数据,得到准确的命中数(但无法抓取超过一万条数据),各个分片返回准确的命中数量。也可以设置整数来表明想要返回数量。

当page*size分页查找的数量超过一万时也会无法查询。

track_total_hits 优化了什么

count 和 hit 是两种类型的查询,count代表精确统计(和关系型数据库的count一致),hit是从相关性的角度来统计的。hit表示,es检索到了和查询条件相关的文档,并按照相关性排序,返回topN,至于到底有多少文档和查询条件相关,其实是不重要的。

索引会优先查到_score高的数据,到一万就直接返回,不用算其他可能不需要的文件的相关性了。

5、总结

ES分页方式对比

分页方式性能优点缺点场景
from + size灵活性好,实现简单深度分页问题数据量比较小,能容忍深度分页问题,一次性返回海量数据
scroll解决了深度分页问题无法反应数据的实时性(快照)维护成本高,需要维护一个 scroll_id海量数据的导出需要查询海量结果集的数据,群发
search_after性能最好不存在深度分页问题能够反映数据的实时变更实现复杂,需要有一个全局唯一的字段连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果海量数据的分页

跳页

mysql可以通过索引进行较快的跳页查询

select * from table where id > (select id from table order by id limit m, 1) limit n; 

es的倒排索引在深分页时跳页很低效!!!

需要有分页需求时,一般只开放前几十页的结果,页数过大时直接在业务层面报错。若需要显示命中的总条数,则设置track_total_hits=true。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐