1. ES 三种分页方案介绍

1.1. From + Size 查询

1.1.1. 定义与原理

通过from(偏移量,跳过前 N 条数据)和size(每页数据量)控制分页,本质是 “逻辑分页”。例如from=20&size=10表示返回第 3 页(跳过前 20 条,取 10 条)。

  • 底层逻辑:分布式环境中,每个分片需扫描from + size条数据,汇总后截断前from条,返回剩余size条。

1.1.2. 优缺点

  • 优点:
    • 实现简单,符合用户直觉(支持直接跳转到任意页码,如第 5 页);
    • 适用于小规模数据分页,响应速度快。
  • 缺点:
    • 深分页性能暴跌:当from过大(如from=10000),需扫描海量数据,内存和 CPU 开销剧增;
    • 受限于max_result_window:默认值 10000,超过会报错(from + size需≤10000);
    • 分布式一致性问题:分片数据刷新可能导致重复或漏数据。

1.1.3 适用场景

  • 浅分页(如前 10 页)、需随机访问页码的场景(如 PC 端搜索引擎支持 “跳至第 N 页”);
  • 数据量较小(总条数≤10000)或仅需返回 Top N 结果(如 “前 100 条热门商品”)。

1.2. Search After 查询

1.2.1. 定义与原理

基于 “游标” 的物理分页,依赖上一页最后一条数据的 “排序字段值”(sort values)作为起点,仅返回后续数据。需配合Point In Time(PIT)确保数据一致性(冻结某一时间点的索引状态)。

  • 底层逻辑:通过唯一排序字段(如price + _id)定位上一页末尾,直接获取后续数据,无需计算偏移量。

1.2.2. 优缺点

  • 优点:
    • 性能稳定:无需扫描冗余数据,支持超深分页(突破max_result_window限制);
    • 数据一致性高:基于排序字段唯一性,避免重复或漏数据;
    • 分布式友好:不依赖分片数据聚合,适合海量数据。
  • 缺点:
    • 不支持随机访问:只能基于前一页向后翻页(无法直接跳至第 N 页);
    • 依赖稳定排序:必须指定唯一排序字段(如_id),否则可能因排序值重复导致分页断裂;
    • 需维护 PIT 或排序游标,实现复杂度高于 From + Size。

1.2.3. 适用场景

  • 深分页场景(总条数>10000),且仅需连续向后翻页(如手机端 “下一页” 滚动加载);
  • 对性能稳定性要求高的场景(如电商商品列表、日志连续查询)。

1.3. Scroll 查询

1.3.1. 定义与原理

类似传统数据库的 “游标”,用于全量遍历数据。首次查询时创建 “上下文快照”(记录索引当前状态),后续通过scroll_id持续获取下一批数据,直到无结果返回。

  • 底层逻辑:快照创建后,索引后续的增删改不影响结果,确保遍历全量数据的完整性。

1.3.2.优缺点

  • 优点:
    • 支持全量数据遍历(不受max_result_window限制);
    • 适合批量导出或处理全量数据(如数据迁移、索引重建)。
  • 缺点:
    • 非实时性:基于快照查询,无法反映索引实时变化;
    • 内存开销大:需保留上下文快照,长时间占用堆内存;
    • 不支持随机访问:只能按顺序遍历,无法跳页。

1.3.3. 适用场景

  • 全量数据导出(如 “导出所有用户订单”);
  • 批量处理任务(如全量数据清洗、索引数据重放);
  • 无需实时性的离线场景(非用户交互类分页)。

1.4. 三种方案核心对比

维度From + SizeSearch AfterScroll
核心能力 随机访问分页(跳至第 N 页) 连续向后分页(基于前一页) 全量数据遍历
性能表现 浅分页快,深分页暴跌 全量分页性能稳定 响应较慢(非实时)
数据一致性 低(可能重复 / 漏数据) 高(依赖 PIT 和唯一排序) 高(基于快照)
适用数据量 小规模(≤10000 条) 大规模(无上限) 全量数据(任意规模)
典型场景 PC 端搜索引擎随机跳页 手机端连续滚动加载 数据导出、批量处理

