深度分页介绍
平时使用es分页查询数据时的写法:
POST test_index/_search
{
"from":1000, -- 要跳过的条数
"size":2000 -- 返回hits最大条数
}
java代码实现:
SearchQuery search = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery())
.withPageable(PageRequest.of(page, size))
.build();
Page<XXX> page = elasticsearchTemplate.continueScroll(searchQuery, XXX.class);
这条语句在es中的执行过程可以分为两大步骤:
第一步:query查询到具体的doc编号;
- 接收客户端请求,协调者节点自身创建from+size大小的优先队列,队列中数据只保留分数以及doc编号;
- 将命令广播到其他分片中;
- 其他分片自身也创建from+size大小的优先队列,队列中的数据也根据条件只保留分数以及doc编号
- 协调者节点获取到全部分片中文档的分数以及编号,根据优先队列取到要查询的doc编号。
第二步:fetcht根据doc编号查询doc文档。
- 根据doc编号,到各个分片中查询doc文档;
- 查询到结果返回给client。
根据上述步骤可以知道采用from+size方式的优缺点。
- 缺点:
如果我们想查询的数据越深(from越大),哪怕只查询一条数据(size大小),在es内部执行过程中也要查询大量的数据,这就带来了深度分页的问题。同时es的配置中还限制了每次查询返回的最大结果(max_result_window),它的默认值是10000。 - 优点:
支持随机翻页,当数据量较小时性能也还好。
解决方案
scroll
search_after的原理是以前一页的结果当作参照点,进行查询这个参照点的下一份匹配数据内容,es命令:
-- scroll=1m为有效期为1分
POST test_index/_search?scroll=1m
{
"from":0,
"size":1
}
POST _search/scroll?scroll=1m
{
-- 上述命令返回的scroll_id,直到数据为空则遍历完成。
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91d******W5kRmV0Y2gBFkdHTWhUYUF4U2E2T0FhR2tlWDg2NlEAAAAAAHb57hY4RHk4bnRDMFRGNmhNbkJSOERDejhR"
}
java代码实现:
ScrolledPage<LiveInfoEntity> scrolledPage = elasticsearchTemplate.startScroll(SCROLL_TIME, searchQuery, XXX.class);
while (CollectionUtils.isNotEmpty(scrolledPage.getContent())) {
result.addAll(scrolledPage.getContent());
scrolledPage = elasticsearchTemplate.continueScroll(scrolledPage.getScrollId(), SCROLL_TIME, XXX.class);
}
它的内部执行过程:
1、将所有符合条件的doc编号缓存起来,形成当前时间的一个快照;
2、每次查询根据doc编号fetch取到doc文档。
这里会有一个误区觉得,doc编号被缓存了,文档没缓存,在查询到一个doc文档之前,把该文档更改了,为什么查询结果还是之前的文档内容。这是因为在es中有一个pit(视图)的概念,创建一个视图时,根据该视图查询,只能查看到创建此视图时间之前的数据;实现的原理类似mysql的视图。
优点:
- 当需要检索全部数据时,可以采用scroll,例如遍历数据进行相关操作。
缺点:
- 响应非实时
- 单次
size不能超过max_result_window - 需要大量的空间来缓存数据
- 仅支持向后翻页
search_after
es的写法:
命令1:
POST test_index/_search
{
"from":0,
"size":2,
"sort": [
{
"brith": { -- 根据生日倒序排序
"order": "desc"
}
}
]
}
结果1:
{
.....
"sort" : [
1724148000000
]
.....
}
命令2:
POST test_index/_search
{
"from": 0,
"size": 2,
"search_after": [
1724148000000 -- 结果最后一条数据的sort中的值。
],
"sort": [
{
"brith": {
"order": "desc"
}
}
]
}
java代码实现:
Query query = new NativeSearchQueryBuilder()
.withPageable(PageRequest.of(page, size))
.withSearchType(SearchType.DEFAULT)
.withSorts(SortBuilders.fieldSort("field"))
.build();
List<Object> value = new ArrayList<>();
// 存储上一次查询条件的值
value.add("value");
query.setSearchAfter(value);
SearchHits hits = elasticsearchTemplate.search(query, XXX.class);
它的原理实际是维护了一个实时游标,它以上一次查询的最后一条记录为游标,方便对下一页的查询,它是一个无状态的查询,因此每次查询的都是最新的数据。
优点:
- 查询数据是实时的,每次更新可以实时反映到结果中;
- 不需要维护快照,可以避免消耗大量的存储空间。
缺点:
- 由于无状态查询,因此在查询期间的变更可能会导致数据跨页面的不一致;
- 排序顺序可能会在执行期间发生变化,具体取决于索引的更新和删除;
- 需要有一个唯一的不重复字段来排序;
- 它不适用于大幅度跳页查询,或者全量导出。
总结
| 方式 | 性能 | 优点 | 缺点 | 场景| |
|---|---|---|---|---|
| from+size | 低 | 随机跳转不同页 | 深度分页性能很差 | 适用于少量数据的分页 |
| scroll | 中 | 解决了深度分页问题 | 维护成本高,无法反应数据的实时性 | 需要遍历全部数据的场景 |
| search_after | 高 | 性能好,维护成本低,解决深度分页问题 | 实现复杂,需要有一个全局唯一的字段,每次都需要上一次查询结果,不适合大幅度跳页的场景 | 海量数据分页 |