You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring从Elasticsearch获取超1万条数据过慢的优化方案咨询

Elasticsearch批量获取数据性能优化方案

问题场景

我有一个包含2万余条文档的Elasticsearch索引,最初使用以下代码获取数据:

final SearchHits<MyTyp> searchHitsMyTyp =
        elasticsearchCustomTemplate.search(
                searchQueryMyTyp,
                MyTyp.class,
                IndexCoordinates.of(indexSettings.indexMyTypName))

这段代码能立即返回1万条文档,但无法获取全部数据,因此修改为基于search_after的分页实现来获取全量数据:

NativeSearchQuery searchQuery =
        new NativeSearchQueryBuilder()
                .withQuery(
                        QueryBuilders.matchQuery(
                                "MyTyp_id", mytyp.getId()))
                .build();
                
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(QueryBuilders.termQuery("MyTyp_id", mytyp.getId()));
queryBuilder.withPageable(PageRequest.of(0, 10000));
queryBuilder.withSort(SortBuilders.fieldSort("_id").order(SortOrder.ASC));

queryBuilder.withAggregations(
        AggregationBuilders.filter(
                "filter_by_id", QueryBuilders.termQuery("MyTyp_id", mytyp.getId())));
NativeSearchQuery searchQueryMyTyp = queryBuilder.build();
SearchHit[] searchHitsMyTyp=null;
final ArrayList<MyTyp> typeList=new ArrayList();

do {
  if(searchHitsMyTyp!=null) {
    searchQuery.setSearchAfter(searchHitsMyTyp[searchHitsMyTyp.length - 1].getSortValues().stream().toList());
  }
  searchHitsMyTyp =  elasticsearchCustomTemplate.search(
          searchQueryMyTyp,
          MyTyp.class,
          IndexCoordinates.of(indexSettings.indexMyTypName)).getSearchHits().toArray(new SearchHit[0]);
  typeList.addAll( Arrays.stream(searchHitsMyTyp).map(p->(MyTyp)p.getContent()).toList());

}while(searchHitsMyTyp.length >= 10000);

现在的问题是该实现速度极慢,导致UI卡顿。按1万条耗时1秒计算,2万条理论上约2秒,但实际远超这个时间,想知道是否有优化方法,或是Elasticsearch与Spring的固有限制?

优化方案

1. 移除冗余聚合操作

代码中添加了和查询条件重复的filter聚合,但实际并未使用聚合结果,该操作会额外消耗ES计算资源,直接删除:

// 删除这段无用的聚合代码
// queryBuilder.withAggregations(
//        AggregationBuilders.filter(
//                "filter_by_id", QueryBuilders.termQuery("MyTyp_id", mytyp.getId())));

2. 修正search_after逻辑错误

代码中存在逻辑漏洞:初始化searchQuery后,循环中始终使用的是未更新search_after的searchQueryMyTyp,导致每次查询都从第一条开始。正确做法是在循环中更新查询的search_after参数:

NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(QueryBuilders.termQuery("MyTyp_id", mytyp.getId()));
queryBuilder.withPageable(PageRequest.of(0, 10000));
queryBuilder.withSort(SortBuilders.fieldSort("_id").order(SortOrder.ASC));

NativeSearchQuery searchQueryMyTyp = queryBuilder.build();
SearchHit[] searchHitsMyTyp = null;
final ArrayList<MyTyp> typeList = new ArrayList();

do {
    if (searchHitsMyTyp != null) {
        // 更新当前查询的search_after参数
        queryBuilder.withSearchAfter(searchHitsMyTyp[searchHitsMyTyp.length - 1].getSortValues());
        searchQueryMyTyp = queryBuilder.build();
    }
    SearchHits<MyTyp> hits = elasticsearchCustomTemplate.search(
            searchQueryMyTyp,
            MyTyp.class,
            IndexCoordinates.of(indexSettings.indexMyTypName));
    searchHitsMyTyp = hits.getSearchHits().toArray(new SearchHit[0]);
    typeList.addAll(Arrays.stream(searchHitsMyTyp).map(p -> p.getContent()).toList());

} while (searchHitsMyTyp.length >= 10000);

3. 异步化查询避免UI阻塞

当前同步执行的查询会阻塞UI线程,可将查询逻辑放到异步线程中执行,比如用Spring的@Async注解或手动线程池:

// 示例:用CompletableFuture异步执行
CompletableFuture.supplyAsync(() -> {
    // 这里放你的查询逻辑
    return typeList;
}, executorService).thenAccept(result -> {
    // 查询完成后通知UI更新数据
});

4. 替换为Scroll API(全量获取场景)

如果是一次性全量获取数据,ES的Scroll API比search_after更稳定,它会创建数据快照,避免后续数据变更影响结果:

// 初始化Scroll查询
NativeSearchQuery scrollQuery = new NativeSearchQueryBuilder()
        .withQuery(QueryBuilders.termQuery("MyTyp_id", mytyp.getId()))
        .withPageable(PageRequest.of(0, 10000))
        .build();

ScrollHits<MyTyp> scrollHits = elasticsearchCustomTemplate.scroll(1000L, scrollQuery, MyTyp.class, IndexCoordinates.of(indexSettings.indexMyTypName));
String scrollId = scrollHits.getScrollId();
List<MyTyp> typeList = new ArrayList<>();

while (scrollHits.hasSearchHits()) {
    typeList.addAll(scrollHits.getSearchHits().stream().map(SearchHit::getContent).toList());
    scrollHits = elasticsearchCustomTemplate.scrollScrollId(1000L, scrollId, MyTyp.class, IndexCoordinates.of(indexSettings.indexMyTypName));
    scrollId = scrollHits.getScrollId();
}

// 清理Scroll上下文,避免资源泄漏
elasticsearchCustomTemplate.clearScroll(scrollId);

5. 字段过滤减少数据传输

如果不需要MyTyp的所有字段,可通过fetchSource指定仅返回需要的字段,降低ES到应用的数据传输量:

// 只返回field1和field2字段
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"field1", "field2"}, null));

关于固有限制

Elasticsearch和Spring Data Elasticsearch本身没有针对2万条数据的性能限制,你的问题主要来自代码逻辑错误、冗余操作以及同步阻塞UI的执行方式,优化后可以达到预期的性能。

内容的提问来源于stack exchange,提问作者user2557930

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 02:12:02