乐忧商城项目总结-4

13.搜索过滤

13.1 生成分类和品牌过滤

对于过滤功能,先看一下想要实现的效果:
在这里插入图片描述
整个过滤部分有3块:

  • 顶部的导航,已经选择的过滤条件展示:
    • 商品分类面包屑,根据用户选择的商品分类变化
    • 其它已选择过滤参数
  • 过滤条件展示,又包含3部分
    • 商品分类展示
    • 品牌展示
    • 其它规格参数
  • 展开或收起的过滤条件的按钮

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

  • 比如用户选择了某个商品分类,则面包屑中才会展示具体的分类
  • 比如用户选择了某个品牌,列表中才会有品牌信息。

所以,这部分需要依赖第二部分:过滤条件的展示和选择。因此我们先不着急去做。展开或收起的按钮是否显示,取决于过滤条件有多少,如果很少,那么就没必要展示。所以也是跟第二部分的过滤条件有关。

这样分析来看,我们必须先做第二部分:过滤条件展示。

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

原来,我们返回的结果是PageResult对象,里面只有total、totalPage、items3个属性。但是现在要对商品分类和品牌进行聚合,数据显然不够用,我们需要对返回的结果进行扩展,添加分类和品牌的数据,由于后面也要返回聚合后的规格参数,因此这里统一扩展SearchResult类。

package com.leyou.search.pojo;

import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.Brand;

import java.util.List;
import java.util.Map;

public class SearchResult extends PageResult<Goods> {
   
    private List<Brand> brands;
    private List<Map<String,Object>> categories;
    private List<Map<String,Object>> specs;

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

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

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

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

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

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

修改SearchServiceImpl:

public SearchResult search(SearchRequest request) {
   

    // 判断查询条件
    if (StringUtils.isBlank(request.getKey())) {
   
        // 返回默认结果集
        return null;
    }

    // 初始化自定义查询构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 添加查询条件
    queryBuilder.withQuery(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));
    // 添加结果集过滤,只需要:id,subTitle, skus
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
   "id", "subTitle", "skus"}, null));

    // 获取分页参数
    Integer page = request.getPage();
    Integer size = request.getSize();
    // 添加分页
    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"));

    // 执行搜索,获取搜索的结果集
    AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsReponsitory.search(queryBuilder.build());

    // 解析聚合结果集
    List<Map<String, Object>> categories = getCategoryAggResult(goodsPage.getAggregation(categoryAggName));
    List<Brand> brands = getBrandAggResult(goodsPage.getAggregation(brandAggName));

    // 封装成需要的返回结果集
    return new SearchResult(goodsPage.getContent(), goodsPage.getTotalElements(), goodsPage.getTotalPages(), categories, brands);
}
/**
     * 解析品牌聚合结果集
     * @param aggregation
     * @return
     */
private List<Brand> getBrandAggResult(Aggregation aggregation) {
   
    // 处理聚合结果集
    LongTerms terms = (LongTerms)aggregation;
    // 获取所有的品牌id桶
    List<LongTerms.Bucket> buckets = terms.getBuckets();
    // 定义一个品牌集合,搜集所有的品牌对象
    List<Brand> brands = new ArrayList<>();
    // 解析所有的id桶,查询品牌
    buckets.forEach(bucket -> {
   
        Brand brand = this.brandClient.queryBrandById(bucket.getKeyAsNumber().longValue());
        brands.add(brand);
    });
    return brands;
    // 解析聚合结果集中的桶,把桶的集合转化成id的集合
    // List<Long> brandIds = terms.getBuckets().stream().map(bucket -> bucket.getKeyAsNumber().longValue()).collect(Collectors.toList());
    // 根据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) {
   
    // 处理聚合结果集
    LongTerms terms = (LongTerms)aggregation;
    // 获取所有的分类id桶
    List<LongTerms.Bucket> buckets = terms.getBuckets();
    // 定义一个品牌集合,搜集所有的品牌对象
    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;
}

页面渲染数据结构
我们可以把所有的过滤条件放入一个数组中,然后在页面利用v-for遍历一次生成。
其基本结构是这样的:

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

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

data: {
    ly,
    search:{
        key: "",
        page: 1
    },
    goodsList:[], // 接收搜索得到的结果
    total: 0, // 总条数
    totalPage: 0, // 总页数
    filters:[] // 过滤参数集合
},

