ES为了避免深分页,不允许使用分页(from&size)查询10000条以后的数据,因此如果要查询第10000条以后的数据,要使用ES提供的 scroll(游标) 来查询
-
假设取的页数较大时(深分页),如请求第20页,Elasticsearch不得不取出所有分片上的第1页到第20页的所有文档,并做排序,最终再取出from后的size条结果作为最终的返回值
-
假设你有16个分片,则需要在coordinate node汇总到 shards* (from+size)条记录,即需要16*(20+10)记录后做一次全局排序
-
所以,当索引非常非常大(千万或亿),是无法使用from + size 做深分页的,分页越深则越容易OOM,即便不OOM,也很消耗CPU和内存资源
-
因此ES使用
index.max_result_window:10000
作为保护措施 ,即默认 from + size 不能超过10000,虽然这个参数可以动态修改,也可以在配置文件配置,但是最好不要这么做,应该改用ES游标来取得数据
scroll游标原理
-
可以把 scroll 理解为关系型数据库里的 cursor,因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发
-
scroll 具体分为初始化和遍历两步
-
初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照
-
在遍历时,从这个快照里取数据
-
也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果
-
-
游标可以增加性能的原因,是因为如果做深分页,每次搜索都必须重新排序,非常浪费,使用scroll就是一次把要用的数据都排完了,分批取出,因此比使用from+size还好
java 实现scroll 分页搜索
public Page scrollPage(Page page, String key, String state, String code, Date begin, Date end) {
int pageSize = page.getPageSize();
int currentPageNo = page.getCurrentPageNo();
TransportClient client = this.elasticsearchTemplate.getTransportClient();
// 组织查询条件
BoolQueryBuilder query = setBoolQueryBuilder(state, begin, end, key, code);
// Scroll查询请求
// 首次搜索 包含数据
SearchResponse searchResponse = client.prepareSearch("my_index")
.setTypes("my_type")
// 执行检索的类别
.setSearchType(SearchType.DEFAULT)
// 指定超时时间
.setScroll(new TimeValue(5000))
// 排序
.addSort("createDate", SortOrder.DESC)
// 查询条件
.setQuery(query)
// 大小
.setSize(page.getPageSize())
.execute()
.actionGet();
// 获取查询总数量
long totalCount = searchResponse.getHits().getTotalHits();
// 设置分页数据总数
page.setTotalCount(totalCount);
List<EntityEsDto> result = new ArrayList<>(pageSize);
// 查询总数量为0,直接返回
if (totalCount == 0) {
page.setResult(result);
return page;
}
// 计算总页数
long pageCount = totalCount % (long) pageSize == 0L ? totalCount / (long) pageSize : totalCount / (long) pageSize + 1L;
// 重新设置当前页数
if (pageCount == 1 || pageCount <= currentPageNo) {
currentPageNo = (int) pageCount;
}
// 获取currentPageNo的数据,从Scroll快照拿数据,首页数据已经拿到,
for (int i = 2; i <= currentPageNo; i++) {
// 从第二页开始,使用上次搜索结果的ScrollId,从Scroll快照拿数据
searchResponse = client
.prepareSearchScroll(searchResponse.getScrollId())
.setScroll(new TimeValue(5000))
.execute()
.actionGet();
}
// 获取分页结果
SearchHits hits = searchResponse.getHits();
EntityEsDto entityEsDto= null;
// 遍历转换结果对象
for (SearchHit searchHit : hits) {
entityEsDto= JSONObject.parseObject(searchHit.getSourceAsString(), EntityEsDto.class);
if (Objects.nonNull(entityEsDto)) {
// 设置主键
entityEsDto.setId(searchHit.getId());
result.add(entityEsDto);
}
}
page.setCurrentPageNo(currentPageNo);
page.setResult(result);
return page;
}
/**
* <p>
* <code>setBoolQueryBuilder</code>组织查询条件
* </p>
*
* @param key 关键字
* @param state 状态
* @param code 标识
* @param begin 开始时间
* @param end 结束时间
* @return
*/
private BoolQueryBuilder setBoolQueryBuilder(String key, String state, String code, Date begin, Date end) {
BoolQueryBuilder query = QueryBuilders.boolQuery();
// 添加 关键字模糊匹配条件 不分词实现sql like模糊匹配效果,使用es wildcard 通配符搜索
if (StringUtils.isNotBlank(key)) {
QueryBuilder query1 = QueryBuilders.wildcardQuery("key", "*" + key + "*");
query.must(query1);
}
// 添加 状态条件
if (StringUtils.isNotBlank(state)) {
QueryBuilder query2 = QueryBuilders.termQuery("state", state);
query.must(query2);
}
// 添加 标识条件
if (StringUtils.isNotBlank(currentUser.getCustomerId())) {
QueryBuilder query3 = QueryBuilders.termQuery("code", code);
query.must(query3);
}
// 添加 开始时间 结束时间范围条件
if (begin != null && end != null) {
QueryBuilder query4 = QueryBuilders.rangeQuery("createTime").from(begin.getTime()).to(end.getTime());
query.must(query4);
}
return query;
}
参考:
https://blog.youkuaiyun.com/weixin_40341116/article/details/80821655