在Elasticsearch的实际应用中,高效的分页查询是开发者经常面临的挑战。当数据量达到百万甚至千万级别时,传统的from + size
分页方式会遭遇严重的性能瓶颈。本文将全面剖析Elasticsearch提供的两种深度分页解决方案:Search After 和Scroll API,从原理到实践,帮助您彻底掌握这两种技术。
一、传统分页的致命缺陷
1. from + size
分页的工作原理
json
json
GET /products/_search
{
"from": 10000, // 从第10000条开始
"size": 10, // 获取10条记录
"query": {
"match_all": {}
}
}
2. 分布式系统中的执行流程
- 查询分发:协调节点向索引的所有分片(假设5个)发送查询请求
- 分片处理 :每个分片必须本地计算并返回10010条数据(from + size)
- 结果聚合:协调节点收集所有分片的结果(5 × 10010 = 50050条)
- 全局排序:对50050条数据进行排序
- 结果截取:最终返回10000-10010条的结果
3. 性能瓶颈分析
数据量 | 分页深度 | 分片数 | 内存消耗计算 | 响应时间 |
---|---|---|---|---|
100万条 | 第1000页 | 5 | (1000×10 + 10)×5 ≈ 5万条 | 800ms |
1000万条 | 第1万页 | 5 | (10000×10 + 10)×5 ≈ 50万条 | 5s+ |
1亿条 | 第10万页 | 5 | (100000×10 + 10)×5 ≈ 500万条 | OOM风险 |
核心问题 :内存消耗与(from + size) × 分片数
成正比,深度分页会导致集群内存溢出!
二、Search After解决方案
1. 核心设计思想
Search After采用游标式分页设计:
- 基于上一页最后一条的排序值作为"书签"
- 完全避免
from
参数的深度翻页计算 - 保持实时查询能力
2. 完整使用示例
首次查询(必须指定稳定排序) :
json
json
GET /orders/_search
{
"size": 10,
"sort": [
{"order_date": "desc"}, // 主排序字段
{"_id": "asc"} // 确保排序唯一性的辅助字段
],
"track_total_hits": false // 禁用总命中数计算提升性能
}
获取下一页:
json
json
GET /orders/_search
{
"size": 10,
"search_after": ["2023-05-20T08:00:00", "order123"], // 上一页最后结果的排序值
"sort": [
{"order_date": "desc"}, // 必须与首次查询一致
{"_id": "asc"}
]
}
3. 关键技术细节
排序字段选择原则
-
必须包含唯一字段 (如
_id
)作为最后排序条件 -
避免使用评分
_score
(可能因分片不同而变化) -
推荐使用日期+ID组合:
json
css"sort": [ {"create_time": {"order": "desc", "format": "strict_date_optional_time_nanos"}}, {"_id": "asc"} ]
性能优化技巧
-
禁用总命中数计算 :
"track_total_hits": false
-
合理设置批次大小:通常50-500条/页
-
使用docvalue_fields替代_source:
json
json{ "docvalue_fields": ["order_date", "status"], "_source": false }
4. 适用场景
- 用户界面的实时分页浏览
- 需要反映最新数据的搜索场景
- 顺序翻页需求(不支持随机跳页)
三、Scroll API解决方案
1. 核心设计思想
Scroll创建搜索上下文快照:
- 初始化时建立数据快照(类似数据库事务快照)
- 通过游标(cursor)批量获取结果
- 适合离线的批处理操作
2. 完整使用示例
初始化Scroll(创建快照) :
json
json
POST /products/_search?scroll=5m // 快照保留5分钟
{
"size": 500, // 每批获取500条
"sort": ["_doc"], // 最优性能排序方式
"query": {
"range": {
"price": {"gte": 100}
}
},
"_source": ["id", "name"] // 只返回必要字段
}
获取后续批次:
json
json
POST /_search/scroll
{
"scroll": "5m", // 每次续期5分钟
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAA..."
}
必须手动释放资源:
json
sql
DELETE /_search/scroll
{
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAA..."
}
3. 关键技术细节
性能优化方案
-
并行加速(Sliced Scroll) :
json
jsonPOST /products/_search?scroll=5m { "slice": { "id": 0, // 分片编号(0到max-1) "max": 4 // 总分片数(通常等于主分片数) }, "size": 500, "sort": ["_doc"] }
-
资源管理最佳实践:
-
设置合理的scroll超时(通常2-10分钟)
-
使用完成后立即释放scroll资源
-
监控打开的scroll上下文数量:
json
sqlGET /_nodes/stats/indices/search
-
内存管理警告
- 每个scroll上下文会占用约1MB堆内存
- 1000个scroll = 1GB堆内存占用
- 必须实现超时自动清理机制
4. 适用场景
- 全量数据导出(ETL流程)
- 大数据量的后台批处理
- 不需要实时性的数据分析
四、深度对比与选型指南
1. 技术特性对比表
特性维度 | Search After | Scroll API |
---|---|---|
实时性 | ✔️ 实时获取最新数据 | ❌ 基于快照创建时的数据状态 |
内存效率 | ✔️ 仅需维护当前批次数据 | ❗ 需在服务端维护搜索上下文 |
排序灵活性 | ❗ 必须指定稳定排序规则 | ✔️ 支持任意排序(但_doc 最快) |
跳页能力 | ❌ 只能顺序翻页 | ❌ 只能顺序遍历 |
最大深度 | ✔️ 理论上无限制 | ✔️ 理论上无限制 |
资源管理 | ✔️ 自动管理 | ❗ 需手动清理 |
典型QPS | ✔️ 高(适合C端接口) | ❗ 低(适合后台任务) |
2. 选型决策树
