es分页必须用from和size,不能用laravel的page逻辑;from需手动计算为(page-1)*size,且须配合唯一排序字段防偏移;total应取实际id数而非es估算值,url参数需withquerystring透传。

ES分页参数必须用 from 和 size,不能套用 Eloquent 的 page 逻辑
直接把 Laravel 的 paginate(15) 思路搬进 ES 查询会出错——ES 不认 page 参数,它只按偏移量(from)和单页数量(size)算。比如第 3 页、每页 16 条,就得手动算:from = (3 - 1) * 16 = 32,再传 size = 16。
常见错误是写成 "from": 3, "size": 16,结果只拿到第 4 条开始的 16 条,漏掉前 3 页数据。也有人误把 page 当成 ES 的原生字段塞进 query body,ES 直接报 parse_exception。
-
from是从第几条开始取(0 起始),不是“第几页” -
size最大建议不超过 10000(受 ESindex.max_result_window限制) - 前端传
?page=2&size=20时,后端要转换:from = ($page - 1) * $size
sort 字段必须有唯一性,否则翻页会重复或丢失数据
ES 的分页依赖排序稳定性。如果只按 score 或 rating 排序,多个文档 score 相同,ES 返回顺序可能波动,导致第 2 页出现第 1 页已返回过的 ID —— 这就是 offset 漂移。真实场景中,用户刷两次第 2 页,看到的记录不一致。
正确做法是:在业务排序字段后追加一个唯一字段兜底,比如 ["sold_count:desc", "id:asc"] 或 ["created_at:desc", "_id:asc"]。注意 _id 是字符串类型,升序降序不影响唯一性,但能强制打破并列。
- 避免单独用
["price:asc"]这类高重复值字段排序 - 不要用
script_score动态算分后排序,性能差且难保证稳定性 - 如果业务允许,优先用
cursorPaginate替代 offset 分页(但 ES 原生不支持游标,需自行实现)
总数 total 来自 ES,但别直接喂给 LengthAwarePaginator
ES 返回的 $result['hits']['total']['value'] 是匹配总数,看起来可以直接当 $total 传给 LengthAwarePaginator。但这里有个坑:ES 的 total 默认是“估算值”("relation": "eq" 才是精确值),尤其数据量大、开了分片时,total 可能比实际少几条。
更关键的是:你最终查数据库用的是 whereIn('id', $ids),而 ES 返回的 _id 可能对应不到 DB 的 id 字段(比如 ES 存的是字符串 ID,DB 是自增整型;或 ES 同步延迟导致部分数据未写入)。这时候 $total 是虚的,LengthAwarePaginator 显示 “共 127 条”,但第 9 页实际查不到数据。
- 稳妥做法:用
$result['hits']['total']['value']显示“约 XX 条”,不用于分页器构造 - 分页器的
$total应设为count($productIds)(即 ES 真实返回的 ID 数量),避免跳转失败 - 如果必须显示精确总数,得在 ES 查询里加
"track_total_hits": true,但会轻微拖慢响应
URL 参数透传要用 withQueryString(),但 page 键名不能硬编码
ES 查询结果渲染的 Blade 页面里调 $products->links(),默认会生成 ?page=2 链接。但你的搜索 URL 可能是 /products?search=phone&order=price_desc&page=2,如果控制器没做透传,点下一页就丢掉 search 和 order,变成纯 ?page=3。
解决方法不是在每个链接里手拼参数,而是用 withQueryString()。但它有个前提:分页器实例必须由你手动 new 出来(因为 ES 查询不走 Eloquent,paginate() 不可用),且 new 时传的 $currentPage 必须从 request 里读,不能写死 request()->input('page', 1) —— 否则换掉参数名(比如改成 p)就失效。
- 正确读法:
$page = $request->input($pageName, 1),然后new LengthAwarePaginator(..., $page, [...'pageName' => $pageName...]) - 调用
$pager->withQueryString()前,确保所有需要保留的参数已在 request 中(search、order等) - 不要在
withPath()里写带查询参数的路径,比如withPath('/products?search=xxx'),这会导致链接嵌套乱码
ES 分页最易被忽略的其实是数据一致性:ES 的 _id 和 DB 主键映射是否 100% 对齐、同步延迟有没有被监控、total 显示要不要加“约”字提示用户——这些不写进代码,光靠 from/size 算对了也没用。











