分页是几乎所有搜索应用都具备的基础功能。在数据量较小时,开发者习惯使用 from 和 size 参数进行翻页,这简单直观。然而,随着数据量增长和页码变深,查询响应时间往往呈指数级上升,甚至引发集群性能抖动。
本文将深入 Easysearch 的分布式原理,解释为什么深度分页会变慢,介绍平台内置的保护机制,并给出高效、稳定的替代方案。
原理:from/size 是如何工作的? #
在 Easysearch 中,from 参数表示跳过的结果数(偏移量),size 表示返回的结果数。
当一个搜索请求被发送到集群时,它通常包含两个阶段:Query(查询)和 Fetch(取回)。
在分布式环境下,数据分布在多个分片(Shard)上。为了返回第 1000 页的数据(假设每页 10 条,即 from=9990, size=10),每个分片都必须先查询出各自的前 10000 条记录(TopDocs)。
随后,这些分片会将结果返回给协调节点(Coordinating Node)。协调节点必须收集所有分片返回的文档 ID 和排序值(如果分片数为 5,则总共收集 50000 条),在内存中进行全局排序,最后丢弃前 9990 条,只保留最后的 10 条返回给客户端。
为什么会慢?深度分页的真实成本 #
理解了上述流程,就不难发现 from/size 在深度分页时的性能杀手:
- CPU 与内存的线性增长:
from值越大,协调节点需要排序和处理的数据量就越大。这是一个线性增长的过程,过大的from会导致 CPU 飙升和大量的内存占用。 - 跨分片归并开销:若指定了自定义排序(如按时间或特定字段),各分片需要返回 TopFieldDocs。协调节点进行的全局归并工作量随着
from的增加而剧增。 - Fetch 阶段的 I/O 压力:虽然 Query 阶段只返回 ID 和排序值,但在 Fetch 阶段,系统需要根据 ID 去拉取文档的
_source内容。如果请求的size很大,或者命中的文档分布在不同的段中,随机 I/O 和反序列化的成本依然不可忽视。 - Total Hits 计算成本:为了告诉前端“一共有多少页”,Easysearch 默认需要统计匹配的总行数。在海量数据下,精确计算
track_total_hits会强制所有分片遍历所有匹配项,这也是一个耗时点。
平台限制:10,000 条的保护线 #
为了防止恶意或无意的深度分页请求拖垮集群,Easysearch 默认设置了一个硬性限制:
index.max_result_window = 10000
这意味着 from + size 的总和不能超过 10,000。如果你的应用尝试请求第 10,001 条数据,系统会直接报错,提示你使用 scroll 或调整索引设置。
此外,使用 from/size 进行深分页还是无状态的。如果在翻页过程中有新数据写入或旧数据删除,可能会导致结果“漂移”(即同一条数据在不同页码重复出现,或某些数据被漏掉)。
最佳实践:如何高效分页? #
针对不同的业务场景,Easysearch 提供了更高效的替代方案:
1. 深度分页:使用 search_after
#
这是目前推荐的主流深度分页方式。
原理:search_after 不使用“跳过前 N 条”的逻辑,而是使用“上一页最后一条数据的排序值”作为游标(Cursor)。查询时,告诉 Easysearch:“请给我排在这个值之后的数据”。
优势:
- 无论翻到多深,每个分片都只需返回
size条数据。 - 协调节点只需合并
shard_num * size条数据,内存和 CPU 开销极低且恒定。
示例:
假设我们按 publish_time 和 _id 排序。
第一页(正常查询):
GET my-index/_search
{
"size": 10,
"query": { "match": { "text": "easysearch" } },
"sort": [{ "publish_time": "desc" }, { "_id": "desc" }]
}
获取第二页(带入上一页最后一条的 sort 值):
GET my-index/_search
{
"size": 10,
"query": { "match": { "text": "easysearch" } },
"sort": [{ "publish_time": "desc" }, { "_id": "desc" }],
"search_after": [1704110400000, "doc-id-abc"]
}
2. 全量导出:使用 scroll
#
如果你需要导出数百万条数据进行分析,而不是给用户看,请使用 scroll。
它会建立一个临时的快照上下文,保证在遍历过程中数据的一致性(即使期间有新数据写入,导出结果也不会变)。但请注意,scroll 维护上下文有资源开销,不适合用于实时的用户端搜索。
3. 稳定视图:使用 PIT (Point In Time) #
结合 search_after 和 PIT,可以实现既高效又具备一致性视图(不漂移)的深度分页。
性能优化清单 #
在设计搜索功能时,建议遵循以下清单:
- ✅ 浅分页(前几页):继续使用
from/size,简单且够用。 - ✅ 深分页(无限滚动/手机端瀑布流):切换为
search_after。 - ✅ 数据导出:使用
scroll接口。 - ✅ 不需要精确总数时:将
track_total_hits设为false或固定数值(如 10000),避免全量计数。 - ✅ 减少网络开销:通过
_source参数仅返回业务需要的字段,避免拉取大字段。 - ❌ 避免修改限制:尽量不要轻易调大
index.max_result_window,除非你非常清楚集群的硬件承载能力。
通过合理选择分页策略,我们可以在保证用户体验的同时,最大程度地保护 Easysearch 集群的稳定性。





