问题描述
遍历全表做数据清洗和处理,当使用JPA默认分页查询时, 当每页数据2000条,查询到第5页以内,正常,查询到第6页时,超过了前10000条,系统会报错
Elasticsearch exception [type=illegal_argument_exception, reason=Result window is too large, from + size must be less than or equal to: [10000] but was [12000].
可是,明明是分页查询,只是查询了第6页而已,就会报错
原因
当索引非常非常大(千万或亿),是无法按照from + size做深分页的,因为分页越深则越容易OOM,即便不OOM,也是很消耗CPU和内存资源的。官方在后2.x版本中已增加限定 index.max_result_window:10000作为保护措施,即默认 from + size 不能超过1万。(from表示从第几行开始,size表示查询多少条数据,from默认为0,size默认为10。)
解决方案
方案一、通过设置index 的设置参数max_result_window的值,eg:
curl -XPUT http://es-ip:9200/_settings -d '{ "index" : { "max_result_window" : 100000000}}
设置后只对已经存在的索引生效,新建的索引需重新设置
或者在config/elasticsearch.yml文件中的最后加上
index.max_result_window: 100000000
方案二:创建索引时设置
"settings":{
"index":{
"max_result_window":1000000
}
}
方案三: 使用scoll
游标查询 或者 seach after
查询
- scoll 是使用了缓存,可以设置本次查询的缓存1分钟,不存在丢弃问题
- search after 是需要首先排序,然后按照排序后的结果,记录每次查询的最后一个结果,作为下一次的开始
方案选择
方案一和方案而二,会占用服务器更多的内存和 CPU 资源, 一般不建议使用。
真的需要查询全部数据或者需要深分页,可以采用scoll
游标或者 seach after
的方式,也是官方推荐的方式。
Scoll 游标解决方案
采用scoll
游标的方式,滚动查询
原理就是记住上次查询结束的位置,下次从这个位置再继续查询。
抽取公共方法之后也可以使用,
ElasticsearchUtil.class
/**
* 获取第一页,保留 scroll id
* @param restHighLevelClient restHighLevelClient
* @param clazz 返回数据类型
* @param indexName 索引名称
* @param size 每页大小
* @throws IOException IOException
*/
public static ScrollResultDTO findFirstScroll(RestHighLevelClient restHighLevelClient, Class<?> clazz, String indexName, int size) throws IOException {
final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
SearchRequest searchRequest = new SearchRequest(indexName);
searchRequest.scroll(scroll);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(size);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
log.info("Scroll starts: index={}, cache time={}, size={}, total counts={}",indexName, scroll.toString(), size, searchResponse.getHits().getTotalHits());
ScrollResultDTO scrollResultDTO = new ScrollResultDTO();
scrollResultDTO.setScrollId(searchResponse.getScrollId());
scrollResultDTO.setResultList(ElasticsearchUtil.getSearchResult(searchResponse, clazz));
return scrollResultDTO;
}
/**
* 滚动索引缓存时间
*/
private static final Scroll SCROLL = new Scroll(TimeValue.timeValueMinutes(1L));
/**
*
* @param restHighLevelClient restHighLevelClient
* @param clazz 返回数据类型
* @param scrollId 游标id
* @throws IOException IOException
*/
public static ScrollResultDTO continueScroll(RestHighLevelClient restHighLevelClient, Class<?> clazz, String scrollId) throws IOException {
SearchScrollRequest scrollRequest = new SearchScrollRequest();
scrollRequest.scroll(SCROLL);
scrollRequest.scrollId(scrollId);
SearchResponse searchResponse = restHighLevelClient.scroll(scrollRequest, RequestOptions.DEFAULT);
SearchHit[] searchHits = searchResponse.getHits().getHits();
if (searchHits == null || searchHits.length <= 0) {
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(searchResponse.getScrollId());
ClearScrollResponse clearScrollResponse = restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
if (clearScrollResponse.isSucceeded()){
return null;
} else {
throw new GeneralException("Scroll failed");
}
}
ScrollResultDTO scrollResultDTO = new ScrollResultDTO();
scrollResultDTO.setScrollId(searchResponse.getScrollId());
scrollResultDTO.setResultList(ElasticsearchUtil.getSearchResult(searchResponse, clazz));
return scrollResultDTO;
}
scrollResultDTO
: 封装返回对象
/**
* ES 滚动查询 结果返回封装参数
*
* @author hycao
*/
@Data
public class ScrollResultDTO {
/**
* 滚动id
*/
private String scrollId;
/**
* 返回结果
*/
private List<?> resultList;
}
如何使用
Resource
: 获取数据,处理数据
// 获取第一页数据
ScrollResultDTO allVillage = villageRepository.findFirstScroll();
while (allVillage != null) {
// 获取游标分页数据
List<Organization> villageList = allVillage.getResultList().stream().parallel()
.map(this::villageToOrganization).collect(Collectors.toList());
// 处理数据
organizationRepository.saveAllNotReplace(villageList);
// 获取下一页数据
allVillage = villageRepository.continueScroll(allVillage.getScrollId());
}
Service
: 直接调用工具类,传入,实体类,索引名称,分页大小即可
@Override
public ScrollResultDTO findFirstScroll() throws IOException {
return ElasticsearchUtil.findFirstScroll(restHighLevelClient, Village.class, IndexConstants.ES_INDEX_VILLAGE, 500);
}
@Override
public ScrollResultDTO continueScroll(String scrollId) throws IOException {
return ElasticsearchUtil.continueScroll(restHighLevelClient, Village.class, scrollId);
}