Elasticsearch(ES)中进行分页查询时,最佳实践取决于具体的使用场景和需求。
以下是对每种分页方法的简要分析以及它们适用的情况:
1. From + Size
- 最常见且直观的方法,通过
from
参数指定跳过多少条记录,size
参数指定每次返回多少条记录。 - 优点:实现简单,适用于小规模或浅层分页,即前几页查询。
- 缺点:随着
from
值增大,查询效率会显著降低,尤其是在深度分页的情况下(例如,查询很多页之后的数据),因为ES需要遍历所有之前的结果才能找到指定偏移的结果集,这对分布式系统来说成本非常高。
2. Scroll API
- 提供了一种持续检索大量数据的方式,创建一个“滚动”上下文,可以在一段时间内保持一致性视图。
- 优点:非常适合大数据量的批量读取或深度分页,尤其是在不需要考虑数据实时更新的情况下,如数据导出或批处理任务。
- 缺点:滚动上下文会占用服务器资源,且对实时性要求高的场景不合适,因为它反映的是某个时间点的快照状态,不能反映出滚动上下文创建后数据的变化。
3. Search After
- 从ES 5.0版本开始提供,用于克服
from+size
在深度分页时的性能瓶颈。 - 优点:利用
_score
或用户定义的排序字段来进行连续查询,避免了大规模跳跃式分页的问题。相比from+size
,它在深度分页时性能更优,同时能够更好地处理实时变化的数据。 - 缺点:需要有稳定的排序字段,并且不是所有场景下都能方便地转换为
search_after
模式。
代码示例
package org.example;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class ESScrollMain {
private static final String indexName = "kibana_sample_data_logs";
public static void main(String[] args) throws IOException {
System.out.println("Hello and welcome!");
RestClientBuilder builder = RestClient.builder(new HttpHost("10.x.x.x", 9200, "http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
SearchRequest searchRequest = new SearchRequest(indexName);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// hit 返回值(bool 查询返回条数)
// searchSourceBuilder.size(0);
// searchSourceBuilder.from(0);
searchSourceBuilder.trackTotalHits(true);
// 超时时间60s
MatchAllQueryBuilder search = QueryBuilders.matchAllQuery();
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchSourceBuilder.size(2000);
searchSourceBuilder.query(search);
long scrollTime = 30L;
searchRequest.source(searchSourceBuilder);
searchRequest.scroll(TimeValue.timeValueSeconds(scrollTime));
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = searchResponse.getScrollId();
SearchHit[] hits = searchResponse.getHits().getHits();
int count = 0;
int batch = 1;
System.out.println("初始结果条数:" + count);
count += hits.length;
System.out.println("滚动第" + batch + "批结果总条数:" + count);
while (hits != null && hits.length > 0) {
batch++;
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueSeconds(scrollTime));
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
scrollId = searchResponse.getScrollId();
hits = searchResponse.getHits().getHits();
count += hits.length;
System.out.println("滚动第" + batch + "批结果总条数:" + count);
}
System.out.println("结束,总计:"+searchResponse.getHits().getTotalHits());
}
}
综合考虑
- 对于网页应用中的普通分页浏览,尤其是前几页,
from+size
足够。 - 如果需要处理大数据集且允许一定的延迟,或者一次性获取所有结果,Scroll API 是更好的选择。
- 对于深度分页且需要实时性较好的场景,应优先考虑
search_after
。
优化方向
此外,针对大型分页查询的性能优化还可以包括:
- 使用高效的过滤条件减少不必要的查询范围。
- 考虑是否真的需要返回全部数据,或者能否通过汇总统计或其他方式减少数据传输量。
- 设置合理的索引策略和分片大小,优化集群配置,如增加合适的内存缓冲区等。