乐优商城(七)搜索过滤

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);

修改 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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值