Elasticsearch中进行深分页(附源码)
简介
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。
如需了解更多请查阅我的例外一篇博客:
分页方式 from+size弊端
es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误,如下:
这篇文章解决es的使用过程的查询条数不能超过10000的限制,因为随着页数越来越大,ES或者关系数据库响应越来越慢,甚至内存溢出OOM。
虽然es可以设置查询最大数量的配置max_result_window ,但是我个人建议不要去改。
在ES中有三种方式可以实现分页:from+size、scroll、search_after,下面介绍3种es提供的分页方式。
ElasticSearch java api使用
最新ES的提供了2种Java客户端,分别为低级REST客户端、高级REST客户端。
官网推荐使用高级Rest客户端,其他的客户端慢慢的都会被高级Rest客户端取代。
一、常见深度分页方式 from+size
- 使用from+size方式进行分页,受max_result_window默认参数10000条文档的限制,不建议针对该参数进行修改。
- 默认分页方式,适用小数据量场景,大数据量场景应避免使用。
- 通过性能测试,随着分页越来越深,执行时间和堆内存使用逐渐升高的趋势,在并发情况下from+size容易,造成集群服务的OOM问题。
参考官网:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html
kibana开发工具操作
GET testpage/_search
{
"size": 10
}
java源码
SearchRequest searchRequest = new SearchRequest(
"testpage");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//设置查询大小
searchSourceBuilder.size(5000);
searchRequest.source(searchSourceBuilder);
//3、发送请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
二、scroll深分页
- Scroll游标方式分页查询适用大数据量场景,只能向后增量查找,无法向前或者跳页查询,适用增量滚动抽取、数据迁移、重建索引等场景。
- 通过性能案例分析,滚动分页查找性能消耗相差不大,不会像from+size方式随着分页的深入性能逐渐升高的问题,且不会存在OOM问题。
- 该分页方式是查询的历史快照,对文档的更改(索引的更新或者删除)只会影响以后的搜索请求,不适用实时性查询场景。
kibana开发工具操作
scroll=5m为Scroll游标过期时间
GET testpage/_search?&scroll=5m
{
"size": 10000
}
GET _search/scroll
{
"scroll":"5m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAADYYWZDBWWnZfcjJRcXEzNEF0eFZtaWpYUQ=="
}
Scroll游标方式分页查询,首先查询第一次的游标id和数据,第二次查询使用第一次的游标id,循环遍历查询。
java源码
SearchRequest searchRequest = new SearchRequest(
"testpage");
//设置滚动对象,并设置游标过期时间
Scroll scroll = new Scroll(TimeValue.timeValueSeconds(60));
searchRequest.scroll(scroll);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//设置查询大小
searchSourceBuilder.size(5000);
searchRequest.source(searchSourceBuilder);
List<Map<String, Object>> result = new ArrayList<>();
//设置游标
String scrollId;
//3、发送请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
do {
for (SearchHit hit : searchResponse.getHits().getHits()) {
//获取需要数据
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
result.add(sourceAsMap);
}
//每次循环完后取得scrollId,用于记录下次将从这个游标开始取数
scrollId = searchResponse.getScrollId();
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(scroll);
//进行下次查询
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
} while (searchResponse.getHits().getHits().length != 0);
//清除滚屏
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
//也可以选择setScrollIds()将多个scrollId一起使用
clearScrollRequest.addScrollId(scrollId);
client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
Clear Scroll API
使用scroll分页之后,es建议可以使用Clear Scroll API删除最后一个scroll标识符 ,以释放搜索上下文。滚动过期时会自动发生这种情况,但是最好在滚动会话完成后立即进行。
scrollRequest.scroll(TimeValue.timeValueSeconds(60L));
scrollRequest.scroll("60s");
滚动间隔为 TimeValue
滚动间隔为 60s
如果没有scroll为设置值SearchScrollRequest,则一旦初始滚动时间到期(即,在初始搜索请求中设置的滚动时间),搜索上下文就会失效。
三、search_after深分页
- 分页方式弥补了 scroll 方式打开scroll 占用内存资源问题
- search_after可并行的拉取大量数据
- search_after分页方式通过唯一排序值定位,将每次需要处理的数据控制在一定范围,避免深度分页带来的开销,适用深度分页的场景
参考官网:https://www.elastic.co/guide/en/elasticsearch/reference/master/search-request-body.html#request-body-search-search-after
相当于sql里面根据id排序,然后获取最后一个id,从最后一个查询,循环遍历
kibana开发工具操作
根据num排序,每次查询10条数据
GET testpage/_search
{
"size": 10,
"search_after": [
"0"
],
"sort": [
{
"num": "asc"
}
]
}
GET testpage/_search
{
"size": 10,
"search_after": [
"10"
],
"sort": [
{
"num": "asc"
}
]
}
java源码
SearchRequest searchRequest = new SearchRequest(
"testpage");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//设置查询大小
searchSourceBuilder.size(2);
//设置唯一排序值定位
searchSourceBuilder.sort("num", SortOrder.ASC);
searchRequest.source(searchSourceBuilder);
List<Map<String, Object>> result = new ArrayList<>();
//3、发送请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
do {
for (SearchHit hit : searchResponse.getHits().getHits()) {
//获取需要数据
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
result.add(sourceAsMap);
}
//取得最后的排序值sort,用于记录下次将从这个地方开始取数
SearchHit[] hits = searchResponse.getHits().getHits();
Object[] lastNum = hits[hits.length - 1].getSortValues();
//设置searchAfter的最后一个排序值
searchSourceBuilder.searchAfter(lastNum);
searchRequest.source(searchSourceBuilder);
//进行下次查询
searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
} while (searchResponse.getHits().getHits().length != 0);
源码
码云:https://gitee.com/lhblearn/EsDemo