全方位深度解读 Elasticsearch 分页查询

标签: dev | 发表时间:2021-05-08 00:00 | 作者:
出处:http://itindex.net/relian

1、关于 Elasticsearch 分页查询,这几个问题经常被问到

  • 问题1:想请问下,一次性获取索引上的某个字段的所有值(100 万左右),除了把 max_result_window 调大 ,还有没有啥方法?

  • 问题2:关于 es 的分页,每次拿 20 条展示在前台,然后点击下一页,在查询后面的20条数据,应该要怎么写?

  • 问题3:From+size、Scroll、search_after 的本质区别和应用场景分别是什么?

2、 Elasticsearch 支持的三种分页查询方式

  • From + Size 查询
  • Search After 查询
  • Scroll 查询

下面我就三种方式的联系与区别、优缺点、适用场景等展开进行解读。

2.1 From + size 分页查询

2.1.1 From + size 分页查询定义与实战案例

如下基础查询:

  GET kibana_sample_data_flights/_search   

默认返回前10个匹配的匹配项。其中:

  • from:未指定,默认值是 0,注意不是1,代表当前页返回数据的起始值。
  • size:未指定,默认值是 10,代表当前页返回数据的条数。

如下指定条件查询和排序:

  GET kibana_sample_data_flights/_search   
{
  "from": 0,
  "size":20,
  "query": {
    "match": {
      "DestWeather": "Sunny"
    }
  },
  "sort": [
    {
      "FlightTimeHour": {
        "order": "desc"
      }
    }
  ]
}

共返回 20 条数据。

其中:from + size 两个参数定义了结果页面显示数据的内容。

2.1.2 From + size 查询优缺点及适用场景

From + size 查询优点

  • 支持随机翻页。

From + size 查询缺点

  • 受制于 max_result_window 设置,不能无限制翻页。

  • 存在深度翻页问题,越往后翻页越慢。

From + size 查询适用场景

第一:非常适合小型数据集或者大数据集返回 Top N(N <= 10000)结果集的业务场景。

第二:类似主流 PC 搜索引擎(谷歌、bing、百度、360、sogou等)支持随机跳转分页的业务场景。


2.1.3 深度翻页不推荐使用 From + size

Elasticsearch 会限制最大分页数,避免大数据量的召回导致性能低下。

Elasticsearch 的 max_result_window 默认值是:10000。也就意味着:如果每页有 10 条数据,会最大翻页至 1000 页。

实际主流搜索引擎都翻不了那么多页,举例:百度搜索“上海”,翻到第 76 页,就无法再往下翻页了,提示信息如下截图所示:

如下的分页查询

  GET kibana_sample_data_flights/_search   
{
  "from": 0,
  "size":10001
}

GET kibana_sample_data_flights/_search
{
  "from": 10001,
  "size":10
}

