按照一般的查询流程来说,如果我想查询前10条数据:
- 客户端请求发给某个节点
- 节点将请求转发到集群其他节点,各节点返回是否包含该请求信息,然后该节点再发送二次请求给具体包含该query倒排的节点上进行计算,查询每个分片上的前10条
- 结果返回给节点,整合数据,提取前10条
- 返回给请求客户端
from + size 浅分页
当查询10-20条数据时,就在相应的各节点上直接查询前20条数据,然后截断前10条,只返回10-20的数据。
做过测试,越往后的分页,执行的效率越低。总体上会随着from的增加,消耗时间也会增加。而且数据量越大,就越明显,当size + from > 10000时,es查询失败,并且提示:
Result window is too large, from + size must be less than or equal to: [10000] but was [10001]
See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.from(0);
sourceBuilder.size(10);
scroll 深分页
Scroll往往是应用于后台批处理任务中,不能用于实时搜索,因为这个scroll相当于维护了一份当前索引段的快照信息,这个快照信息是你执行这个scroll查询时的快照。在这个查询后的任何新索引进来的数据,都不会在这个快照中查询到。但是它相对于from和size,不是查询所有数据然后剔除不要的部分,而是记录一个读取的位置,保证下一次快速继续读取。查询时会自动返回一个_scroll_id,通过这个id可以继续查询。
try {
SearchRequest request = new SearchRequest(indexName);
request.scroll(TimeValue.timeValueMinutes(60L));
QueryBuilder queryBuilder = QueryBuilders.termQuery("citycode", cityCode);
request.source(new SearchSourceBuilder()
.fetchSource(new String[]{"address"}, null)
.timeout(this.timeout)
.size(this.pageSize)
.postFilter(queryBuilder));
SearchResponse response = ESClient.restClient().search(request, RequestOptions.DEFAULT);
String scrollId = response.getScrollId();
while (true) {
SearchHit[] hits = response.getHits().getHits();
for (SearchHit hit : hits) {
...
}
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueMinutes(60L));
response = ESClient.restClient().scroll(scrollRequest, RequestOptions.DEFAULT);
}
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
ClearScrollResponse clearScrollResponse = ESClient.restClient().clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
if (!clearScrollResponse.isSucceeded()) {
this.logger.error("删除search scroll失败!!! gbcitycode = [{}], scrollId = [{}]", cityCode, scrollId);
}
} catch (Exception e) {
e.printStackTrace();
throw new AppException("请求文档失败 !!!");
}
search_after 深分页
search_after通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景,一般用于客户端的分页查询。
SearchRequest request = new SearchRequest(searchIndexName);
request.source(new SearchSourceBuilder()
.fetchSource(new String[] {"address"},null)
.query(query)
.sort(sort)
.size(size)
.searchAfter(sortValues));
SearchResponse response = restClient.search(request, RequestOptions.DEFAULT);
注意事项:
- 搜索时,需要指定sort,并且保证值是唯一的(可以通过加入_id或者文档body中的业务唯一值来保证);
- 再次查询时,使用上一次最后一个文档的sort值作为search_after的值来进行查询;
- 不能使用随机跳页,只能是下一页或者小范围的跳页(一次查询出小范围内各个页数,利用缓存等技术,来实现小范围分页,比较麻烦,比如从第一页调到第五页,则依次查询出2,3,4页的数据,利用每一次最后一个文档的sort值进行下一轮查询,客户端或服务端都可以进行,如果跳的比较多,则可能该方法并不适用)
总结
分页方式 | 性能 | 优点 | 缺点 | 场景 |
---|
from + size | 低 | 灵活性好,实现简单 | 深度分页问题 | 数据量比较小,能容忍深度分页问题 | scroll | 中 | 解决了深度分页问题 | 解决了深度分页问题 | 无法反应数据的实时性(快照版本) 维护成本高,需要维护一个 scroll_id | search_after | 高 | 性能最好 不存在深度分页问题 能够反映数据的实时变更 | 实现复杂,需要有一个全局唯一的字段 连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果 | 海量数据的分页 |
|