谷粒商城——商品搜索之基本检索

项目结构

在这里插入图片描述

SearchController

package com.atguigu.gmall.search.controller;

import com.atguigu.gmall.search.pojo.SearchParamVo;
import com.atguigu.gmall.search.pojo.SearchResponseVo;
import com.atguigu.gmall.search.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class SearchController {
    @Autowired
    private SearchService searchService;

    @GetMapping("search")
    public String search(SearchParamVo paramVo, Model model){
        SearchResponseVo responseVo = this.searchService.search(paramVo);
        model.addAttribute("response", responseVo);
        model.addAttribute("searchParam", paramVo);
        return "search";
    }
}

pojo

Goods

package com.atguigu.gmall.search.pojo;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

@Document(indexName = "goods", shards = 3, replicas = 2)
@Data
public class Goods {

    @Id
    private Long skuId;
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title;
    @Field(type = FieldType.Keyword, index = false)
    private String subTitle;
    @Field(type = FieldType.Double)
    private BigDecimal price;
    @Field(type = FieldType.Keyword, index = false)
    private String defaultImage;

    // 排序及过滤
    @Field(type = FieldType.Long)
    private Long sales = 0l;
    @Field(type = FieldType.Date, format = DateFormat.date_time)
    private Date createTime;
    @Field(type = FieldType.Boolean)
    private Boolean store = false;

    // 品牌过滤
    @Field(type = FieldType.Long)
    private Long brandId;
    @Field(type = FieldType.Keyword)
    private String brandName;
    @Field(type = FieldType.Keyword)
    private String logo;

    // 分类过滤
    @Field(type = FieldType.Long)
    private Long categoryId;
    @Field(type = FieldType.Keyword)
    private String categoryName;

    // 规格参数过滤
    @Field(type = FieldType.Nested)
    private List<SearchAttrValueVo> searchAttrs;
}

SearchAttrValueVo

package com.atguigu.gmall.search.pojo;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Data
public class SearchAttrValueVo {
    @Field(type= FieldType.Long)
    private Long attrId;

    @Field(type= FieldType.Keyword)
    private String attrName;

    @Field(type= FieldType.Keyword)
    private String attrValue;

}

SearchParamVo

package com.atguigu.gmall.search.pojo;

import lombok.Data;

import java.util.List;

/**
 * search.gmall.com/search?keyword=手机&brandId=1,2,3&categoryId=225&props=4:8G-12G&props=5:128G-256G
 *      &priceFrom=1000&priceTo=5000&store=true&sort=1&pageNum=1
 */
@Data
public class SearchParamVo {
    //搜索关键字
    private String keyword;

    //品牌的过滤条件
    private List<Long> brandId;

    //分类的过滤条件
    private List<Long> categoryId;

    //规格参数的过滤条件 ["4:8G-12G","5:128G-256G"]
    private List<String> props;

    //价格区间的过滤条件
    private Double priceFrom;
    private Double priceTo;

    //是否有货的过滤
    private Boolean store;

    //排序字段: 0-得分降序  1-价格降序  2-价格升序  3-销量降序  4-新品降序
    private Integer sort = 0;

    //分页参数
    private Integer pageNum=1;
    private final Integer pageSize=20;

}

SearchResponseAttrVo

package com.atguigu.gmall.search.pojo;

import lombok.Data;

import java.util.List;

@Data
public class SearchResponseAttrVo {

    private Long attrId;

    private String attrName;

    private List<String> attrValues;
}

SearchResponseVo

package com.atguigu.gmall.search.pojo;

import com.atguigu.gmall.pms.entity.BrandEntity;
import com.atguigu.gmall.pms.entity.CategoryEntity;
import lombok.Data;

import java.util.List;

@Data
public class SearchResponseVo {
    // 过滤
    //品牌列表:id name logo
    private List<BrandEntity> brands;

    //分类列表:id name
    private List<CategoryEntity> categories;

    // 规格参数列表:[{attrId: 8, attrName: "内存", attrValues: ["8G", "12G"]}, {attrId: 9, attrName: "机身存储", attrValues: ["128G", "256G"]}]
    private List<SearchResponseAttrVo> filters;

    // 分页参数
    private Integer pageNum;
    private Integer pageSize;
    //总记录数
    private Long total;

    // 当前页商品列表
    private List<Goods> goodsList;
}

SearchService

