项目结构
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 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;
@Data
public class SearchParamVo {
//搜索关键字
private String keyword;
//品牌的过滤条件
private List brandId;
//分类的过滤条件
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;
}
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 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 brands;
//分类列表: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;
}
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;
}
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 goodsList = 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;
}
}
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);
}
}