12.搜索過濾

0.學習目標

  • 瞭解過濾功能的基本思路
  • 獨立實現分類和品牌展示
  • 瞭解規格參數展示
  • 實現過濾條件篩選
  • 實現已選過濾項回顯
  • 實現取消選擇過濾項

1.過濾功能分析

首先看下頁面要實現的效果:
在這裏插入圖片描述

整個過濾部分有3塊:

  • 頂部的導航,已經選擇的過濾條件展示:
    • 商品分類麪包屑,根據用戶選擇的商品分類變化
    • 其它已選擇過濾參數
  • 過濾條件展示,又包含3部分
    • 商品分類展示
    • 品牌展示
    • 其它規格參數
  • 展開或收起的過濾條件的按鈕

頂部導航要展示的內容跟用戶選擇的過濾條件有關。

  • 比如用戶選擇了某個商品分類,則麪包屑中才會展示具體的分類
  • 比如用戶選擇了某個品牌,列表中才會有品牌信息。

所以,這部分需要依賴第二部分:過濾條件的展示和選擇。因此我們先不着急去做。

展開或收起的按鈕是否顯示,取決於過濾條件有多少,如果很少,那麼就沒必要展示。所以也是跟第二部分的過濾條件有關。

這樣分析來看,我們必須先做第二部分:過濾條件展示。

2.生成分類和品牌過濾

先來看分類和品牌。在我們的數據庫中已經有所有的分類和品牌信息。在這個位置,是不是把所有的分類和品牌信息都展示出來呢?

顯然不是,用戶搜索的條件會對商品進行過濾,而在搜索結果中,不一定包含所有的分類和品牌,直接展示出所有商品分類,讓用戶選擇顯然是不合適的。

無論是分類信息,還是品牌信息,都應該從搜索的結果商品中進行聚合得到。

2.1.擴展返回的結果

原來,我們返回的結果是PageResult對象,裏面只有total、totalPage、items3個屬性。但是現在要對商品分類和品牌進行聚合,數據顯然不夠用,我們需要對返回的結果進行擴展,添加分類和品牌的數據。

那麼問題來了:以什麼格式返回呢?

看頁面:
在這裏插入圖片描述

分類:頁面顯示了分類名稱,但背後肯定要保存id信息。所以至少要有id和name

品牌:頁面展示的有logo,有文字,當然肯定有id,基本上是品牌的完整數據

我們新建一個類,繼承PageResult,然後擴展兩個新的屬性:分類集合和品牌集合:

public class SearchResult extends PageResult<Goods>{

    private List<Category> categories;

    private List<Brand> brands;

    public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Category> categories, List<Brand> brands) {
        super(total, totalPage, items);
        this.categories = categories;
        this.brands = brands;
    }
}

2.2.聚合商品分類和品牌

我們修改搜索的業務邏輯,對分類和品牌聚合。

因爲索引庫中只有id,所以我們根據id聚合,然後再根據id去查詢完整數據。

所以,商品微服務需要提供一個接口:根據品牌id集合,批量查詢品牌。

2.2.1.提供查詢品牌接口

BrandApi

@RequestMapping("brand")
public interface BrandApi {

    @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.queryBrandByIds(ids);
    if(list == null){
        new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(list);
}

BrandService

public List<Brand> queryBrandByIds(List<Long> ids) {
    return this.brandMapper.selectByIdList(ids);
}

BrandMapper

繼承通用mapper的 SelectByIdListMapper即可

public interface BrandMapper extends Mapper<Brand>, SelectByIdListMapper<Brand,Long> {}

2.2.2.搜索功能改造

添加BrandClient

@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}

修改SearchService:

@Service
public class SearchService {

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private BrandClient brandClient;

    private static final Logger logger = LoggerFactory.getLogger(SearchService.class);

