ES复杂查询-组装条件优化

ES复杂查询-组装条件优化

一、实现概述

采用 Builder 模式 + 策略模式,将 ES 查询条件组装从"手动拼装 Map"升级为"声明式 API",业务层只需关注业务条件,无需了解 ES 底层查询细节。

二、核心代码

2.1 基础枚举类

文件

路径

说明

QueryScene.java

enums/resource/

查询场景枚举(按实际业务场景定义)

ResourceSortType.java

enums/resource/

排序类型枚举(默认排序/仅相关性排序)

2.2通用过滤器类

文件

路径

说明

QueryFilter.java

service/resource/query/filter

自定义条件过滤器,实现动态组装条件过滤

2.3核心构建器

文件

路径

说明

ResourceQueryBuilder.java

service/resource/query/

ES 查询条件构建器(Builder 模式),提供链式 API

核心功能:

  • ✅ 链式 API:业务查询条件:businessConditionQuery(), 排序:orderBy(), 分页:page(), offset()

  • ✅ 自动注入通用过滤:默认实现的过滤器

  • ✅ 手动注入业务场景过滤:按查询场景实现

2.4 上下文与请求对象

文件

路径

说明

UserQueryContext.java

service/resource/query/

用户查询上下文(封装业务依赖:schoolId、clientType等)

ResQueryRequest.java

service/resource/query/

统一查询请求对象(封装所有查询参数)

2.5 策略模式

文件

路径

说明

IListServiceStrategy.java

service/resource/query/

ES查询策略接口

ListSceneResService.java

service/resource/query/impl/

业务场景资源查询-策略实现

2.6 ES通用查询对象

文件

路径

说明

EsQueryIDTO.java

service/resource/query/

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

文件

路径

说明

ESSearchRepository.java

service/resource/query/

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 字段名(KEY_applicationCodeKEY_gradeCode 等)

业务层只需调用 .applicationCodes().grades()

业务层需要知道查询类型(termQuerytermsQuerymatchQuery 等)

业务层无需关心查询类型,Builder 自动处理

通用逻辑(区域过滤、容器过滤)需手动调用

通用逻辑自动注入,业务层无感知

场景特殊逻辑散落在 handleEsQueryIDTO

场景特殊逻辑封装在 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 模式 + 策略模式,成功实现了:

  1. 业务层与 ES 查询解耦:业务层只需关注业务条件,无需了解 ES 字段名和查询类型

  2. 统一查询条件构建:提供链式 API,代码可读性大幅提升(代码量减少 91%)

  3. 自动注入通用逻辑:区域过滤、容器过滤等通用逻辑自动处理

  4. 高扩展性:新增查询条件、过滤器、场景均无需修改核心代码

核心价值:将 ES 查询条件组装从"手动拼装 Map"升级为"声明式 API",大幅降低维护成本,提升代码质量。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值