对于大量的数据而言,我们尽量避免使用 from+size 这种方法。这里的原因是 index.max_result_window 的默认值是 10000,也就是说 from+size 的最大值是1万。搜索请求占用堆内存和时间与 from+size 成比例,这限制了内存。
假如你想 hit 从 990 到 1000,那么每个 shard 至少需要 1000 个文档。
使用 from and size 的深度分页,是非常低效的,因为排序的结果必须从每个分片上取出并重新排序最后返回 10 条。这个过程需要对每个请求页重复。
scroll API 保持了那些结果已经返回的记录,所以能更加高效地返回排序的结果。
本文,我们将讲述了如何运用 scroll 接口来对大量数据来进行有效地分页。
Scroll 查询可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。
游标查询允许我们先做查询初始化,然后再批量地拉取结果。这有点儿像传统数据库中的 cursor 。
游标查询会取某个时间点的快照数据。查询初始化之后索引上的任何变化会被它忽略。它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引视图一样。
深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话,查询结果的成本就会很低。游标查询默认用字段 _doc 来排序。这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。
启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。
可以把 scroll 理解为关系型数据库里的 cursor,因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发。
scroll 具体分为初始化和遍历两步:
也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果。游标可以增加性能的原因,是因为如果做深分页,每次搜索都必须重新排序,非常浪费,使用scroll就是一次把要用的数据都排完了,分批取出,因此比使用from+size还好。
注意:从 scroll 请求返回的结果反映了 search 发生时刻的索引状态,就像一个快照。后续的对文档的改动(索引、更新或者删除)都只会影响后面的搜索请求。
为了使用 scroll,初始搜索请求应该在查询中指定 scroll 参数,这可以告诉 Elasticsearch 需要保持搜索的上下文环境多久,如 ?scroll=5m。
下面的DSL 查询命令,使用order_id 进行排序,保持游标查询窗口5分钟。
GET kibana_sample_data_ecommerce/_search?scroll=5m
{ "query": { "match_all": {}}, "sort" : ["order_id"], "size": 1000
}
这个查询的返回结果包括一个字段 _scroll_id, 它是一个base64编码的长字符串,如图所示:
现在我们能传递字段 _scroll_id 到 _search/scroll 查询接口获取下一批结果。
每次对 scroll API 的调用返回了结果的下一个批次,直到没有更多的结果返回,也就是直到 hits 数组空了。
GET _search/scroll
{ "scroll": "5m", "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmV0cjE1Q3JfUmpXeF95NVlyVDlFUncAAAAAAACmmBZTc29KeGx0dFEyYUo2VTVEVWMtdnF3"}
注意再次设置游标查询过期时间为5分钟。
这个游标查询返回的下一批结果。尽管我们指定字段 size 的值为1000,我们有可能取到超过这个值数量的文档。当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards。
这里说的是,从所有分片(N个)里每个拿1000,然后按条件排序,然后按scroll的方式每次返回1000给你,一共能scroll N次。
注意游标查询每次返回一个新字段 _scroll_id。每次我们做下一次游标查询, 我们必须把前一次查询返回的字段 _scroll_id 传递进去。当没有更多的结果返回的时候,我们就处理完所有匹配的文档了。
如果完成此过程,则需要清理上下文,因为上下文在超时之前仍会占用计算资源。如下面的DSL 命令所示,可以使用 scroll_id 参数在 DELETE API 中指定一个或多个上下文:
DELETE _search/scroll{ "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmV0cjE1Q3JfUmpXeF95NVlyVDlFUncAAAAAAACmmBZTc29KeGx0dFEyYUo2VTVEVWMtdnF3"}
如果需要查询大量的数据,可以考虑使用 Search Scroll API,这是一种更加高效的方式。还可以和Spring Boot 整合使用,参考如下示例代码:
final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
SearchRequest searchRequest = new SearchRequest("kibana_sample_data_ecommerce");
searchRequest.scroll(scroll);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(matchQuery("title", "Elasticsearch"));
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = searchResponse.getScrollId();
SearchHit[] searchHits = searchResponse.getHits().getHits();while (searchHits != null && searchHits.length > 0) {
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(scroll);
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
scrollId = searchResponse.getScrollId();
searchHits = searchResponse.getHits().getHits();
}
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
ClearScrollResponse clearScrollResponse = client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
boolean succeeded = clearScrollResponse.isSucceeded();
除了第一次查询外,后续的查询都需要携带scrollId,可以理解为游标,用它来控制分页。和from+size模式中页码是一个作用。
查询结束后,需要使用client.clearScroll() 方法清除 scroll。
使用scroll api就无法实现跳页查询了,因为除了第一次查询外的其它查询都要依赖上一次查询返回的scrollId,这一点需要注意。
来源:
https://www.toutiao.com/article/7103451700878443041/?log_from=ac4a7d751d6ca_1657597450247
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
来都来了,走啥走,留个言呗~
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!