第五阶段 40 · 深分页与性能(from/size、search_after、scroll、PIT)

40 · 深分页与性能(from/size、search_after、scroll、PIT)

阶段:第五阶段 / 进阶与实战

ES:from/size 上限、search_after、scroll、PIT | PostgreSQL:LIMIT/OFFSET + 键集分页


1. 概念

翻页有三条路线:

方式 适用场景 SQL 对照
from + size 前几页、浅分页(默认上限 10000) LIMIT ? OFFSET ?
search_after 深分页、无限下拉、导出 键集分页(keyset)
scroll 大批量一次性导出(旧方案) 游标 cursor
PIT + search_after 深分页导出(推荐新方案) 一致性快照 + 键集

2. PostgreSQL 对照

sql 复制代码
-- 浅分页(OFFSET 越大越慢)
SELECT * FROM salesdata ORDER BY id LIMIT 20 OFFSET 100000;

-- 键集分页(推荐,稳定快)
SELECT * FROM salesdata
WHERE id > :lastId
ORDER BY id
LIMIT 20;

3. ES DSL

from/size(浅分页)

复制代码
GET salesdata_idx/_search
{ "from": 20, "size": 10, "sort": [ { "record_id": "asc" } ] }

search_after(深分页)

复制代码
GET salesdata_idx/_search
{
  "size": 1000,
  "sort": [ { "record_id": "asc" } ],
  "search_after": [ "last-record-id-from-prev-page" ]
}

PIT + search_after(一致性深分页)

复制代码
POST salesdata_idx/_pit?keep_alive=1m     # 拿到 pit id
GET _search
{
  "pit": { "id": "<pit-id>", "keep_alive": "1m" },
  "size": 1000,
  "sort": [ { "record_id": "asc" } ],
  "search_after": [ "..." ]
}

4. Spring Boot 实现

java 复制代码
@Component
public class Doc40DeepPaging {

    @Autowired
    private ElasticsearchClient elasticsearchClient;

    /** search_after 全量遍历:稳定深分页,适合导出 */
    public void scanAll(String indexName, Consumer<Map<String, Object>> consumer) throws IOException {
        List<FieldValue> searchAfter = null;
        while (true) {
            List<FieldValue> after = searchAfter;
            SearchResponse<Map> resp = elasticsearchClient.search(s -> {
                s.index(indexName)
                 .size(1000)
                 .sort(so -> so.field(f -> f.field("record_id").order(SortOrder.Asc)));
                if (after != null) {
                    s.searchAfter(after);
                }
                return s;
            }, Map.class);

            var hits = resp.hits().hits();
            if (hits.isEmpty()) {
                break;
            }
            hits.forEach(h -> { if (h.source() != null) consumer.accept(h.source()); });
            // 用最后一条的排序值作为下一页游标
            searchAfter = hits.get(hits.size() - 1).sort();
        }
    }

    /** PIT + search_after:一致性快照下的全量遍历(推荐新方案) */
    public void scanAllWithPit(String indexName, Consumer<Map<String, Object>> consumer)
            throws IOException {
        // 1) 开 PIT,拿到一致性快照,遍历期间数据变化不会漏/重
        String pitId = elasticsearchClient.openPointInTime(p -> p
                .index(indexName)
                .keepAlive(t -> t.time("2m"))).id();
        try {
            List<FieldValue> searchAfter = null;
            while (true) {
                List<FieldValue> after = searchAfter;
                SearchResponse<Map> resp = elasticsearchClient.search(s -> {
                    s.size(1000)
                     .pit(p -> p.id(pitId).keepAlive(t -> t.time("2m")))   // 注意:带 PIT 时不要再写 index
                     .sort(so -> so.field(f -> f.field("record_id").order(SortOrder.Asc)));
                    if (after != null) {
                        s.searchAfter(after);
                    }
                    return s;
                }, Map.class);

                var hits = resp.hits().hits();
                if (hits.isEmpty()) {
                    break;
                }
                hits.forEach(h -> { if (h.source() != null) consumer.accept(h.source()); });
                searchAfter = hits.get(hits.size() - 1).sort();
            }
        } finally {
            // 2) PIT 用完必须释放,异常也要清理
            String finalPit = pitId;
            elasticsearchClient.closePointInTime(c -> c.id(finalPit));
        }
    }
}

import:co.elastic.clients.elasticsearch._types.FieldValue...SortOrder

带 PIT 时索引信息已在 PIT 里,SearchRequest 不要再写 .index(...)


5. 坑与最佳实践

  1. from + size 上限 10000 :超过报错,深翻页必须换 search_after
  2. search_after 需要稳定排序 :排序字段要唯一(加 record_id 兜底),否则会漏/重。
  3. scroll 已不推荐新用 :占资源、快照久;导出优先 PIT + search_after
  4. PIT 用完要释放DELETE _pit,否则占用段资源。
  5. 深分页本质贵:能用条件缩小范围就别硬翻页;导出场景用批量扫描。

相关推荐
倒流时光三十年1 天前
第三阶段 29 · suggester 自动补全(搜索框联想 / 拼写纠错)
es
倒流时光三十年2 天前
第三阶段 24 · filter / filters 桶聚合
es
倒流时光三十年2 天前
第二阶段 17 · multi_match 多字段匹配
es
倒流时光三十年3 天前
第三阶段 23 · 嵌套与子聚合 / top_hits
es
倒流时光三十年3 天前
第二阶段 19 · _source 字段裁剪
es
倒流时光三十年4 天前
第二阶段 15 · prefix / wildcard / fuzzy 模糊匹配
es
粗体鱼4 天前
RAG/Agent 记忆混合检索多路召回:RRF 算法与Chunk RRF、Document RRF如何决策TopK
postgresql·milvus·es·rag·rff·mermory
huisheng_qaq2 个月前
【项目篇-01】Vmware虚拟机和环境安装配置
redis·mysql·canal·rocketmq·es·vaware虚拟机
JAVA面经实录9172 个月前
Elasticsearch 完整版完整知识体系
java·elasticsearch·搜索引擎·es