package com.atguigu.gmall.search.service;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.pms.entity.BrandEntity;
import com.atguigu.gmall.pms.entity.CategoryEntity;
import com.atguigu.gmall.search.pojo.Goods;
import com.atguigu.gmall.search.pojo.SearchParamVo;
import com.atguigu.gmall.search.pojo.SearchResponseAttrVo;
import com.atguigu.gmall.search.pojo.SearchResponseVo;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class SearchService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    public SearchResponseVo search(SearchParamVo paramVo) {

        try {
            //this.restHighLevelClient.search(搜索请求体,请求的个性化参数信息)         public final SearchResponse search(SearchRequest searchRequest, RequestOptions options)
            //RequestOptions.DEFAULT:请求的个性化参数信息                     RequestOptions options

            //new SearchRequest(指定搜索的索引库,指定搜索条件):搜索请求体    SearchRequest(String[] indices, SearchSourceBuilder source)
            //new String[]{"goods"}:搜索的索引库                         String[] indices
            //buildDsl(paramVo):搜索条件                                SearchSourceBuilder source
            SearchResponse response = this.restHighLevelClient.search(new SearchRequest(new String[]{"goods"}, buildDsl(paramVo)), RequestOptions.DEFAULT);
            // 解析搜索结果集
            SearchResponseVo responseVo = this.parseResult(response);
            // 分页参数只能从搜索条件中获取
            responseVo.setPageNum(paramVo.getPageNum());
            responseVo.setPageSize(paramVo.getPageSize());

            return responseVo;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 解析搜索结果集方法
     *
     * @param response
     * @return
     */
    private SearchResponseVo parseResult(SearchResponse response) {
        SearchResponseVo responseVo = new SearchResponseVo();

        //1. 解析搜索结果集
        SearchHits hits = response.getHits();
        responseVo.setTotal(hits.getTotalHits().value);
        // 获取当前页的数据
        SearchHit[] hitsHits = hits.getHits();
        // 把hitsHit集合 转化成 Goods集合
        if (hitsHits != null || hitsHits.length > 0) {
            //通过stream流把每一个hitsHit转化为一个Goods对象,最后得到Goods集合
            List<Goods> goodsList = Stream.of(hitsHits).map(hitsHit -> {
                String json = hitsHit.getSourceAsString();
                // 把json类型_source反序列化为Goods对象
                Goods goods = JSON.parseObject(json, Goods.class);
                // 解析出高亮的title,替换掉普通的title   下面需要多次判断非空(省略了)
                Map<String, HighlightField> highlightFields = hitsHit.getHighlightFields();//返回map类型的高亮对象
                HighlightField highlightField = highlightFields.get("title");//获取高亮map中的的高亮title  类型为HighlightField
                String highlightTitle = highlightField.fragments()[0].string();  //highlightField.fragments()得到是Text[]类型的数组,因为这里数组中只有一个对象,所以[0]
                goods.setTitle(highlightTitle);
                return goods;
            }).collect(Collectors.toList());
            //把转化好的Goods集合设置给返回对象
            responseVo.setGoodsList(goodsList);
        }

        //2. 解析聚合结果集
        Aggregations aggregations = response.getAggregations();

        //2.1. 获取品牌聚合结果集    Terms聚合结果集
        ParsedLongTerms brandIdAgg = aggregations.get("brandIdAgg");//ParsedLongTerms 得到的结果是一个可解析的Long型聚合结果集 所以把Long类型换成ParsedLongTerms
        // 获取品牌id聚合结果集中的桶
        List<? extends Terms.Bucket> brandIdAggBuckets = brandIdAgg.getBuckets();

        //2.1.0 把桶集合 转化成 brandEntity集合
        if (!CollectionUtils.isEmpty(brandIdAggBuckets)) {
            //通过stream流把每一个bucket转化为一个BrandEntity对象,最后得到brandEntity集合
            List<BrandEntity> brands = brandIdAggBuckets.stream().map(bucket -> {
                BrandEntity brandEntity = new BrandEntity();
                //2.1.1.设置品牌id
                brandEntity.setId(((Terms.Bucket) bucket).getKeyAsNumber().longValue());//id是Long类型的,所以.longValue()

                // 获取桶中的子聚合
                Aggregations subAggs = ((Terms.Bucket) bucket).getAggregations();

                //2.1.2.设置品牌名称
                // 获取子聚合中的品牌名称的子聚合
                ParsedStringTerms brandNameAgg = subAggs.get("brandNameAgg");
                List<? extends Terms.Bucket> nameAggBuckets = brandNameAgg.getBuckets();//获取名称桶集合
                if (!CollectionUtils.isEmpty(nameAggBuckets)) {
                    brandEntity.setName(nameAggBuckets.get(0).getKeyAsString());
                }

                //2.1.3.设置品牌logo
                // 获取子聚合中的品牌logo子聚合
                ParsedStringTerms logoAgg = subAggs.get("logoAgg");
                List<? extends Terms.Bucket> logoAggBuckets = logoAgg.getBuckets();
                if (!CollectionUtils.isEmpty(logoAggBuckets)) {
                    brandEntity.setLogo(logoAggBuckets.get(0).getKeyAsString());
                }
                return brandEntity;
            }).collect(Collectors.toList());
            //把转化好的brandEntity集合设置给返回对象
            responseVo.setBrands(brands);
        }

        //2.2.解析分类的聚合结果集
        ParsedLongTerms categoryIdAgg = aggregations.get("categoryIdAgg");
        List<? extends Terms.Bucket> categoryIdAggBuckets = categoryIdAgg.getBuckets();
        if (!CollectionUtils.isEmpty(categoryIdAggBuckets)) {
            List<CategoryEntity> categories = categoryIdAggBuckets.stream().map(bucket -> {
                CategoryEntity categoryEntity = new CategoryEntity();
                //2.2.1. 设置分类id
                categoryEntity.setId(((Terms.Bucket) bucket).getKeyAsNumber().longValue());

                //2.2.2. 设置分类name  name在子聚合中,先获取子聚合
                //获取分类名称的子聚合
                ParsedStringTerms categoryNameAgg = ((Terms.Bucket) bucket).getAggregations().get("categoryNameAgg");//ParsedStringTerms可解析的字符串聚合结果集
                List<? extends Terms.Bucket> buckets = categoryNameAgg.getBuckets();
                if (!CollectionUtils.isEmpty(buckets)) {
                    categoryEntity.setName(buckets.get(0).getKeyAsString());//设置分类name
                }
                return categoryEntity;
            }).collect(Collectors.toList());
            responseVo.setCategories(categories);
        }


        //2.3.解析规格参数的聚合结果集
        ParsedNested attrAgg = aggregations.get("attrAgg");//ParsedNested可解析的嵌套聚合结果集
        //获取嵌套聚合结果集中的规格参数id的子聚合
        ParsedLongTerms attrIdAgg = attrAgg.getAggregations().get("attrIdAgg");
        List<? extends Terms.Bucket> buckets = attrIdAgg.getBuckets();
        if (!CollectionUtils.isEmpty(buckets)) {
            List<SearchResponseAttrVo> filters = buckets.stream().map(bucket -> {
                SearchResponseAttrVo searchResponseAttrVo = new SearchResponseAttrVo();

                //2.3.1. 设置规格参数id
                searchResponseAttrVo.setAttrId(((Terms.Bucket) bucket).getKeyAsNumber().longValue());

                Aggregations subAggs = ((Terms.Bucket) bucket).getAggregations();
                //2.3.2. 设置规格参数name
                //获取规格参数名称的子聚合
                ParsedStringTerms attrNameAgg = subAggs.get("attrNameAgg");
                List<? extends Terms.Bucket> nameAggBuckets = attrNameAgg.getBuckets();
                if (!CollectionUtils.isEmpty(nameAggBuckets)) {
                    //设置规格参数name
                    searchResponseAttrVo.setAttrName(nameAggBuckets.get(0).getKeyAsString());
                }

                //2.3.3. 设置规格参数values
                //获取规格参数值得子聚合
                ParsedStringTerms attrValueAgg = subAggs.get("attrValueAgg");
                List<? extends Terms.Bucket> valueAggBuckets = attrValueAgg.getBuckets();
                if (!CollectionUtils.isEmpty(valueAggBuckets)) {
                    List<String> attrValues = valueAggBuckets.stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());
                    // 设置规格参数values
                    searchResponseAttrVo.setAttrValues(attrValues);
                }
                return searchResponseAttrVo;
            }).collect(Collectors.toList());
            responseVo.setFilters(filters);
        }
        return responseVo;
    }


    /**
     * 构建DSL语句
     *
     * @param paramVo
     * @return
     */
    private SearchSourceBuilder buildDsl(SearchParamVo paramVo) {
        //如果搜索关键字为空,直接抛出异常
        String keyword = paramVo.getKeyword();
        if (StringUtils.isBlank(keyword)) {
            //TODO:返回广告商品
            throw new RuntimeException("搜索条件不能为空");
        }

        //搜索源构建器,构建搜索源
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        //1.构建查询及过滤条件
        //(bool查询)
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        sourceBuilder.query(boolQueryBuilder);
        //1.1.构建匹配查询条件
        boolQueryBuilder.must(QueryBuilders.matchQuery("title", keyword).operator(Operator.AND));

        //1.2.构建过滤条件
        //1.2.1.构建品牌过滤
        List<Long> brandId = paramVo.getBrandId();
        if (!CollectionUtils.isEmpty(brandId)) {
            boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", brandId));
        }

        //1.2.2.构建分类的过滤
        List<Long> categoryId = paramVo.getCategoryId();
        if (!CollectionUtils.isEmpty(categoryId)) {
            boolQueryBuilder.filter(QueryBuilders.termsQuery("categoryId", categoryId));
        }
        //1.2.3.构建价格区间的过滤
        Double priceFrom = paramVo.getPriceFrom();
        Double priceTo = paramVo.getPriceTo();
        //如果有任何一个价格不为空,都要有价格范围的过滤
        if (priceFrom != null || priceTo != null) {
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
            boolQueryBuilder.filter(rangeQuery);
            if (priceFrom != null) {
                rangeQuery.gte(priceFrom);
            }
            if (priceTo != null) {
                rangeQuery.lte(priceTo);
            }
        }

        //1.2.4.构建是否有货
        Boolean store = paramVo.getStore();
        if (store != null) {
            //正常情况下,只可以查看有货,这里是为了方便演示
            boolQueryBuilder.filter(QueryBuilders.termQuery("store", store));
        }

        //1.2.5.构建规格参数过滤
        List<String> props = paramVo.getProps();
        if (!CollectionUtils.isEmpty(props)) {
            props.forEach(prop -> {   // 4:8G-12G
                String[] attrs = StringUtils.split(prop, ":");  //得到的数组是  [4,8G-12G]
                //判断attrs不为空,并且长度为2,第一位是数字
                if (attrs != null && attrs.length == 2 && NumberUtils.isCreatable(attrs[0])) {
                    String attrId = attrs[0];
                    String attrValueString = attrs[1];
                    String[] attrValues = StringUtils.split(attrValueString, "-");
                    // 如果规格参数的过滤条件合法,添加规格参数嵌套过滤
                    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); //生成一个布尔查询
                    // 规格参数id过滤
                    boolQuery.must(QueryBuilders.termQuery("searchAttrs.attrId", attrId));
                    // 规格参数值的过滤
                    boolQuery.must(QueryBuilders.termsQuery("searchAttrs.attrValue", attrValues));
                    //嵌套查询  public static NestedQueryBuilder nestedQuery(String path, QueryBuilder query, ScoreMode scoreMode)
                    // path:"searchAttrs"   嵌套类型的字段名
                    // query:boolQuery 查询条件
                    // scoreMode:ScoreMode.None    评分模式  嵌套查询不影响评分 所以用ScoreMode.None
                    NestedQueryBuilder searchAttrs = QueryBuilders.nestedQuery("searchAttrs", boolQuery, ScoreMode.None);
                    boolQueryBuilder.filter(searchAttrs);
                }
            });
        }

        //2.构建排序
        Integer sort = paramVo.getSort();
        switch (sort) {
            case 1:
                sourceBuilder.sort("price", SortOrder.DESC);
                break;
            case 2:
                sourceBuilder.sort("price", SortOrder.ASC);
                break;
            case 3:
                sourceBuilder.sort("sales", SortOrder.DESC);
                break;
            case 4:
                sourceBuilder.sort("createTime", SortOrder.DESC);
                break;
            default:
                sourceBuilder.sort("_score", SortOrder.DESC);
                break;
        }

        //3.构建分页
        Integer pageNum = paramVo.getPageNum();
        Integer pageSize = paramVo.getPageSize();
        sourceBuilder.from((pageNum - 1) * pageSize);
        sourceBuilder.size(pageSize);

        //4.构建高亮
        sourceBuilder.highlighter(new HighlightBuilder().field("title")
                .preTags("<font style='color:red;'>")
                .postTags("</font>"));

        //5.构建聚合
        // 5.1. 品牌聚合
        //brandId聚合下面的子聚合:brandName和logo
        // terms("")    词条聚合 指定聚合名称
        // field("")    指定聚合字段
        sourceBuilder.aggregation(AggregationBuilders.terms("brandIdAgg").field("brandId")
                .subAggregation(AggregationBuilders.terms("brandNameAgg").field("brandName"))
                .subAggregation(AggregationBuilders.terms("logoAgg").field("logo")));

        //5.2. 分类聚合
        sourceBuilder.aggregation(AggregationBuilders.terms("categoryIdAgg").field("categoryId")
                .subAggregation(AggregationBuilders.terms("categoryNameAgg").field("categoryName")));

        //5.3.规格参数的聚合
        //嵌套聚合
        sourceBuilder.aggregation(AggregationBuilders.nested("attrAgg", "searchAttrs")
                .subAggregation(AggregationBuilders.terms("attrIdAgg").field("searchAttrs.attrId")
                        .subAggregation(AggregationBuilders.terms("attrNameAgg").field("searchAttrs.attrName"))
                        .subAggregation(AggregationBuilders.terms("attrValueAgg").field("searchAttrs.attrValue"))));

        // 6. 构建结果集过滤
        sourceBuilder.fetchSource(new String[]{"skuId", "title", "subTitle", "price", "defaultImage"}, null);

        System.out.println(sourceBuilder);
        return sourceBuilder;
    }
}

GmallSearchApplication

package com.atguigu.gmall.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class GmallSearchApplication {

    public static void main(String[] args) {
        SpringApplication.run(GmallSearchApplication.class, args);
    }

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值