    public PageResult<Goods> search(SearchRequest request) {
        // 判斷是否有搜索條件,如果沒有,直接返回null。不允許搜索全部商品
        if (StringUtils.isBlank(request.getKey())) {
            return null;
        }

        // 1、構建查詢條件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        
        // 1.1、基本查詢
        queryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey()));
        // 通過sourceFilter設置返回的結果字段,我們只需要id、skus、subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(
                new String[]{"id", "skus", "subTitle"}, null));

        // 1.2.分頁排序
        searchWithPageAndSort(queryBuilder,request);
        
        // 1.3、聚合
        String categoryAggName = "category"; // 商品分類聚合名稱
        String brandAggName = "brand"; // 品牌聚合名稱
        // 對商品分類進行聚合
        queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
        // 對品牌進行聚合
        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 = (total.intValue() + request.getSize() - 1) / request.getSize();
     	// 3.2、商品分類的聚合結果
        List<Category> categories = 
            getCategoryAggResult(pageInfo.getAggregation(categoryAggName));
        // 3.3、品牌的聚合結果
        List<Brand> brands = getBrandAggResult(pageInfo.getAggregation(brandAggName));

        // 返回結果
         return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(), categories, brands);
    }
    

    // 解析品牌聚合結果
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        try {
            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);
        } catch (Exception e){
            logger.error("品牌聚合出現異常:", e);
            return null;
        }
    }

    // 解析商品分類聚合結果
    private List<Category> getCategoryAggResult(Aggregation aggregation) {
        try{
            List<Category> categories = new ArrayList<>();
            LongTerms categoryAgg = (LongTerms) aggregation;
            List<Long> cids = new ArrayList<>();
            for (LongTerms.Bucket bucket : categoryAgg.getBuckets()) {
                cids.add(bucket.getKeyAsNumber().longValue());
            }
            // 根據id查詢分類名稱
            List<String> names = this.categoryClient.queryNameByIds(cids);

            for (int i = 0; i < names.size(); i++) {
                Category c = new Category();
                c.setId(cids.get(i));
                c.setName(names.get(i));
                categories.add(c);
            }
            return categories;
        } catch (Exception e){
            logger.error("分類聚合出現異常:", e);
            return null;
        }
    }

    // 構建基本查詢條件
    private void searchWithPageAndSort(NativeSearchQueryBuilder queryBuilder, SearchRequest request) {
        // 準備分頁參數
        int page = request.getPage();
        int size = request.getSize();

        // 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.3.頁面渲染數據

2.3.1.過濾參數數據結構

來看下頁面的展示效果:
在這裏插入圖片描述

雖然分類、品牌內容都不太一樣,但是結構相似,都是key和value的結構。

而且頁面結構也極爲類似:
在這裏插入圖片描述

所以,我們可以把所有的過濾條件放入一個數組中,然後在頁面利用v-for遍歷一次生成。

其基本結構是這樣的:

[
    {
        k:"過濾字段名",
        options:[{/*過濾字段值對象*/},{/*過濾字段值對象*/}]
    }
]

我們先在data中定義數組:filter,等待組裝過濾參數:

data: {
    ly,
    search:{
        key: "",
        page: 1
    },
    goodsList:[], // 接收搜索得到的結果
    total: 0, // 總條數
    totalPage: 0, // 總頁數
    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.生成規格參數過濾

3.1.謀而後動

有四個問題需要先思考清楚:

  • 什麼時候顯示規格參數過濾?
  • 如何知道哪些規格需要過濾?
  • 要過濾的參數,其可選值是如何獲取的?
  • 規格過濾的可選值,其數據格式怎樣的?

什麼情況下顯示有關規格參數的過濾?

如果用戶尚未選擇商品分類,或者聚合得到的分類數大於1,那麼就沒必要進行規格參數的聚合。因爲不同分類的商品,其規格是不同的。

因此,我們在後臺需要對聚合得到的商品分類數量進行判斷,如果等於1,我們才繼續進行規格參數的聚合

如何知道哪些規格需要過濾?

我們不能把數據庫中的所有規格參數都拿來過濾。因爲並不是所有的規格參數都可以用來過濾,參數的值是不確定的。

值的慶幸的是,我們在設計規格參數時,已經標記了某些規格可搜索,某些不可搜索。

因此,一旦商品分類確定,我們就可以根據商品分類查詢到其對應的規格,從而知道哪些規格要進行搜索。

要過濾的參數,其可選值是如何獲取的?

雖然數據庫中有所有的規格參數,但是不能把一切數據都用來供用戶選擇。

與商品分類和品牌一樣,應該是從用戶搜索得到的結果中聚合,得到與結果品牌的規格參數可選值。

規格過濾的可選值,其數據格式怎樣的?

我們直接看頁面效果:
在這裏插入圖片描述

我們之前存儲時已經將數據分段,恰好符合這裏的需求

3.3.實戰

接下來,我們就用代碼實現剛纔的思路。

總結一下,應該是以下幾步:

  • 1)用戶搜索得到商品,並聚合出商品分類
  • 2)判斷分類數量是否等於1,如果是則進行規格參數聚合
  • 3)先根據分類,查找可以用來搜索的規格
  • 4)對規格參數進行聚合
  • 5)將規格參數聚合結果整理後返回

