ES深度分页查询详解

本文详细介绍了Elasticsearch的From+Size、Scroll遍历、SearchAfter查询和Point-in-Time (PIT)在深度分页中的应用,探讨了各自优缺点,以及为何官方不再推荐Scroll API。此外,还提供了业务层面的优化策略应对大规模分页问题。

一、ES支持的三种分页查询方式

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

在这里插入图片描述
说明:
官方已经不再推荐采用Scroll API进行深度分页。如果遇到超过10000的深度分页,推荐采用search_after + PIT。

官方文档地址

二、分布式系统中的深度分页问题

为什么分布式存储系统中对深度分页支持都不怎么友好呢?

首先我们看一下分布式存储系统中分页查询的过程。

假设在一个有 4 个主分片的索引中搜索,每页返回10条记录。

当我们请求结果的第1页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 40 个结果排序得到全部结果的前 10 个。

当我们请求第 99 页(结果从 990 到 1000),需要从每个分片中获取满足查询条件的前1000个结果,返回给协调节点, 然后协调节点对全部 4000 个结果排序,获取前10个记录。

当请求第10000页,每页10条记录,则需要先从每个分片中获取满足查询条件的前100010个结果,返回给协调节点。然后协调节点需要对全部(100010 * 分片数4)的结果进行排序,然后返回前10个记录。

可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。
这就是 web 搜索引擎对任何查询都不要返回超过 10000 个结果的原因。

在这里插入图片描述

三、From + Size 查询

1、准备数据

PUT user_index
{
  "mappings": {
     "properties": {
        "id":  {"type": "integer"},
        "name":   {"type": "keyword"}
     }
  }
}

POST user_index/_bulk
{ "create":  {  "_id": "1" }}
{ "id":1,"name":"老万"}
{ "create":  {  "_id": "2" }}
{ "id":2,"name":"老王"}
{ "create":  {  "_id": "3" }}
{ "id":3,"name":"老刘"}
{ "create":  {  "_id": "4" }}
{ "id":4,"name":"小明"}
{ "create":  {  "_id": "5" }}
{ "id":5,"name":"小红"}

2、查询演示

无条件查询

POST user_index/_search

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

指定from+size查询

POST user_index/_search
{
    "from": 0, 
    "size": 10,
    "query": {
        "match_all": {}
    },
    "sort": [
        {"id": "asc"}    
    ]
}

3、max_result_window

es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的。
比如from = 5000, size=10, es 需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条
数据返回,这种方式类似于mongo的 skip + size。

除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000
也就是当 from + size > max_result_window 时,es 将返回错误。

POST user_index/_search
{
    "from": 10000, 
    "size": 10,
    "query": {
        "match_all": {}
    },
    "sort": [
        {"id": "asc"}    
    ]
}

这是ElasticSearch最简单的分页查询,但以上命令是会报错的。

报错信息,指window默认是10000。

"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."
      }
    ],
    "type": "search_phase_execution_exception",

怎么解决这个问题,首先能想到的就是调大这个window。

PUT user_index/_settings
{ 
    "index" : { 
        "max_result_window" : 20000
    }
}

然后这种方式只能暂时解决问题,当es 的使用越来越多,数据量越来越大,深度分页的场景越来越复杂时,如何解决这种问题呢?

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

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

  • 搜索请求通常跨越多个分片,每个分片必须将其请求的命中内容以及任何先前页面的命中内容加载到内存中。
  • 对于翻页较深的页面或大量结果,这些操作会显著增加内存和 CPU 使用率,从而导致性能下降或节点故障。

四、Search After 查询

search_after 参数使用上一页中的一组排序值来检索下一页的数据。
使用 search_after 需要具有相同查询和排序值的多个搜索请求。 如果在这些请求之间发生刷新,结果的顺序可能会发生变化,从而导致跨页面的结果不一致。 为防止出现这种情况,您可以创建一个时间点 (PIT) 以保留搜索中的当前索引状态。

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

注意⚠️:
es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。
Point In Time(PIT)是 Elasticsearch 7.10 版本之后才有的新特性。

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

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

#1、给索引user_index创建pit
POST /user_index/_pit?keep_alive=5m

#2、统计当前记录数 5
POST /user_index/_count

#3、根据pit统计当前记录数 5
GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
	    "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
	    "keep_alive": "5m"
  },
  "sort": [
        {"id": "asc"}    
    ]
}
#4、插入一条数据
POST user_index/_bulk
{ "create":  {  "_id": "6" }}
{ "id":6,"name":"老李"}

#5、数据总量 6
POST /user_index/_count

#6、根据pit统计数据总量还是 5 ,说明是根据时间点的视图进行统计。
GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
	    "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
	    "keep_alive": "5m"
  },
  "sort": [
        {"id": "asc"}    
    ]
}

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

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

