搜索过滤(乐优)
- 了解過(guò)濾功能的基本思路
- 實(shí)現(xiàn)分類和品牌展示
- 了解規(guī)格參數(shù)展示
- 實(shí)現(xiàn)過(guò)濾條件篩選
- 實(shí)現(xiàn)已選過(guò)濾項(xiàng)回顯
- 實(shí)現(xiàn)取消選擇過(guò)濾項(xiàng)
1.過(guò)濾功能分析
首先看下頁(yè)面要實(shí)現(xiàn)的效果:
整個(gè)過(guò)濾部分有3塊:
- 頂部的導(dǎo)航,已經(jīng)選擇的過(guò)濾條件展示:
- 商品分類面包屑,根據(jù)用戶選擇的商品分類變化
- 其它已選擇過(guò)濾參數(shù)
- 過(guò)濾條件展示,又包含3部分
- 商品分類展示
- 品牌展示
- 其它規(guī)格參數(shù)
- 展開或收起的過(guò)濾條件的按鈕
頂部導(dǎo)航要展示的內(nèi)容跟用戶選擇的過(guò)濾條件有關(guān)。
- 比如用戶選擇了某個(gè)商品分類,則面包屑中才會(huì)展示具體的分類
- 比如用戶選擇了某個(gè)品牌,列表中才會(huì)有品牌信息。
所以,這部分需要依賴第二部分:過(guò)濾條件的展示和選擇。因此我們先不著急去做。
展開或收起的按鈕是否顯示,取決于過(guò)濾條件有多少,如果很少,那么就沒必要展示。所以也是跟第二部分的過(guò)濾條件有關(guān)。
這樣分析來(lái)看,我們必須先做第二部分:過(guò)濾條件展示。
2.生成分類和品牌過(guò)濾
先來(lái)看分類和品牌。在我們的數(shù)據(jù)庫(kù)中已經(jīng)有所有的分類和品牌信息。在這個(gè)位置,是不是把所有的分類和品牌信息都展示出來(lái)呢?
顯然不是,用戶搜索的條件會(huì)對(duì)商品進(jìn)行過(guò)濾,而在搜索結(jié)果中,不一定包含所有的分類和品牌,直接展示出所有商品分類,讓用戶選擇顯然是不合適的。
無(wú)論是分類信息,還是品牌信息,都應(yīng)該從搜索的結(jié)果商品中進(jìn)行聚合得到。
2.1.擴(kuò)展返回的結(jié)果
原來(lái),我們返回的結(jié)果是PageResult對(duì)象,里面只有total、totalPage、items3個(gè)屬性。但是現(xiàn)在要對(duì)商品分類和品牌進(jìn)行聚合,數(shù)據(jù)顯然不夠用,我們需要對(duì)返回的結(jié)果進(jìn)行擴(kuò)展,添加分類和品牌的數(shù)據(jù)。
那么問(wèn)題來(lái)了:以什么格式返回呢?
看頁(yè)面:
分類:頁(yè)面顯示了分類名稱,但背后肯定要保存id信息。所以至少要有id和name
品牌:頁(yè)面展示的有l(wèi)ogo,有文字,當(dāng)然肯定有id,基本上是品牌的完整數(shù)據(jù)
我們新建一個(gè)類,繼承PageResult,然后擴(kuò)展兩個(gè)新的屬性:分類集合和品牌集合:
public class SearchResult extends PageResult<Goods> {private List<Map<String, Object>> categories;private List<Brand> brands;public SearchResult() {}public SearchResult(List<Map<String, Object>> categories, List<Brand> brands) {this.categories = categories;this.brands = brands;}public SearchResult(List<Goods> items, Long total, List<Map<String, Object>> categories, List<Brand> brands) {super(items, total);this.categories = categories;this.brands = brands;}public SearchResult(List<Goods> items, Long total, Integer totalPage, List<Map<String, Object>> categories, List<Brand> brands) {super(items, total, totalPage);this.categories = categories;this.brands = brands;}public List<Map<String, Object>> getCategories() {return categories;}public void setCategories(List<Map<String, Object>> categories) {this.categories = categories;}public List<Brand> getBrands() {return brands;}public void setBrands(List<Brand> brands) {this.brands = brands;} }2.2.聚合商品分類和品牌
我們修改搜索的業(yè)務(wù)邏輯,對(duì)分類和品牌聚合。
因?yàn)樗饕龓?kù)中只有id,所以我們根據(jù)id聚合,然后再根據(jù)id去查詢完整數(shù)據(jù)。
所以,商品微服務(wù)需要提供一個(gè)接口:根據(jù)品牌id集合,批量查詢品牌。
修改SearchService:
public SearchResult search(SearchRequest request) {// 判斷查詢條件if (StringUtils.isBlank(request.getKey())) {// 返回默認(rèn)結(jié)果集return null;}// 初始化自定義查詢構(gòu)建器NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 添加查詢條件queryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));// 添加結(jié)果集過(guò)濾,只需要:id,subTitle, skusqueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null));// 獲取分頁(yè)參數(shù)Integer page = request.getPage();Integer size = request.getSize();// 添加分頁(yè)queryBuilder.withPageable(PageRequest.of(page - 1, size));String categoryAggName = "categories";String brandAggName = "brands";queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));// 執(zhí)行搜索,獲取搜索的結(jié)果集AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsReponsitory.search(queryBuilder.build());// 解析聚合結(jié)果集List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));// 封裝成需要的返回結(jié)果集return new SearchResult(goodsPage.getContent(), goodsPage.getTotalElements(), goodsPage.getTotalPages(), categories, brands); } /*** 解析品牌聚合結(jié)果集* @param aggregation* @return*/ private List<Brand> getBrandAggResult(Aggregation aggregation) {// 處理聚合結(jié)果集LongTerms terms = (LongTerms)aggregation;// 獲取所有的品牌id桶List<LongTerms.Bucket> buckets = terms.getBuckets();// 定義一個(gè)品牌集合,搜集所有的品牌對(duì)象List<Brand> brands = new ArrayList<>();// 解析所有的id桶,查詢品牌buckets.forEach(bucket -> {Brand brand = this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue());brands.add(brand);});return brands;// 解析聚合結(jié)果集中的桶,把桶的集合轉(zhuǎn)化成id的集合// List<Long> brandIds = terms.getBuckets().stream().map(bucket -> bucket.getKeyAsNumber().longValue()).collect(Collectors.toList());// 根據(jù)ids查詢品牌//return brandIds.stream().map(id -> this.brandClient.queryBrandById(id)).collect(Collectors.toList());// return terms.getBuckets().stream().map(bucket -> this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue())).collect(Collectors.toList()); }/*** 解析分類* @param aggregation* @return*/ private List<Map<String,Object>> getCategoryAggResult(Aggregation aggregation) {// 處理聚合結(jié)果集LongTerms terms = (LongTerms)aggregation;// 獲取所有的分類id桶List<LongTerms.Bucket> buckets = terms.getBuckets();// 定義一個(gè)品牌集合,搜集所有的品牌對(duì)象List<Map<String, Object>> categories = new ArrayList<>();List<Long> cids = new ArrayList<>();// 解析所有的id桶,查詢品牌buckets.forEach(bucket -> {cids.add(bucket.getKeyAsNumber().longValue());});List<String> names = this.categoryClient.queryNamesByIds(cids);for (int i = 0; i < cids.size(); i++) {Map<String, Object> map = new HashMap<>();map.put("id", cids.get(i));map.put("name", names.get(i));categories.add(map);}return categories; }測(cè)試:
2.3.頁(yè)面渲染數(shù)據(jù)
2.3.1.過(guò)濾參數(shù)數(shù)據(jù)結(jié)構(gòu)
來(lái)看下頁(yè)面的展示效果:
雖然分類、品牌內(nèi)容都不太一樣,但是結(jié)構(gòu)相似,都是key和value的結(jié)構(gòu)。
而且頁(yè)面結(jié)構(gòu)也極為類似:
所以,我們可以把所有的過(guò)濾條件放入一個(gè)數(shù)組中,然后在頁(yè)面利用v-for遍歷一次生成。
其基本結(jié)構(gòu)是這樣的:
[{k:"過(guò)濾字段名",options:[{/*過(guò)濾字段值對(duì)象*/},{/*過(guò)濾字段值對(duì)象*/}]} ]我們先在data中定義數(shù)組:filters,等待組裝過(guò)濾參數(shù):
data: {ly,search:{key: "",page: 1},goodsList:[], // 接收搜索得到的結(jié)果total: 0, // 總條數(shù)totalPage: 0, // 總頁(yè)數(shù)filters:[] // 過(guò)濾參數(shù)集合 },然后在查詢搜索結(jié)果的回調(diào)函數(shù)中,對(duì)過(guò)濾參數(shù)進(jìn)行封裝:
然后刷新頁(yè)面,通過(guò)瀏覽器工具,查看封裝的結(jié)果:
2.3.2.頁(yè)面渲染數(shù)據(jù)
首先看頁(yè)面原來(lái)的代碼:
我們注意到,雖然頁(yè)面元素是一樣的,但是品牌會(huì)比其它搜索條件多出一些樣式,因?yàn)槠放剖且詧D片展示。需要進(jìn)行特殊處理。數(shù)據(jù)展示是一致的,我們采用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>結(jié)果:
3.生成規(guī)格參數(shù)過(guò)濾
3.1.謀而后動(dòng)
有四個(gè)問(wèn)題需要先思考清楚:
- 什么時(shí)候顯示規(guī)格參數(shù)過(guò)濾? 分類只有一個(gè)
- 如何知道哪些規(guī)格需要過(guò)濾?
- 要過(guò)濾的參數(shù),其可選值是如何獲取的?
- 規(guī)格過(guò)濾的可選值,其數(shù)據(jù)格式怎樣的?
什么情況下顯示有關(guān)規(guī)格參數(shù)的過(guò)濾?
如果用戶尚未選擇商品分類,或者聚合得到的分類數(shù)大于1,那么就沒必要進(jìn)行規(guī)格參數(shù)的聚合。因?yàn)椴煌诸惖纳唐?#xff0c;其規(guī)格是不同的。
因此,我們?cè)诤笈_(tái)需要對(duì)聚合得到的商品分類數(shù)量進(jìn)行判斷,如果等于1,我們才繼續(xù)進(jìn)行規(guī)格參數(shù)的聚合。
如何知道哪些規(guī)格需要過(guò)濾?
我們不能把數(shù)據(jù)庫(kù)中的所有規(guī)格參數(shù)都拿來(lái)過(guò)濾。因?yàn)椴⒉皇撬械囊?guī)格參數(shù)都可以用來(lái)過(guò)濾,參數(shù)的值是不確定的。
值的慶幸的是,我們?cè)谠O(shè)計(jì)規(guī)格參數(shù)時(shí),已經(jīng)標(biāo)記了某些規(guī)格可搜索,某些不可搜索。
因此,一旦商品分類確定,我們就可以根據(jù)商品分類查詢到其對(duì)應(yīng)的規(guī)格,從而知道哪些規(guī)格要進(jìn)行搜索。
要過(guò)濾的參數(shù),其可選值是如何獲取的?
雖然數(shù)據(jù)庫(kù)中有所有的規(guī)格參數(shù),但是不能把一切數(shù)據(jù)都用來(lái)供用戶選擇。
與商品分類和品牌一樣,應(yīng)該是從用戶搜索得到的結(jié)果中聚合,得到與結(jié)果品牌的規(guī)格參數(shù)可選值。
規(guī)格過(guò)濾的可選值,其數(shù)據(jù)格式怎樣的?
我們直接看頁(yè)面效果:
我們之前存儲(chǔ)時(shí)已經(jīng)將數(shù)據(jù)分段,恰好符合這里的需求
3.2.實(shí)戰(zhàn)
接下來(lái),我們就用代碼實(shí)現(xiàn)剛才的思路。
總結(jié)一下,應(yīng)該是以下幾步:
- 1)用戶搜索得到商品,并聚合出商品分類
- 2)判斷分類數(shù)量是否等于1,如果是則進(jìn)行規(guī)格參數(shù)聚合
- 3)先根據(jù)分類,查找可以用來(lái)搜索的規(guī)格
- 4)對(duì)規(guī)格參數(shù)進(jìn)行聚合
- 5)將規(guī)格參數(shù)聚合結(jié)果整理后返回
3.2.1.擴(kuò)展返回結(jié)果
返回結(jié)果中需要增加新數(shù)據(jù),用來(lái)保存規(guī)格參數(shù)過(guò)濾條件。這里與前面的品牌和分類過(guò)濾的json結(jié)構(gòu)類似:
[{"k":"規(guī)格參數(shù)名","options":["規(guī)格參數(shù)值","規(guī)格參數(shù)值"]} ]因此,在java中我們用List<Map<String, Object>>來(lái)表示。
public class SearchResult extends PageResult<Goods> {private List<Map<String, Object>> categories;private List<Brand> brands;private List<Map<String, Object>> specs;public SearchResult() {}public SearchResult(List<Map<String, Object>> categories, List<Brand> brands, List<Map<String, Object>> specs) {this.categories = categories;this.brands = brands;this.specs = specs;}public SearchResult(List<Goods> items, Long total, List<Map<String, Object>> categories, List<Brand> brands, List<Map<String, Object>> specs) {super(items, total);this.categories = categories;this.brands = brands;this.specs = specs;}public SearchResult(List<Goods> items, Long total, Integer totalPage, List<Map<String, Object>> categories, List<Brand> brands, List<Map<String, Object>> specs) {super(items, total, totalPage);this.categories = categories;this.brands = brands;this.specs = specs;}public List<Map<String, Object>> getCategories() {return categories;}public void setCategories(List<Map<String, Object>> 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;} }3.2.2.判斷是否需要聚合
首先,在聚合得到商品分類后,判斷分類的個(gè)數(shù),如果是1個(gè)則進(jìn)行規(guī)格聚合:
我們將聚合的代碼抽取到了一個(gè)getParamAggResult方法中。
3.2.3.獲取需要聚合的規(guī)格參數(shù)
然后,我們需要根據(jù)商品分類,查詢所有可用于搜索的規(guī)格參數(shù):
要注意的是,這里我們需要根據(jù)分類id查詢規(guī)格,而規(guī)格參數(shù)接口需要從商品微服務(wù)提供
3.2.4.聚合規(guī)格參數(shù)
因?yàn)橐?guī)格參數(shù)保存時(shí)不做分詞,因此其名稱會(huì)自動(dòng)帶上一個(gè).keyword后綴:
3.2.5.解析聚合結(jié)果
3.2.6.最終的完整代碼
public SearchResult search(SearchRequest request) {// 判斷查詢條件if (StringUtils.isBlank(request.getKey())) {// 返回默認(rèn)結(jié)果集return null;}// 初始化自定義查詢構(gòu)建器NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 添加查詢條件MatchQueryBuilder basicQuery = QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND);queryBuilder.withQuery(basicQuery);// 添加結(jié)果集過(guò)濾,只需要:id,subTitle, skusqueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null));// 獲取分頁(yè)參數(shù)Integer page = request.getPage();Integer size = request.getSize();// 添加分頁(yè)queryBuilder.withPageable(PageRequest.of(page - 1, size));String categoryAggName = "categories";String brandAggName = "brands";queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));// 執(zhí)行搜索,獲取搜索的結(jié)果集AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsReponsitory.search(queryBuilder.build());// 解析聚合結(jié)果集List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));// 判斷分類聚合的結(jié)果集大小,等于1則聚合List<Map<String, Object>> specs = null;if (categories.size() == 1) {specs = getParamAggResult((Long)categories.get(0).get("id"), basicQuery);}// 封裝成需要的返回結(jié)果集return new SearchResult(goodsPage.getContent(), goodsPage.getTotalElements(), goodsPage.getTotalPages(), categories, brands, specs); }/*** 聚合出規(guī)格參數(shù)過(guò)濾條件* @param id* @param basicQuery* @return*/ private List<Map<String,Object>> getParamAggResult(Long id, QueryBuilder basicQuery) {// 創(chuàng)建自定義查詢構(gòu)建器NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();// 基于基本的查詢條件,聚合規(guī)格參數(shù)queryBuilder.withQuery(basicQuery);// 查詢要聚合的規(guī)格參數(shù)List<SpecParam> params = this.specificationClient.queryParams(null, id, null, true);// 添加聚合params.forEach(param -> {queryBuilder.addAggregation(AggregationBuilders.terms(param.getName()).field("specs." + param.getName() + ".keyword"));});// 只需要聚合結(jié)果集,不需要查詢結(jié)果集queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{}, null));// 執(zhí)行聚合查詢AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsReponsitory.search(queryBuilder.build());// 定義一個(gè)集合,收集聚合結(jié)果集List<Map<String, Object>> paramMapList = new ArrayList<>();// 解析聚合查詢的結(jié)果集Map<String, Aggregation> aggregationMap = goodsPage.getAggregations().asMap();for (Map.Entry<String, Aggregation> entry : aggregationMap.entrySet()) {Map<String, Object> map = new HashMap<>();// 放入規(guī)格參數(shù)名map.put("k", entry.getKey());// 收集規(guī)格參數(shù)值List<Object> options = new ArrayList<>();// 解析每個(gè)聚合StringTerms terms = (StringTerms)entry.getValue();// 遍歷每個(gè)聚合中桶,把桶中key放入收集規(guī)格參數(shù)的集合中terms.getBuckets().forEach(bucket -> options.add(bucket.getKeyAsString()));map.put("options", options);paramMapList.add(map);}return paramMapList; }3.2.7.測(cè)試結(jié)果
3.3.頁(yè)面渲染
3.3.1.渲染規(guī)格過(guò)濾條件
首先把后臺(tái)傳遞過(guò)來(lái)的specs添加到filters數(shù)組:
要注意:分類、品牌的option選項(xiàng)是對(duì)象,里面有name屬性,而specs中的option是簡(jiǎn)單的字符串,所以需要進(jìn)行封裝,變?yōu)橄嗤慕Y(jié)構(gòu):
最后的結(jié)果:
3.3.2.展示或收起過(guò)濾條件
是不是感覺顯示的太多了,我們可以通過(guò)按鈕點(diǎn)擊來(lái)展開和隱藏部分內(nèi)容:
我們?cè)赿ata中定義變量,記錄展開或隱藏的狀態(tài):
然后在按鈕綁定點(diǎn)擊事件,以改變show的取值:
在展示規(guī)格時(shí),對(duì)show進(jìn)行判斷:
OK!
4.過(guò)濾條件的篩選
當(dāng)我們點(diǎn)擊頁(yè)面的過(guò)濾項(xiàng),要做哪些事情?
- 把過(guò)濾條件保存在search對(duì)象中(watch監(jiān)控到search變化后就會(huì)發(fā)送到后臺(tái))
- 在頁(yè)面頂部展示已選擇的過(guò)濾項(xiàng)
- 把商品分類展示到頂部面包屑
4.1.保存過(guò)濾項(xiàng)
4.1.1.定義屬性
我們把已選擇的過(guò)濾項(xiàng)保存在search中:
要注意,在created構(gòu)造函數(shù)中會(huì)對(duì)search進(jìn)行初始化,所以要在構(gòu)造函數(shù)中對(duì)filter進(jìn)行初始化:
search.filter是一個(gè)對(duì)象,結(jié)構(gòu):
{"過(guò)濾項(xiàng)名":"過(guò)濾項(xiàng)值" }4.1.2.綁定點(diǎn)擊事件
給所有的過(guò)濾項(xiàng)綁定點(diǎn)擊事件:
要注意,點(diǎn)擊事件傳2個(gè)參數(shù):
- k:過(guò)濾項(xiàng)的key
- option:當(dāng)前過(guò)濾項(xiàng)對(duì)象
在點(diǎn)擊事件中,保存過(guò)濾項(xiàng)到selectedFilter:
selectFilter(k, o){const obj = {};Object.assign(obj, this.search);if(k === '分類' || k === '品牌'){o = o.id;}obj.filter[k] = o.name || o;this.search = obj; }另外,這里search對(duì)象中嵌套了filter對(duì)象,請(qǐng)求參數(shù)格式化時(shí)需要進(jìn)行特殊處理,修改common.js中的一段代碼:
我們刷新頁(yè)面,點(diǎn)擊后通過(guò)瀏覽器功能查看search.filter的屬性變化:
并且,此時(shí)瀏覽器地址也發(fā)生了變化:
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網(wǎng)絡(luò)請(qǐng)求也正常發(fā)出:
4.2.后臺(tái)添加過(guò)濾條件
既然請(qǐng)求已經(jīng)發(fā)送到了后臺(tái),那接下來(lái)我們就在后臺(tái)去添加這些條件:
4.2.1.拓展請(qǐng)求對(duì)象
我們需要在請(qǐng)求類:SearchRequest中添加屬性,接收過(guò)濾屬性。過(guò)濾屬性都是鍵值對(duì)格式,但是key不確定,所以用一個(gè)map來(lái)接收即可。
4.2.2.添加過(guò)濾條件
目前,我們的基本查詢是這樣的:
現(xiàn)在,我們要把頁(yè)面?zhèn)鬟f的過(guò)濾條件也加入進(jìn)去。
因此不能在使用普通的查詢,而是要用到BooleanQuery,基本結(jié)構(gòu)是這樣的:
GET /heima/_search {"query":{"bool":{"must":{ "match": { "title": "小米手機(jī)",operator:"and"}},"filter":{"range":{"price":{"gt":2000.00,"lt":3800.00}}}}} }所以,我們對(duì)原來(lái)的基本查詢進(jìn)行改造:(SearchService中的search方法)
因?yàn)楸容^復(fù)雜,我們將其封裝到一個(gè)方法中:
/*** 構(gòu)建bool查詢構(gòu)建器* @param request* @return*/ private BoolQueryBuilder buildBooleanQueryBuilder(SearchRequest request) {BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();// 添加基本查詢條件boolQueryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));// 添加過(guò)濾條件if (CollectionUtils.isEmpty(request.getFilter())){return boolQueryBuilder;}for (Map.Entry<String, Object> entry : request.getFilter().entrySet()) {String key = entry.getKey();// 如果過(guò)濾條件是“品牌”, 過(guò)濾的字段名:brandIdif (StringUtils.equals("品牌", key)) {key = "brandId";} else if (StringUtils.equals("分類", key)) {// 如果是“分類”,過(guò)濾字段名:cid3key = "cid3";} else {// 如果是規(guī)格參數(shù)名,過(guò)濾字段名:specs.key.keywordkey = "specs." + key + ".keyword";}boolQueryBuilder.filter(QueryBuilders.termQuery(key, entry.getValue()));}return boolQueryBuilder; }其它不變。
4.3.頁(yè)面測(cè)試
我們先不點(diǎn)擊過(guò)濾條件,直接搜索手機(jī):
總共184條
接下來(lái),我們點(diǎn)擊一個(gè)過(guò)濾條件:
得到的結(jié)果:
5.頁(yè)面展示選擇的過(guò)濾項(xiàng)(作業(yè))
5.1.商品分類面包屑
當(dāng)用戶選擇一個(gè)商品分類以后,我們應(yīng)該在過(guò)濾模塊的上方展示一個(gè)面包屑,把三級(jí)商品分類都顯示出來(lái)。
用戶選擇的商品分類就存放在search.filter中,但是里面只有第三級(jí)分類的id:cid3
我們需要根據(jù)它查詢出所有三級(jí)分類的id及名稱
5.1.1.提供查詢分類接口
我們?cè)谏唐肺⒎?wù)中提供一個(gè)根據(jù)三級(jí)分類id查詢1~3級(jí)分類集合的方法:
Controller
/*** 根據(jù)3級(jí)分類id,查詢1~3級(jí)的分類* @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); }測(cè)試:
5.1.2.頁(yè)面展示面包屑
后臺(tái)提供了接口,下面的問(wèn)題是,我們?cè)谀睦锶ゲ樵兘涌?#xff1f;
大家首先想到的肯定是當(dāng)用戶點(diǎn)擊以后。
但是我們思考一下:用戶點(diǎn)擊以后,就會(huì)重新發(fā)起請(qǐng)求,頁(yè)面刷新,那么你渲染的結(jié)果就沒了。
因此,應(yīng)該是在頁(yè)面重新加載完畢后,此時(shí)因?yàn)檫^(guò)濾條件中加入了商品分類的條件,所以查詢的結(jié)果中只有1個(gè)分類。
我們判斷商品分類是否只有1個(gè),如果是,則查詢?nèi)?jí)商品分類,添加到面包屑即可。
渲染:
刷新頁(yè)面:
5.2.其它過(guò)濾項(xiàng)
接下來(lái),我們需要在頁(yè)面展示用戶已選擇的過(guò)濾項(xiàng),如圖:
我們知道,所有已選擇過(guò)濾項(xiàng)都保存在search.filter中,因此在頁(yè)面遍歷并展示即可。
但這里有個(gè)問(wèn)題,filter中數(shù)據(jù)的格式:
基本有四類數(shù)據(jù):
- 商品分類:這個(gè)不需要展示,分類展示在面包屑位置
- 品牌:這個(gè)要展示,但是其key和值不合適,我們不能顯示一個(gè)id在頁(yè)面。需要找到其name值
- 數(shù)值類型規(guī)格:這個(gè)展示的時(shí)候,需要把單位查詢出來(lái)
- 非數(shù)值類型規(guī)格:這個(gè)直接展示其值即可
因此,我們?cè)陧?yè)面上這樣處理:
<!--已選擇過(guò)濾項(xiàng)--> <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'說(shuō)明是商品分類,直接忽略
- 判斷k === 'brandId'說(shuō)明是品牌,頁(yè)面顯示品牌,其它規(guī)格則直接顯示k的值
- 值的處理比較復(fù)雜,我們用一個(gè)方法getFilterValue(k,v)來(lái)處理,調(diào)用時(shí)把k和v都傳遞
方法內(nèi)部:
getFilterValue(k,v){// 如果沒有過(guò)濾參數(shù),我們跳過(guò)展示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; }然后刷新頁(yè)面,即可看到效果:
5.3.隱藏已經(jīng)選擇的過(guò)濾項(xiàng)
現(xiàn)在,我們已經(jīng)實(shí)現(xiàn)了已選擇過(guò)濾項(xiàng)的展示,但是你會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題:
已經(jīng)選擇的過(guò)濾項(xiàng),在過(guò)濾列表中依然存在:
這些已經(jīng)選擇的過(guò)濾項(xiàng),應(yīng)該從列表中移除。
怎么做呢?
你必須先知道用戶選擇了什么。用戶選擇的項(xiàng)保存在search.filter中:
我們可以編寫一個(gè)計(jì)算屬性,把filters中的 已經(jīng)被選擇的key過(guò)濾掉:
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));} }然后頁(yè)面不再直接遍歷filters,而是遍歷remainFilters
刷新頁(yè)面:
最后發(fā)現(xiàn),還剩下一堆沒選過(guò)的。但是都只有一個(gè)可選項(xiàng),此時(shí)再過(guò)濾沒有任何意義,應(yīng)該隱藏,所以,在剛才的過(guò)濾條件中,還應(yīng)該添加一條:如果只剩下一個(gè)可選項(xiàng),不顯示
6.取消過(guò)濾項(xiàng)
我們能夠看到,每個(gè)過(guò)濾項(xiàng)后面都有一個(gè)小叉,當(dāng)點(diǎn)擊后,應(yīng)該取消對(duì)應(yīng)條件的過(guò)濾。
思路非常簡(jiǎn)單:
- 給小叉綁定點(diǎn)擊事件
- 點(diǎn)擊后把過(guò)濾項(xiàng)從search.filter中移除,頁(yè)面會(huì)自動(dòng)刷新,OK
綁定點(diǎn)擊事件:
綁定點(diǎn)擊事件時(shí),把k傳遞過(guò)去,方便刪除
刪除過(guò)濾項(xiàng)
removeFilter(k){this.search.filter[k] = null; }7.優(yōu)化
搜索系統(tǒng)需要優(yōu)化的點(diǎn):
- 查詢規(guī)格參數(shù)部分可以添加緩存
- 聚合計(jì)算interval變化頻率極低,所以可以設(shè)計(jì)為定時(shí)任務(wù)計(jì)算(周期為天),然后緩存起來(lái)。
- elasticsearch本身有查詢緩存,可以不進(jìn)行優(yōu)化
- 商品圖片應(yīng)該采用縮略圖,減少流量,提高頁(yè)面加載速度
- 圖片采用延遲加載
- 圖片還可以采用CDN服務(wù)器
- sku信息應(yīng)該在頁(yè)面異步加載,而不是放到索引庫(kù)
總結(jié)