036 搭建搜索微服务04----分类和品牌的过滤

先来看分类和品牌。在我们的数据库中已经有所有的分类和品牌信息。在这个位置,是不是把所有的分类和品牌信息都展示出来呢?

显然不是,用户搜索的条件会对商品进行过滤,而在搜索结果中,不一定包含所有的分类和品牌,直接展示出所有商品分类,让用户选择显然是不合适的。

无论是分类信息,还是品牌信息,都应该从搜索的结果商品中进行聚合得到。

1.扩展返回的结果

 

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

 

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

 

看页面:

036 搭建搜索微服务04----分类和品牌的过滤

 

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

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

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

package lucky.leyou.domain;

import lucky.leyou.common.domain.PageResult;
import lucky.leyou.item.domain.Brand;

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

public class SearchResult extends PageResult<Goods> {

    private List<Map<String, Object>> categories;
    private List<Brand> brands;

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

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

    public SearchResult(Long total, Integer totalPage, List<Goods> items, List<Map<String, Object>> categories, List<Brand> brands) {
        super(total, totalPage, items);
        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.聚合商品分类和品牌

 

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

 

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

 

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

 

修改controller:

/**
     * 搜索商品
     *
     * @param request
     * @return
     */
    @PostMapping("page")
    public ResponseEntity<SearchResult> search(@RequestBody SearchRequest request) {
        SearchResult result = this.searchService.search(request);
        if (result == null) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(result);
    }

修改SearchService:

package lucky.leyou.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lucky.leyou.client.BrandClient;
import lucky.leyou.client.CategoryClient;
import lucky.leyou.client.GoodsClient;
import lucky.leyou.client.SpecificationClient;
import lucky.leyou.common.domain.PageResult;
import lucky.leyou.domain.Goods;
import lucky.leyou.domain.SearchRequest;
import lucky.leyou.domain.SearchResult;
import lucky.leyou.item.domain.*;
import lucky.leyou.reponsitory.GoodsRepository;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
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.stereotype.Service;

import java.io.IOException;
import java.util.*;

/**
 * 搜索服务
 */
@Service
public class 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
     * @throws IOException
     */
    public Goods buildGoods(Spu spu) throws IOException {

        // 创建goods对象
        Goods goods = new Goods();

        // 根据品牌id查询品牌
        Brand brand = this.brandClient.queryBrandById(spu.getBrandId());

        // 查询分类名称,Arrays.asList该方法能将方法所传参数转为List集合
        List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));