1.5. 总结

三种方案的选择需结合业务场景:

  • 需随机跳页且数据量小 → 选From + Size
  • 需深分页且仅向后翻页 → 选Search After
  • 需全量遍历(非实时) → 选Scroll

实际业务中,常以From + Size(浅分页)配合Search After(深分页)实现灵活且高效的分页逻辑,避免单一方案的局限性。
 

2. 深度优化方案:应对随机访问与深度分页混合场景的 ES 分页策略

2.1 场景痛点与核心矛盾

当用户行为呈现 “随机访问(如直接跳第 10 页)→ 深度分页(如从第 10 页跳第 100 页)→ 再次随机访问(如从第 100 页跳回第 20 页)” 的无规律模式时,单纯的From + SizeSearch After均存在明显缺陷:

  • From + Size:深分页(from过大)时性能暴跌,且受限于max_result_window(默认 10000 条),无法支持超深随机访问。
  • Search After:依赖上一页的排序游标,无法直接跳转到任意页码,无法满足随机访问需求。

核心矛盾在于:随机访问需要 “偏移量” 逻辑,而深度分页需要 “游标定位” 效率,需通过混合策略同时满足两种需求。

2.2 优化方案:基于 “阈值划分 + 游标缓存 + PIT 快照” 的混合分页

通过以下三层机制实现无规律分页场景的高效支持:
1. 阈值划分:浅分页用From + Size,深分页用Search After
设定一个阈值(如from ≤ 1000,可根据业务调整),区分浅分页与深分页:

  • 浅分页(from ≤ 1000):直接使用From + Size,利用其随机访问优势,同时记录每页最后一条数据的排序游标(sort values) 并缓存。
  • 深分页(from > 1000):禁止直接使用From + Size,强制通过 “浅分页游标 +Search After” 间接跳转,避免深分页性能问题。
2. 游标缓存:存储关键页的排序值,支撑深分页随机访问
为解决Search After无法直接跳页的问题,需缓存 “关键页” 的排序游标(如每 100 页缓存一次),作为深分页随机访问的 “跳板”。

  • 缓存设计:
    • 键(Key):索引名_页码(如products_100)。
    • 值(Value):该页最后一条数据的sort values(如[99.9, "product_10000"],包含业务排序字段 +_id确保唯一性)。
  • 缓存时机:
    • 浅分页时,自动缓存当前页游标。
    • 深分页通过Search After访问时,每间隔 N 页(如 100 页)主动缓存一次游标。
3. Point in Time (PIT):保证分页数据一致性
由于 ES 数据实时更新(如新增 / 删除文档),不同分页查询可能因索引刷新导致结果不一致(如重复或漏数据)。通过 PIT 创建查询快照,确保所有分页操作基于同一时间点的索引状态:

  • PIT 创建:查询前生成快照,有效期内(如 5 分钟)所有分页操作复用该快照。
  • PIT 清理:分页结束后手动释放,避免资源泄露。

2.3 具体实现步骤与示例

以电商商品索引products为例,排序字段为price(价格)+_id(唯一标识),每页size=10,阈值from=1000(即第 100 页为浅 / 深分页分界)。
场景 1:用户随机访问浅分页(如第 5 页)
  • 步骤 1:创建 PIT 快照
    POST /products/_pit?keep_alive=5m

    返回 PIT ID:pit_id: "products:123456..."
  • 步骤 2:用From + Size查询第 5 页
     
    GET /_search
    {
      "pit": {"id": "products:123456..."},
      "from": 40,  // (5-1)*10=40
      "size": 10,
      "sort": [{"price": "asc"}, {"_id": "asc"}]
    }
    
     
  • 步骤 3:缓存第 5 页游标
    提取返回结果中最后一条数据的sort值(如[89.0, "product_49"]),存入缓存products_5 → [89.0, "product_49"]
