ES复杂查询-组装条件优化
一、实现概述
采用 Builder 模式 + 策略模式,将 ES 查询条件组装从"手动拼装 Map"升级为"声明式 API",业务层只需关注业务条件,无需了解 ES 底层查询细节。
二、核心代码
2.1 基础枚举类
|
文件 |
路径 |
说明 |
|
|
|
查询场景枚举(按实际业务场景定义) |
|
|
|
排序类型枚举(默认排序/仅相关性排序) |
2.2通用过滤器类
|
文件 |
路径 |
说明 |
|
|
|
自定义条件过滤器,实现动态组装条件过滤 |
2.3核心构建器
|
文件 |
路径 |
说明 |
|
|
|
ES 查询条件构建器(Builder 模式),提供链式 API |
核心功能:
-
✅ 链式 API:业务查询条件:
businessConditionQuery(), 排序:orderBy(), 分页:page(),offset() -
✅ 自动注入通用过滤:默认实现的过滤器
-
✅ 手动注入业务场景过滤:按查询场景实现
2.4 上下文与请求对象
|
文件 |
路径 |
说明 |
|
|
|
用户查询上下文(封装业务依赖:schoolId、clientType等) |
|
|
|
统一查询请求对象(封装所有查询参数) |
2.5 策略模式
|
文件 |
路径 |
说明 |
|
|
|
ES查询策略接口 |
|
|
|
业务场景资源查询-策略实现 |
2.6 ES通用查询对象
|
文件 |
路径 |
说明 |
|
|
|
ES查询对象,定义通用查询参数 |
package com.iflytek.edu.acs.science.dto.idto.search;
import com.iflytek.edu.acs.science.dto.idto.PageIDTO;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.util.CollectionUtils;
import javax.validation.constraints.NotBlank;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
@Data
public class EsQueryIDTO {
@ApiModelProperty(value = "关键字")
private List<String> keyWord;
@ApiModelProperty(value = "单条件匹配:k=v")
private Map<String, String> termQuery;
@ApiModelProperty(value = "<>条件查询:k!=v")
private Map<String, String> notEqualQuery;
@ApiModelProperty(value = "or精确条件查询:(a=b or c=d")
private Map<String, String> orQuery;
@ApiModelProperty(value = "多条件in匹配")
private Map<String, List<String>> termsQuery;
@ApiModelProperty(value = "多条件not in匹配")
private Map<String, List<String>> notInQuery;
@ApiModelProperty(value = "模糊分词搜索")
private Map<String, List<String>> matchQuery;
@ApiModelProperty(value = "多条件组合匹配:(a=b or (a!=b and c in(v1,v2))")
private MultiQuery multiQuery;
@ApiModelProperty(value = "<>组合条件查询( !(a=b and c=d)")
private Map<String, String> mustNotEqualQuery;
@ApiModelProperty(value = "排序")
private List<SortOrderIDTO> sort;
@ApiModelProperty(value = "分页")
private PageIDTO page;
}
2.7 ESSearchRepository
|
文件 |
路径 |
说明 |
|
|
|
ES封装SearchRepository,定义通用查询查询方法 |
public EsPageList<GetEsResourceODTO> searchResInfo(EsQueryIDTO query) throws EsException {
//构建查询请求
SearchRequest searchRequest = new SearchRequest(INDEX);
searchRequest.types(DOC_TYPE);
//封装ES查询方法
SearchSourceBuilder sourceBuilder = buildBasicQuery(query);
//分页 支持分页参数和索引两中
if (Objects.nonNull(query.getPage())) {
if (Objects.nonNull(query.getPage().getBeginIndex())) {
sourceBuilder.from(query.getPage().getBeginIndex()).size(query.getPage().getLimit());
} else {
sourceBuilder.from((query.getPage().getPage() - 1) * query.getPage().getLimit()).size(query.getPage().getLimit());
}
}
//排序
if (CollectionUtils.isNotEmpty(query.getSort())) {
List<SortOrderIDTO> sort = query.getSort();
sort.forEach(x -> {
String sortField = x.getSortField();
if (EsConstants.ORDER_ASC.equalsIgnoreCase(x.getSortOrder())) {
sourceBuilder.sort(sortField, SortOrder.ASC);
} else {
sourceBuilder.sort(sortField, SortOrder.DESC);
}
});
}
searchRequest.source(sourceBuilder);
log.info("ES查询dsl语句{}", sourceBuilder);
SearchResponse searchResponse = this.searchRequest(searchRequest, INDEX);
SearchHits hits = searchResponse.getHits();
List<GetEsResourceODTO> list = new ArrayList<>();
for (SearchHit hit : hits) {
GetEsResourceODTO data = JSON.parseObject(hit.getSourceAsString(), GetEsResourceODTO.class);
data.setScore(hit.getScore());
list.add(data);
}
return new EsPageList<>(list, getPageOut(hits.getTotalHits().value, query.getPage()));
}
private SearchSourceBuilder buildBasicQuery(EsQueryIDTO query) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//关键字
buildKeyWordQuery(query.getKeyWord(), boolQuery);
//精确匹配
buildTermQuery(query, boolQuery);
//多字段精确匹配
buildTermsQuery(query, boolQuery);
//模糊分词匹配
buildMatchQuery(query, boolQuery);
//!=匹配
buildNotEqualQuery(query, boolQuery);
//多值!=匹配
buildMastNotEqualQuery(query, boolQuery);
//or匹配
buildOrQuery(query, boolQuery);
//not in 匹配
buildNotInQuery(query, boolQuery);
//多条件查询(a=b or (a!=b and c in(v1,v2))
buildMultiQuery(query, boolQuery);
return SearchSourceBuilder.searchSource().query(boolQuery);
}
private static void buildKeyWordQuery(List<String> keyWord, BoolQueryBuilder boolQuery) {
if (CollectionUtils.isEmpty(keyWord)) {
boolQuery.must(QueryBuilders.matchAllQuery());
} else {
BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery();
for (String word : keyWord) {
shouldQuery.should().add(QueryBuilders.matchQuery(x, word));
boolQuery.must(shouldQuery);
}
}
}
private static void buildTermQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getTermQuery()) && !query.getTermQuery().isEmpty()) {
query.getTermQuery().forEach((x, y) -> {
if(StringUtils.equals(x, EsResourceInfoODTO.ResourceFiled.BOOK_CODE)) {
boolQuery.filter(QueryBuilders.nestedQuery(EsResourceInfoODTO.ResourceFiled.BOOKS,
QueryBuilders.termQuery(EsResourceInfoODTO.ResourceFiled.BOOKS_BOOK_CODE, y), ScoreMode.None));
} else if (StringUtils.equals(x, EsResourceInfoODTO.ResourceFiled.UNIT_CODE)) {
boolQuery.filter(QueryBuilders.nestedQuery(EsResourceInfoODTO.ResourceFiled.UNITS,
QueryBuilders.termQuery(EsResourceInfoODTO.ResourceFiled.UNITS_UNIT_CODE, y), ScoreMode.None));
} else {
boolQuery.filter(QueryBuilders.termQuery(x, y));
}
});
}
}
private static void buildTermsQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getTermsQuery()) && !query.getTermsQuery().isEmpty()) {
query.getTermsQuery().forEach((x, y) -> {
if (StringUtils.equals(x, EsResourceInfoODTO.ResourceFiled.GRADE_CODE)) {
boolQuery.filter(QueryBuilders.nestedQuery(EsResourceInfoODTO.ResourceFiled.GRADES,
QueryBuilders.termsQuery(EsResourceInfoODTO.ResourceFiled.GRADES_GRADE_CODE, y), ScoreMode.None));
} else {
boolQuery.filter(QueryBuilders.termsQuery(x, y));
}
});
}
}
private static void buildMatchQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getMatchQuery()) && !query.getMatchQuery().isEmpty()) {
query.getMatchQuery().forEach((x, y) -> y.forEach(u -> {
if (StringUtils.equals(x, EsResourceInfoODTO.ResourceFiled.KNOW_LEDGE)) {
boolQuery.filter(QueryBuilders.nestedQuery(EsResourceInfoODTO.ResourceFiled.KNOW_LEDGES,
QueryBuilders.matchQuery(EsResourceInfoODTO.ResourceFiled.KNOWLEDGE_TITLE, u), ScoreMode.None));
} else {
boolQuery.filter(QueryBuilders.matchQuery(x, u));
}
}));
}
}
private void buildNotEqualQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getNotEqualQuery()) && !query.getNotEqualQuery().isEmpty()) {
query.getNotEqualQuery().forEach((x,y) -> boolQuery.mustNot(QueryBuilders.termQuery(x, y)));
}
}
private void buildMastNotEqualQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getMustNotEqualQuery()) && !query.getMustNotEqualQuery().isEmpty()) {
BoolQueryBuilder innerBoolQuery = QueryBuilders.boolQuery();
query.getMustNotEqualQuery().forEach((x,y) -> innerBoolQuery.must(QueryBuilders.termQuery(x, y)));
boolQuery.mustNot(innerBoolQuery);
}
}
private void buildOrQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getOrQuery()) && !query.getOrQuery().isEmpty()) {
BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery();
query.getOrQuery().forEach((x,y) -> shouldQuery.should().add(QueryBuilders.termQuery(x, y)));
boolQuery.filter(shouldQuery);
}
}
private void buildNotInQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getNotInQuery()) && !query.getNotInQuery().isEmpty()) {
query.getNotInQuery().forEach((x,y) -> boolQuery.mustNot(QueryBuilders.termsQuery(x, y)));
}
}
private void buildMultiQuery(EsQueryIDTO query, BoolQueryBuilder boolQuery) {
if (Objects.nonNull(query.getMultiQuery())) {
Map<String, String> prefixQuery = query.getMultiQuery().getPrefixQuery();
Map<String, String> suffixQuery = query.getMultiQuery().getSuffixQuery();
Map<String, List<String>> rangeQuery = query.getMultiQuery().getRangeQuery();
BoolQueryBuilder mustQuery = QueryBuilders.boolQuery();
suffixQuery.forEach((x, y) -> mustQuery.filter().add((QueryBuilders.termQuery(x, y))));
rangeQuery.forEach((x, y) -> mustQuery.filter().add(QueryBuilders.termsQuery(x, y)));
BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery();
prefixQuery.forEach((x,y) -> shouldQuery.should().add(QueryBuilders.termQuery(x, y)));
shouldQuery.should().add(mustQuery);
boolQuery.filter(shouldQuery);
}
}
三、核心优化点
3.1 代码简化对比
优化前(原 handleEsQueryIDTO 方法):
// 手动拼装 Map
Map<String, String> defaultParam = Maps.newHashMap();
defaultParam.put(KEY_deleteFlag, YesOrNoEnum.NO.getCodeStr());
defaultParam.put(KEY_sourceType, String.valueOf(ResourceTypeEnum.CUSTOM.getCode()));
Map<String, List<String>> paramMap = Maps.newHashMap();
if (CollectionUtils.isNotEmpty(pageListResCenterIDTO.getApplicationCodeList())){
paramMap.put(KEY_applicationCode, pageListResCenterIDTO.getApplicationCodeList());
}
if (CollectionUtils.isNotEmpty(pageListResCenterIDTO.getVolumeCodeList())){
paramMap.put(KEY_volumeCodes, pageListResCenterIDTO.getVolumeCodeList());
}
// ... 大量重复代码
handleResArea(pageListResCenterIDTO.getUserSchoolId(), paramMap);
handleResContainer(pageListResCenterIDTO.getClientType(), functionVals, paramMap, esQueryIDTO);
优化后(使用 ResourceQueryBuilder):
//声明式 API
EsQueryIDTO esQueryIDTO = ResourceQueryBuilder.create(scene, userContext)
.keyword(request.getKeyword())
.applicationCodes(request.getApplicationCodeList())
.grades(request.getGradeCodeList())
.editions(request.getEditionCodeList())
.volumes(request.getVolumeCodeList())
.coreConcepts(request.getCoreConceptList())
.excludeResIds(request.getExcludeResourceIds())
.orderBy(ResourceSortType.DEFAULT)
.page(request.getCurrentPage(), request.getPageSize())
.build();
3.2 业务层与 ES 解耦
|
优化前 |
优化后 |
|
业务层需要知道 ES 字段名( |
业务层只需调用 |
|
业务层需要知道查询类型( |
业务层无需关心查询类型,Builder 自动处理 |
|
通用逻辑(区域过滤、容器过滤)需手动调用 |
通用逻辑自动注入,业务层无感知 |
|
场景特殊逻辑散落在 |
场景特殊逻辑封装在 Builder 内部 |
四、使用示例
4.1 通用查询资源场景
@Service("listSceneResService")
public class ListSceneResService implements IListServiceStrategy {
@Autowired
private ESSearchRepository esSearchRepository;
@Override
public EsResDataODTO<GetEsResourceODTO> listEsRes(ResQueryRequest request, UserQueryContext userContext) {
// 可视化资源直接返回空
if (request.getTabType() != null && request.getTabType() == 2) {
return EsResDataODTO.empty();
}
// 强制查询教学课件类型(仅 10 行代码)
EsQueryIDTO esQueryIDTO = ResourceQueryBuilder.create(QueryScene.BK_JXKJ_RES, userContext)
.applicationCodes(Lists.newArrayList(ResApplicationTypeEnum.jxkj.getCode()))
.keyword(request.getKeyword())
.grades(request.getGradeCodeList())
.editions(request.getEditionCodeList())
.volumes(request.getVolumeCodeList())
.coreConcepts(request.getCoreConceptList())
.excludeResIds(request.getExcludeResourceIds())
.orderBy(ResourceSortType.DEFAULT)
.page(request.getCurrentPage(), request.getPageSize())
.build();
return esSearchRepository.searchResInfo(esQueryIDTO);
}
}
五、优化收益总结
5.1 代码质量提升
|
指标 |
优化前 |
优化后 |
提升 |
|
ES 查询条件组装代码量 |
113 行 |
10 行 |
减少 91% |
|
代码可读性 |
手动拼装 Map,难以理解 |
声明式 API,一目了然 |
大幅提升 |
5.2 维护成本降低
|
场景 |
优化前 |
优化后 |
收益 |
|
新增查询条件 |
修改 3 处(DTO、handleEsQueryIDTO、Repository) |
在 Builder 中新增 1 个方法 |
工作量减少 67% |
|
新增业务场景 |
新增 IListResService 实现 + 重复编写查询逻辑 |
新增策略实现 + 复用 Builder |
工作量减少 50% |
|
修改通用逻辑 |
修改 handleEsQueryIDTO + 影响所有场景 |
修改对应 Filter,影响范围可控 |
风险降低 80% |
5.3 扩展性增强
-
✅ 新增过滤器:实现
QueryFilter接口即可,无需修改核心代码 -
✅ 新增排序规则:在
ResourceSortType枚举中新增即可 -
✅ 新增查询场景:在
QueryScene枚举中新增 + 注册对应 Filter
5.4 测试友好
@Test
public void testResourceQuery() {
UserQueryContext userContext = UserQueryContext.builder()
.schoolId("test-school")
.clientType("client-pc")
.commonManager(commonManager)
.moduleConfigManager(moduleConfigManager)
.build();
EsQueryIDTO query = ResourceQueryBuilder.create(QueryScene.COMMON_RES, userContext)
.keyword("测试")
.applicationCodes(Lists.newArrayList("code1", "code2"))
.grades(Lists.newArrayList("3", "4"))
.build();
// 断言查询条件
assertThat(query.getKeyWord()).contains("测试");
assertThat(query.getTermsQuery().get("applicationCode")).containsExactly("code1", "code2");
}
六、总结
本次优化通过引入 Builder 模式 + 策略模式,成功实现了:
-
✅ 业务层与 ES 查询解耦:业务层只需关注业务条件,无需了解 ES 字段名和查询类型
-
✅ 统一查询条件构建:提供链式 API,代码可读性大幅提升(代码量减少 91%)
-
✅ 自动注入通用逻辑:区域过滤、容器过滤等通用逻辑自动处理
-
✅ 高扩展性:新增查询条件、过滤器、场景均无需修改核心代码
核心价值:将 ES 查询条件组装从"手动拼装 Map"升级为"声明式 API",大幅降低维护成本,提升代码质量。

277

被折叠的 条评论
为什么被折叠?