然后在查询搜索结果的回调函数中,对过滤参数进行封装:
在这里插入图片描述
页面渲染数据
我们注意到,虽然页面元素是一样的,但是品牌会比其它搜索条件多出一些样式,因为品牌是以图片展示。需要进行特殊处理。数据展示是一致的,我们采用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>

13.3 生成规格参数过滤

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

  • 什么时候显示规格参数过滤? 分类只有一个
  • 如何知道哪些规格需要过滤?
  • 要过滤的参数,其可选值是如何获取的?
  • 规格过滤的可选值,其数据格式怎样的?

1.什么情况下显示有关规格参数的过滤?
如果用户尚未选择商品分类,或者聚合得到的分类数大于1,那么就没必要进行规格参数的聚合。因为不同分类的商品,其规格是不同的。因此,我们在后台需要对聚合得到的商品分类数量进行判断,如果等于1,我们才继续进行规格参数的聚合。
**2.如何知道哪些规格需要过滤? **
我们不能把数据库中的所有规格参数都拿来过滤。因为并不是所有的规格参数都可以用来过滤,参数的值是不确定的。我们在设计规格参数时,已经标记了某些规格可搜索,某些不可搜索。因此,一旦商品分类确定,我们就可以根据商品分类查询到其对应的规格,从而知道哪些规格要进行搜索。
3.要过滤的参数,其可选值是如何获取的?
虽然数据库中有所有的规格参数,但是不能把一切数据都用来供用户选择。与商品分类和品牌一样,应该是从用户搜索得到的结果中聚合,得到与结果品牌的规格参数可选值。
4.要过滤的可选值,其数据格式怎样的?
在这里插入图片描述

具体怎么实现呢?总结为以下五步:

  • 1)用户搜索得到商品,并聚合出商品分类
  • 2)判断分类数量是否等于1,如果是则进行规格参数聚合
  • 3)先根据分类,查找可以用来搜索的规格
  • 4)对规格参数进行聚合
  • 5)将规格参数聚合结果整理后返回

具体的实现过程就不一一分析了,直接贴上代码即可:
GoodsServiceImpl类:

package com.leyou.search.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecificationClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
import com.leyou.search.repository.GoodsRepository;
import com.leyou.search.service.SearchService;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class SearchServiceImpl implements SearchService {
   
    @Autowired
    private BrandClient brandClient;

    @Autowired
    private CategoryClient categoryClient;

    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private SpecificationClient specificationClient;

    @Autowired
    private GoodsRepository goodsRepository;

    private static final ObjectMapper MAPPER = new ObjectMapper();
    /**
     * 将spu转化为Goods
     * @param spu
     * @return
     */
    @Override
    public Goods spuToGoods(Spu spu) throws IOException {
   
        Goods goods = new Goods();
        goods.setId(spu.getId());
        List<Long> ids = Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3());
        List<String> names = this.categoryClient.queryNamesById(ids);
        String brandName = this.brandClient.queryByid(spu.getBrandId()).getName();
        String all = spu.getTitle() + " " + StringUtils.join(names, " ") + " " + brandName;
        goods.setAll(all);
        goods.setSubTitle(spu.getSubTitle());
        goods.setBrandId(spu.getBrandId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setCreateTime(spu.getCreateTime());
        List<Long> prices = new ArrayList<>();
        List<Map<String,Object>> skus = new ArrayList<>();
        List<Sku> skusList = this.goodsClient.querySkusBySpuId(spu.getId());
        skusList.forEach(sku -> {
   
            prices.add(sku.getPrice());
            Map<String,Object> map = new HashMap<>();
            map.put("id",sku.getId());
            map.put("title",sku.getTitle());
            String images = sku.getImages();
            map.put("images",StringUtils.isEmpty(images)?"":images.split(",")[0]);
            map.put("price",sku.getPrice());
            skus.add(map);
        });
        goods.setPrice(prices);
        goods.setSkus(MAPPER.writeValueAsString(skus));

        Map<String,Object> specs = new HashMap<>();
        List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true);
        SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spu.getId());
        String genericSpec = spuDetail.getGenericSpec();
        String specialSpec = spuDetail.getSpecialSpec();
        Map<String,Object> genericSpecMap = MAPPER.readValue(genericSpec, new TypeReference<Map<String, Object>>(
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值