基于ElasticSearch聚合的联动下拉框实现优化咨询
优化ElasticSearch联动过滤下拉框的两次查询实现
问题背景
当前搜索组件基于ElasticSearch(ES)的terms聚合实现下拉框,需求为:选中某下拉框后过滤其他下拉框,但不对自身进行进一步过滤。目前通过两次ES查询实现逻辑:
- 首次查询设
size=0,基于排除最新选中过滤器的条件,获取该最新过滤器对应的聚合结果; - 第二次查询获取搜索结果,同时获取除最新过滤器外其他过滤器的聚合结果。
现有核心代码
聚合查询构建方法
Func<AggregationContainerDescriptor<Index>, IAggregationContainer> FilterOptionsAggregationClosure(List<FilterButton> filterButtons) { return delegate (AggregationContainerDescriptor<Index> aggregationContainerDescriptor) { foreach (var filterButton in filterButtons) { aggregationContainerDescriptor = aggregationContainerDescriptor .Terms(filterButton.AggregationName, t => t .Field(filterButton.FieldToSearch) .Size(10000) ); } return aggregationContainerDescriptor; }; }
查询条件构建方法(简化版)
Func<QueryContainerDescriptor<Index>, QueryContainer> QueryClosure(string phrase, Fields fields, List<FilterApplied> filtersApplied) { return delegate (QueryContainerDescriptor<Index> queryContainerDescriptor) { QueryContainer queryContainer = GetKeywordQuery(phrase, fields); QueryContainer filterQuery = new(); foreach (var filter in filtersApplied) { QueryContainer sameFilterQuery = new QueryContainerDescriptor<Index>() .Bool(boolQuery => boolQuery .Filter(f => f .Terms(terms => terms .Field(filter.FieldToSearch) .Terms(filter.Values)))); filterQuery = filterQuery && sameFilterQuery; } return queryContainer && filterQuery; }; }
当前两次查询逻辑
// 最新选中过滤器的聚合查询 var newestFilterResponse = await _client.SearchAsync<Index>(s => s .Index(_index) .From(0) .Size(0) .Query(QueryClosure(phrase, fields, filtersMinusLast)) .Aggregations(FilterOptionsAggregationClosure(lastFilter))); // 获取结果及其他过滤器聚合的实际查询 var response = await _client.SearchAsync<Index>(s => s .Index(_index) .From(0) .Size(500) .Query(QueryClosure(phrase, fields, allFilters)) .Aggregations(FilterOptionsAggregationClosure(allFiltersExceptLast)));
优化建议
1. 合并为单次查询,利用Filter聚合实现"自身不过滤"逻辑
无需两次查询,在单次请求中给每个下拉框的聚合套一层Filter聚合,排除当前下拉框已选中的过滤条件,确保每个聚合基于"除自身外的所有过滤器"计算结果,一次查询即可拿到搜索结果和所有下拉框的聚合数据。
修改聚合构建方法:
Func<AggregationContainerDescriptor<Index>, IAggregationContainer> FilterOptionsAggregationClosure(List<FilterButton> filterButtons, List<FilterApplied> allFilters, string phrase, Fields fields) { return (aggregationContainerDescriptor) => { foreach (var filterButton in filterButtons) { // 排除当前下拉框对应的已应用过滤器 var filtersWithoutCurrent = allFilters.Where(f => f.FieldToSearch != filterButton.FieldToSearch).ToList(); // 构建当前聚合的前置过滤条件:基础查询 + 除自身外的所有过滤器 var aggregationFilter = QueryClosure(phrase, fields, filtersWithoutCurrent); aggregationContainerDescriptor = aggregationContainerDescriptor .Filter($"{filterButton.AggregationName}_filter", f => f .Filter(aggregationFilter) .Aggregations(a => a .Terms(filterButton.AggregationName, t => t .Field(filterButton.FieldToSearch) .Size(10000) ) ) ); } return aggregationContainerDescriptor; }; }
单次查询获取全部数据:
var response = await _client.SearchAsync<Index>(s => s .Index(_index) .From(0) .Size(500) .Query(QueryClosure(phrase, fields, allFilters)) .Aggregations(FilterOptionsAggregationClosure(allFilterButtons, allFilters, phrase, fields)));
解析聚合结果时,从对应的Filter聚合下提取内部Terms聚合:
foreach (var filterButton in allFilterButtons) { var filterAgg = response.Aggregations.Filter($"{filterButton.AggregationName}_filter"); var termsAgg = filterAgg.Terms(filterButton.AggregationName); // 处理termsAgg的桶数据 }
2. 优化查询构建的性能
- 避免循环中重复创建
QueryContainerDescriptor,直接在同一个描述器上构建过滤条件:
Func<QueryContainerDescriptor<Index>, QueryContainer> QueryClosure(string phrase, Fields fields, List<FilterApplied> filtersApplied) { return (queryContainerDescriptor) => { QueryContainer queryContainer = GetKeywordQuery(phrase, fields); if (!filtersApplied.Any()) return queryContainer; var boolQuery = new BoolQueryDescriptor<Index>(); foreach (var filter in filtersApplied) { boolQuery = boolQuery.Filter(f => f .Terms(terms => terms .Field(filter.FieldToSearch) .Terms(filter.Values))); } return queryContainer && boolQuery; }; }
- 对于频繁复用的过滤条件,可缓存查询片段,减少重复构建开销。
3. 合理限制聚合桶大小
当前设置Size(10000)可能引发内存压力,建议根据业务实际需求调整为合理数值;若需处理高基数字段,可开启show_term_doc_count_error平衡精度与资源消耗。
4. 高基数场景改用Composite聚合
若下拉框选项数量达10万级以上,Terms聚合的内存开销会急剧上升,此时可使用Composite聚合实现分页式查询,避免一次性加载大量数据到内存。
内容的提问来源于stack exchange,提问作者pbordeaux
相关产品推荐
相关产品推荐

