Elasticsearch 深分页问题

发布于:2025-08-16 ⋅ 阅读:(26) ⋅ 点赞:(0)

Elasticsearch 深分页问题

1️⃣ 什么是深分页 (Deep Pagination)

在 Elasticsearch 中,我们可以通过 from + size 进行分页查询:

GET my_index/_search
{
  "from": 100000,
  "size": 10
}

这表示 跳过前 100000 条,取 10 条记录
这种情况就属于 “深分页”,因为需要跨过几十万甚至上百万条数据后再取结果。

📌 官方定义:深分页是指 from + size 很大,尤其是 from 很大(如几十万、百万级)。


2️⃣ 为什么深分页有性能问题?

原因 1:ES 基于 Lucene,查询需要拉取全部数据再丢弃

  • Lucene 不支持直接 O(1) 跳过前 N 条。
  • 即使 from=100000 只需要 10 条,ES 仍需:
    1. 从不同的 Shard 拉取 前 100010 条候选文档
    2. score(相关性)排序
    3. 丢掉前 100000 条,只保留最后 10 条

带来的影响:

  • 内存消耗巨大
  • CPU 排序开销大
  • Shard 越多,代价越大(每个分片都要返回 from+size 条数据)

原因 2:分布式聚合 & 排序代价高

假设 5 个分片:

  1. 每个分片返回 from + size 条数据到协调节点(Coordinating Node)
  2. 协调节点合并排序
  3. 丢掉前 from

深分页时,网络传输 & 合并瓶颈显著。

💡 结论from 很大 → 查询性能急剧下降(甚至 OOM)


3️⃣ 官方建议和优化方案

✅ 方案 1:search_after(推荐)

  • 原理:基于上一页最后一条数据的排序值获取下一页
    不用 from,避免重复扫描
  • 缺点:不能直接跳到第 N 页,只能顺序翻页
  • 要求:有唯一且稳定的排序字段(如:时间戳 + _id

第一次请求:

GET my_index/_search
{
  "size": 10,
  "sort": [
    { "timestamp": "asc" },
    { "_id": "asc" }
  ]
}

记住最后一条的 (timestamp, _id)

下一页请求:

GET my_index/_search
{
  "size": 10,
  "sort": [
    { "timestamp": "asc" },
    { "_id": "asc" }
  ],
  "search_after": [ "2024-06-26T10:00:00Z", "abc123" ]
}

优点

  • 避免全量扫描
  • 性能可控稳定

缺点

  • 只能顺序翻页,不能跳转任意页

✅ 方案 2:scroll API(大批量导出)

  • 用于批量数据导出(不是普通分页展示)
  • 固定查询“快照”,不会随数据变更
  • 持有游标的代价较高

首次查询:

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

拉取下一批:

POST _search/scroll
{
  "scroll": "1m",
  "scroll_id": "DxF...abc"
}

✅ 方案 3:Point In Time(PIT)

  • ES 7.10+ 新增
  • 类似 scroll,但轻量,不固定数据快照
  • 一般结合 search_after 使用

创建 PIT:

POST my_index/_pit?keep_alive=1m

✅ 方案 4:业务层优化

  • 限制分页深度(例如最多翻到第 100 页)
  • 改用时间范围 + 条件分页(search_after 按时间+ID 排序更稳定)
  • 建立预聚合/索引表,提前规整数据

4️⃣ 方案对比

方法 跳任意页 性能 场景
from+size 小数据量分页
search_after 顺序分页大数据
scroll 批量导出/全量遍历
PIT+search_after 实时数据流式分页

🎯 最佳实践

  • 数据量小(<1w):直接 from+size
  • 大数据 + 顺序翻页search_after
  • 大批量导出scroll
  • 实时数据深翻:PIT + search_after

网站公告

今日签到

点亮在社区的每一天
去签到