一、前言:
ES 普通的分页查询有深分页限制,默认是10000条。(因为越往后面分越耗内存)所以要查询1万条以后的数据,要不就缩小查询范围,要不用别的方法。
ES 提供了 scroll 查询。第一次查询获取到scroll_id,下次查询通过scroll_id直接查询。scroll相当于维护了一份当前索引段的快照信息,这个快照信息是你执行这个scroll查询时的快照。在这个查询后的任何新索引进来的数据,都不会在这个快照中查询到。但是它相对于from和size,不是查询所有数据然后剔除不要的部分,而是记录一个读取的位置,保证下一次快速继续读取。而且也不能排序,只能按默认的文档顺序,比较适合查询扫描全量数据。
1、看一个DEMO
private static void scrollSearch(String indexName, String typeName,
String... ids) {
IdsQueryBuilder qb = QueryBuilders.idsQuery().addIds(ids);
SearchResponse sResponse = client.prepareSearch(indexName)
.setTypes(typeName)
.setQuery(qb)
.setScroll(new TimeValue(5000))
.setSize(50)
.execute()
.actionGet();
int tShards = sResponse.getTotalShards();
long timeCost = sResponse.getTookInMillis();
int sShards = sResponse.getSuccessfulShards();
System.out.println(tShards+","+timeCost+","+sShards);
while (true) {
SearchHits hits = sResponse.getHits();
SearchHit[] hitArray = hits.getHits();
for(int i = 0; i < hitArray.length; i++) {
SearchHit hit = hitArray[i];
Map fields = hit.getSource();
for(String key : fields.keySet()) {
System.out.println(key);
System.out.println(fields.get(key));
}
}
sResponse = client.prepareSearchScroll(sResponse.getScrollId()).setScroll(new TimeValue(5000)).execute().actionGet();
//Break condition: No hits are returned
if (sResponse.getHits().getHits().length == 0) {
break;
}
}
}
2、自己实现的基础API
// 接口定义
/**
* Scroll 游标全量数据查询 (不支持排序,只按照doc_id排序)
*
* @param clazz 实体类类型
* @param query 查询参数
* @param scrollId 游标
* @param size 一次拿取数据多少。
* @param
* @return
*/
EsScrollResponse listByQueryScroll(Class clazz, IQuery query, String scrollId,int size);
// 具体实现
@Override
public EsScrollResponse listByQueryScroll(Class clazz, IQuery query, String scrollId, int size) {
// 参数校验
if (size < 1 || size > 200) {
throw new RuntimeException("ES 查询一次请求的数了超出范围,请在 1-200 之间");
}
// 返回结果初始化
EsScrollResponse response = new EsScrollResponse<>();
List result = new ArrayList<>();
// 获取文档信息
Document document = clazz.getAnnotation(Document.class);
// 全局使用的查询返回参数初始化。
SearchResponse searchResponse = null;
// 第一次查询没有游标,获取数据并记录游标
if (StringUtils.isEmpty(scrollId)) {
SearchRequestBuilder searchRequestBuilder = esDataSource.getClient().prepareSearch(document.indexName()).setTypes(document.type()).setQuery(query.buildQuery()).setScroll(TimeValue.timeValueMinutes(ES_TIME_OUT_MINUTES)).setSize(size);
searchResponse = searchRequestBuilder.setTimeout(TimeValue.timeValueMinutes(ES_TIME_OUT_MINUTES)).execute().actionGet();
} else {
// 有游标按游标进行查询
SearchScrollRequestBuilder searchScrollRequestBuilder = esDataSource.getClient().prepareSearchScroll(scrollId).setScroll(TimeValue.timeValueMinutes(ES_TIME_OUT_MINUTES));
searchResponse = searchScrollRequestBuilder.execute().actionGet();
}
// 处理返回结果
for (SearchHit hit : searchResponse.getHits()) {
T rt = JSON.parseObject(hit.getSourceAsString(), clazz);
result.add(rt);
}
response.setData(result);
response.setScrollId(searchResponse.getScrollId());
return response;
}
/**
* ES Scroll 查询的封装实体
*/
@Data
public class EsScrollResponse {
/**
* 存放数据
*/
private List data;
/**
* 指定游标
*/
private String scrollId;
}
三、两种分页方式比较
分页方式
说明
优点
缺点
场景
from + size
常用的分页方式,指定分页大小和偏移量可以直接获取到需要的数据。但内存消耗特别大,且速度很一般,当我们指定from = 100000,size = 10 的时候,每个node都会取出top 100000的数据,再进行汇总排序,假设3个node,那么就需要取出3*100000条数据进行排序后,再取top10的数据进行返回。所以ES默认的from+size的限制设置为10000。在数据量到达十万级百万级的时候这种分页方式显然不合理。
数据量小的情况使用最方便,灵活性好,实现简单
内存消耗大,速度一般,数据量大的情况面临深度分页问题
数据量较小且能容忍深度分页问题
scroll(游标)
一种快照的查询形式,快照一旦形成,本次滚动查询内便无法查出来新增的那些数据,而且scroll是无法进行排序的,也无法指定from,那么我们想查看指定页码的数据就必须将该页数据之前的全部数据取出来再进行丢弃,所以scroll一般用于导出全量数据。
导出全量数据时性能最好
无法反应数据的实时性(因为是快照版本),维护成本高,需要维护一个 scroll_id,且不支持排序,只按照doc_id排序
全量数据的导出