乐优商城(二十)——搜索过滤

一、过滤功能分析

首先看下页面要实现的效果:

乐优商城(二十)——搜索过滤

整个过滤部分有3块:

  • 顶部的导航,已经选择的过滤条件展示:

    • 商品分类面包屑,根据用户选择的商品分类变化

    • 其它已选择过滤参数

  • 过滤条件展示,又包含3部分

    • 商品分类展示

    • 品牌展示

    • 其它规格参数

  • 展开或收起的过滤条件的按钮

顶部导航要展示的内容跟用户选择的过滤条件有关。

  • 比如用户选择了某个商品分类,则面包屑中才会展示具体的分类

  • 比如用户选择了某个品牌,列表中才会有品牌信息。

所以,这部分需要依赖第二部分:过滤条件的展示和选择。

展开或收起的按钮是否显示,取决于过滤条件有多少,如果很少,那么就没必要展示。所以也是跟第二部分的过滤条件有关。

所以重中之重就是第二部分:过滤条件展示

二、生成品牌和分类过滤

在这个位置,不是把所有的分类和品牌信息都展示出来。因为用户搜索的条件会对商品进行过滤,而在搜索结果中,不一定包含所有的分类和品牌,直接展示出所有商品分类,让用户选择显然是不合适的。无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。

2.1 扩展返回的结果

原来,返回的结果是PageResult对象,里面只有total、totalPage、items3个属性。但是现在要对商品分类和品牌进行聚合,数据显然不够用,所以需要对返回的结果进行扩展,添加分类和品牌的数据。

那么问题来了:以什么格式返回呢?

看页面:

乐优商城(二十)——搜索过滤

分类:页面显示了分类名称,但背后肯定要保存id信息。所以至少要有id和name

品牌:页面展示的有logo,有文字,当然肯定有id,基本上是品牌的完整数据

需要新建一个类,继承PageResult,然后扩展两个新的属性:分类集合和品牌集合。

package com.leyou.vo;

import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Category;
import java.util.List;
import java.util.Map;

/**
 * @Author: 98050
 * Time: 2018-10-12 21:06
 * Feature: 搜索结果存储对象
 */
public class SearchResult<Goods> extends PageResult<Goods> {

    /**
     * 分类的集合
     */
    private List<Category> categories;

    /**
     * 品牌的集合
     */
    private List<Brand> brands;

    /**
     * 规格参数的过滤条件
     */
    private List<Map<String,Object>> specs;

    public List<Category> getCategories() {
        return categories;
    }

    public void setCategories(List<Category> categories) {
        this.categories = categories;
    }

    public List<Brand> getBrands() {
        return brands;
    }

    public void setBrands(List<Brand> brands) {
        this.brands = brands;
    }

    public List<Map<String, Object>> getSpecs() {
        return specs;
    }

    public void setSpecs(List<Map<String, Object>> specs) {
        this.specs = specs;
    }

    public SearchResult(List<Category> categories, List<Brand> brands, List<Map<String,Object>> specs){
        this.categories = categories;
        this.brands = brands;
        this.specs = specs;
    }

    public SearchResult(Long total, List<Goods> item,List<Category> categories, List<Brand> brands, List<Map<String,Object>> specs){
        super(total,item);
        this.categories = categories;
        this.brands = brands;
        this.specs = specs;
    }

    public SearchResult(Long total,Long 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;
    }
}

2.2 聚合商品分类和品牌

修改搜索的业务逻辑,对分类和品牌聚合。

因为索引库中只有id,所以根据id聚合,然后再根据id去查询完整数据。

所以,商品微服务需要提供一个接口:根据品牌id集合,批量查询品牌。

2.2.1 提供查询品牌接口

BrandApi

package com.leyou.item.api;

import com.leyou.item.pojo.Brand;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

/**
 * @Author: 98050
 * Time: 2018-10-11 20:04
 * Feature:品牌服务接口
 */
@RequestMapping("brand")
public interface BrandApi {
    /**
     * 根据品牌id结合,查询品牌信息
     * @param ids
     * @return
     */
    @GetMapping("list")
    List<Brand> queryBrandByIds(@RequestParam("ids") List<Long> ids);
}