3.3.1.擴展返回結果

返回結果中需要增加新數據,用來保存規格參數過濾條件。這裏與前面的品牌和分類過濾的json結構類似:

[
    {
        "k":"規格參數名",
        "options":["規格參數值","規格參數值"]
    }
]

因此,在java中我們用List<Map<String, String>>來表示。

public class SearchResult extends PageResult<Goods>{

    private List<Category> categories;// 分類過濾條件

    private List<Brand> brands; // 品牌過濾條件

    private List<Map<String,String>> specs; // 規格參數過濾條件

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

3.3.2.判斷是否需要聚合

首先,在聚合得到商品分類後,判斷分類的個數,如果是1個則進行規格聚合:
在這裏插入圖片描述

我們將聚合的代碼抽取到了一個getSpecs方法中。

3.3.3.獲取需要聚合的規格參數

在這裏插入圖片描述

要注意的是,這裏我們需要根據id查詢規格,而規格參數接口需要從商品微服務提供

3.3.4.聚合規格參數

因爲規格參數保存時不做分詞,因此其名稱會自動帶上一個.keyword後綴:
在這裏插入圖片描述

3.3.5.解析聚合結果

在這裏插入圖片描述

3.3.6.最終的完整代碼

@Service
public class SearchService {

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecificationClient specificationClient;

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    private ObjectMapper mapper = new ObjectMapper();

    private static final Logger logger = LoggerFactory.getLogger(SearchService.class);

    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);
        queryBuilder.withQuery(basicQuery);
        // 通過sourceFilter設置返回的結果字段,我們只需要id、skus、subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"}, null));

        // 分頁
        searchWithPageAndSort(queryBuilder, request);

