title: 乐优商城学习笔记十七-搜索过滤(二)
date: 2019-04-20 14:37:56
tags:
- 乐优商城
- java
- springboot
categories:
- 乐优商城
3.生成规格参数过滤
3.1.谋而后动
有四个问题需要先思考清楚:
- 什么时候显示规格参数过滤?
- 如何知道哪些规格需要过滤?
- 要过滤的参数,其可选值是如何获取的?
- 规格过滤的可选值,其数据格式怎样的?
什么情况下显示有关规格参数的过滤?
如果用户尚未选择商品分类,或者聚合得到的分类数大于1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格是不同的。
因此,我们在后台需要对聚合得到的商品分类数量进行判断,如果等于1,我们才继续进行规格参数的聚合。
如何知道哪些规格需要过滤?
我们不能把数据库中的所有规格参数都拿来过滤。因为并不是所有的规格参数都可以用来过滤,参数的值是不确定的。
值的庆幸的是,我们在设计规格参数时,已经标记了某些规格可搜索,某些不可搜索。
因此,一旦商品分类确定,我们就可以根据商品分类查询到其对应的规格,从而知道哪些规格要进行搜索。
要过滤的参数,其可选值是如何获取的?
虽然数据库中有所有的规格参数,但是不能把一切数据都用来供用户选择。
与商品分类和品牌一样,应该是从用户搜索得到的结果中聚合,得到与结果品牌的规格参数可选值。
规格过滤的可选值,其数据格式怎样的?
我们直接看页面效果:
我们之前存储时已经将数据分段,恰好符合这里的需求
3.3.实战
接下来,我们就用代码实现刚才的思路。
总结一下,应该是以下几步:
- 1)用户搜索得到商品,并聚合出商品分类
- 2)判断分类数量是否等于1,如果是则进行规格参数聚合
- 3)先根据分类,查找可以用来搜索的规格
- 4)对规格参数进行聚合
- 5)将规格参数聚合结果整理后返回
3.3.1.扩展返回结果
返回结果中需要增加新数据,用来保存规格参数过滤条件。这里与前面的品牌和分类过滤的json结构类似:
[
{
"k":"规格参数名",
"options":["规格参数值","规格参数值"]
}
]
因此,在java中我们用List<Map<String,Object>>来表示。
/**
* @Author smallmartial
* @Date 2019/4/19
* @Email smallmarital@qq.com
*/
@Data
public class SearchResult extends PageResult<Goods> {
private List<Category> categories;//分类过滤条件
private List<Brand> brands;//品牌过滤条件
private List<Map<String,Object>> specs; // 规格参数过滤条件
public SearchResult(){}
public SearchResult(Long total, Integer totalPage, List<Goods> item, List<Category> categories, List<Brand> brands, List<Map<String, Object>> specs) {
super(total, totalPage, item);
this.categories = categories;
this.brands = brands;
this.specs = specs;
}
}
3.3.2.判断是否需要聚合
首先,在聚合得到商品分类后,判断分类的个数,如果是1个则进行规格聚合:
if (categories !=null && categories.size() == 1){
specs = buildSpecificationAgg(categories.get(0).getId(),basicQuery);
}
我们将聚合的代码抽取到了一个buildSpecificationAgg
方法中。
3.3.3.获取需要聚合的规格参数
然后,我们需要根据商品分类,查询所有可用于搜索的规格参数:
List<SpecParam> params = specificationClient.querySpecSpecParam(null, cid, true, null);
要注意的是,这里我们需要根据id查询规格,而规格参数接口需要从商品微服务提供
商品微服务:ly-item-interface中提供接口:
@RequestMapping("spec")
public interface SpecificationApi {
@GetMapping("/params")
List<SpecParam> querySpecParam(SpecParam specParam);
}
搜索服务中调用:
@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
}
3.3.4.聚合规格参数
因为规格参数保存时不做分词,因此其名称会自动带上一个.keyword后缀:
for (SpecParam param : params) {
String name = param.getName();
queryBuilder.addAggregation(AggregationBuilders.terms(name)
.field("specs."+name+".keyword"));
}
3.3.5.解析聚合结果
//解析结果
Aggregations aggs = result.getAggregations();
for (SpecParam param : params) {
//规格参数名
Map<String,Object> map = new HashMap();
//准备map
String name = param.getName();
map.put("k",name);
StringTerms terms = (StringTerms) aggs.get(name);
map.put("options",terms.getBuckets().stream().map(b -> b.getKeyAsString()).collect(Collectors.toList()));
specs.add(map);
}
3.3.6.最终的代码
/**
* 聚合规格参数查询
* @param cid
* @param basicQuery
* @return
*/
private List<Map<String,Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQuery) {
List<Map<String, Object>> specs = new ArrayList<>();
//查询所需要的结果
List<SpecParam> params = specificationClient.querySpecSpecParam(null, cid, true, null);
//聚合
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//带上查询条件
queryBuilder.withQuery(basicQuery);
for (SpecParam param : params) {
String name = param.getName();
queryBuilder.addAggregation(AggregationBuilders.terms(name)
.field("specs."+name+".keyword"));
}
//获取结果
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(), Goods.class);
// 查询
//解析结果
Aggregations aggs = result.getAggregations();
for (SpecParam param : params) {
//规格参数名
Map<String,Object> map = new HashMap();
//准备map
String name = param.getName();
map.put("k",name);
StringTerms terms = (StringTerms) aggs.get(name);
map.put("options",terms.getBuckets().stream().map(b -> b.getKeyAsString()).collect(Collectors.toList()));
specs.add(map);
}
return specs;
}
结果
3.4.2.展示或收起过滤条件
是不是感觉显示的太多了,我们可以通过按钮点击来展开和隐藏部分内容:
我们在data中定义变量,记录展开或隐藏的状态:
然后在按钮绑定点击事件,以改变show的取值:
在展示规格时,对show进行判断:
OK!
4.过滤条件的筛选
当我们点击页面的过滤项,要做哪些事情?
- 把过滤条件保存在search对象中(watch监控到search变化后就会发送到后台)
- 在页面顶部展示已选择的过滤项
- 把商品分类展示到顶部面包屑
4.1.保存过滤项
4.1.1.定义属性
我们把已选择的过滤项保存在search中:
要注意,在created构造函数中会对search进行初始化,所以要在构造函数中对filter进行初始化:
search.filter是一个对象,结构:
{
"过滤项名":"过滤项值"
}
4.1.2.绑定点击事件
给所有的过滤项绑定点击事件:
要注意,点击事件传2个参数:
- k:过滤项的key
- option:当前过滤项对象
在点击事件中,保存过滤项到selectedFilter
:
selectFilter(k, o){
const obj = {};
Object.assign(obj, this.search);
if(k === 'cid3' || k === 'brandId'){
o = o.id;
}
obj.filter[k] = o;
this.search = obj;
}
另外,这里search对象中嵌套了filter对象,请求参数格式化时需要进行特殊处理,修改common.js中的一段代码:
我们刷新页面,点击后通过浏览器功能查看search.filter
的属性变化:
4.2.1.拓展请求对象
我们需要在请求类:SearchRequest
中添加属性,接收过滤属性。过滤属性都是键值对格式,但是key不确定,所以用一个map来接收即可。
4.2.2.添加过滤条件
目前,我们的基本查询是这样的:
private QueryBuilder buildBasicQuery(SearchRequest request) {
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 基本查询条件
queryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));
// 过滤条件构建器
BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
// 整理过滤条件
Map<String, String> filter = request.getFilter();
for (Map.Entry<String, String> entry : filter.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
// 商品分类和品牌要特殊处理
if (key != "cid3" && key != "brandId") {
key = "specs." + key + ".keyword";
}
// 字符串类型,进行term查询
filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
}
// 添加过滤条件
queryBuilder.filter(filterQueryBuilder);
return queryBuilder;
}
总结
页面过滤部分功能未能实现,点击品牌分类无法查询。