文章目录
1. 搜索过滤分析
前面我们已经实现了基本搜索,页面上已经可以展示出搜索的结果了。接下来就来实现一下搜索过滤,先来看一下要实现的效果:
搜索过滤有 3 部分组成:
- 顶部的导航,已经选择的过滤条件展示
- 商品分类面包屑,根据用户选择的商品分类变化
- 其它已选择过滤参数
- 过滤条件展示,包含 3 部分
- 商品分类展示
- 品牌展示
- 其它规格参数
- 展开或收起的过滤条件的按钮
实现顺序
顶部导航要展示的内容跟用户选择的过滤条件有关,展开或收起的按钮是否显示也取决于用户选择的过滤条件的多少(如果很少,那么就没必要显示)。看来我们必须先需要实现过滤条件展示。
2. 展示分类和品牌过滤
2.1 实现思路
首先,我们要展示出的分类和品牌信息,肯定不是所用商品的分类和品牌,而是用户进行搜索后的商品的分类和品牌。也就是说无论是分类信息,还是品牌信息,都应该从搜索的商品中进行聚合得到。
2.2 拓展搜索结果
之前我们返回的搜索结果是 PageResult 对象,其中有 total、totalPage、items 这 3 个属性。但现在还要返回商品的分类和品牌信息,我们就需要对返回的结果进行扩展,添加分类和品牌的属性。
那么问题来了,分类和品牌的属性应该使用什么数据类型呢?
- 分类:页面需要分类名称,但背后肯定要保存 id 信息。我们可以以键值对的方式进行存取,那就可以使用
List<Map<String, Object>>
- 品牌:页面需要品牌图片,如果没有图片时,就展示品牌名称,但背后肯定要保存 id 信息。基本上是品牌的完整数据,所以可以使用
List<Brand>
在 leyou-search 工程 pojo 包中创建 SearchResult 类,继承 PageResult
public class SearchResult extends PageResult<Goods> {
private List<Brand> brands;
private List<Map<String,Object>> categories;
public SearchResult() {
}
public SearchResult(Long total, List items, Integer totalPage, List<Brand> brands, List<Map<String, Object>> categories) {
super(total, items, totalPage);
this.brands = brands;
this.categories = categories;
}
public List<Brand> getBrands() {
return brands;
}
public void setBrands(List<Brand> brands) {
this.brands = brands;
}
public List<Map<String, Object>> getCategories() {
return categories;
}
public void setCategories(List<Map<String, Object>> categories) {
this.categories = categories;
}
}
2.3 聚合商品分类和品牌
-
修改 leyou-search 工程 service 包中的 SearchService 中的 search 方法
/** * 根据搜索条件搜索数据 * * @param request * @return */ public SearchResult search(SearchRequest request) { // 获取搜索条件 String key = request.getKey(); // 判断是否有搜索条件,如果没有,直接返回 null。不允许搜索全部商品 if (StringUtils.isBlank(key)) { return null; } // 构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 对 key 进行匹配查询 queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND)); // 通过 sourceFilter 设置返回的结果字段,我们只需要 id、skus、subTitle queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null)); // 分页 int page = request.getPage(); int size = request.getSize(); queryBuilder.withPageable(PageRequest.of(page - 1, size)); // 加入请求中的排序条件 String sortBy = request.getSortBy(); Boolean descending = request.getDescending(); if (StringUtils.isNotBlank(sortBy)) { queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(descending ? SortOrder.DESC : SortOrder.ASC)); } // 品牌和分类的聚合名称 String categoryAggName = "categories"; String brandAggName = "brands"; // 添加品牌和分类的聚合 queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId")); // 执行查询 AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build()); // 解析聚合结果集 List<Map<String, Object>> categories = getCategoryAggResult((LongTerms)goodsPage.getAggregation(categoryAggName)); List<Brand> brands = getBrandAggResult((LongTerms)goodsPage.getAggregation(brandAggName)); // 封装结果并返回 return new SearchResult(goodsPage.getTotalElements(), goodsPage.getContent(), goodsPage.getTotalPages(), brands, categories); }
-
在 SearchService 中添加解析聚合结果集的方法
/** * 解析品牌聚合结果集 * @param aggregation * @return */ private List<Brand> getBrandAggResult(LongTerms aggregation) { ArrayList<Brand> brands = new ArrayList<>(); // 获取桶 List<LongTerms.Bucket> buckets = aggregation.getBuckets(); // 遍历桶 for (LongTerms.Bucket bucket : buckets) { // 根据品牌 id 查询品牌 Brand brand = brandClient.queryBrandById(bucket.getKeyAsNumber().longValue()); brands.add(brand); } return brands; } /** * 解析分类聚合结果集 * @param aggregation * @return */ private List<Map<String, Object>> getCategoryAggResult(LongTerms aggregation) { ArrayList<Map<String, Object>> categories = new ArrayList<>(); // 获取桶 List<LongTerms.Bucket> buckets = aggregation.getBuckets(); // 遍历桶 for (LongTerms.Bucket bucket : buckets) { HashMap<String, Object> category = new HashMap<>(); // 获取 key long id = bucket.getKeyAsNumber().longValue(); // 根据商品分类 id,查询商品分类名称 List<String> names = categoryClient.queryNamesByIds(Arrays.asList(id)); category.put("id", id); category.put("name",names.get(0)); categories.add(category); } return categories; }
2.4 测试
-
重启 leyou-search 工程
-
再次搜索手机,成功返回分类和品牌过滤条件
2.5 页面渲染
略,交给前端吧。最终效果如下:
3. 展示规格参数过滤
3.1 实现思路
首先思考来下面几个问题:
什么时候需要展示规格参数过滤信息?
如果用户尚未选择商品分类,或者聚合得到的分类数大于 1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格参数是不同的。
因此,需要对聚合得到的商品分类数量进行判断,如果等于 1,我们才继续进行规格参数的聚合。
哪些规格参数需要过滤?
我们在设计规格参数时,已经标记了某些规格可搜索。
因此,商品分类确定后,我们就可以根据商品分类查询到其对应的可搜索的规格参数,这些规格参数就是需要过滤的。
如何获取需要过滤规格参数的值?
虽然数据库中有所有该分类下的规格参数值,但是不能都用来给供用户选择,因为有些规格参数值并不在用户的搜索结果中。
应该是从用户搜索得到的结果中聚合规格参数,从而得到规格参数可选值。
3.2 拓展搜索结果
搜索结果中需要增加一个属性,用来保存规格参数过滤条件。
那么问题来了,规格参数过滤条件的属性应该使用什么数据类型呢?
来看看规格参数过滤条件的 JSON 结构,所以我们用 List<Map<String, Object>>
来表示。
[
{
"k":"规格参数名",
"options":["规格参数值","规格参数值"]
}
]
在 SearchResult 类中增加 specs 属性
public class SearchResult extends PageResult<Goods> {
private List<Brand> brands;
private List<Map<String,Object>> categories;
private List<Map<String,Object>> specs;
// Constructor、getter、setter、toString 方法省略
}
3.3 聚合规格参数
-
修改 leyou-search 工程 service 包中的 SearchService 中的 search 方法
/** * 根据搜索条件搜索数据 * * @param request * @return */ public SearchResult search(SearchRequest request) { // 获取搜索条件 String key = request.getKey(); // 判断是否有搜索条件,如果没有,直接返回 null。不允许搜索全部商品 if (StringUtils.isBlank(key)) { return null; } // 构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 添加查询条件 MatchQueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND); // 对 key 进行匹配查询 queryBuilder.withQuery(basicQuery); // 通过 sourceFilter 设置返回的结果字段,我们只需要 id、skus、subTitle queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null)); // 分页 int page = request.getPage(); int size = request.getSize(); queryBuilder.withPageable(PageRequest.of(page - 1, size)); // 加入请求中的排序条件 String sortBy = request.getSortBy(); Boolean descending = request.getDescending(); if (StringUtils.isNotBlank(sortBy)) { queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(descending ? SortOrder.DESC : SortOrder.ASC)); } // 品牌和分类的聚合名称 String categoryAggName = "categories"; String brandAggName = "brands"; // 添加品牌和分类的聚合 queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId")); // 执行查询 AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build()); // 解析聚合结果集 List<Map<String, Object>> categories = getCategoryAggResult((LongTerms)goodsPage.getAggregation(categoryAggName)); List<Brand> brands = getBrandAggResult((LongTerms)goodsPage.getAggregation(brandAggName)); // 判断分类聚合的结果集大小,等于1则聚合 List<Map<String, Object>> specs = null; if (categories.size() == 1) { specs = getParamAggResult((Long)categories.get(0).get("id"), basicQuery); } // 封装结果并返回 return new SearchResult(goodsPage.getTotalElements(), goodsPage.getContent(), goodsPage.getTotalPages(), brands, categories, specs); }
-
在 SearchService 中添加聚合规格参数过滤条件的方法
/** * 聚合出规格参数过滤条件 * @param id * @param basicQuery * @return */ private List<Map<String,Object>> getParamAggResult(Long id, QueryBuilder basicQuery) { // 创建自定义查询构建器 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 基于基本的查询条件,聚合规格参数 queryBuilder.withQuery(basicQuery); // 查询要聚合的规格参数 List<SpecParam> params = this.specificationClient.querySpecParams(null, id, null, true); // 添加聚合 params.forEach(param -> { queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs." + param.getName() + ".keyword")); }); // 只需要聚合结果集,不需要查询结果集 queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null)); // 执行聚合查询 AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build()); // 定义一个集合,收集聚合结果集 List<Map<String, Object>> paramMapList = new ArrayList<>(); // 解析聚合查询的结果集 Map<String, Aggregation> aggregationMap = goodsPage.getAggregations().asMap(); for (Map.Entry<String, Aggregation> entry : aggregationMap.entrySet()) { Map<String, Object> map = new HashMap<>(); // 放入规格参数名 map.put("k", entry.getKey()); // 收集规格参数值 List<Object> options = new ArrayList<>(); // 解析每个聚合 StringTerms terms = (StringTerms)entry.getValue(); // 遍历每个聚合中桶,把桶中key放入收集规格参数的集合中 terms.getBuckets().forEach(bucket -> options.add(bucket.getKeyAsString())); map.put("options", options); paramMapList.add(map); } return paramMapList; }
3.4 测试
-
重启 leyou-search 工程
-
再次搜索手机,成功返回规格参数过滤条件
3.5 页面渲染
略,交给前端吧。最终效果如下:
4. 实现搜索过滤
前面已经实现了展示过滤条件,接下来就可以实现点击过滤条件实现搜索过滤了。
4.1 前端保存过滤项
当我们点击展示的过滤条件后,应该将这个过滤条件保存起来,然后再作为请求参数发给后台。
我们把已选择的过滤项保存在 search 中:
search.filter 是一个对象,结构如下:
{
"过滤项名":"过滤项值"
}
我们点击几个过滤条件,再来查看请求参数:
4.2 后台实现搜索过滤
4.2.1 拓展搜索请求对象
我们需要在 SearchRequest 中添加过滤条件属性,可以使用 Map<String, String>
来表示。
在 SearchRequest 类中增加 filter 属性
public class SearchRequest {
private String key;// 搜索条件
private Integer page;// 当前页
private String sortBy; // 排序字段
private Boolean descending; // 是否降序
private Map<String,String> filter; // 过滤条件
// getter、setter、toString 方法省略
}
4.2.2 添加过滤条件
现在我们的基础查询,如下:
MatchQueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);
但我们要把页面传递的过滤条件也加入进去,就不能在使用普通的查询,而是要用到 BooleanQuery。
BoolQueryBuilder basicQuery = buildBoolQueryBuilder(request);
-
修改 leyou-search 工程 service 包中的 SearchService 中的 search 方法,将基本查询改为布尔查询
/** * 根据搜索条件搜索数据 * * @param request * @return */ public SearchResult search(SearchRequest request) { // 获取搜索条件 String key = request.getKey(); // 判断是否有搜索条件,如果没有,直接返回 null。不允许搜索全部商品 if (StringUtils.isBlank(key)) { return null; } // 构建查询条件 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 添加布尔查询条件 BoolQueryBuilder basicQuery = buildBoolQueryBuilder(request); // 对 key 进行匹配查询 queryBuilder.withQuery(basicQuery); // 通过 sourceFilter 设置返回的结果字段,我们只需要 id、skus、subTitle queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null)); // 分页 int page = request.getPage(); int size = request.getSize(); queryBuilder.withPageable(PageRequest.of(page - 1, size)); // 加入请求中的排序条件 String sortBy = request.getSortBy(); Boolean descending = request.getDescending(); if (StringUtils.isNotBlank(sortBy)) { queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(descending ? SortOrder.DESC : SortOrder.ASC)); } // 品牌和分类的聚合名称 String categoryAggName = "categories"; String brandAggName = "brands"; // 添加品牌和分类的聚合 queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId")); // 执行查询 AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build()); // 解析聚合结果集 List<Map<String, Object>> categories = getCategoryAggResult((LongTerms) goodsPage.getAggregation(categoryAggName)); List<Brand> brands = getBrandAggResult((LongTerms) goodsPage.getAggregation(brandAggName)); // 判断分类聚合的结果集大小,等于1则聚合 List<Map<String, Object>> specs = null; if (categories.size() == 1) { specs = getParamAggResult((Long) categories.get(0).get("id"), basicQuery); } // 封装结果并返回 return new SearchResult(goodsPage.getTotalElements(), goodsPage.getContent(), goodsPage.getTotalPages(), brands, categories, specs); }
-
添加布尔查询构建器方法
/** * 布尔查询构建器 * @param request * @return */ private BoolQueryBuilder buildBoolQueryBuilder(SearchRequest request) { BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 添加基本查询条件 boolQueryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND)); // 判断过滤条件是否为空 if (CollectionUtils.isEmpty(request.getFilter())){ return boolQueryBuilder; } // 添加过滤条件 for (Map.Entry<String, String> entry : request.getFilter().entrySet()) { String key = entry.getKey(); // 如果过滤条件是品牌, 过滤的字段名:brandId if (StringUtils.equals("品牌", key)) { key = "brandId"; } else if (StringUtils.equals("分类", key)) { // 如果是分类,过滤字段名:cid3 key = "cid3"; } else { // 如果是规格参数名,过滤字段名:specs.key.keyword key = "specs." + key + ".keyword"; } boolQueryBuilder.filter(QueryBuilders.termQuery(key, entry.getValue())); } return boolQueryBuilder; }
4.3 测试
-
重启 leyou-search 工程
-
再次搜索手机,并选择品牌 “小米”
5. 展示选择过滤项
5.1 展示商品分类面包屑
当用户选择一个商品分类以后,我们应该在过滤模块的上方展示一个面包屑,把三级商品分类都显示出来。
用户选择的商品分类就存放在 search.filter 中,但是里面只有第三级分类的 id:cid3。
因此,我们需要根据它查询出所有三级分类的 id 及名称。
5.1.1 后台提供查询分类接口
-
在 leyou-item-service 项目中的 CategoryController 中添加方法 queryAllByCid3
/** * 根据第 3 级分类 id,查询 1~3 级的分类 * @param id * @return */ @GetMapping("/all/level") public ResponseEntity<List<Category>> queryAllByCid3(@RequestParam("id") Long id){ List<Category> categories = this.categoryService.queryAllByCid3(id); if (CollectionUtils.isEmpty(categories)) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(categories); }
-
在 leyou-item-service 项目中的 CategoryService 中添加方法 queryAllByCid3
/** * 根据第 3 级分类 id,查询 1~3 级的分类 * @param id * @return */ public List<Category> queryAllByCid3(Long id) { Category category3 = categoryMapper.selectByPrimaryKey(id); Category category2 = categoryMapper.selectByPrimaryKey(category3.getParentId()); Category category1 = categoryMapper.selectByPrimaryKey(category2.getParentId()); return Arrays.asList(category1,category2,category3); }
-
打开浏览器,输入以下地址,测试一下
http://api.leyou.com/api/item-service/category/all//level?id=242
5.1.2 页面渲染
略,交给前端吧。最终效果如下:
5.2 展示其他过滤项
略,交给前端吧。最终效果如下: