乐优商城(七)搜索过滤

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 聚合商品分类和品牌

  1. 修改 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);
    }
    
  2. 在 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 测试

  1. 重启 leyou-search 工程

  2. 再次搜索手机,成功返回分类和品牌过滤条件

    在这里插入图片描述

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 聚合规格参数

  1. 修改 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);
    }
    
  2. 在 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 测试

  1. 重启 leyou-search 工程

  2. 再次搜索手机,成功返回规格参数过滤条件

    在这里插入图片描述

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);
  1. 修改 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);
    }
    
  2. 添加布尔查询构建器方法

    /**
     * 布尔查询构建器
     * @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 测试

  1. 重启 leyou-search 工程

  2. 再次搜索手机,并选择品牌 “小米”

    在这里插入图片描述

5. 展示选择过滤项

5.1 展示商品分类面包屑

当用户选择一个商品分类以后,我们应该在过滤模块的上方展示一个面包屑,把三级商品分类都显示出来。

在这里插入图片描述

用户选择的商品分类就存放在 search.filter 中,但是里面只有第三级分类的 id:cid3。

因此,我们需要根据它查询出所有三级分类的 id 及名称。

5.1.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);
    }
    
  2. 在 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);
    }
    
  3. 打开浏览器,输入以下地址,测试一下

    http://api.leyou.com/api/item-service/category/all//level?id=242
    

    在这里插入图片描述

5.1.2 页面渲染

略,交给前端吧。最终效果如下:

在这里插入图片描述

5.2 展示其他过滤项

略,交给前端吧。最终效果如下:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值