        // 聚合
        queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("cid3"));
        queryBuilder.addAggregation(AggregationBuilders.terms("category").field("brandId"));

        // 執行查詢獲取結果集
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());

        // 獲取聚合結果集
        // 商品分類的聚合結果
        List<Category> categories =
                getCategoryAggResult(goodsPage.getAggregation("brands"));
        // 品牌的聚合結果
        List<Brand> brands = getBrandAggResult(goodsPage.getAggregation("category"));

        // 根據商品分類判斷是否需要聚合
        List<Map<String, Object>> specs = new ArrayList<>();
        if (categories.size() == 1) {
            // 如果商品分類只有一個才進行聚合,並根據分類與基本查詢條件聚合
            specs = getSpec(categories.get(0).getId(), basicQuery);
        }

        return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(), categories, brands, specs);
    }

    /**
     * 聚合出規格參數
     *
     * @param cid
     * @param query
     * @return
     */
    private List<Map<String, Object>> getSpec(Long cid, QueryBuilder query) {
        try {
            // 不管是全局參數還是sku參數,只要是搜索參數,都根據分類id查詢出來
            List<SpecParam> params = this.specificationClient.querySpecParam(null, cid, true, null);
            List<Map<String, Object>> specs = new ArrayList<>();

            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            queryBuilder.withQuery(query);

            // 聚合規格參數
            params.forEach(p -> {
                String key = p.getName();
                queryBuilder.addAggregation(AggregationBuilders.terms(key).field("specs." + key + ".keyword"));

            });

            // 查詢
            Map<String, Aggregation> aggs = this.elasticsearchTemplate.query(queryBuilder.build(),
                    SearchResponse::getAggregations).asMap();

            // 解析聚合結果
            params.forEach(param -> {
                Map<String, Object> spec = new HashMap<>();
                String key = param.getName();
                spec.put("k", key);
                StringTerms terms = (StringTerms) aggs.get(key);
                spec.put("options", terms.getBuckets().stream().map(StringTerms.Bucket::getKeyAsString));
                specs.add(spec);
            });

            return specs;
        } catch (
                Exception e)

        {
            logger.error("規格聚合出現異常:", e);
            return null;
        }

    }

    // 構建基本查詢條件
    private void searchWithPageAndSort(NativeSearchQueryBuilder queryBuilder, SearchRequest request) {
        // 準備分頁參數
        int page = request.getPage();
        int size = request.getSize();

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

    // 解析品牌聚合結果
    private List<Brand> getBrandAggResult(Aggregation aggregation) {
        try {
            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);
        } catch (Exception e) {
            logger.error("品牌聚合出現異常:", e);
            return null;
        }
    }

    // 解析商品分類聚合結果
    private List<Category> getCategoryAggResult(Aggregation aggregation) {
        try {
            List<Category> categories = new ArrayList<>();
            LongTerms categoryAgg = (LongTerms) aggregation;
            List<Long> cids = new ArrayList<>();
            for (LongTerms.Bucket bucket : categoryAgg.getBuckets()) {
                cids.add(bucket.getKeyAsNumber().longValue());
            }
            // 根據id查詢分類名稱
            List<String> names = this.categoryClient.queryNameByIds(cids);

            for (int i = 0; i < names.size(); i++) {
                Category c = new Category();
                c.setId(cids.get(i));
                c.setName(names.get(i));
                categories.add(c);
            }
            return categories;
        } catch (Exception e) {
            logger.error("分類聚合出現異常:", e);
            return null;
        }
    }
}

3.3.7.測試結果

在這裏插入圖片描述

3.4.頁面渲染

3.4.1.渲染規格過濾條件

首先把後臺傳遞過來的specs添加到filters數組:

要注意:分類、品牌的option選項是對象,裏面有name屬性,而specs中的option是簡單的字符串,所以需要進行封裝,變爲相同的結構:
在這裏插入圖片描述

最後的結果:
在這裏插入圖片描述

3.4.2.展示或收起過濾條件

是不是感覺顯示的太多了,我們可以通過按鈕點擊來展開和隱藏部分內容:
在這裏插入圖片描述

我們在data中定義變量,記錄展開或隱藏的狀態:
在這裏插入圖片描述

然後在按鈕綁定點擊事件,以改變show的取值:
在這裏插入圖片描述

在展示規格時,對show進行判斷:
在這裏插入圖片描述

OK!

4.過濾條件的篩選

當我們點擊頁面的過濾項,要做哪些事情?

  • 把過濾條件保存在search對象中(watch監控到search變化後就會發送到後臺)
  • 在頁面頂部展示已選擇的過濾項
  • 把商品分類展示到頂部麪包屑

4.1.保存過濾項

4.1.1.定義屬性

我們把已選擇的過濾項保存在search中:
在這裏插入圖片描述

要注意,在created構造函數中會對search進行初始化,所以要在構造函數中對filter進行初始化:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-5D4XgT19-1572751215918)(assets/1526902467385.png)]

search.filter是一個對象,結構:

{
    "過濾項名":"過濾項值"
}

4.1.2.綁定點擊事件

給所有的過濾項綁定點擊事件:
在這裏插入圖片描述

要注意,點擊事件傳2個參數:

  • k:過濾項的key
  • option:當前過濾項對象

在點擊事件中,保存過濾項到selectedFilter

selectFilter(k, o){
    const obj = {};
    Object.assign(obj, this.search);
    if(k === '分類' || k === '品牌'){
        o = o.id;
    }
    obj.filter[k] = o.name;
    this.search = obj;
}

另外,這裏search對象中嵌套了filter對象,請求參數格式化時需要進行特殊處理,修改common.js中的一段代碼:
在這裏插入圖片描述