1、获取索引的pit

POST /user_index/_pit?keep_alive=5m

2、根据pit首次查询

说明:根据pit查询的时候,不用指定索引名称。

GET /_search
{
  "query": {
        "match_all": {}
    },
  "pit": {
	    "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIODBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
	    "keep_alive": "1m"
  },
  "sort": [
        {"id": "asc"}    
    ]
}

查询结果:返回的sort值为2.

hits" : [
      {
        "_index" : "user_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : null,
        "_source" : {
          "id" : 2,
          "name" : "老王"
        },
        "sort" : [
          2
        ]
      }
    ]

3、根据search_after和pit进行翻页查询

说明:
search_after指定为上一次查询返回的sort值。
要获得下一页结果,请使用最后一次命中的排序值(包括 tiebreaker)作为 search_after 参数重新运行先前的搜索。 如果使用 PIT,请在 pit.id 参数中使用最新的 PIT ID。 搜索的查询和排序参数必须保持不变。 如果提供,则 from 参数必须为 0(默认值)或 -1。

GET /_search
{
  "size": 1, 
  "query": {
        "match_all": {}
    },
  "pit": {
	    "id":  "i6-xAwEKdXNlcl9pbmRleBZYTXdtSFRHeVJrZVhCby1OTjlHMS1nABZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3AAAAAAAAIOJ7FmdBWEd2UmFVVGllZldNdnhPZDJmX0EBFlhNd21IVEd5UmtlWEJvLU5OOUcxLWcAAA==", 
	    "keep_alive": "5m"
  },
  "sort": [
        {"id": "asc"}    
    ],
  "search_after": [                                
    2
  ]
}

优缺点分析

search_after 查询仅支持向后翻页。
不严格受制于 max_result_window,可以无限制往后翻页。
单次请求值不能超过 max_result_window;但总翻页结果集可以超过。

思考🤔???

1、为什么采用search_after 查询能解决深度分页的问题?
2、search_after + pit分页查询过程中,PIT视图过期怎么办?
3、search_after 查询,如果需要回到前几页怎么办?

五、Scroll 遍历查询

ES官方不再推荐使用Scroll API 进行深度分页。 如果您需要在分页超过 10,000 个点击时保留索引状态,请使用带有时间点 (PIT) 的 search_after 参数。
相比于 From + size 和 search_after 返回一页数据,Scroll API 可用于从单个搜索请求中检索大量结果(甚至所有结果),其方式与传统数据库中游标(cursor)类似。

Scroll API 原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。

所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id

1、首次查询,并获取_scroll_id

POST /user_index/_search?scroll=1m
{
  "size": 1,
  "query": {
        "match_all": {}
    }
}

返回结果:

{
  "_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlQBZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3",
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 6,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "user_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "id" : 1,
          "name" : "老万"
        }
      }
    ]
  }
}

2、根据scroll_id遍历数据

POST /_search/scroll                                                               
{
  "scroll" : "1m",                                                                 
  "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3" 
}

3、删除游标scroll

DELETE /_search/scroll
{
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmdBWEd2UmFVVGllZldNdnhPZDJmX0EAAAAAACDlKxZ0TEpMcVRuNFRxaWI4cXFTVERhOHR3"
}

优缺点

scroll查询的相应数据是非实时的,这点和PIT视图比较类似,如果遍历过程中插入新的数据,是查询不到的。
并且保留上下文需要足够的堆内存空间。

适用场景

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

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

六、业务层面优化

很多时候,技术解决不了的问题,可以通过业务层面变通下来解决!
比如,针对分页场景,我们可以采用如下优化方案。

1、增加默认的筛选条件

通过尽可能的增加默认的筛选条件,如:时间周期和最低评分,减少满足条件的数据量,避免出现深度分页的情况。

2、采用滚动增量显示

典型场景比如手机上面浏览微博,可以一直往下滚动加载。
示例:
如下列表展示中,取消了分页按钮,通过滚动条增量加载数据。
在这里插入图片描述

3、小范围跳页

通过对分页组件的设计,禁止用户直接跳转到非常大的页码中。比如直接跳转到最后一页这种操作。
示例:
google搜索的小范围跳页
在这里插入图片描述


总结

分布式存储引擎的深度分页目前没有完美的解决方案。
比如针对百度、google这种全文检索的查询,通过From+ size返回Top 10000 条数据完全能满足使用需求,末尾查询评分非常低的结果一般参考意义都不大。

  • From+ size:需要随机跳转不同分页(类似主流搜索引擎)、Top 10000 条数据之内分页显示场景。
  • search_after:仅需要向后翻页的场景及超过Top 10000 数据需要分页场景。
  • Scroll:需要遍历全量数据场景 。
  • max_result_window:调大治标不治本,不建议调过大。
  • PIT:本质是视图。
    在这里插入图片描述

