【电商项目】商品搜索开发复盘(2):构造ES搜索条件逻辑实现与思考
2026/8/26 18:25:01 网站建设 项目流程

目录

一、整体思路

二、分模块代码实现

2.1 关键字条件

2.2 品牌条件

2.3 价格条件

2.4 规格项条件

2.5 分页处理

2.6 排序处理

最后返回

代码整体展示

本篇小结


在(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.3builder.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();

代码整体展示

/* * 构造搜索条件 * @param goodsSearchParam 查询条件对象 * @return 搜索条件对象 */ public NativeQuery buildQuery(GoodsSearchParam goodsSearchParam) { // 1.创建复杂查询条件对象 NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder(); BoolQuery.Builder builder = new BoolQuery.Builder(); // 2.如果查询条件有关键词,关键词可以匹配商品名、副标题、品牌字段;否则查询所有商品 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()); } // 3.如果查询条件有品牌,则精准匹配品牌 String brand = goodsSearchParam.getBrand(); if(StringUtils.hasText(brand)){ TermQuery termQuery = TermQuery.of(q -> q .field("brand") .value(brand)); builder.must(termQuery._toQuery()); } // 4.如果查询条件有价格,则匹配价格 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()); } // 5.如果查询条件有规格项,则精准匹配规格项 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()); // 6.添加分页条件 PageRequest pageRequest = PageRequest.of(goodsSearchParam.getPage() - 1, goodsSearchParam.getSize()); nativeQueryBuilder.withPageable(pageRequest); // 7.如果查询条件有排序,则添加排序条件 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); } // 8.返回查询条件对象 return nativeQueryBuilder.build(); }

本篇小结

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询