我們刷新頁面,點擊後通過瀏覽器功能查看search.filter的屬性變化:
在這裏插入圖片描述

並且,此時瀏覽器地址也發生了變化:

http://www.leyou.com/search.html?key=%E6%89%8B%E6%9C%BA&page=1&filter.%E5%93%81%E7%89%8C=2032&filter.CPU%E5%93%81%E7%89%8C=%E6%B5%B7%E6%80%9D%EF%BC%88Hisilicon%EF%BC%89&filter.CPU%E6%A0%B8%E6%95%B0=%E5%8D%81%E6%A0%B8

網絡請求也正常發出:
在這裏插入圖片描述

4.2.後臺添加過濾條件

既然請求已經發送到了後臺,那接下來我們就在後臺去添加這些條件:

4.2.1.拓展請求對象

我們需要在請求類:SearchRequest中添加屬性,接收過濾屬性。過濾屬性都是鍵值對格式,但是key不確定,所以用一個map來接收即可。
在這裏插入圖片描述

4.2.2.添加過濾條件

目前,我們的基本查詢是這樣的:
在這裏插入圖片描述

現在,我們要把頁面傳遞的過濾條件也進入進去。

因此不能在使用普通的查詢,而是要用到BooleanQuery,基本結構是這樣的:

GET /heima/_search
{
    "query":{
        "bool":{
        	"must":{ "match": { "title": "小米手機",operator:"and"}},
        	"filter":{
                "range":{"price":{"gt":2000.00,"lt":3800.00}}
        	}
        }
    }
}

所以,我們對原來的基本查詢進行改造:
在這裏插入圖片描述

因爲比較複雜,我們將其封裝到一個方法中:

// 構建基本查詢條件
private QueryBuilder buildBasicQueryWithFilter(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;
}

其它不變。

4.3.頁面測試

我們先不點擊過濾條件,直接搜索手機:
在這裏插入圖片描述

總共184條

接下來,我們點擊一個過濾條件:
在這裏插入圖片描述

得到的結果:
在這裏插入圖片描述

5.頁面展示選擇的過濾項(作業)

5.1.商品分類麪包屑

當用戶選擇一個商品分類以後,我們應該在過濾模塊的上方展示一個麪包屑,把三級商品分類都顯示出來。
在這裏插入圖片描述

用戶選擇的商品分類就存放在search.filter中,但是裏面只有第三級分類的id:cid3

我們需要根據它查詢出所有三級分類的id及名稱

5.1.1.提供查詢分類接口

我們在商品微服務中提供一個根據三級分類id查詢1~3級分類集合的方法:

Controller

/**
     * 根據3級分類id,查詢1~3級的分類
     * @param id
     * @return
     */
