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 ListSearchAttrValueVosearchAttrs; }
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; @Data public class SearchParamVo { //搜索关键字 private String keyword; //品牌的过滤条件 private ListSearchResponseAttrVobrandId; //分类的过滤条件 private List categoryId; //规格参数的过滤条件 ["4:8G-12G","5:128G-256G"] private List 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; }
package com.atguigu.gmall.search.pojo; import lombok.Data; import java.util.List; @Data public class SearchResponseAttrVo { private Long attrId; private String attrName; private ListSearchResponseVoattrValues; }
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 ListSearchServicebrands; //分类列表:id name private List categories; // 规格参数列表:[{attrId: 8, attrName: "内存", attrValues: ["8G", "12G"]}, {attrId: 9, attrName: "机身存储", attrValues: ["128G", "256G"]}] private List filters; // 分页参数 private Integer pageNum; private Integer pageSize; //总记录数 private Long total; // 当前页商品列表 private List goodsList; }
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; } 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集合 ListGmallSearchApplicationgoodsList = Stream.of(hitsHits).map(hitsHit -> { String json = hitsHit.getSourceAsString(); // 把json类型_source反序列化为Goods对象 Goods goods = JSON.parseObject(json, Goods.class); // 解析出高亮的title,替换掉普通的title 下面需要多次判断非空(省略了) Map 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 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 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 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 attrValues = valueAggBuckets.stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList()); // 设置规格参数values searchResponseAttrVo.setAttrValues(attrValues); } return searchResponseAttrVo; }).collect(Collectors.toList()); responseVo.setFilters(filters); } return responseVo; } 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 brandId = paramVo.getBrandId(); if (!CollectionUtils.isEmpty(brandId)) { boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", brandId)); } //1.2.2.构建分类的过滤 List 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 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("") .postTags("")); //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; } }
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条)