在使用es进行数据查询时,由于es官方默认限制了索引一次性最多只能查询10000条数据,查询第10001条数据开始就会报错,
错误的内容大致为:Result window is too large, from + size must be less than or equal to
通常,见到的一些网站的做法有:
但是在更多的场景下,我们还是希望在查询时,可以突破这种限制,那么这里提供下面3种方式进行解决:
方案1:在设置索引属性时解除索引最大查询数的限制
put _all/_settings
{
"index.max_result_window":200000
}
_all表示所有索引,针对单个索引的话修改成索引名称即可
方案2::在创建索引的时候加上
"settings":{
"index":{
"max_result_window": 500000
}
}
这样设置完毕之后还不行,如果继续使用原来的API进行查询,查询到的结果最大数量仍然是10000,这时候需要在API中添加这样一行代码:
searchSourceBuilder.trackTotalHits(true);
如果是使用kibana等工具的dsl语句,可以参考下面编写
GET 索引名/_search
{
"query": {
"match_all": {}
},
"track_total_hits":true
}
在实际使用场景中,通常都是需要结合分页进行的,对于es来说,通常只需要传入 pageIndex 和pageSize即可获取到指定的页码数据,如果结合上面的问题一起来看,即突破1万条数据量的限制,单纯使用分页肯定不是最好的办法,因为es在突破1万的数据查询时候,查询性能会有一定的损耗,这就像从mysql中,从1000万条数据查询最后10条数据是一样的道理;
为了合理的解决这个问题,es中提供了深度分页的概念,具体来说,在API代码层面,可以使用:searchAfter 这个API ,searchAfter需要在查询时传入上一次最后的那一条数据唯一标识的参数,这样的话,每次查询分页数据时,就从这一条开始向后查询10条即可
基本思想
searchAfter的方式可立即为维护了一个实时游标来记录每次查询的最后那条数据游标值,能保证滚动顺序读取数据,可用于实时请求和高并发场景,但search_after缺点是不能够随机跳转分页,只能一页一页向后翻,并且需要至少指定一个唯一不重复字段来排序。它与滚动API非常相似,但与它不同,search_after参数是无状态的,它始终针对最新版本的搜索器进行解析。因此,排序顺序可能会在步行期间发生变化(具体取决于索引的更新和删除)
核心代码展示
public Pagination<ComptrollerLogDTO> list(ComptrollerLogQuery logQuery) {
SearchRequest searchRequest = new SearchRequest();
//设置查询的索引
searchRequest.indices(EsManager.LOG_INDEX_NAME_ALL);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 如果未设置排序字段,默认用提交时间
if (StringUtils.isNotBlank(logQuery.getSortField())) {
if (DESC.toString().equals(logQuery.getOrderBy())) {
searchSourceBuilder.sort(logQuery.getSortField(), DESC);
} else {
searchSourceBuilder.sort(logQuery.getSortField(), ASC);
}
} else {
searchSourceBuilder.sort(SUBMIT_TIME, DESC);
}
searchSourceBuilder.sort(LOG_ID, DESC);
searchSourceBuilder.query(getQueryBuilder(logQuery));
searchSourceBuilder.trackTotalHits(true);
searchRequest.source(searchSourceBuilder);
Integer pageSize = logQuery.getPageSize();
if (pageSize == null || pageSize < 0) {
pageSize = 100;
}
if (logQuery.getPageIndex() == null) {
ComptrollerLogDTO logDTO = get(logQuery.getAfterLogId());
logQuery.setPageIndex(0);
searchSourceBuilder.from(0);
if (logDTO != null) {
searchSourceBuilder.searchAfter(new Object[]{String.valueOf(logDTO.getSubmitTime()), logQuery.getAfterLogId()});
}
} else {
searchSourceBuilder.from((logQuery.getPageIndex() - 1) * pageSize);
}
searchSourceBuilder.size(pageSize);
SearchResponse search = EsManager.search(searchRequest);
SearchHits hits = search.getHits();
List<ComptrollerLogDTO> comptrollerLogs = convertToLogBeans(hits);
return new Pagination<>(logQuery.getPageIndex(), pageSize, hits.getTotalHits().value, comptrollerLogs);
}
重点关注这里
上面这种做法可以解决顺序分页并突破查询10000条数据限制的问题,但使用场景也有一定的局限性,这个可以结合自身的需求合理使用,如果数据量特别大,但仍然希望一次性查询所有数据,又该如何处理呢?下面简单介绍下es提供的另一种强大的滚动查询方式
这么讲吧,之前用的搜索,相当于MySQL的limit,即分页,数据量少的话无所谓啦,但如果量大呢,比如上亿的数据,你再使用Es分页试试,分页的前提是都查出来,然后排序,然后在内存做处理,你想,内存能有多大,多大的内存也顶不住这么大的数据量
当然了,ES针对这个情况提前就考虑到了,于是就出现了滚动查询–Scroll,什么是滚动插叙呢,原理是什么呢?滚动查询和原生查询,一个相当于我们翻书,可以跳,一个相当于长图,看到哪里就显示哪里,这也是它们异同点:滚动查询无论查多少数据都可以,但不能翻页,也不支持分页,而普通查询,支持翻页分页,但是只支持一万笔以内的数据量(默认情况下,当然使用上面深度分页可以解决这个问题)
Java中使用滚动查询API
searchRequest.scroll(TimeValue.timeValueMinutes(1L));
简单理解,查询条件中设置了滚动条件之后,在滚动的有效时间范围内,比如上面设置的是1分钟内,可以保持搜索的上下文环境的时间(滚动时间),在具体查询过程中,每滚动一次,es会产生一个scrollId,用于记录每次滚动的结果,直到滚动到无法继续查询数据的时候结束
这么一来,我们就可以利用scroll的特性,从头到尾一直滚下去,每次设置一个合适的滚动数量,就可以将所有的数据一次性查询出来了,scroll使用完毕之后,一定要做清理
演示代码
public void exportLogCsv(Long startTime, Long endTime, String source, String details, String behaviourType, HttpServletResponse response) {
List<ComptrollerLog> logList = new LinkedList<>();
EsExpComptrollerLog scrollLogs = createScroll(UserContext.getBusinessUnitId(), startTime, endTime, source, details, behaviourType, 5000);
String scrollId = "";
if (scrollLogs != null) {
scrollId = scrollLogs.getScrollId();
logList.addAll(scrollLogs.getComptrollerLogList());
}
while (true) {
EsExpComptrollerLog s = listLogByScrollId(scrollId);
if (s == null || CollectionUtils.isEmpty(s.getComptrollerLogList())) {
break;
}
scrollId = s.getScrollId();
logList.addAll(s.getComptrollerLogList());
}
//后续业务处理,使用上面的 logList
}
public EsExpComptrollerLog createScroll(String tenantId, Long startTime, Long endTime, String source,
String details, String behaviourType, Integer rows) {
SearchRequest searchRequest = new SearchRequest(EsManager.LOG_INDEX_NAME_ALL);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
if (StringUtils.isNotBlank(details)) {
queryBuilder = queryBuilder.must(QueryBuilders.multiMatchQuery(details, "details", "realname", "email"));
}
if (StringUtils.isNotBlank(source)) {
queryBuilder = queryBuilder.must(QueryBuilders.termQuery("source", source));
}
searchSourceBuilder.sort("submitTime", SortOrder.DESC);
searchSourceBuilder.query(queryBuilder);
searchSourceBuilder.size(rows);
searchRequest.scroll(TimeValue.timeValueMinutes(1L));
searchRequest.source(searchSourceBuilder);
SearchResponse search = EsManager.search(searchRequest);
return transformEsDataToComptrollerLog(search);
}
public EsExpComptrollerLog listLogByScrollId(String scrollId) {
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueSeconds(30));
SearchResponse searchScrollResponse = EsManager.scroll(scrollRequest);
return transformEsDataToComptrollerLog(searchScrollResponse);
}
public static SearchResponse scroll(SearchScrollRequest scrollRequest) {
try {
return getClient().scroll(scrollRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
checkOrInit(e);
return null;
}
}
public static void clearScroll(ClearScrollRequest clearScrollRequest) {
try {
getClient().clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
checkOrInit(e);
}
}
注意点: