【Project】云商城 - 检索服务

本文将详细介绍云商城的检索服务。云商城完整项目介绍见文章 【Project】云商城

检索服务

检索服务 mall-search 负责实现的功能:

  • 商品上架:在后台管理系统中【上架】某个商品时,将其 SPU 传给商品服务。商品服务会查询出该 SPU 所包含的所有 SKU 的详细信息并封装成一个个 SkuEsModel,然后远程调用检索服务将这些 SKU 信息保存到 ElasticSearch 中,用于在商城页面快速查询出某个 SKU 的详细信息
  • 检索 SKU:根据前端传来的关键词等参数对商品(SKU)进行检索。

商品上架

在开始编写商城业务之前,首先我们需要先考虑商城内应该出现哪些数据:只有在后台管理系统选择 【上架】 的商品才会出现在商城首页,同时这些商品也将被存储到 ElatiscSearch 中,这样就能被快速检索到。后台管理系统界面:

image-20220109205352254

  1. 首先定义要存储到 ES 中的模型类 SkuEsModel,将其存放在 mall-common 微服务的 to 包下,包含的具体属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Data
public class SkuEsModel {
/**
* 后台管理系统只传来 spuId,将根据该值查询得到下面的其他信息
*/
private Long spuId;

/**
* sku 信息,从 pms_sku_info 表中查询
*/
private Long skuId;
private String skuTitle;
private BigDecimal skuPrice;
private String skuImg;
private Long saleCount;

/**
* 是否还有库存,远程调用库存服务(从 wms_ware_sku 表中查询)
*/
private Boolean hasStock;

/**
* 评分热度,未来扩展
*/
private Long hotScore;

/**
* 分类信息,从 pms_category 表中查询
*/
private Long catalogId;
private String catalogName;

/**
* 品牌的信息,从 pms_brand 表中查询
*/
private Long brandId;
private String brandName;
private String brandImg;

/**
* 商品的属性值,从 pms_attr 表中查询
*/
private List<Attrs> attrs;

@Data
public static class Attrs {
private Long attrId;
private String attrName;
private String attrValue;
}
}

attrs 属性是所有 spu 共享一份的,该 spu 下的所有 sku 都有相同的值。这种存储方法虽然造成了大量的 attrs 信息冗余,但是其却节省了大量的查询时间,否则每个 sku 还要再去单独检索其对应的 spu 的 attrs,无疑会浪费很多时间,在高并发下会发生严重阻塞。所以选择这种冗余方式存储 attrs,虽然浪费了空间,但是节省了时间

  1. 点击上架后,将发送该商品的 spuId 到商品服务。商品服务响应该请求后将根据该值查询得到下面的其他信息。具体逻辑见 SpuInfoServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* 上架商品到ES中。先抽取出需要封装的所有信息,然后再远程调用检索服务将数据保存到ES中
* @param spuId
*/
@Override
public void up(Long spuId) {
// 1. 查询当前spu的所有“可以被检索出来的”规格属性,
// 因为规格属性是根据spu来查询的,所以放在外面先查询出来,而不应该放在sku的循环内部查询
List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrListForSpu(spuId);
List<Long> attrIds = baseAttrs.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());

// 2. 查出当前spu的商品属性(可以被检索出来的属性),这些数据将保存到EsModel中。
// 因为这些属性信息是和spu挂钩的,因此所有sku都对应唯一的一份属性值,因此先在外面查询好这些属性值,再一起赋值给每一个sku
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
Set<Long> idSet = new HashSet<>(searchAttrIds);

// 3. 将商品属性封装到 SkuESModel.Attrs 里
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream()
.filter(item -> idSet.contains(item.getAttrId()))
.map(item -> {
// 把每一个商品属性保存到list中,后面就要保存到每个EsModel里
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);
return attrs;
}).collect(Collectors.toList());

// 4. 查出当前 spuid 对应的所有 sku 信息、品牌名
List<SkuInfoEntity> skus = skuInfoService.getSkuBySpuId(spuId);
List<Long> skuIdList = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());

// 5. 发送远程调用,查询库存服务中当前 sku 的库存是否为0
Map<Long, Boolean> stockMap = null;
// 远程调用可能会出异常,但不应该影响下面的服务,所以要捕获异常,能让下面的保存正常进行
try {
List<SkuHasStockVo> skuHasStockList = wareFeignService.getSkusHasStock(skuIdList);
stockMap = skuHasStockList.stream()
.collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因 {}", e);
}

// 6. 封装每个 sku 的信息
Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> upProducts = skus.stream().map(sku -> {
// 组装需要的数据
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku, esModel);
// 设置其他属性值
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());

// 设置库存信息
if (finalStockMap == null) {
esModel.setHasStock(true);
} else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
// TODO 热度评分,先设置为0,未来可以扩展
esModel.setHotScore(0L);

// 设置品牌信息
BrandEntity brandEntity = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brandEntity.getName());
esModel.setBrandImg(brandEntity.getLogo());
// 设置分类信息
CategoryEntity categoryEntity = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(categoryEntity.getName());
// 设置检索属性
esModel.setAttrs(attrsList);
// 封装完毕
return esModel;
}).collect(Collectors.toList());


// 7. 最后将封装好的 SkuEsModel 数据发送给 ES 进行保存,调用远程检索服务保存该数据
R r = searchFeignService.productStatusUp(upProducts);
if (r.getCode() == 0) {
// 远程调用成功,修改当前spu的状态
this.baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
// TODO 重复调用?接口幂等性
}
}
  1. 检索服务 mall-search 将传来的 SkuEsModel 数据保存到 ElasticSearch 中:

ElasticSaveController 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 后台管理系统中点击上架商品后,远程调用该方法保存 SkuEsModel 数据
* @param skuEsModelList
* @return
*/
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModelList) {
boolean status = false;
try {
status = productSaveService.productStatusUp(skuEsModelList);
} catch (IOException e) {
log.error("ElasticSaveController - 商品上架错误: ", e);
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}

if (status) {
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
} else {
return R.ok();
}
}

ProductSaveService代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 上架sku数据,保存到 ES 中
* @param skuEsModelList
*/
@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModelList) throws IOException {
// 1. 先创建索引: product,并建立好映射关系
// 事先创建好索引,包括每个字段的类型,创建索引的 JSON 语句见下文

// 2. 在 ES 中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModelList) {
//构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString());
String jsonString = JSON.toJSONString(skuEsModel);
indexRequest.source(jsonString, XContentType.JSON);
bulkRequest.add(indexRequest);
}
// 3. 批量保存
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, MallElasticSearchConfig.COMMON_OPTIONS);

// TODO 如果批量错误
boolean hasFailures = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList());
log.info("商品上架完成:{}", collect);

return hasFailures;
}

其中,向 ES 中创建的 product 索引的语句为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
PUT product
{
"mappings":{
"properties": {
"skuId":{ "type": "long" },
"spuId":{ "type": "keyword" }, // 不可分词
"skuTitle": {
"type": "text",
"analyzer": "ik_smart" // 中文分词器
},
"skuPrice": { "type": "keyword" },
"skuImg" : { "type": "keyword" },
"saleCount":{ "type":"long" },
"hasStock": { "type": "boolean" },
"hotScore": { "type": "long" },
"brandId": { "type": "long" },
"catalogId": { "type": "long" },
"brandName": { "type": "keyword" },
"brandImg":{ "type": "keyword" },
"catalogName": {"type": "keyword" },
"attrs": {
"type": "nested", // 设置成嵌入式,不会被扁平化处理
"properties": {
"attrId": { "type": "long" },
"attrName": { "type": "keyword" },
"attrValue": { "type": "keyword" }
}
}
}
}
}

注意 attrs 字段的类型为 nested,表示嵌入式字段,不会被扁平化处理。

检索 SKU

前端页面中用户可以用来进行检索的条件:

image-20220110185226858

商城首页在点击某个分类后即会向检索服务发送请求,查询选中的分类(keyword 关键字)所包含的所有 SKU 信息:

image-20220110153628156

数据模型设计

我们需要根据这些条件构造出搜索条件实体类 SearchParam,该对象中的每个属性都对应了前端传来的查询参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 封装页面所有可能传递过来的查询条件
* @author yuyun.zhao
* @Create 2022-01-07
*/
@Data
public class SearchParam {
/**
* 页面传递过来的全文匹配关键字
*/
private String keyword;
/**
* 三级分类id
*/
private Long catalog3Id;
/**
* sort=saleCout_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
* 排序条件
*/
private String sort;
/**
* 是否显示有货。hasStock(是否有货) skuPrice区间,brandId、catalog3Id、attrs
*/
private Integer hasStock = 0;
/**
* 价格区间查询
*/
private String skuPrice;
/**
* 按照品牌进行查询,可以多选
*/
private List<Long> brandId;
/**
* 按照属性进行筛选
*/
private List<String> attrs;
/**
* 页码
*/
private Integer pageNum = 1;
}

同时我们还需要为检索结果设计一个 VO 类 SearchResult ,该类负责保存检索到的所有商品信息 SkuEsModel(具体定义见商品上架),并且保存查询结果所涉及到的品牌、商品分类以及商品属性等信息。这些信息将返回给前端进行展示,这样前端就可以根据用户的检索条件显示出符合该条件的所有产品以及其所涉及到的品牌、商品分类以及商品属性等信息。

查询结果实体类 SearchResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* 查询结果返回
* @author yuyun.zhao
* @Create 2022-01-07
*/
@Data
public class SearchResult {
/**
* 查询到所有商品的商品信息
*/
private List<SkuEsModel> products;
/**
* 以下是分页信息
* 当前页码
*/
private Integer pageNum;
/**
* 总共记录数
*/
private Long total;
/**
* 总页码
*/
private Integer totalPages;
/**
* 当前查询到的结果,所有设计的品牌
*/
private List<BrandVo> brands;
/**
* 当前查询结果,所有涉及到的分类
*/
private List<CatalogVo> catalogs;
/**
* 当前查询到的结果,所有涉及到的所有属性
*/
private List<AttrVo> attrs;
/**
* 页码
*/
private List<Integer> pageNavs;

// ================== 以上是要返回给页面的所有信息 ==================
@Data
public static class BrandVo {
/**
* 品牌id
*/
private Long brandId;
/**
* 品牌名字
*/
private String brandName;
/**
* 品牌图片
*/
private String brandImg;
}
@Data
public static class CatalogVo {
/**
* 分类id
*/
private Long catalogId;
/**
* 品牌名字
*/
private String CatalogName;
}
@Data
public static class AttrVo {
/**
* 属性id
*/
private Long attrId;

/**
* 属性名字
*/
private String attrName;
/**
* 属性值
*/
private List<String> attrValue;
}
}

检索 DSL 语句

构建出 DSL 语句,要包含以下几个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET product/_search
{
"query": {
"bool": {
"must": [...], // 必须满足的条件
"filter": [...] // 过滤条件
}
},
"sort": [...], // 排序条件
"from": 0, // 起始页
"size": 1, // 分页大小
"hightlight": {...}, // 高亮显示查询的keyword
"aggs": {} // 聚合查询
}

DSL 语句示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
GET product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为" // 按照关键字查询
}
}
],
"filter": [
{
"term": {
"catalogId": "225" // 根据分类id过滤
}
},
{
"terms": {
"brandId": [ // 根据品牌id过滤
"1",
"5",
"9"
]
}
},
{
"nested": { // 嵌套查询:根据属性id以及属性值进行过滤
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "8"
}
}
},
{
"terms": {
"attrs.attrValue": [
"2019"
]
}
}
]
}
}
}
},
{
"term": { // 是否有库存
"hasStock": {
"value": "false"
}
}
},
{
"range": { // 价格区间
"skuPrice": {
"gte": 0,
"lte": 7000
}
}
}
]
}
},
"sort": [ // 排序
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size":4,
"highlight": { // 对搜索条件进行高亮
"fields": {"skuTitle": {}},
"pre_tags": "<b style=color:red>",
"post_tags": "</b>"
},
"aggs": {
"brand_agg": { // 品牌进行聚合
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": { // 品牌名字
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg": { // 品牌图片
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": { // 分类
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": { // 分类名字
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg":{
"nested": {
"path": "attrs"
},
"aggs": { // 属性聚合
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": { // 属性名字
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg":{ // 属性的值
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}

业务代码

整体逻辑:

  • 前端发送用户选择的检索条件
  • Spring MVC 将前端发来的请求中的查询参数自动封装到 SearchParam 对象中
  • Service 层负责根据 SearchParam 去 ES 中检索出符合的商品数据 SkuEsModel,并将其涉及到的品牌、商品分类以及商品属性等信息封装到 SearchResult
  • Spring MVC 将 SearchResult 放到请求域中转发给检索页 search.html,其进行数据渲染

Controller 层代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Spring MVC 会将前端发来的请求中的查询参数自动封装到 SearchParam 对象中
* @param searchParam
* @return
*/
@GetMapping(value = {"/search.html","/"})
public String getSearchPage(SearchParam searchParam, Model model, HttpServletRequest request) {
// 获取前端传来的完整查询条件
searchParam.set_queryString(request.getQueryString());
SearchResult result = mallSearchService.getSearchResult(searchParam);
model.addAttribute("result", result);
return "search";
}

Service 层代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/**
* @author yuyun zhao
* @date 2022/1/9 17:41
*/
@Slf4j
@Service
public class MallSearchSeviceImpl implements MallSearchService {
@Autowired
private RestHighLevelClient restHighLevelClient;

@Override
public SearchResult getSearchResult(SearchParam searchParam) {
SearchResult searchResult = null;
// 1. 准备检索请求
SearchRequest request = buildSearchRequest(searchParam);
try {
// 2. 执行检索请求
SearchResponse searchResponse = restHighLevelClient.search(request, MallElasticSearchConfig.COMMON_OPTIONS);
// 3. 分析响应数据,将结果封装成 searchResult
searchResult = buildSearchResult(searchParam, searchResponse);
} catch (IOException e) {
e.printStackTrace();
}
return searchResult;
}

/**
* 1. 准备检索请求
*/
private SearchRequest buildSearchRequest(SearchParam searchParam) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//1. 构建bool query
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
//1.1 bool must
if (!StringUtils.isEmpty(searchParam.getKeyword())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", searchParam.getKeyword()));
}

//1.2 bool filter
//1.2.1 catalog
if (searchParam.getCatalog3Id() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", searchParam.getCatalog3Id()));
}
//1.2.2 brand
if (searchParam.getBrandId() != null && searchParam.getBrandId().size() > 0) {
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", searchParam.getBrandId()));
}
//1.2.3 hasStock
if (searchParam.getHasStock() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", searchParam.getHasStock() == 1));
}
//1.2.4 priceRange
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
if (!StringUtils.isEmpty(searchParam.getSkuPrice())) {
String[] prices = searchParam.getSkuPrice().split("_");
if (prices.length == 1) {
if (searchParam.getSkuPrice().startsWith("_")) {
rangeQueryBuilder.lte(Integer.parseInt(prices[0]));
} else {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
} else if (prices.length == 2) {
//_6000会截取成["","6000"]
if (!prices[0].isEmpty()) {
rangeQueryBuilder.gte(Integer.parseInt(prices[0]));
}
rangeQueryBuilder.lte(Integer.parseInt(prices[1]));
}
boolQueryBuilder.filter(rangeQueryBuilder);
}
//1.2.5 attrs-nested
//attrs=1_5寸:8寸&2_16G:8G
List<String> attrs = searchParam.getAttrs();
BoolQueryBuilder queryBuilder = new BoolQueryBuilder();
if (attrs != null && attrs.size() > 0) {
attrs.forEach(attr -> {
String[] attrSplit = attr.split("_");
queryBuilder.must(QueryBuilders.termQuery("attrs.attrId", attrSplit[0]));
String[] attrValues = attrSplit[1].split(":");
queryBuilder.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
});
}
NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs", queryBuilder, ScoreMode.None);
boolQueryBuilder.filter(nestedQueryBuilder);
//1. bool query构建完成
searchSourceBuilder.query(boolQueryBuilder);

//2. sort eg:sort=saleCount_desc/asc
if (!StringUtils.isEmpty(searchParam.getSort())) {
String[] sortSplit = searchParam.getSort().split("_");
searchSourceBuilder.sort(sortSplit[0], sortSplit[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
}

//3. 分页
searchSourceBuilder.from((searchParam.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

//4. 高亮highlight
if (!StringUtils.isEmpty(searchParam.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
searchSourceBuilder.highlighter(highlightBuilder);
}

//5. 聚合
//5.1 按照brand聚合
TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg").field("brandId");
TermsAggregationBuilder brandNameAgg = AggregationBuilders.terms("brandNameAgg").field("brandName");
TermsAggregationBuilder brandImgAgg = AggregationBuilders.terms("brandImgAgg").field("brandImg");
brandAgg.subAggregation(brandNameAgg);
brandAgg.subAggregation(brandImgAgg);
searchSourceBuilder.aggregation(brandAgg);

//5.2 按照catalog聚合
TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalogAgg").field("catalogId");
TermsAggregationBuilder catalogNameAgg = AggregationBuilders.terms("catalogNameAgg").field("catalogName");
catalogAgg.subAggregation(catalogNameAgg);
searchSourceBuilder.aggregation(catalogAgg);

//5.3 按照attrs聚合
NestedAggregationBuilder nestedAggregationBuilder = new NestedAggregationBuilder("attrs", "attrs");
//按照attrId聚合
TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");
//按照attrId聚合之后再按照attrName和attrValue聚合
TermsAggregationBuilder attrNameAgg = AggregationBuilders.terms("attrNameAgg").field("attrs.attrName");
TermsAggregationBuilder attrValueAgg = AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue");
attrIdAgg.subAggregation(attrNameAgg);
attrIdAgg.subAggregation(attrValueAgg);

nestedAggregationBuilder.subAggregation(attrIdAgg);
searchSourceBuilder.aggregation(nestedAggregationBuilder);

log.debug("构建的DSL语句 {}", searchSourceBuilder.toString());

SearchRequest request = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
return request;
}

/**
* 3. 分析响应数据,将结果封装成 searchResult
*/
private SearchResult buildSearchResult(SearchParam searchParam, SearchResponse searchResponse) {
SearchResult result = new SearchResult();
SearchHits hits = searchResponse.getHits();
//1. 封装查询到的商品信息
if (hits.getHits() != null && hits.getHits().length > 0) {
List<SkuEsModel> skuEsModels = new ArrayList<>();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
//设置高亮属性
if (!StringUtils.isEmpty(searchParam.getKeyword())) {
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String highLight = skuTitle.getFragments()[0].string();
skuEsModel.setSkuTitle(highLight);
}
skuEsModels.add(skuEsModel);
}
result.setProduct(skuEsModels);
}

//2. 封装分页信息
//2.1 当前页码
result.setPageNum(searchParam.getPageNum());
//2.2 总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
//2.3 总页码
Integer totalPages = (int) total % EsConstant.PRODUCT_PAGESIZE == 0 ?
(int) total / EsConstant.PRODUCT_PAGESIZE : (int) total / EsConstant.PRODUCT_PAGESIZE + 1;
result.setTotalPages(totalPages);
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);

//3. 查询结果涉及到的品牌
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
Aggregations aggregations = searchResponse.getAggregations();
//ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
ParsedLongTerms brandAgg = aggregations.get("brandAgg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
//3.1 得到品牌id
Long brandId = bucket.getKeyAsNumber().longValue();

Aggregations subBrandAggs = bucket.getAggregations();
//3.2 得到品牌图片
ParsedStringTerms brandImgAgg = subBrandAggs.get("brandImgAgg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
//3.3 得到品牌名字
Terms brandNameAgg = subBrandAggs.get("brandNameAgg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandName, brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);

//4. 查询涉及到的所有分类
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
//4.1 获取分类id
Long catalogId = bucket.getKeyAsNumber().longValue();
Aggregations subcatalogAggs = bucket.getAggregations();
//4.2 获取分类名
ParsedStringTerms catalogNameAgg = subcatalogAggs.get("catalogNameAgg");
String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo(catalogId, catalogName);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);

//5 查询涉及到的所有属性
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
//ParsedNested用于接收内置属性的聚合
ParsedNested parsedNested = aggregations.get("attrs");
ParsedLongTerms attrIdAgg = parsedNested.getAggregations().get("attrIdAgg");
for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
//5.1 查询属性id
Long attrId = bucket.getKeyAsNumber().longValue();

Aggregations subAttrAgg = bucket.getAggregations();
//5.2 查询属性名
ParsedStringTerms attrNameAgg = subAttrAgg.get("attrNameAgg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
//5.3 查询属性值
ParsedStringTerms attrValueAgg = subAttrAgg.get("attrValueAgg");
List<String> attrValues = new ArrayList<>();
for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
String attrValue = attrValueAggBucket.getKeyAsString();
attrValues.add(attrValue);
List<SearchResult.NavVo> navVos = new ArrayList<>();
}
SearchResult.AttrVo attrVo = new SearchResult.AttrVo(attrId, attrName, attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);

// 6. 构建面包屑导航
List<String> attrs = searchParam.getAttrs();
if (attrs != null && attrs.size() > 0) {
List<SearchResult.NavVo> navVos = attrs.stream().map(attr -> {
String[] split = attr.split("_");
SearchResult.NavVo navVo = new SearchResult.NavVo();
//6.1 设置属性值
navVo.setNavValue(split[1]);
//6.2 查询并设置属性名
try {
R r = productFeignService.info(Long.parseLong(split[0]));
if (r.getCode() == 0) {
AttrResponseVo attrResponseVo = JSON.parseObject(JSON.toJSONString(r.get("attr")), new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(attrResponseVo.getAttrName());
}
} catch (Exception e) {
log.error("远程调用商品服务查询属性失败", e);
}
//6.3 设置面包屑跳转链接
String queryString = searchParam.get_queryString();
String replace = queryString.replace("&attrs=" + attr, "").replace("attrs=" + attr+"&", "").replace("attrs=" + attr, "");
navVo.setLink("http://search.gulimall.com/search.html" + (replace.isEmpty()?"":"?"+replace));
return navVo;
}).collect(Collectors.toList());
result.setNavs(navVos);
}
return result;
}
}