当 Elasticsearch 查询需要遍历大量数据时,建议放弃传统的 from/size 分页,改用 search_after 参数进行顺序游标式查询,这适用于后台导出、大数据量扫描等场景。
先说结论:深分页性能瓶颈通常源于 from 值过大,search_after 能避免收集前置文档,但无法随机跳页。
- 先定位:确认业务是否真的需要随机跳页,还是顺序遍历。
- 先做:为查询语句添加唯一排序字段,并使用 search_after 携带上一页的 sort 值。
- 再验证:对比修改前后的查询耗时,确保结果集连续性无误。
完整请求与响应演示
以下是一个完整的 cURL 查询示例,注意 sort 字段必须包含唯一值(如 _id),否则分页可能不稳定:
curl -X POST "localhost:9200/my-index/_search" -H 'Content-Type: application/json' -d '
{
"query": { "match_all": {} },
"size": 10,
"sort": [
{ "timestamp": "desc" },
{ "_id": "desc" }
],
"search_after": [1678886400000, "abc123"]
}'查询响应中,需从 hits.hits 最后一项提取 sort 值用于下一页:
{
"hits": {
"hits": [
{
"_id": "doc_1",
"sort": [1678886400000, "abc123"]
},
...
{
"_id": "doc_10",
"sort": [1678886300000, "xyz789"] // 取此值作为下一页 search_after
}
]
}
}客户端代码集成
在 Python 客户端中,可以通过提取最后一次结果的 sort 字段实现循环遍历:
from elasticsearch import Elasticsearch
es = Elasticsearch("http://localhost:9200")
search_after = None
while True:
body = {
"query": { "match_all": {} },
"size": 100,
"sort": [{ "timestamp": "desc" }, { "_id": "desc" }]
}
if search_after:
body["search_after"] = search_after
res = es.search(index="my-index", body=body)
hits = res["hits"]["hits"]
if not hits:
break
# 处理数据
for hit in hits:
print(hit["_source"])
# 更新游标
search_after = hits[-1]["sort"]原理简述
传统的 from/size 分页在深度查询时,每个分片都需要收集 from + size 个文档,然后协调节点再进行全局排序和裁剪。当 from 很大时,内存和 CPU 开销会线性增长。search_after 则是基于上一页最后一个结果的排序值继续查询,不需要回溯之前的数据,因此开销相对固定。
分步处理
- 检查排序字段:确保 sort 字段能唯一标识文档,通常组合时间戳和 _id。
- 首次查询:不带 search_after 参数,正常执行查询并记录响应中 hits.hits 最后一项的 sort 值。
- 后续查询:将上一步记录的 sort 数组填入 search_after 参数,保持 sort 顺序一致。
- 终止条件:当返回的文档数量小于 size 时,说明已遍历完毕。
怎么验证是否生效
观察 Profile API 的输出或慢查询日志,确认查询耗时不再随页码增加而显著上升。同时核对总文档数,确保没有遗漏或重复。
常见坑
- 数据实时变化:search_after 不是快照,查询期间若有新文档插入且排序值介于两页之间,可能导致漏数或重复。
- 排序不唯一:如果 sort 值相同且没有 _id 兜底,分页结果会出现混乱。
- 无法跳页:不支持直接访问第 1000 页,只能一页页往后翻。
参考来源
1. Elastic, "Paginate search results", Elasticsearch Guide, https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html