@GetMapping("all/level")
public ResponseEntity<List<Category>> queryAllByCid3(@RequestParam("id") Long id){
    List<Category> list = this.categoryService.queryAllByCid3(id);
    if (list == null || list.size() < 1) {
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    return ResponseEntity.ok(list);
}

Service

public List<Category> queryAllByCid3(Long id) {
    Category c3 = this.categoryMapper.selectByPrimaryKey(id);
    Category c2 = this.categoryMapper.selectByPrimaryKey(c3.getParentId());
    Category c1 = this.categoryMapper.selectByPrimaryKey(c2.getParentId());
    return Arrays.asList(c1,c2,c3);
}

測試:
在這裏插入圖片描述

5.1.2.頁面展示麪包屑

後臺提供了接口,下面的問題是,我們在哪裏去查詢接口?

大家首先想到的肯定是當用戶點擊以後。

但是我們思考一下:用戶點擊以後,就會重新發起請求,頁面刷新,那麼你渲染的結果就沒了。

因此,應該是在頁面重新加載完畢後,此時因爲過濾條件中加入了商品分類的條件,所以查詢的結果中只有1個分類。

我們判斷商品分類是否只有1個,如果是,則查詢三級商品分類,添加到麪包屑即可。
在這裏插入圖片描述

渲染:
在這裏插入圖片描述

刷新頁面:
在這裏插入圖片描述

5.2.其它過濾項

接下來,我們需要在頁面展示用戶已選擇的過濾項,如圖:
在這裏插入圖片描述

我們知道,所有已選擇過濾項都保存在search.filter中,因此在頁面遍歷並展示即可。

但這裏有個問題,filter中數據的格式:
在這裏插入圖片描述

基本有四類數據:

  • 商品分類:這個不需要展示,分類展示在麪包屑位置
  • 品牌:這個要展示,但是其key和值不合適,我們不能顯示一個id在頁面。需要找到其name值
  • 數值類型規格:這個展示的時候,需要把單位查詢出來
  • 非數值類型規格:這個直接展示其值即可

因此,我們在頁面上這樣處理:

<!--已選擇過濾項-->
<ul class="tags-choose">
    <li class="tag" v-for="(v,k) in search.filter" v-if="k !== 'cid3'" :key="k">
        {{k === 'brandId' ? '品牌' : k}}:<span style="color: red">{{getFilterValue(k,v)}}</span></span>
<i class="sui-icon icon-tb-close"></i>
</li>
</ul>
  • 判斷如果 k === 'cid3'說明是商品分類,直接忽略
  • 判斷k === 'brandId'說明是品牌,頁面顯示品牌,其它規格則直接顯示k的值
  • 值的處理比較複雜,我們用一個方法getFilterValue(k,v)來處理,調用時把kv都傳遞

方法內部:

getFilterValue(k,v){
    // 如果沒有過濾參數,我們跳過展示
    if(!this.filters || this.filters.length === 0){
        return null;
    }
    let filter = null;
    // 判斷是否是品牌
    if(k === 'brandId'){
        // 返回品牌名稱
        return this.filters.find(f => f.k === 'brandId').options[0].name;
    }
    return v;
}

然後刷新頁面,即可看到效果:
在這裏插入圖片描述

5.3.隱藏已經選擇的過濾項

現在,我們已經實現了已選擇過濾項的展示,但是你會發現一個問題:

已經選擇的過濾項,在過濾列表中依然存在:
在這裏插入圖片描述

這些已經選擇的過濾項,應該從列表中移除。

怎麼做呢?

你必須先知道用戶選擇了什麼。用戶選擇的項保存在search.filter中:
在這裏插入圖片描述

我們可以編寫一個計算屬性,把filters中的 已經被選擇的key過濾掉:

computed:{
    remainFilters(){
        const keys = Object.keys(this.search.filter);
        if(this.search.filter.cid3){
            keys.push("cid3")
        }
        if(this.search.filter.brandId){
            keys.push("brandId")
        }
        return this.filters.filter(f => !keys.includes(f.k));
    }
}

然後頁面不再直接遍歷filters,而是遍歷remainFilters
在這裏插入圖片描述

刷新頁面:
在這裏插入圖片描述

最後發現,還剩下一堆沒選過的。但是都只有一個可選項,此時再過濾沒有任何意義,應該隱藏,所以,在剛纔的過濾條件中,還應該添加一條:如果只剩下一個可選項,不顯示
在這裏插入圖片描述

在這裏插入圖片描述

6.取消過濾項(作業)

我們能夠看到,每個過濾項後面都有一個小叉,當點擊後,應該取消對應條件的過濾。

思路非常簡單:

  • 給小叉綁定點擊事件
  • 點擊後把過濾項從search.filter中移除,頁面會自動刷新,OK

綁定點擊事件:
在這裏插入圖片描述

綁定點擊事件時,把k傳遞過去,方便刪除

刪除過濾項

removeFilter(k){
    this.search.filter[k] = null;
}

7.優化

搜索系統需要優化的點:

  • 查詢規格參數部分可以添加緩存
  • 聚合計算interval變化頻率極低,所以可以設計爲定時任務計算(週期爲天),然後緩存起來。
  • elasticsearch本身有查詢緩存,可以不進行優化
  • 商品圖片應該採用縮略圖,減少流量,提高頁面加載速度
  • 圖片採用延遲加載
  • 圖片還可以採用CDN服務器
  • sku信息應該在頁面異步加載,而不是放到索引庫
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章