场景 2:用户从第 5 页跳至深分页第 200 页(from=1990 > 1000
  • 步骤 1:检查缓存中是否有 “跳板页”(如第 100 页)的游标
    假设缓存中存在products_100 → [199.0, "product_999"]
  • 步骤 2:从跳板页(第 100 页)用Search After向后翻 100 页至第 200 页
    GET /_search
    {
      "pit": {"id": "products:123456..."},
      "size": 10,
      "sort": [{"price": "asc"}, {"_id": "asc"}],
      "search_after": [199.0, "product_999"],  // 第100页的游标
      "from": 0  // Search After模式下from必须为0
    }
    
     

    重复执行上述查询 100 次(每次用前一页最后一条的sort值作为search_after),最终到达第 200 页。
  • 步骤 3:缓存第 200 页游标
    存入缓存products_200 → [299.0, "product_1999"]
场景 3:用户从第 200 页跳回第 50 页(浅分页)
  • 直接使用From + Size查询第 50 页(from=490 ≤ 1000):
    GET /_search
    {
      "pit": {"id": "products:123456..."},
      "from": 490,  // (50-1)*10=490
      "size": 10,
      "sort": [{"price": "asc"}, {"_id": "asc"}]
    }
    
     

    (复用同一 PIT 确保数据一致性,无需依赖缓存)
场景 4:用户从第 200 页继续向后翻至第 201 页(连续深分页)
  • 直接用Search After基于第 200 页的缓存游标查询:
    GET /_search
    {
      "pit": {"id": "products:123456..."},
      "size": 10,
      "sort": [{"price": "asc"}, {"_id": "asc"}],
      "search_after": [299.0, "product_1999"]  // 第200页的缓存游标
    }
    
     

2.4 方案优势与注意事项

优势
  1. 兼顾随机访问与深分页效率:浅分页用From + Size满足灵活跳转,深分页通过 “缓存跳板 +Search After” 避免性能暴跌。
  2. 突破max_result_window限制:深分页依赖Search After,理论上支持无限分页。
  3. 数据一致性保障:PIT 快照确保所有分页操作基于同一数据版本,避免重复 / 漏数据。
  4. 缓存降低深分页跳转成本:关键页游标缓存减少Search After的连续翻页次数(如从第 1 页跳第 1000 页,通过 10 个跳板页即可实现)。
注意事项
  1. 阈值与缓存粒度设计:
    • 阈值(如from=1000)需根据 ES 集群性能测试确定,过大(如from=10000)会导致浅分页性能下降,过小则增加缓存与Search After的使用频率。
    • 缓存粒度(如每 100 页缓存一次)需平衡缓存空间与跳转效率,高频访问的深度页可加密缓存。
  2. 排序字段唯一性:
    必须包含_id等唯一字段(即使业务排序字段相同,_id也能确保sort values唯一),否则Search After可能因游标重复导致数据漏读。
  3. PIT 资源管理:
    • PIT 有效期不宜过长(如 5-10 分钟),避免占用过多内存。
    • 前端需在分页结束(如关闭页面)时调用接口清理 PIT,防止资源泄露。
  4. 异常处理:
    • 若缓存的游标失效(如对应文档被删除),需降级为从最近的有效跳板页重新计算。
    • 深分页跳转时,若中间页数据量不足(如总页数 < 目标页),需返回空结果并提示用户。

2.5 总结

该方案通过 “阈值划分 + 游标缓存 + PIT 快照” 的三层机制,将From + Size的随机访问优势与Search After的深分页效率结合,完美应对无规律分页场景:

  • 浅分页(from ≤ 阈值):用From + Size直接跳转,缓存游标作为深分页跳板。
  • 深分页(from > 阈值):通过缓存的关键页游标,用Search After间接实现随机访问,同时支持连续向后翻页。
  • 全流程复用 PIT 快照,确保数据一致性。

此方案在电商、日志分析等需要灵活分页的场景中可显著提升用户体验与系统稳定性,是 ES 复杂分页场景的最优实践。
 posted on 2025-07-30 18:04  xibuhaohao  阅读(66)  评论(0)    收藏  举报