2026/2/12 15:57:18
网站建设
项目流程
廉江网站开发公司,忽悠别人做商城网站,高德地图可以看国外的地图吗,区块链app定制一文讲透 Elasticsearch 分页与深度分页#xff1a;从原理到实战你有没有遇到过这样的场景#xff1f;前端同学说#xff1a;“用户点了第500页#xff0c;怎么卡住了#xff1f;”运维报警#xff1a;“ES节点CPU爆了#xff0c;查一下是不是有人在翻几万条数据#x…一文讲透 Elasticsearch 分页与深度分页从原理到实战你有没有遇到过这样的场景前端同学说“用户点了第500页怎么卡住了”运维报警“ES节点CPU爆了查一下是不是有人在翻几万条数据”面试官问“Elasticsearch 为什么不能深度分页from/size到底慢在哪”这些问题的背后其实都指向同一个核心机制——Elasticsearch 的分页实现方式及其在分布式环境下的代价。今天我们就来彻底搞清楚-Elasticsearch 是怎么执行一次分页查询的-为什么越往后翻越慢-search_after和scroll真的能解决这个问题吗它们之间又有什么区别-实际项目中到底该用哪种方案别急我们一步步拆解。这不仅是一篇技术解析文更是一份你可以直接拿去用的生产级分页选型指南。from/size 不是“跳过”而是“先拉再切”我们先来看最常见的分页写法{ from: 9990, size: 10, query: { match_all: {} } }看起来很简单跳过前9990条取接下来的10条。就像 SQL 中的LIMIT 9990, 10。但问题来了——Elasticsearch 真的是“跳过”了吗答案是不是。分布式系统里的“全局排序”有多贵Elasticsearch 是分布式的。你的索引可能被分成 5 个分片分布在不同的节点上。当你发起一个from9990, size10的请求时协调节点coordinating node会做这几件事向每个分片发送子查询要求返回本地排序后的 top 10000 条结果每个分片自己执行查询、打分、排序返回自己的前 10000 条协调节点把所有分片返回的结果合并起来重新排序形成全局有序列表最后截取[9990, 10000)这 10 条数据返回给客户端。 关键点每个分片都要处理from size条记录哪怕其中 9990 条最终都会被丢弃这意味着什么偏移量每个分片需返回总传输数据量假设5分片from01050from90100500from99901000050,000看到没随着from增大资源消耗几乎是线性增长的。CPU、内存、网络带宽全都被浪费在“搬运无效中间结果”上。这也是为什么 ES 默认设置了index.max_result_window: 10000一旦from size 10000就会报错Result window is too large, from size must be less than or equal to 10000这是保护机制——防止一个请求拖垮整个集群。所以下次面试官问你“ES 为什么不支持深度分页”你可以这样答“因为from/size在分布式环境下需要各分片返回大量中间结果在协调节点进行全局排序和截断。当偏移量很大时这种‘拉取-合并-丢弃’模式会造成严重的性能浪费甚至导致 OOM。”search_after用“游标”代替“跳过”既然from/size太重那有没有办法只拿“真正需要的数据”有就是search_after。它不靠数字偏移而是靠“上一页最后一个文档的位置”作为锚点继续往下读。有点像数据库里的“键集分页”Keyset Pagination也像翻书时记住“上次看到哪一页”。它是怎么工作的假设你要按时间倒序查看日志{ size: 10, sort: [ { timestamp: desc }, { _id: asc } ], query: { range: { timestamp: { lte: now } } } }第一次请求没有search_after直接返回最新的10条。拿到结果后取出最后一条文档的排序值比如[1680000000000, log_001]下一次请求带上这个值{ size: 10, sort: [ { timestamp: desc }, { _id: asc } ], query: { range: { timestamp: { lte: now } } }, search_after: [1680000000000, log_001] }ES 就知道“哦你要找比(1680000000000, log_001)更大的记录”于是每个分片只需扫描后续数据无需加载前面成千上万条。性能对比惊人方案查询延迟from9990内存占用是否可扩展from/size800ms高❌search_after~50ms极低✅差距不止十倍。而且search_after不受max_result_window限制理论上可以一直翻到一百万页。但它也有前提条件必须指定明确的排序规则排序字段组合必须能唯一标识顺序否则可能出现漏读或重复。举个例子如果你只按timestamp desc排序而很多文档时间戳相同那么 ES 无法确定“下一个”是谁。这时候就必须补充一个唯一字段比如_id或业务主键sort: [ { timestamp: desc }, { _id: asc } ]这样才能保证每次都能精准定位“下一个”。Java 实现示例// 第一次请求 SearchSourceBuilder builder new SearchSourceBuilder(); builder.query(QueryBuilders.rangeQuery(timestamp).lte(now)); builder.sort(timestamp, SortOrder.DESC); builder.sort(_id, SortOrder.ASC); builder.size(10); SearchRequest request new SearchRequest(logs); request.source(builder); SearchResponse response client.search(request, RequestOptions.DEFAULT); ListObject[] sortValuesList new ArrayList(); for (SearchHit hit : response.getHits()) { System.out.println(hit.getSourceAsString()); sortValuesList.add(hit.getSortValues()); } // 如果还有数据构造下一页 if (!sortValuesList.isEmpty()) { Object[] lastSortValues sortValuesList.get(sortValuesList.size() - 1); SearchSourceBuilder nextBuilder new SearchSourceBuilder(); nextBuilder.query(QueryBuilders.rangeQuery(timestamp).lte(now)); nextBuilder.sort(timestamp, SortOrder.DESC); nextBuilder.sort(_id, SortOrder.ASC); nextBuilder.size(10); nextBuilder.searchAfter(lastSortValues); // 设置游标 SearchRequest nextRequest new SearchRequest(logs); nextRequest.source(nextBuilder); SearchResponse nextPage client.search(nextRequest, RequestOptions.DEFAULT); // 处理 nextPage ... }这就是典型的“连续拉取”逻辑。注意它是无状态的翻页依赖客户端维护上一次的排序值。scroll API为批量任务而生的“数据快照”如果说search_after是为“高效顺滑地浏览数据”设计的那scroll就是为“一次性搬完所有数据”服务的。它适用于数据导出、迁移、报表生成等离线任务。核心思想保存搜索上下文当你发起第一个scroll请求时POST /logs/_search?scroll1m { size: 100, query: { match_all: {} }, sort: [_doc] }ES 会- 创建一个“搜索上下文”search context- 基于当前 Lucene 版本创建一个数据快照- 返回第一批数据和一个scroll_id。之后你拿着scroll_id不断请求POST /_search/scroll { scroll: 1m, scroll_id: DnF1ZXJ5VGhlbkZldGNo... }ES 就会根据上下文继续返回下一批直到数据全部读完。优势很明显数据一致性强你在遍历过程中看到的始终是初始时刻的数据视图不会因新写入而“抖动”适合大数据量导出可稳定处理百万级文档推荐使用_doc排序这是 Lucene 内部顺序不需要额外排序速度最快。但它也有硬伤占用服务器资源每个 scroll 上下文会锁定缓存、文件句柄消耗堆内存不能长期持有默认 1 分钟超时需及时拉取不支持随机访问只能顺序读不适合高并发交互场景大量活跃的 scroll 会导致集群负载升高。更关键的是自 Elasticsearch 7.10 起官方已明确建议用 PITPoint in Time替代传统 scroll。scroll 正在被淘汰PIT 才是未来没错scroll虽然还在用但已经是“老古董”了。它的替代者叫Point in TimePIT结合search_after使用既能保持快照一致性又能享受游标的高性能。PIT 的工作流程先打开一个 PIT获取一个pit_idPOST /logs/_pit?keep_alive1m在查询中使用 PIT并配合search_after分页{ size: 10, sort: [ { timestamp: desc }, { _id: asc } ], query: { range: { timestamp: { gte: now-1h } } }, pit: { id: 46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxKgZub2RlXzEAAAAAAAAAAAEBYQACAWh1aWRqaBZub2RlXzIAAAAAAAAAAAI, keep_alive: 1m }, search_after: [1680000000000, log_001] }每次返回新的pit信息用于下一轮请求完成后显式关闭 PIT 释放资源。为什么 PIT 更好对比项scrollPIT search_after数据一致性✅ 快照✅ 快照性能一般高无需维护上下文资源占用高context 占内存低可扩展性差好是否推荐❌逐渐废弃✅官方推荐简单说PIT 把“快照能力”和“高效分页”完美结合还不占资源。新项目强烈建议直接上PIT search_after组合拳。到底该怎么选一张表说清适用场景方案支持跳页性能稳定性数据一致性适用场景推荐指数from/size✅❌随偏移恶化⚠️ 实时变化前台分页展示1万条、简单列表⭐⭐☆search_after❌仅顺序✅稳定⚠️ 实时变化深度分页、日志流、消息中心、实时查询⭐⭐⭐⭐scroll❌✅批次稳定✅快照数据导出、迁移、旧系统兼容⭐⭐PIT search_after❌✅✅✅快照大数据量一致性遍历新项目首选⭐⭐⭐⭐⭐实战建议别让分页拖垮你的系统1. 前端分页尽量加过滤条件与其让用户无脑翻到第1000页不如引导他们通过关键词、时间范围、分类筛选缩小结果集。用户真需要看一万条以前的商品吗大概率不需要。2. 深度分页坚决不用from/size如果业务允许强制限制最大页码如最多显示100页超出提示“请优化搜索条件”。或者干脆改成分页模式只允许“上一页/下一页”背后用search_after实现。3. 导出任务优先考虑 PIT不要用scroll写脚本跑百万数据用 PIT 更安全、更轻量。4. 监控活跃的 search context 数量设置告警规则监控nodes.stats.indices.search.open_contexts防止scroll泛滥导致内存溢出。5. 排序字段一定要唯一用search_after时务必组合时间戳 ID 或其他唯一字段避免因排序模糊导致数据跳跃。面试题也能轻松应对现在回过头来看那些高频 es 面试题Q1Elasticsearch 为什么不适合深度分页因为from/size在分布式环境下需要每个分片返回from size条数据协调节点合并后再排序截断。当偏移量很大时会产生大量无效计算和数据传输性能急剧下降。因此默认限制max_result_window10000。Q2如何优化深度分页改用search_after基于上一页最后一个文档的排序值作为游标避免全局排序和中间结果加载性能几乎不受偏移影响。Q3search_after和scroll有什么区别search_after是无状态的适合实时交互式分页scroll有状态维护搜索上下文提供数据快照适合批量导出但scroll资源消耗高已被 PIT search_after 取代。写在最后分页看似简单但在分布式系统中却藏着很深的设计权衡。from/size简单直观但代价高昂search_after高效稳定但放弃随机跳转scroll曾经强大如今正被 PIT 取代。作为开发者我们要做的不是死记硬背语法而是理解每种方案背后的成本模型和适用边界。当你下次面对“第500页加载慢”的问题时希望你能从容地说“我们换个分页方式吧用search_after保证流畅到底。”这才是真正的技术底气。如果你正在做搜索、日志、监控类系统欢迎收藏本文也可以转发给团队一起讨论你们现在的分页方案真的合适吗