ElasticSearch分页与深度分页问题解决
全方位深度解读 Elasticsearch 分页查询

<think>嗯,用户想了解Elasticsearch深度分页查询Search_after,并且要求图文教程。首先,我得回忆一下Search_after的作用和适用场景。记得之前学的时候,Elasticsearch的分页有from+size的方式,但深度分页时性能会下降,特别是数据量大的时候。Search_after应该是解决这个问题的。 那Search_after的原理是什么呢?应该是基于上一页的排序值来获取下一页的数据,避免深度翻页时的性能损耗。不过需要基于唯一的排序字段,比如时间戳或者_id,否则可能会有重复。要确保排序字段的值是唯一的,否则分页可能会出错。 接下来,用户可能需要具体的步骤和示例。应该分步骤讲解,先讲前置条件,比如索引创建和数据准备,然后讲如何第一次查询,获取search_after的值,再讲如何用这个值进行下一页的查询。同时要注意,使用Search_after时必须指定排序,而且排序字段要唯一,否则分页可能不准确。 可能还需要对比一下Search_afterScroll API的区别,Scroll适合大量数据导出,而Search_after适合实时分页。另外,用户可能想知道Search_after的优缺点,比如不能跳页,只能逐页翻,但性能更好。 图文教程的话,可能需要插入一些示例代码和结果截图。虽然现在不能实际贴图,但可以用文字描述示例输出,比如第一次查询返回的数据和sort值,下一页查询使用search_after参数的情况。 还要注意用户可能遇到的常见问题,比如排序字段不唯一导致分页重复或遗漏,或者忘记添加排序条件。需要提醒用户确保排序字段的唯一性,比如使用_id作为第二排序字段。 最后,总结一下适用场景,比如移动端加载更多,或者需要深度分页但不需要跳页的情况。可能用户的实际应用场景是这样的,所以明确说明Search_after的适用性很重要。</think>Elasticsearch 深度分页查询 Search_after 图文教程(文字版) 一、为什么需要 Search_after? ![分页性能对比图] 传统分页(from+size)在深度分页时性能骤降,Search_after 通过游标机制实现高效深度分页,适合处理百万级数据的分页场景。 二、核心原理 1. 基于排序字段的值作为游标 2. 每次查询携带上一页最后一条的排序值 3. 避免全局排序,性能损耗稳定($O(1)$) 三、具体使用步骤 1. 前置条件:创建测试索引 ```json PUT /products { "mappings": { "properties": { "name": { "type": "text" }, "price": { "type": "double" }, "timestamp": { "type": "date" } } } } ``` 2. 首次查询(必须包含排序) ```json GET /products/_search { "size": 2, "sort": [ { "timestamp": "desc" }, { "_id": "asc" } // 确保排序唯一性 ] } ``` 响应示例: ```json { "hits": { "hits": [ { "_id": "1", "_source": { "name": "Phone", "price": 599.99 }, "sort": [1658800000000, "1"] }, { "_id": "2", "_source": { "name": "Laptop", "price": 1299.99 }, "sort": [1658700000000, "2"] } ] } } ``` 3. 后续查询使用 search_after ```json GET /products/_search { "size": 2, "sort": [ { "timestamp": "desc" }, { "_id": "asc" } ], "search_after": [1658700000000, "2"] // 使用上页最后记录的sort值 } ``` 四、注意事项(⚠️关键点) 1. 必须指定唯一性排序组合(推荐:业务字段 + _id) 2. 不能跳页查询,只能顺序翻页 3. 索引更新可能导致分页结果变化(实时性特征) 4. 需要客户端维护上下文状态 五、与 Scroll API 对比 | 特性 | Search_after | Scroll | |---------------------|------------------------|----------------------| | 实时性 | ✅ 实时 | ❌ 快照 | | 内存消耗 | 低 | 高 | | 适用场景 | 用户实时分页 | 大数据量导出 | | 最大返回 | 无限制 | 取决于设置 | 六、最佳实践 1. 组合排序字段示例: ```json "sort": [ { "create_time": "desc" }, { "score": "desc" }, { "_id": "asc" } ] ``` 2. 客户端实现逻辑: ``` 首次查询 → 保存sort值 → 下次请求携带search_after → 循环直到无结果 ``` 七、常见问题解决 1. 出现重复数据 → 检查排序字段唯一性 2. 分页中断 → 确保使用相同的排序条件 3. 性能下降 → 避免在text字段上排序 适用场景:移动端「加载更多」、日志分批处理、大数据量报表生成等需要深度分页但不需要跳页的场合。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

斗者_2013

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值