BrandController

    /**
     * 根据品牌id结合,查询品牌信息
     * @param ids
     * @return
     */
    @GetMapping("list")
    public ResponseEntity<List<Brand>> queryBrandByIds(@RequestParam("ids") List<Long> ids){
        List<Brand> list = this.brandService.queryBrandByBrandIds(ids);
        if (list == null){
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }
        return ResponseEntity.ok(list);
    }

BrandService

乐优商城(二十)——搜索过滤

BrandServiceImpl

乐优商城(二十)——搜索过滤

BrandMapper

需要继承通用mapper的 SelectByIdListMapper

乐优商城(二十)——搜索过滤

2.2.2 搜索功能改造

添加BrandClient

乐优商城(二十)——搜索过滤

修改SearchService:

    public SearchResult<Goods> search(SearchRequest searchRequest) {
        String key = searchRequest.getKey();

        /**
         * 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
         */
        if (StringUtils.isBlank(key)){
            return null;
        }
        //1.构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        //1.1.对关键字进行全文检索查询
        queryBuilder.withQuery(QueryBuilders.matchQuery("all",key).operator(Operator.AND));
        //1.2.通过sourceFilter设置返回的结果字段,只需要id,skus,subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","skus","subTitle"},null));
        //1.3.分页和排序
        searchWithPageAndSort(queryBuilder,searchRequest);
        //1.4.聚合
        /**
         * 商品分类聚合名称
         */
        String categoryAggName = "category";
        /**
         * 品牌聚合名称
         */
        String brandAggName = "brand";
        //1.4.1。对商品分类进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        //1.4.2.对品牌进行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
        //2.查询、获取结果
        AggregatedPage<Goods> pageInfo = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());
        //3.解析查询结果
        //3.1 分页信息
        Long total = pageInfo.getTotalElements();
        int totalPage = pageInfo.getTotalPages();
        //3.2 商品分类的聚合结果
        List<Category> categories = getCategoryAggResult(pageInfo.getAggregation(categoryAggName));
        //3.3 品牌的聚合结果
        List<Brand> brands = getBrandAggResult(pageInfo.getAggregation(brandAggName));
        //3.封装结果,返回
        return new SearchResult<>(total, (long)totalPage,pageInfo.getContent(),categories,brands);
    }

    /**
     * 解析品牌聚合结果
     * @param aggregation
     * @return
     */
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        LongTerms brandAgg = (LongTerms) aggregation;
        List<Long> bids = new ArrayList<>();
        for (LongTerms.Bucket bucket : brandAgg.getBuckets()){
            bids.add(bucket.getKeyAsNumber().longValue());
        }
        //根据品牌id查询品牌
        return this.brandClient.queryBrandByIds(bids);
    }

    private List<Category> getCategoryAggResult(Aggregation aggregation) {
        LongTerms brandAgg = (LongTerms) aggregation;
        List<Long> cids = new ArrayList<>();
        for (LongTerms.Bucket bucket : brandAgg.getBuckets()){
            cids.add(bucket.getKeyAsNumber().longValue());
        }
        //根据id查询分类名称
        return this.categoryClient.queryCategoryByIds(cids).getBody();
    }

    /**
     * 构建基本查询条件
     * @param queryBuilder
     * @param request
     */
    private void searchWithPageAndSort(NativeSearchQueryBuilder queryBuilder, SearchRequest request) {
        // 准备分页参数
        int page = request.getPage();
        int size = request.getDefaultSize();

        // 1、分页
        queryBuilder.withPageable(PageRequest.of(page - 1, size));
        // 2、排序
        String sortBy = request.getSortBy();
        Boolean desc = request.getDescending();
        if (StringUtils.isNotBlank(sortBy)) {
            // 如果不为空,则进行排序
            queryBuilder.withSort(SortBuilders.fieldSort(sortBy).order(desc ? SortOrder.DESC : SortOrder.ASC));
        }
    }

2.2.3 SearchController

乐优商城(二十)——搜索过滤

2.2.4 测试

乐优商城(二十)——搜索过滤

2.3 页面渲染数据

2.3.1 过滤参数数据结构

页面展示效果:

乐优商城(二十)——搜索过滤

虽然分类、品牌内容都不太一样,但是结构相似,都是key和value的结构。

而且页面结构也极为类似:

乐优商城(二十)——搜索过滤

所以,可以把所有的过滤条件放入一个数组中,然后在页面利用v-for遍历一次生成。