        // 根据spuid查询spu下的所有sku
        List<Sku> skus = this.goodsClient.querySkuBySpuId(spu.getId());
        //初始化一个价格集合,收集所有的sku的价格
        List<Long> prices = new ArrayList<>();
        //收集sku的必要的字段信息
        List<Map<String, Object>> skuMapList = new ArrayList<>();
        // 遍历skus,获取价格集合
        skus.forEach(sku ->{
            prices.add(sku.getPrice());
            Map<String, Object> skuMap = new HashMap<>();
            skuMap.put("id", sku.getId());
            skuMap.put("title", sku.getTitle());
            skuMap.put("price", sku.getPrice());
            //获取sku中的图片,数据库中的图片可能是多张,多张是以,分隔,所以也以逗号进行切割返回图片数组,获取第一张图片
            skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0] : "");
            skuMapList.add(skuMap);
        });

        // 以tb_spec_param表中的分类cid字段和searching字段为查询条件查询出tb_spec_param表中所有的搜索规格参数
        //将每一个查询结果封装成SpecParam这个bean对象中,将bean对象放入map中构成查询结果集
        List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true);
        // 根据spuid查询spuDetail(即数据库表tb_spu_detail中的一行数据)。获取规格参数值
        SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spu.getId());
        // 获取通用的规格参数,利用jackson工具类json转换为object对象(反序列化),参数1:要转化的json数据,参数2:要转换的数据类型格式
        Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() {
        });
        // 获取特殊的规格参数
        Map<Long, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() {
        });
        // 定义map接收{规格参数名,规格参数值}
        Map<String, Object> paramMap = new HashMap<>();
        params.forEach(param -> {
            // 判断是否通用规格参数
            if (param.getGeneric()) {
                // 获取通用规格参数值
                String value = genericSpecMap.get(param.getId()).toString();
                // 判断是否是数值类型
                if (param.getNumeric()){
                    // 如果是数值的话,判断该数值落在那个区间
                    value = chooseSegment(value, param);
                }
                // 把参数名和值放入结果集中
                paramMap.put(param.getName(), value);
            } else {
                paramMap.put(param.getName(), specialSpecMap.get(param.getId()));
            }
        });

        // 设置参数
        goods.setId(spu.getId());
        goods.setCid1(spu.getCid1());
        goods.setCid2(spu.getCid2());
        goods.setCid3(spu.getCid3());
        goods.setBrandId(spu.getBrandId());
        goods.setCreateTime(spu.getCreateTime());
        goods.setSubTitle(spu.getSubTitle());
        goods.setAll(spu.getTitle() +" "+ StringUtils.join(names, " ")+" "+brand.getName());
        //获取spu下的所有sku的价格
        goods.setPrice(prices);
        //获取spu下的所有sku,并使用jackson包下ObjectMapper工具类,将任意的Object对象转化为json字符串
        goods.setSkus(MAPPER.writeValueAsString(skuMapList));
        //获取所有的规格参数{name:value}
        goods.setSpecs(paramMap);

        return goods;
    }

    /**
     * 判断value值所在的区间
     * 范例:value=5.2 Segments:0-4.0,4.0-5.0,5.0-5.5,5.5-6.0,6.0-
     * @param value
     * @param p
     * @return
     */
    private String chooseSegment(String value, SpecParam p) {
        double val = NumberUtils.toDouble(value);
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = NumberUtils.toDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if(segs.length == 2){
                end = NumberUtils.toDouble(segs[1]);
            }
            // 判断是否在范围内
            if(val >= begin && val < end){
                if(segs.length == 1){
                    result = segs[0] + p.getUnit() + "以上";
                }else if(begin == 0){
                    result = segs[1] + p.getUnit() + "以下";
                }else{
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    public SearchResult search(SearchRequest request) {
        String key = request.getKey();
        // 判断是否有搜索条件,如果没有,直接返回null。不允许搜索全部商品
        if (StringUtils.isBlank(key)) {
            return null;
        }

        // 自定义查询构建器,构建查询条件
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        // 1、对key进行全文检索查询
        queryBuilder.withQuery(QueryBuilders.matchQuery("all", key).operator(Operator.AND));

        // 2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(
                new String[]{"id","skus","subTitle"}, null));

        // 3、分页
        // 准备分页参数
        int page = request.getPage();
        int 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"));

        // 4、查询,获取结果
        //Page<Goods> pageInfo = this.goodsRepository.search(queryBuilder.build());
// 执行搜索,获取搜索的结果集
        AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>)this.goodsRepository.search(queryBuilder.build());

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

        // 封装结果并返回
        return new SearchResult(goodsPage.getTotalElements(), goodsPage.getTotalPages(), goodsPage.getContent(),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.queryNameByIds(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;
    }

}

测试:

036 搭建搜索微服务04----分类和品牌的过滤

3.页面渲染数据

 

(1)过滤参数数据结构

首先看页面原来的代码:

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

其基本结构是这样的:

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

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

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

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

loadData(){
                // ly.http.post("/search/page", ly.stringify(this.search)).then(resp=>{
                //注意:http在common.js文件定义的,实际上就是axios
                //resp表示后台响应的数据对象,resp.data为数据
                ly.http.post("/search/page", this.search).then(resp=>{
                    if(resp.data.items.length===0){
                        return
                    }
                    this.total=resp.data.total;
                    this.totalPage=resp.data.totalPage;

                    //遍历goodsList集合
                    resp.data.items.forEach(goods=>{
                        //将skus字段这个json字符串转换为json对象
                        goods.skus=JSON.parse(goods.skus);
                        //扩展一个selected属性
                        goods.selected=goods.skus[0];
                    });
                    this.goodsList=resp.data.items;
                    //初始化分类过滤项
                    this.filters.push({
                        k:"分类",
                        options:data.categories
                    });
                    //初始化品牌过滤项
                    this.filters.push({
                        k:"品牌",
                        options:data.brands
                    });
                });
            },

测试:

036 搭建搜索微服务04----分类和品牌的过滤

(2)页面渲染数据

<!--selector-->
        <div class="clearfix selector">
            <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>

结果:

036 搭建搜索微服务04----分类和品牌的过滤

 

 

 

 

 

上一篇:GameFi的崛起之路


下一篇:ERA5数据处理,简单记录。Arcpy