(1)我们了解了大体实现思路,这一篇我们实现第一个内容:把前端传过来的JSON业务参数,转换成ES客户端能够识别执行的查询格式。

文章内容均为小编开发过程中思考的记录,由于是新手所以很多地方不理解,问题也就比较多,发出来见笑了。

一、整体思路

处理流程:接收参数 → 重新组装成ES查询语法

先创建两个核心对象:

  • NativeQueryBuilder:大容器,存放完整的一次ES查询请求(查询条件、分页、排序全部往里装)
  • BoolQuery.Builder:布尔查询构造器,专门拼装一个个小查询条件
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder();
BoolQuery.Builder builder = new BoolQuery.Builder();

我们直接看入参实体 GoodsSearchParam,就知道需要处理哪些条件:关键字、品牌、价格、规格、排序、分页。遍历每一项参数,按需拼装到布尔查询中。

二、分模块代码实现

每一个筛选条件,统一思路:接收参数,做非空判断,组装为ES可识别的查询对象。

2.1 关键字条件

逻辑:没有关键字,查询全部商品;有关键字,多字段全文检索。

String keyword = goodsSearchParam.getKeyword();
if(!StringUtils.hasText(keyword)){
    MatchAllQuery matchAllQuery = new MatchAllQuery.Builder().build();
    builder.must(matchAllQuery._toQuery());
}else{
    MultiMatchQuery multiMatchQuery = MultiMatchQuery.of(q -> q
            .query(keyword)
            .fields("goodsName", "caption", "brand"));
    builder.must(multiMatchQuery._toQuery());
}
2.1.1 MatchAllQuery、MultiMatchQuery、_toQuery()是什么
  • MatchAllQuery:匹配全部文档,用户不输入关键词时,返回全部商品
  • MultiMatchQuery:多字段全文匹配,会对关键词分词检索
  • _toQuery():把具体查询对象,转为ES客户端通用的Query类型,统一交给布尔查询使用
2.1.2 为什么MatchAllQuery需要.Builder().build(),BoolQuery只需要.Builder()

Builder是建造者,只负责拼接参数;调用build()才生成最终完整查询对象。BoolQuery.Builder我们后续还会持续追加条件,所以暂时不build;MatchAllQuery参数一次性设置完毕,直接build拿到对象。

2.1.3 builder.must(multiMatchQuery._toQuery()):

must()等价于SQL的AND,里面的条件必须全部满足;传入经过_toQuery()转换后的查询对象,加入布尔查询。

2.1.4 MultiMatchQuery为什么不用new实体化,可以直接使用?

新版本ES Java客户端大量使用静态工厂方法of(),替代new构造对象,属于官方推荐写法。

2.2 品牌条件

品牌属于精准匹配,不分词,使用TermQuery。

String brand = goodsSearchParam.getBrand();
if(StringUtils.hasText(brand)){
    TermQuery termQuery = TermQuery.of(q -> q
            .field("brand")
            .value(brand));
    builder.must(termQuery._toQuery());
}
2.2.1 TermQuery 和 MatchAllQuery 的区别:
  • TermQuery:精确匹配,不分词,适合品牌、规格、ID这类确定值
  • MatchAllQuery:查询全部文档,不做过滤
2.2.2 为什么关键字用query+fields,品牌用field+value?
  1. 关键字是MultiMatchQuery全文检索:关键词需要分词,同时搜索多个字段。query是搜索词,复数fields指定要检索的多个字段。
  2. 品牌是TermQuery精准匹配:明确知道要查哪一个字段,单数field指定字段,value填写完整匹配值。

小技巧:看到复数fields就是多字段全文搜索;单数field一般是精准单字段查询。

2.3 价格条件

价格是数值区间,使用RangeQuery做范围过滤。

Double highPrice = goodsSearchParam.getHighPrice();
Double lowPrice = goodsSearchParam.getLowPrice();
if(highPrice != null && highPrice != 0){
    RangeQuery price = RangeQuery.of(q -> q
            .field("price")
            .lte(JsonData.of(highPrice)));
    builder.must(price._toQuery());
}
if(lowPrice != null && lowPrice != 0){
    RangeQuery price = RangeQuery.of(q -> q
            .field("price")
            .gte(JsonData.of(lowPrice)));
    builder.must(price._toQuery());
}
2.3.1 RangeQuery说明:

是专门用来做数值区间筛选的(gte大于等于,lte小于等于)。

2.3.2 为什么要用JsonData.of()包裹数值?

ES客户端强制要求,把Java数值包装为JsonData,明确告诉ES这是数字类型,避免ES把数字当成字符串对比,导致区间查询出错。

2.4 规格项条件

前端传规格JSON对象,解析成Map,循环每一组键值,拼接ES字段路径生成TermQuery。

Map<String, String> specificationOption = goodsSearchParam.getSpecificationOption();
if(specificationOption != null && !specificationOption.isEmpty()){
    Set<Map.Entry<String, String>> entrySet = specificationOption.entrySet();
    for (Map.Entry<String, String> es : entrySet) {
        String key = es.getKey();
        String value = es.getValue();
        TermQuery termQuery = TermQuery.of( q -> q
                .field("specification." + key + ".keyword")
                .value(value));
        builder.must(termQuery._toQuery());
    }
}
nativeQueryBuilder.withQuery(builder.build()._toQuery());
2.4.1 entrySet:

把Map拆成一堆键值对,方便遍历