其基本结构是这样的:

[
    {
        k:"过滤字段名",
        options:[{/*过滤字段值对象*/},{/*过滤字段值对象*/}]
    }
]

先在data中定义数组:filters,等待组装过滤参数:

乐优商城(二十)——搜索过滤

然后在查询搜索结果的回调函数中,对过滤参数进行封装:

乐优商城(二十)——搜索过滤

然后刷新页面,查看封装的结果:

乐优商城(二十)——搜索过滤

2.3.2 页面渲染数据

乐优商城(二十)——搜索过滤

虽然页面元素是一样的,但是品牌会比其它搜索条件多出一些样式,因为品牌是以图片展示。需要进行特殊处理。数据展示是一致的,采用v-for处理:

<div class="type-wrap" v-for="(f,i) in filters" :key="i" v-if="f.k !== '品牌'">
    <div class="fl key">{{f.k}}</div>
    <div class="fl value">
        <ul class="type-list">
            <li v-for="(option, j) in f.options" :key="j">
                <a>{{option.name}}</a>
            </li>
        </ul>
    </div>
    <div class="fl ext"></div>
</div>
<div class="type-wrap logo" v-else>
    <div class="fl key brand">{{f.k}}</div>
    <div class="value logos">
        <ul class="logo-list">
            <li v-for="(option, j) in f.options" v-if="option.image">
                <img :src="option.image" />
            </li>
            <li style="text-align: center" v-else>
                <a style="line-height: 30px; font-size: 12px" href="#">{{option.name}}</a>
            </li>
        </ul>
    </div>
    <div class="fl ext">
        <a href="javascript:void(0);" class="sui-btn">多选</a>
    </div>
</div>

测试:

乐优商城(二十)——搜索过滤

三、生成规格参数过滤

3.1 分析

有四个问题需要先思考清楚:

  • 什么时候显示规格参数过滤?

  • 如何知道哪些规格需要过滤?

  • 要过滤的参数,其可选值是如何获取的?

  • 规格过滤的可选值,其数据格式怎样的?

什么情况下显示有关规格参数的过滤?

如果用户尚未选择商品分类,或者聚合得到的分类数大于1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格是不同的。

因此,在后台需要对聚合得到的商品分类数量进行判断,如果等于1,我们才继续进行规格参数的聚合

如何知道哪些规格需要过滤?

不能把数据库中的所有规格参数都拿来过滤。因为并不是所有的规格参数都可以用来过滤,参数的值是不确定的。

在设计规格参数时,已经标记了某些规格可搜索,某些不可搜索。

因此,一旦商品分类确定,我们就可以根据商品分类查询到其对应的规格,从而知道哪些规格要进行搜索。

要过滤的参数,其可选值是如何获取的?

虽然数据库中有所有的规格参数,但是不能把一切数据都用来供用户选择。

与商品分类和品牌一样,应该是从用户搜索得到的结果中聚合,得到与结果品牌的规格参数可选值

规格过滤的可选值,其数据格式怎样的?

乐优商城(二十)——搜索过滤

之前存储时已经将数据分段,恰好符合这里的需求 。

3.2 实现

3.2.1 扩展返回结果

3.2.2 判断是否需要聚合

3.2.3 获取需要聚合的规格参数

3.2.4 聚合规格参数

3.2.5 解析聚合结果

3.2.6 完整代码

3.2.7 测试

乐优商城(二十)——搜索过滤

3.3 页面渲染

3.3.1 渲染规格过滤条件

首先把后台传递过来的specs添加到filters数组:

要注意:分类、品牌的option选项是对象,里面有name属性,而specs中的option是简单的字符串,所以需要进行封装,变为相同的结构:

乐优商城(二十)——搜索过滤

渲染:

显示单位

乐优商城(二十)——搜索过滤

最后的结果:

乐优商城(二十)——搜索过滤

3.3.2 展示或收起过滤条件

可以通过按钮点击来展开和隐藏部分内容:

乐优商城(二十)——搜索过滤

在data中定义变量,记录展开或隐藏的状态:

乐优商城(二十)——搜索过滤

然后再按钮绑定点击事件,以改变show的取值:

乐优商城(二十)——搜索过滤

在展示规格时,对show进行判断:

默认显示四个:分类、品牌、后置摄像头、CPU频率

乐优商城(二十)——搜索过滤