ElasticSearch深度分页
es的分页查询有3种方式:from+size,scroll,search_after,下面比较一下这3种方式。一、from+sizefrom+size的分页查询称为"浅"分页,它的原理很简单,就是查询前20条数据,然后截断前10条,只返回10-20的数据。这样其实白白浪费了前10条的查询。在深度分页的情况下,这种使用方式效率是非常低的,比如from = 50000, size=10, es 需要在
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);
}
}
}
更多推荐
所有评论(0)