报错如下:

  {   
  "error" : {
    "root_cause" : [
      {
        "type" : "illegal_argument_exception",
        "reason" : "Result window is too large, from + size must be less than or equal to: [10000] but was [10001]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
      }
    ],

什么原因?超过了最大窗口的限制,index.max_result_window 默认值为10000。

报错信息还同时给出了两个解决方案:

  • 方案一:大数据集召回数据使用:scroll api。

后面会详细讲解。

  • 方案二:调大 index.max_result_window 默认值。
  PUT kibana_sample_data_flights/_settings   
{
    "index.max_result_window":50000
}

官方建议:避免过度使用 from 和 size 来分页或一次请求太多结果。

不推荐使用 from + size 做深度分页查询的核心原因:

  • 搜索请求通常跨越多个分片,每个分片必须将其请求的命中内容以及任何先前页面的命中内容加载到内存中。

  • 对于翻页较深的页面或大量结果,这些操作会显著增加内存和 CPU 使用率,从而导致性能下降或节点故障。

什么意思呢?

  GET kibana_sample_data_flights/_search   
{
  "from": 10001,
  "size": 10
}

共 10 条数据加载到内存吗?不是的!

共:10011 条数据加载到内存,然后经过后台处理后返回了最后 10 条我们想要的数据。

那也就意味着,越往后翻页(也就是深度翻页)需要加载的数据量越大,势必会越耗费 CPU + 内存资源,响应也会越慢!

2.2 search_after 查询

2.2.1 search_after 查询定义与实战案例

search_after 查询 本质:使用前一页中的一组排序值来检索匹配的下一页。

前置条件:使用 search_after 要求后续的多个请求返回与第一次查询相同的排序结果序列。也就是说,即便在后续翻页的过程中,可能会有新数据写入等操作,但这些操作不会对原有结果集构成影响。

如何实现呢?

可以创建一个时间点 Point In Time(PIT)保障搜索过程中保留特定事件点的索引状态。

Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。

PIT的 本质:存储索引数据状态的轻量级视图。

如下示例能很好的解读 PIT 视图的内涵。

  # 创建 PIT   
POST kibana_sample_data_logs/_pit?keep_alive=1m

# 获取数据量 14074
POST kibana_sample_data_logs/_count

# 新增一条数据
POST kibana_sample_data_logs/_doc/14075
{
  "test":"just testing"
}

# 数据总量为 14075
POST kibana_sample_data_logs/_count


# 查询PIT,数据依然是14074,说明走的是之前时间点的视图的统计。
POST /_search
{
  "track_total_hits": true, 
  "query": {
    "match_all": {}
  }, 
   "pit": {
    "id": "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEN3RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA"
  }
}

有了 PIT,search_after 的后续查询都是基于 PIT 视图进行,能有效保障数据的一致性。

search_after 分页查询可以简单概括为如下几个步骤。

步骤 1:创建 PIT 视图,这是前置条件不能省。

  # Step 1: 创建 PIT   
POST kibana_sample_data_logs/_pit?keep_alive=5m

返回结果如下:

  {   
  "id" : "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA"
}

keep_alive=5m,类似scroll的参数,代表视图保留时间是 5 分钟,超过 5 分钟执行会报错如下:

    "type" : "search_context_missing_exception",   
  "reason" : "No search context found for id [91600]"

步骤 2:创建基础查询语句,这里要设置翻页的条件。

  # Step 2: 创建基础查询   
GET /_search
{
  "size":10,
  "query": {
    "match" : {
      "host" : "elastic"
    }
  },
  "pit": {
     "id":  "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA", 
     "keep_alive": "1m"
  },
  "sort": [ 
    {"response.keyword": "asc"}
  ]
}
  • 设置了PIT,检索时候就不需要再指定索引。

  • id 是基于步骤1 返回的 id 值。

  • 排序 sort 指的是:按照哪个关键字排序。

在每个返回文档的最后,会有两个结果值,如下所示:

   "sort" : [   
          "200",
          4
        ]
  • 其中,“200”就是我们指定的排序方式:基于 {"response.keyword": "asc"} 升序排列。

而 4 代表什么含义呢?

  • 4 代表——隐含的排序值,是基于_shard_doc 的升序排序方式。

官方文档把这种隐含的字段叫做:tiebreaker (决胜字段),tiebreaker 等价于_shard_doc。

tiebreaker 本质含义:每个文档的唯一值,确保分页不会丢失或者分页结果数据出现重复(相同页重复或跨页重复)。

步骤3:实现后续翻页。

  # step 3 : 开始翻页   
GET /_search
{
  "size": 10,
  "query": {
    "match" : {
      "host" : "elastic"
    }
  },
  "pit": {
     "id":  "48myAwEXa2liYW5hX3NhbXBsZV9kYXRhX2xvZ3MWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAWdG1TOWFMTF9UdTZHdVZDYmhoWUljZwAAAAAAAAEg5RZGOFJCMGVrZVNndTk3U1I0SG81V3R3AAEWM2hGWXpxLXFSSGlfSmZIaXJWN0dxUQAA", 
     "keep_alive": "1m"
  },
  "sort": [
    {"response.keyword": "asc"}
  ],
  "search_after": [                                
    "200",
    4
  ]
}

后续翻页都需要借助 search_after 指定前一页的最后一个文档的 sort 字段值。

如下代码所示:

    "search_after": [                                   
    "200",
    4
  ]

显然,search_after 查询仅支持向后翻页。

2.2.2 search_after 查询优缺点及适用场景

search_after 优点

  • 不严格受制于 max_result_window,可以无限制往后翻页。

ps:不严格含义:单次请求值不能超过 max_result_window;但总翻页结果集可以超过。

search_after 缺点
  • 只支持向后翻页,不支持随机翻页。

search_after 适用场景

  • 类似:今日头条分页搜索  https://m.toutiao.com/search

不支持随机翻页,更适合手机端应用的场景。

2.3 Scroll 遍历查询

2.3.1 Scroll 遍历查询定义与实战案例

相比于 From + size 和 search_after 返回一页数据,Scroll API 可用于从单个搜索请求中检索大量结果(甚至所有结果),其方式与传统数据库中游标(cursor)类似。

如果把  From + size 和 search_after 两种请求看做近实时的请求处理方式,那么 scroll 滚动遍历查询显然是非实时的。数据量大的时候,响应时间可能会比较长。

scroll 核心执行步骤如下:

步骤 1:指定检索语句同时设置 scroll 上下文保留时间。

实际上,scroll 已默认包含了 search_after 的PIT 的视图或快照功能。

从 Scroll 请求返回的结果反映了发出初始搜索请求时索引的状态,类似在那一个时刻做了快照。随后对文档的更改(写入、更新或删除)只会影响以后的搜索请求。

  POST kibana_sample_data_logs/_search?scroll=3m   
{
  "size": 100,
  "query": {
    "match": {
      "host": "elastic"
    }
  }
}

步骤 2:向后翻页继续获取数据,直到没有要返回的结果为止。

  POST _search/scroll                                      
{
  "scroll" : "3m",
  "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFkY4UkIwZWtlU2d1OTdTUjRIbzVXdHcAAAAAAAGmkBZ0bVM5YUxMX1R1Nkd1VkNiaGhZSWNn" 
}

scroll_id 值是步骤 1 返回的结果值。

2.3.2 Scroll 遍历查询优缺点及适用场景

    scroll 查询优点

  • 支持全量遍历。

ps:单次遍历的 size 值也不能超过 max_result_window 大小。

    scroll 查询缺点

  • 响应时间非实时。

  • 保留上下文需要足够的堆内存空间。

scroll 查询适用场景

  • 全量或数据量很大时遍历结果数据,而非分页查询。

  • 官方文档强调:不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:PIT + search_after。

3、小结

  • From+ size:需要随机跳转不同分页(类似主流搜索引擎)、Top 10000 条数据之内分页显示场景。

  • search_after:仅需要向后翻页的场景及超过Top 10000 数据需要分页场景。

  • Scroll:需要遍历全量数据场景 。

  • max_result_window:调大治标不治本,不建议调过大。

  • PIT:本质是视图。



本文说法有不严谨的地方,以官方文档为准。

欢迎大家就自己的分页实践进行留言讨论。

参考:

1. https://coralogix.com/log-analytics-blog/how-to-optimize-your-elasticsearch-queries-using-pagination

2. https://www.javatpoint.com/elasticsearch-pagination

3. https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html


推荐:

相关 [方位 深度 elasticsearch] 推荐:

全方位深度解读 Elasticsearch 分页查询

- - IT瘾-dev
1、关于 Elasticsearch 分页查询,这几个问题经常被问到. 问题1:想请问下,一次性获取索引上的某个字段的所有值(100 万左右),除了把 max_result_window 调大 ,还有没有啥方法. 问题2:关于 es 的分页,每次拿 20 条展示在前台,然后点击下一页,在查询后面的20条数据,应该要怎么写.

ElasticSearch 亿级数据检索深度优化

- - IT瘾-dev
数据平台已迭代三个版本,从头开始遇到很多常见的难题,终于有片段时间整理一些已完善的文档,在此分享以供所需朋友的实现参考,少走些弯路,在此篇幅中偏重于ES的优化,关于HBase,Hadoop的设计优化估计有很多文章可以参考,不再赘述. 在一业务系统中,部分表每天的数据量过亿,已按天分表,但业务上受限于按天查询,并且DB中只能保留3个月的数据(硬件高配),分库代价较高.

Elasticsearch深度应用(下) - 女友在高考 - 博客园

- -
query then fetch(默认搜索方式). 找到所有匹配的文档,并使用本地的Term/Document Frequery信息进行打分. 返回关于结果的元数据到请求节点. 注意,实际文档还没有发送,只是分数. 来自所有shard的分数合并起来,并在请求节点上进行排序,文档被按照查询要去进行选择.

Elasticsearch深度应用(上) - 女友在高考 - 博客园

- -
众所周知,Elasticsearch存储的基本单元是shard,ES中一个index可能分为多个shard,事实上每个shard都是一个Lucece的Index,并且每个Lucece Index由多个Segment组成,每个Segment事实上是一些倒排索引的集合,每次创建一个新的Document,都会归属一个新的Segment,而不会去修改原来的Segment.

[译]elasticsearch mapping

- - an74520的专栏
es的mapping设置很关键,mapping设置不到位可能导致索引重建. 请看下面各个类型介绍^_^. 每一个JSON字段可以被映射到一个特定的核心类型. JSON本身已经为我们提供了一些输入,支持 string,  integer/ long,  float/ double,  boolean, and  null..

Elasticsearch as Database - taowen - SegmentFault

- -
【北京上地】滴滴出行基础平台部招聘 Elasticsearch 与 Mysql binlog databus 开发工程师. 内推简历投递给: [email protected] 推销Elasticsearch. 时间序列数据库的秘密(1)—— 介绍. 时间序列数据库的秘密(2)——索引.

ElasticSearch 2 的节点调优(ElasticSearch性能)

- - 行业应用 - ITeye博客
一个ElasticSearch集群需要多少个节点很难用一种明确的方式回答,但是,我们可以将问题细化成一下几个,以便帮助我们更好的了解,如何去设计ElasticSearch节点的数目:. 打算建立多少索引,支持多少应用. elasticsearch版本: elasticsearch-2.x. 需要回答的问题远不止以上这些,但是第五个问题往往是容易被我们忽视的,因为单个ElasticSearch集群有能力支持多索引,也就能支持多个不同应用的使用.

elasticsearch的javaAPI之query

- - CSDN博客云计算推荐文章
elasticsearch的javaAPI之query API. the Search API允许执行一个搜索查询,返回一个与查询匹配的结果(hits). 它可以在跨一个或多个index上执行, 或者一个或多个types. 查询可以使用提供的 query Java API 或filter Java API.

Elasticsearch基础教程

- - 开源软件 - ITeye博客
转自:http://blog.csdn.net/cnweike/article/details/33736429.     Elasticsearch有几个核心概念. 从一开始理解这些概念会对整个学习过程有莫大的帮助.     接近实时(NRT).         Elasticsearch是一个接近实时的搜索平台.

ElasticSearch索引优化

- - 行业应用 - ITeye博客
ES索引的过程到相对Lucene的索引过程多了分布式数据的扩展,而这ES主要是用tranlog进行各节点之间的数据平衡. 所以从上我可以通过索引的settings进行第一优化:. 这两个参数第一是到tranlog数据达到多少条进行平衡,默认为5000,而这个过程相对而言是比较浪费时间和资源的. 所以我们可以将这个值调大一些还是设为-1关闭,进而手动进行tranlog平衡.