Laravel + ES 深度分页卡死的根本原因是 from/size 在 ES 中需跳过大量已排序文档,导致内存与 GC 压力剧增;应改用 search_after 实现游标分页或 scroll 用于导出,禁用调大 max_result_window。

为什么 Laravel + ES 的 from/size 分页在大数据下会卡死?
不是 Laravel 慢,也不是 ES 配置错,而是 from + size 在 ES 侧根本就扛不住深度分页。当你在 Laravel 中写 ->paginate(100) 并传给 ES 查询时,底层实际构造的是 {"from": 9900, "size": 100} —— 这意味着 ES 要在所有匹配文档中排序后跳过前 9900 条,再取 100 条。6 亿条命中数据下,这个操作要合并、重排、丢弃大量中间结果,协调节点内存和 GC 压力飙升,10 分钟响应都算乐观。
Laravel 中如何安全切换到 search_after?
search_after 是唯一能兼顾实时性与性能的替代方案,但它不能直接套用 Laravel 的 paginate()。你必须手动管理游标:
- 每次查询返回结果时,提取排序字段值(如
timestamp和_uuid_),拼成数组:["1744214400000", "abc123"] - 下一页请求时,把该数组塞进
search_after参数,同时确保sort字段顺序、类型、missing策略完全一致 - Laravel 控制器里不要依赖
$request->page,改用$request->cursor接收 Base64 编码的游标(避免 URL 中出现特殊字符) - 注意:
search_after不支持倒序翻页(比如从最后一页跳第 3 页),只能单向“下一页”
scroll 不适合 Web 分页,但导出场景可直接用
如果你的“分页”其实是后台导出全量数据(比如报表 CSV),别硬套分页逻辑,直接上 scroll:
- 第一次请求加
"scroll": "2m",拿到_scroll_id - 后续用
GET /_search/scroll+_scroll_id持续拉取,每次返回一批(size可设为 1000 或更高) - Laravel 中用
while循环 +yield流式写入文件,避免内存爆掉 - 切记:
scroll是快照查询,期间新写入的数据不会被包含;且_scroll_id两分钟不用就失效,不适合用户交互式翻页
max_result_window 调大是饮鸩止渴
看到 Result window is too large 就去改 index.max_result_window?危险操作:
- 调到 50000 甚至 100000,ES 仍可能 OOM——因为每个分片都要缓存
from + size条排序结果 - 集群负载不均时,某个分片扛不住,整条查询失败
- 一旦业务增长,问题只会更早复现,而不是消失
- 真正该做的,是在 Laravel 层拦截超限请求:比如
if ($from > 10000) { throw new BadRequestHttpException('超出最大页码'); }
深度分页从来不是“能不能查出来”的问题,而是“该不该这么查”的设计判断。游标分页(search_after)和滚动遍历(scroll)不是备选方案,是大数据量下唯一合理的路径。漏掉排序字段唯一性约束、忽略 missing 处理差异、或在游标里混用不同类型的值——这些细节才是线上查不出数据的真正元凶。


