2.4.2 entrySet已经拿到键值对,为什么不能直接丢ES,还要循环取key、value?

entrySet只是Java集合对象,ES不识别Map,必须循环逐个构建TermQuery查询条件。

2.4.3 Map本身存键值,为什么要用entrySet,不直接遍历?

Map的keySet、values是两套独立集合,一轮循环拿不到成对key‑value;entrySet把一组key+value封装为Entry,不用额外调用map.get(key)。

2.4.4 entrySet 之后,是不是如果没有 entrySet,直接拿 key 和 value 会配对错乱?

是的。只遍历 keySet,再遍历 values,两个集合分开,顺序不能保证一一对应,容易键值匹配错误。Entry 把 key 和 value 绑定成一个单元,遍历就不会乱。

2.4.5 entrySet 是把键值对应,然后形成一组组数据的吗?

不是。entrySet没法同时拿到一组配套的 key+value:单独遍历 keySet 只拿到一堆键、遍历 values 只拿到一堆值,两组集合分开,没法保证顺序配对,极易错乱。

2.4.6 规格过滤为什么接收Map,不用别的集合?

取决于前端传参。

  • 前端传{"颜色":"红","内存":"16G"}JSON 对象 → Spring 自动解析为 Map;
  • 如果用List<Spec>,前端就得传数组格式[{"key":"颜色","value":"红"}]。 这里选 Map 是适配前端传参。

2.4.7为什么 编写品牌时field直接写字段名brand,而规格要拼接specification.+key+.keyword,以及各部分含义是什么?

ES 文档层级不一样

  1. brand顶层单层字段,直接写字段名即可;
  2. specification是嵌套对象,里面规格名(颜色 / 内存)是动态变化的 key,真实查询字段路径是specification.颜色.keyword,字段名一部分运行时才确定,代码只能字符串拼接完整路径。
2.4.8 nativeQueryBuilder 和 builder.must 关系:

builder负责拼装一堆must条件;拼装完成,调用build生成完整布尔查询,交给nativeQueryBuilder容器保存。

逻辑:

前端规格 JSON → 解析为 Map → entrySet 得到 Entry 集合 → 循环取出成对 key、value → 拼接 ES 嵌套字段路径 → 循环生成 TermQuery 加入布尔查询,完成规格过滤。

2.5 分页处理

PageRequest pageRequest = PageRequest.of(goodsSearchParam.getPage() - 1, goodsSearchParam.getSize());
nativeQueryBuilder.withPageable(pageRequest);

注意:ES分页从0开始,前端传的页码是从1开始,所以要做减1处理。

2.5.1 NativeQueryBuilder到底是什么

相当于一个大箱子,存放一次ES查询全部组件:

  • nativeQueryBuilder.withQuery(...):存放查询条件(关键词、品牌、价格等)
  • nativeQueryBuilder.withPageable(...):存放分页信息
  • nativeQueryBuilder.withSort(...):存放排序规则
2.5.2 withQuery要build()+_toQuery(),withPageable直接传对象,为什么

BoolQuery.Builder是临时构造器,必须build生成布尔查询对象,再转统一Query对象,这是查询条件的格式要求。 PageRequest本身就是Spring提供完整分页实体,不需要构建转换,直接传入即可。

2.6 排序处理

String sortFiled = goodsSearchParam.getSortFiled();  // 排序字段
String sort = goodsSearchParam.getSort();  // 排序方式
if (StringUtils.hasText(sortFiled) && StringUtils.hasText(sort)){
    Sort sortParam = null;  // 组装好的排序规则
    // 新品的正序是id的倒序
    if (sortFiled.equals("NEW")){
        if (sort.equals("ASC")){  // 升序
            sortParam = Sort.by(Sort.Direction.DESC, "id"); // id从小到大 → 旧商品在前
        }
        if (sort.equals("DESC")){  // 降序
            sortParam = Sort.by(Sort.Direction.ASC, "id"); // id从大到小 → 新商品在前
        }
    }
    // 价格的正序是price的正序
    if (sortFiled.equals("PRICE")){
        if (sort.equals("ASC")){
            sortParam = Sort.by(Sort.Direction.ASC, "price");
        }
        if (sort.equals("DESC")){
            sortParam = Sort.by(Sort.Direction.DESC, "price");
        }
    }
    nativeQueryBuilder.withSort(sortParam);
}
2.6.1 变量说明
  • sortFiled:按什么维度排序,"NEW"代表新品,"PRICE"代表价格
  • sort:排序方向,"ASC"升序,"DESC"降序
  • sortParam:Spring Sort对象,组装完成的排序规则
  • Sort.by(Direction.DESC, "id"):创建排序规则,第一个参数方向,第二个ES字段名
2.6.2 为什么只判断NEW和PRICE

业务上商品搜索就两种常用排序:按新品、按价格,不需要支持任意字段排序。

2.6.3 新品排序为什么逻辑是反转的

id越大代表商品越新。业务认知:选新品,我们希望新商品排在最前面。前端传递的ASC/DESC不能直接拿来用,需要手动转换映射到id字段。

最后返回

// 返回组装完毕的ES查询条件对象
return nativeQueryBuilder.build();

本篇小结

整个过程本质就是翻译:前端业务参数 → ES客户端对象。

  • 全文检索:MultiMatchQuery
  • 精准匹配:TermQuery
  • 数值区间:RangeQuery
  • 所有小条件交给BoolQuery.Builder拼装
  • 全部条件、分页、排序,统一装进NativeQueryBuilder容器

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