【电商项目】商品搜索开发复盘(2):构造ES搜索条件逻辑实现与思考
(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?
- 关键字是
MultiMatchQuery全文检索:关键词需要分词,同时搜索多个字段。query是搜索词,复数fields指定要检索的多个字段。 - 品牌是
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 文档层级不一样
brand是顶层单层字段,直接写字段名即可;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容器
更多推荐



所有评论(0)