ES 深度分页问题及其解决方案详解

ES 深度分页问题及其解决方案详解

 

1、简介

Elasticsearch(ES)的深度分页问题是指在数据量很大的情况下,使用from和size参数进行分页时,随着页码的增加,性能会急剧下降,甚至可能导致内存溢出。这是因为ES的分页原理导致的。

在ES中,当我们执行一个查询并指定from和size时,ES需要为每个分片创建一个大小为from+size的优先级队列,然后协调节点需要收集所有分片的结果,再全局排序,最后返回第from到from+size条结果。这意味着,当from值很大时,每个分片都需要构建一个很大的优先级队列,并且协调节点需要处理大量的数据,这会消耗大量的CPU、内存和网络资源。

例如,如果我们有10个分片,要获取第10000到10010条记录(即from=10000, size=10),那么每个分片都需要返回10010条记录给协调节点,然后协调节点对10*10010=100100条记录进行全局排序,最后返回10条记录。显然,这非常低效。

因此,ES默认限制最大只能查询10000条记录(通过index.max_result_window设置,默认10000)。如果超过这个限制,就需要使用其他解决方案。

 

2、解决方案

在Elasticsearch中,分页查询的实现主要通过两个参数from和size来实现。正常情况下分页代码如实下面这样的:
# 查询第一页5条数据
GET /employee/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0,
  "size": 5  
}
但是如果我们查询的数据页数特别大,当from + size大于10000的时候,就会出现问题,如下图报错信息所示:

image

分析可知,查询结果的窗口大小超过了最大窗口的限制,而index. max_result_window默认值为10000
Elasticsearch会限制最大分页数,避免因大数据量的召回导致系统性能低下。Elasticsearch的max_result_window默认值是10000,意味着每页有10条数据,会最大翻页至1000页。主流搜索引擎实际都翻不了那么多页。解决方案如下:
 

1、Scroll Search滚动查询

Scroll API适用于需要大量数据导出或离线处理的场景,但不适合实时分页。它通过创建一个快照,然后持续滚动获取数据,但快照的数据在滚动期间不会变化。​这种方式与传统数据库中的游标(cursor)类似。scroll滚动遍历查询是非实时的,数据量大的时候,响应时间可能会比较长。
ES7之后,官方已经不再建议使用scroll API进行深度分页。如果要分页检索超过 Top 10,000+ 结果时,推荐使用:search_after

Scroll API(滚动查询)步骤:

  1. 发起一个滚动搜索请求,指定scroll参数(表示快照的保持时间)和size(每次滚动返回的文档数)。

  2. 从响应中获取scroll_id,然后使用scroll_id来获取下一批数据。

  3. 重复步骤2直到没有数据返回。

注意:滚动查询会占用大量的资源,特别是在快照存活期间。因此,在完成后应该及时清除滚动查询。
示例:
初始查询:
POST /my_index/_search?scroll=1m
{
  "size": 100,
  "query": {
  "match_all": {}
  }
}

然后使用返回的_scroll_id来获取下一批数据:
POST /_search/scroll
{
"scroll": "1m",
"scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAA..."
}
删除游标scroll
scroll超过超时后,搜索上下文会自动删除。然而,保持scroll打开是有代价的,因此一旦不再使用,就应明确清除scroll上下文。
DELETE /_search/scroll
{
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmNwcVdjblRxUzVhZXlicG9HeU02bWcAAAAAAABmzRY2YlV3Z0o5VVNTdWJobkE5Z3MtXzJB"
}
scroll查询的优缺点如下:

scroll查询优点:支持全量遍历,是检索大量文档的重要方法,但单次遍历的size值不能超过max_result_window的大小。

scroll查询缺点:

  • 响应是非实时的;
  • 保留上下文需要具有足够的堆内存空间;
  • 需要通过更多的网络请求才能获取所有结果。
 

2、search_after查询

 Search After是一种基于上一页最后一行的排序值来检索下一页的方法,适用于实时滚动分页。它要求排序字段的值是唯一的(通常使用_id字段)。

 查询步骤:

  1. 第一次查询需要指定排序字段(最好是一个唯一字段,如_id)和size。

  2. 从响应中获取最后一个文档的排序值,然后将这个值作为search_after参数进行下一次查询。

 示例:

# 首次查询
GET /products/_search
{
  "size": 10,
  "query": {
    "match_all": {}
  },
  "sort": [
    {"price": "asc"},
    {"_id": "asc"}  # 二级排序确保唯一性
  ]
}

# 响应包含 sort 值
{
  "took": 15,
  "hits": {
    "hits": [
      {
        "_id": "1",
        "_score": null,
        "_source": {...},
        "sort": [100, "1"]  # 排序字段值
      },
      ...
    ]
  }
}

# 下一页查询
GET /products/_search
{
  "size": 10,
  "query": {
    "match_all": {}
  },
  "sort": [
    {"price": "asc"},
    {"_id": "asc"}
  ],
  "search_after": [100, "1"]  # 使用上一页最后一条的sort值
}

 

3、Point In Time + Search After(ES 7.10+)

 PIT(Point in Time)为搜索创建一个时间点视图,确保在滚动过程中数据不会发生变化。PIT是Elasticsearch 7.10版本之后才有的新特性,实际上是存储索引数据状态的轻量级视图。

步骤:

  1. 创建一个PIT,指定索引和保持时间(默认5分钟)。

  2. 使用PIT的id进行搜索,结合Search After进行分页。

  3. 完成后删除PIT以释放资源。

示例:

# 1. 创建PIT(有效期5分钟)
POST /products/_pit?keep_alive=5m

# 响应
{
  "id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2Rl
XzIAAAAAAAAAAAwBYgACBXV1aWQxAAAFdXVpZDIAAAt0ZXN0LWluZGV4AAA=" } # 2. 使用PIT进行Search After查询,设置了PIT,因此检索时候就不需要再指定索引
GET /_search
{
  "size": 10,
  "query": {
    "match": {
      "title": "商品"
    }
  },
  "pit": {
    "id": "上面的ID",
    "keep_alive": "5m"
  },
  "sort": [
    {"_shard_doc": "asc"}  # ES内部排序,性能最优
  ]
}

# 3. 后续查询携带search_after
GET /_search
{
  "size": 10,
  "query": {...},
  "pit": {...},
  "sort": [...],
  "search_after": [上次最后一条的sort值]
}

# 4. 删除PIT
DELETE /_pit
{
  "id": "PIT_ID"
}
search_after缺点:只支持向后翻页,不支持随机翻页。search_after不支持随机翻页,更适合在手机端应用的场景中使用,类似今日头条等产品的分页搜索。

 

总结

image

 

posted @ 2026-01-12 16:08  邓维-java  阅读(6)  评论(0)    收藏  举报