es的分页查询有3种方式:from+size,scroll,search_after,下面比较一下这3种方式。

一、from+size

from+size的分页查询称为"浅"分页,它的原理很简单,就是查询前20条数据,然后截断前10条,只返回10-20的数据。这样其实白白浪费了前10条的查询。

在深度分页的情况下,这种使用方式效率是非常低的,比如from = 50000, size=10, es 需要在各个分片上匹配排序并得到50010条数据,协调节点拿到这些数据再进行全局排序处理,然后结果集中取最后10条数据返回。

深度分页的代价根源是结果集全局排序。

二、scroll

官网介绍:游标查询 Scroll | Elasticsearch: 权威指南 | Elastic

scroll 类似于sql中的cursor,使用scroll,每次只能获取一页的内容,然后会返回一个scroll_id。根据返回的这个scroll_id可以不断地获取下一页的内容,所以scroll并不适用于有跳页的情景。

该查询实现类似于消息消费的机制,首次查询的时候会在内存中保存一个历史快照以及游标(scroll_id),记录当前消息查询的终止位置,下次查询的时候将基于游标进行消费。性能良好,维护成本高,在游标失效前,不会更新数据,不够灵活,一旦游标创建size就不可改变,适用于需要用到匹配到的全量数据,例如大量数据导出或者索引重建。

三、search_after

scroll 的方式,官方的建议不用于实时的请求(一般用于数据导出),因为每一个 scroll_id 不仅会占用大量的资源,而且会生成历史快照,对于数据的变更不会反映到快照上。

search_after 分页的方式是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。但是需要注意,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求,无法指定页数,只能实现“下一页”这种需求。

为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。

四、对比

分页方式

1~10

49000~49010

99000~99010

form+size

8ms

30ms

117ms

scroll

7ms

66ms

36ms

search_after

5ms

8ms

7ms

分页方式

性能

优点

缺点

场景

from + size

灵活性好,实现简单

深度分页问题

数据量比较小,能容忍深度分页问题

scroll

解决了深度分页问题

无法反应数据的实时性(快照版本)

维护成本高,需要维护一个 scroll_id

海量数据的导出

需要查询海量结果集的数据

search_after

性能最好

不存在深度分页问题

能够反映数据的实时变更

实现复杂,需要有一个全局唯一的字段

连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果

海量数据的分页

form+size:列表查询(查询前1000页的数据可满足一般需求)

scroll:列表导出

search_after:超级深的分页

form+size 适用于常见的查询,例如需要支持跳页并实时查询的场景。但查询深度过深时,会有深度分页的问题,造成 OOM.

如果在业务上,可以不选择跳页的方式,可以使用的 search-after 的方式避免深度分页的问题。但如果一定要跳页的话,只能采用限制最大分页数的方式。

但对于超大数据量,以及需要高并发获取等离线场景,scroll 是比较好的一种方式。

五、代码示例

ES 7.X版本 Search After 的Java代码示例:

public class EsSearchAfterHandler {
    private static final String UID = "_id";

    /**
     * Search for hit.
     *
     * @param searchRequest the search request
     * @param consumer      the consumer
     */
    public static void searchForHit(RestHighLevelClient client, SearchRequest searchRequest,
                                    Consumer<SearchHit> consumer) throws IOException {
        searchForResponse(client, searchRequest, searchResponse -> forEachHits(searchResponse, consumer));
    }

    /**
     * Search for response.
     *
     * @param searchRequest the search request
     * @param consumer      the consumer
     */
    public static void searchForResponse(RestHighLevelClient client, SearchRequest searchRequest,
                                         Consumer<SearchResponse> consumer) throws IOException {
        if (searchRequest == null || consumer == null) {
            return;
        }
        SearchSourceBuilder sourceBuilder = searchRequest.source();
        // sourceBuilder.size(100); //在构建查询条件时,即可设置大小
        //设置排序字段
        sourceBuilder.sort(UID, SortOrder.ASC);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHit[] searchHits = searchResponse.getHits().getHits();
        while (searchHits.length > 0) {
            consumer.accept(searchResponse);
            SearchHit last = searchHits[searchHits.length - 1];
            sourceBuilder.searchAfter(last.getSortValues());
            searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            searchHits = searchResponse.getHits().getHits();
        }
    }
    
    public static void forEachHits(SearchResponse searchResponse, Consumer<SearchHit> consumer) {
        if (searchResponse == null) {
            return;
        }
        SearchHit[] searchHits = searchResponse.getHits().getHits();
        for (SearchHit searchHit : searchHits) {
            consumer.accept(searchHit);
        }
    }
    
}

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