抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

Hello world!

SpringBoot整合ES-Rest-Client

基础搭建

模块

创建一个独立的module

依赖

1
2
3
4
5
6
7
8
9
10
  <properties>
<!-- 覆盖springboot的默认版本,必须和client保持一致才行 -->
<elasticsearch.version>7.4.2</elasticsearch.version>
</properties>
<!-- elasticsearch.client -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>

配置

nacos、springserver等

1
2
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search

配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class GulimallElasticSearchConfig {
// request设置项
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer " + TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}
// 注入RestHighLevelClient
@Bean
public RestHighLevelClient esRestClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.128.129", 9200, "http")));
return client;
}
}

TEST & DEBUG

error creating bean with name ‘‘DataSource’

没有配置数据源,如果不需要就在application类上添加@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

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
package com.atguigu.gulimall.gulimallsearch;

import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.gulimallsearch.config.GulimallElasticSearchConfig;
import lombok.Data;
import lombok.ToString;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
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.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {

@Autowired
private RestHighLevelClient client;

/*\
* 测试复杂检索
*/
@Test
public void complexSearch() throws IOException {
// 创建检索请求
SearchRequest searchRequest = new SearchRequest();
// 指定索引
searchRequest.indices("bank");
// 指定检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
/*
* searchSourceBuilder.query();
* searchSourceBuilder.aggregation();
* searchSourceBuilder.from()
* searchSourceBuilder.size()
*/
/*
* searchSourceBuilder.query(QueryBuilders.matchAllQuery());
* searchSourceBuilder.query(QueryBuilders.matchPhraseQuery());
* searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
*/
/*
* searchSourceBuilder.aggregation(AggregationBuilders.avg("").field(""));
* searchSourceBuilder.aggregation(AggregationBuilders.terms("").field("").size(99));
* ……
*/
searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
//1.2)、按照年龄的值分布进行聚合
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
searchSourceBuilder.aggregation(ageAgg);

//1.3)、计算平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
searchSourceBuilder.aggregation(balanceAvg);
System.out.println("检索条件: " + searchSourceBuilder.toString());
// 指定检索条件对象
searchRequest.source(searchSourceBuilder);
// 执行检索
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
// 分析结果;
// 获得hit结果
SearchHits hits = response.getHits();
SearchHit[] hitsHits = hits.getHits();
for (SearchHit hit : hitsHits) {
String sourceAsString = hit.getSourceAsString();
Accout accout = JSON.parseObject(sourceAsString, Accout.class);
System.out.println("accout:" + accout);
}
//获取这次检索到的分析信息;
Aggregations aggregations = response.getAggregations();
// for (Aggregation aggregation : aggregations.asList()) {
// System.out.println("当前聚合:"+aggregation.getName());
// }
Terms terms = aggregations.get("ageAgg");
for (Terms.Bucket bucket : terms.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:" + keyAsString + "==>" + bucket.getDocCount());
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg1.getValue());
}

@ToString
@Data
static class Accout {

private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;

}

/*\
* 测试存储
*/
@Test
public void indexData() throws IOException {
// 创建indexRequest,传入index值
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
// 传入文档(jsonString格式)
User user = new User("张三", "男", 18);
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON);
// 执行操作并获得响应数据
IndexResponse response = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(response);
}

// 自定义类型
@Data
static class User {
private String username;
private String gender;
private Integer age;

public User(String username, String gender, Integer age) {
this.username = username;
this.gender = gender;
this.age = age;
}
}

@Test
public void contextLoads() {
System.out.println(client);
}

}

商品上架

商品系统—商品维护—SPU管理—上架

上架即将商品数据保存至ES中,只有上架的商品才能被全文检索。

SKU在ES中存储模型

检索会根据:SKU的标题、价格、销量、品牌、分类、商品规格等进行检索

模型设计需要花费一定心思,详见文档09、商城业务

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
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "keyword"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"saleCount": {
"type": "long"
},
"hasStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catalogId": {
"type": "long"
},
"brandName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"brandImg": {
"type": "keyword",
"index": false,
"doc_values": false
},
"catalogName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword",
"index": false,
"doc_values": false
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}

“index”:false
“doc_values”:false

表示该冗余的字段不需要用于检索与聚合

检索服务回来表示,这个设置数据类型的步骤很重要,特别是nested这种

这里的模型必须修改,超级感谢一个大佬,文档很详细

检索服务 · 语雀 (yuque.com)

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
# 查看原来的映射规则
GET gulimall_product/_mapping
# 修改为新的映射 并创建新的索引,下面进行数据迁移
PUT /mall_product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "long"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hosStock": {
"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"
}
}
}
}
}
}
# 数据迁移
POST _reindex
{
"source": {
"index": "gulimall_product"
},
"dest": {
"index": "mall_product"
}
}

gulimall-product

SkuEsModel

写在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
@Data
public class SkuEsModel {
private Long skuId;

private Long spuId;

private String skuTitle;

private BigDecimal skuPrice;

private String skuImg;

private Long saleCount;

private Boolean hasStock;

private Long hotScore;

private Long brandId;

private Long catalogId;

private String brandName;

private String brandImg;

private String catalogName;

private List<Attrs> attrs;

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

SkuHasStockTo

1
2
3
4
5
@Data
public class SkuHasStockTo {
private Long skuId;
private Boolean hasStock;
}

SpuInfoController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
@Autowired
private SpuInfoService spuInfoService;

/**
* 商品上架,保存到ES中
*/
@PostMapping("/{spuId}/up}")
public R spuUp(@PathVariable("spuId") Long spuId) {
spuInfoService.spuUp(spuId);
return R.ok();
}
}

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
    /*
* 商品上架
*/
@Override
public void spuUp(Long spuId) {
// 将数据存入SkuEsModel对象当中

// 1 查出sku品牌和分类信息(不需要对每个sku都查询,所以放在stream外面)
SpuInfoEntity spuInfoEntity = this.getById(spuId);
BrandEntity brandEntity = brandService.getById(spuInfoEntity.getBrandId());
Long brandId = brandEntity.getBrandId();
String brandLogo = brandEntity.getLogo();
String brandName = brandEntity.getName();
CategoryEntity categoryEntity = categoryService.getById(spuInfoEntity.getCatalogId());
String categoryName = categoryEntity.getName();

// 2 查出sku的attr,并且是可以被全文检索的
List<ProductAttrValueEntity> baseAttrList = productAttrValueService.baseAttrListForSpu(spuId);
List<Long> attrIdList = baseAttrList.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());

// 根据查出的所有attr找出可以被全文检索的
List<Long> searchAttrIdList = attrService.getIdsCanSearch(attrIdList);
// 使用SET集合方便判断是否存在指定id
Set<Long> idSet = new HashSet<>(searchAttrIdList);

List<SkuEsModel.Attrs> skuEsModelAttrsList = baseAttrList.stream().filter(attr -> {
return idSet.contains(attr.getAttrId());
}).map(attr -> {
SkuEsModel.Attrs attrEntity = new SkuEsModel.Attrs();
BeanUtils.copyProperties(attr, attrEntity);
return attrEntity;
}).collect(Collectors.toList());


// 3 查出当前spuid对应的sku信息
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkuListBySpuId(spuId);

Map<Long, Boolean> skuHasStockCollect = new HashMap<>();
try {
// 3.5 查出所有sku的是否有库存信息
List<SkuHasStockTo> skuHasStockList = wareFeignService.getSkuHasStockList(skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList()));
// 封装到map当中,可以直接查询对应key的value
skuHasStockCollect = skuHasStockList.stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务远程调用异常:原因{}", e);
}

// 4 封装每个sku信息
Map<Long, Boolean> finalSkuHasStockCollect = skuHasStockCollect;
List<SkuEsModel> upSku = skuInfoEntities.stream().map(skuInfo -> {
SkuEsModel skuEsModel = new SkuEsModel();
BeanUtils.copyProperties(skuInfo, skuEsModel);
skuEsModel.setSkuPrice(skuInfo.getPrice());
skuEsModel.setSkuImg(skuInfo.getSkuDefaultImg());
// 远程调用库存系统是否有库存
if (finalSkuHasStockCollect == null) {
skuEsModel.setHasStock(true);
} else {
skuEsModel.setHasStock(finalSkuHasStockCollect.get(skuInfo.getSkuId()));
}
skuEsModel.setHotScore(0L);
skuEsModel.setBrandId(brandId);
skuEsModel.setBrandImg(brandLogo);
skuEsModel.setBrandName(brandName);
skuEsModel.setCatalogName(categoryName);
skuEsModel.setAttrs(skuEsModelAttrsList);
return skuEsModel;
}).collect(Collectors.toList());

// 5 发给ES
R r = searchFeignService.productStatusUp(upSku);
if (r.getCode() == 0) {
// 远程调用成功
// 修改spu的状态为上架
SpuInfoEntity spu = new SpuInfoEntity();
spu.setId(spuId);
spu.setPublishStatus(ProductConstant.StatusEnum.SPU_UP.getCode());
this.updateById(spu);
} else {
// TODO 远程调用失败
}
}
}

WareFeignService

1
2
3
4
5
6
7
8
@FeignClient("gulimall-ware")
public interface WareFeignService {
/*
* 检查每个sku是否又库存
*/
@PostMapping("/ware/waresku/hasstock")
R getSkuHasStockList(@RequestBody List<Long> skuIdList);
}

SearchFeignService

1
2
3
4
5
@FeignClient("gulimall-search")
public interface SearchFeignService {
@PostMapping("/search/save/product")
R productStatusUp(@RequestBody List<SkuEsModel> skuEsModelList);
}

AttrDao

1
2
3
4
5
6
7
8
9
10
<!-- 在指定的集合中找到所有可以别全文检索的  -->
<select id="selectIdsWithSearchAttr" resultType="java.lang.Long">
SELECT attr_id
from gulimall_pms.pms_attr
WHERE attr_id IN
<foreach collection="attrIdList" item="id" separator="," open="(" close=")">
#{id}
</foreach>
AND search_type = 1
</select>

R

给R加上泛型,用于方便远程调用直接传输对应数据(别的方法也可以)

我不想写泛型,泛型也还是要加一个TO,直接传一个TO呗,我也不想知道R的其他信息

1
2
3
4
5
6
7
8
9
10
11
12
public class R<T> extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;

private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

gulimall-ware

WareSkuController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
@Autowired
private WareSkuService wareSkuService;
/*
* 检查每个sku是否又库存
*/
@PostMapping("/hasstock")
public List<SkuHasStockTo> getSkuHasStockList(@RequestBody List<Long> skuIdList) {
List<SkuHasStockTo> hasStock = wareSkuService.getSkuHasStockList(skuIdList);
return hasStock;
}
}

WareSkuServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 检查每个sku是否又库存
*/
@Override
public List<SkuHasStockTo> getSkuHasStockList(List<Long> skuIdList) {
List<SkuHasStockTo> voList = skuIdList.stream().map(skuId -> {
SkuHasStockTo skuHasStockVo = new SkuHasStockTo();
Long count = baseMapper.getSkuStock(skuId);
skuHasStockVo.setSkuId(skuId);
// 可能没有库存返回null
skuHasStockVo.setHasStock(count != null && count > 0);
return skuHasStockVo;
}).collect(Collectors.toList());
return voList;
}

WareSkuDao

1
2
3
4
5
<select id="getSkuStock" resultType="java.lang.Long">
SELECT SUM(stock - stock_locked)
FROM gulimall_wms.wms_ware_sku
WHERE sku_id = #{skuId};
</select>

ElasticSearchController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/search")
public class ElasticSearchController {

@Autowired
ElasticSearchService searchService;

/**
* 上架商品
*
* @param skuEsModelList
* @return
*/
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModelList) {
searchService.productUp(skuEsModelList);
return R.ok();
}

}

ElasticSearchServiceImpl

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
@Slf4j
@Service
public class ElasticSearchServiceImpl implements ElasticSearchService {

@Autowired
RestHighLevelClient client;

/**
* 上架商品
*
* @param skuEsModelList
* @return
*/
@Override
public Boolean productUp(List<SkuEsModel> skuEsModelList) throws IOException {
// 创建索引
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModelList) {
//构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(String.valueOf(skuEsModel.getSkuId()));
indexRequest.source(JSON.toJSONString(skuEsModel), XContentType.JSON);
// 保存请求添加到bulk中
bulkRequest.add(indexRequest);
}
// 执行批量操作
BulkResponse bulk = client.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
// TODO 如果有错误可以处理
boolean hasFailures = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(item -> {
return item.getId();
}).collect(Collectors.toList());
log.error("商品上架信息:{}", collect);
return hasFailures;
}
}

DEBUG

懒得写,主要是hasFailures返回值得判断,以及远程调用返回值(使用TO就不会,R泛型需要修改)

kibana -> GET product/_search查看是否有数据

跟着做顺畅,但是还是要再看一遍然后自己做

商城首页

整合themeleaf

震惊,竟然要用模板引擎,想念VUE的第一天

依赖

1
2
3
4
5
6
<!--  thymleaf模板引擎  -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>

配置

关闭缓存等

所有前台的controller统一存放在web文件夹下,而后台在app当中

1
2
3
springboot:
thymeleaf:
cache: false

静态资源

静态资源放在resources -> static下,模板页面放在templates文件夹下即可

也可以修改resources location配置

引入dev-tools

热部署

依赖

1
2
3
4
5
6
<!-- dev tool -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

配置

渲染一级分类

简直是文艺复兴了

以后thymeleaf的模板修改我就不PO了,麻烦

IndexController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller // 不适用RESTController
public class IndexController {
@Autowired
CategoryService categoryService;
@GetMapping({"/", "/index.html"})
public String indexPage(Model model) {
// 1 查出所有一级分类
List<CategoryEntity> categoryEntityList = categoryService.getLevel1List();

// thymeleaf已经有了默认前缀,定位到classpath:/resources/templates
// 默认后缀 html
model.addAttribute("categoryList", categoryEntityList);
return "index";
}
}

CategoryServiceImpl

1
2
3
4
5
6
7
8
/**
* 前台:渲染首页
*/
@Override
public List<CategoryEntity> getLevel1List() {
List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntityList;
}

index.html

1
2
3
4
5
6
7
8
9
10
11
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--轮播主体内容-->
<div class="header_main">
<div class="header_banner">
<div class="header_main_left">
<ul>
<li th:each="category : ${categoryList}">
<a href="/static/#" class="header_main_left_a" th:attr="ctg-data=${category.catId}"><b
th:text="${category.name}">家用电器</b></a>
</li>
</ul>

渲染子级分类

CategoryFrontVo

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
/*
* 二级分类VO
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Catalog2Vo {
// 这个属性名要和前端需要的JSON对应
private String catalog1Id; // 一级父分类id
private List<Catalog3Vo> catalog3List; // 三级分类(本vo为二级分类)
private String id;
private String name;

/*
* 三级分类VO
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
public static class Catalog3Vo {
private String catalog2Id;
private String id;
private String name;
}
}

IndexController

1
2
3
4
5
6
7
8
9
10
/*
* 渲染二级、三级分类
* 这里的路径与catalogLoader.js中发送请求的路径一致
*/
@ResponseBody // 返回值以JSON方式返回,且不需要跳转页面
@GetMapping("/index/catalog.json")
public Map<String, List<Catalog2Vo>> getCatalogJson(Model model) {
Map<String, List<Catalog2Vo>> category = categoryService.getCatalogJson();
return category;
}

CategoryServiceImpl

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
/*
* 渲染二级、三级分类
*/
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
List<CategoryEntity> level1List = this.getLevel1List();
// 封装数据
Map<String, List<Catalog2Vo>> catalogMap = level1List.stream().collect(Collectors.toMap(k -> {
return k.getCatId().toString();
}, parent -> {
// 将二级分类的内容封装到一级分类当中
List<CategoryEntity> children2List = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", parent.getCatId()));
List<Catalog2Vo> catalog2VoList = null;
if (children2List != null) {
catalog2VoList = children2List.stream().map(child2 -> {
Catalog2Vo catalog2Vo = new Catalog2Vo(parent.getCatId().toString(), null, child2.getCatId().toString(), child2.getName());
// 封装三级分类信息
List<CategoryEntity> children3List = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", child2.getCatId()));
if (children3List != null) {
List<Catalog2Vo.Catalog3Vo> catalog3VoList = children3List.stream().map(child3 -> {
Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(child2.getCatId().toString(), child3.getCatId().toString(), child3.getName());
return catalog3Vo;
}).collect(Collectors.toList());
catalog2Vo.setCatalog3List(catalog3VoList);
}
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2VoList;
}));
return catalogMap;
}

为啥老师这么喜欢嵌套?

优化子级分类

CategoryServiceImpl

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
/*
* 渲染二级、三级分类
*/
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
/*
* 优化
* 多次查询数据库 -》 一次查询
*/
// 一次查询数据库,获得所有的分类
List<CategoryEntity> allCatList = baseMapper.selectList(null);
// 获得一级分类
List<CategoryEntity> level1List = getCatListByParentCid(allCatList, 0L);
// 封装数据
Map<String, List<Catalog2Vo>> catalogMap = level1List.stream().collect(Collectors.toMap(k -> {
return k.getCatId().toString();
}, parent -> {
// 将二级分类的内容封装到一级分类当中
List<CategoryEntity> children2List = getCatListByParentCid(allCatList, parent.getCatId());
List<Catalog2Vo> catalog2VoList = null;
if (children2List != null) {
catalog2VoList = children2List.stream().map(child2 -> {
Catalog2Vo catalog2Vo = new Catalog2Vo(parent.getCatId().toString(), null, child2.getCatId().toString(), child2.getName());
// 封装三级分类信息
List<CategoryEntity> children3List = getCatListByParentCid(allCatList, child2.getCatId());
if (children3List != null) {
List<Catalog2Vo.Catalog3Vo> catalog3VoList = children3List.stream().map(child3 -> {
Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(child2.getCatId().toString(), child3.getCatId().toString(), child3.getName());
return catalog3Vo;
}).collect(Collectors.toList());
catalog2Vo.setCatalog3List(catalog3VoList);
}
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2VoList;
}));
return catalogMap;
}

/*
* 根据parentCid查询下级分类
*/
private List<CategoryEntity> getCatListByParentCid(List<CategoryEntity> allCatList, Long parentCid) {
// return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", parent.getCatId()));
List<CategoryEntity> collect = allCatList.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid().equals(parentCid);
}).collect(Collectors.toList());
return collect;
}

配置反向代理💡

正向代理:如科学上网,隐藏客户端信息

反向代理:屏蔽内网服务器信息,负载均衡访问

image-20211104134348607

配置host

C:\Windows\System32\drivers\etc\host 文件添加上地址

使用switchHost软件可以快速应用方案

192.168.128.129/192.168.128.129:80(nginx默认地址) -> gulimall.com

配置nginx

虚拟机外部挂载下的conf文件

/mydata/nginx/conf/nginx.conf

/mydata/nginx/conf/conf.d/default.conf

可以先复制有份conf在配置

image-20211104135932605

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http {
upstream gulimall{
server 192.168.128.1:88;
}
}

server {
listen 80;
server_name gulimall.com;

#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;

location / {
# 需要配置header,添加上host,因为nginx转发代理给网关的时候会丢失许多信息
proxy_set_header Host $host;
proxy_pass http://gulimall;
}

TODO 转不了为什么?nginx可以访问到,但是nginx转发不到本机,我感觉还是proxy_pass配置的问题,但是我每个ip都试过了?

我不理解:192.168.128.1:10000是可以的,http://192.168.128.129:5601也是可以的,虚拟机ping 192.168.128.1也是可以的?

image-20211104155128119

目测是本机 -> 虚拟机nginx是成功的,但是nginx -> 本机192.168.128.1:10000失败了

重点说明!!!以上问题已解决,是本机防火墙的原因,操作如下Windows 通过命令行关闭防火墙-百度经验 (baidu.com)

感谢:谷粒商城高级篇p139、p140Nginx反向代理,网关配置不生效解决_含雷的博客-CSDN博客(被ping通欺骗了,所以一开始排除了防火墙)

关掉本机的防火墙,虚拟机的nginx就能转发到本机了!!!

配置网关

1
2
3
4
5
# -----------------------nginx,一定要放在最后,要不然/api/下的配置会被覆盖-----------------------
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=**.gulimall.com,gulimall.com

总结一下实现:虚拟机192.168.128.129 通过修改win的host文件,相当于gulimall.com,192.168.128.1:10000 是本机商品服务的地址,现在需要通过请求nginx,然后nginx代理网关,网关再转发给对应服务地址,所以在虚拟机的nginx中配置,请求192.168.128.129:80(nginx端口80),本来是请求虚拟机的nginx,然后nginx配置80端口代理地址192.168.128.1:88(即本机的网关地址),然后网关配置**.gulimall.com转发到gulimall-product服务实现。

本机请求 -> 虚拟机nginx -> 本机网关 -> 本地服务地址

性能压测

压力测试

具体介绍见课件04

Jmeter

正好测试课要写测试工具报告

详细见测试工具报告

性能监控

堆内存与垃圾回收

详见P144

jvisualvm

命令行启动:jvisualvm

安装插件 visual gc(建议手动下载然后添加插件即可)

简单优化吞吐量

开启thymeleaf缓存,关闭日志打印,数据库添加索引

nginx动静分离

将静态资源存放在nginx里,所以静态请求直接从nginx里拿资源即可,不需要过网关到服务器Tomcat里拿

规定:/static/***下的所有请求由nginx直接返回

存放静态资源

将product服务的resource下的所有静态文件(static下)存放到nginx下的html文件夹下的static下(记录一下虚拟机移动文件 mv xxx xxx)

将html中的静态资源路径添加上/static/前缀

nginx配置

当访问/static下的资源时,要求nginx为我们请求nginx下的html文件夹下的static文件夹

1
2
3
location /static {
root /usr/share/nginx/html;
}

如果请求出现403的问题需要修改nginx文件夹的权限

chmod -R 777 /mydata/nginx/html

缓存

1优化业务逻辑 —> 2使用缓存

适用于访问量达、更新频率不高、及时性数据一致性要求不要的数据,极大提升系统性能

image-20211105154718024

在开发中, 凡是放入缓存中的数据我们都应该指定过期时间, 使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。 避免业务崩溃导致的数据永久不一致问题。

最直接的缓存:一个Map对象,是本地缓存
在分布式下,一个服务存在多个服务器,就会产生多个缓存,可能数据不一致

缓存中间件:redis

Redis

依赖

1
2
3
4
5
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

使用spring-boot-starter的redis,版本由父项目统一管理

配置

1
2
3
4
5
spring:
redis:
# redis地址
host: 192.168.128.129
port: 6379

测试

1
2
3
4
5
6
7
8
9
@Test
public void redis() {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
// 保存
valueOperations.set("hello", "hello_" + UUID.randomUUID());
// 查询
String hello = valueOperations.get("hello");
System.out.println("之前保存的数据是: " + hello);
}

可视化工具

RDM、ARDM(后者好看)

优化分类业务

CategoryServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 渲染二级、三级分类(使用缓存)
*/
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson() {
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
// 缓存中没有
// 查询并封装
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
// 放入缓存 (注意存入JSON字符串)
redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogJsonFromDb));
return catalogJsonFromDb;
}
// 从缓存中拿数据
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}

解决OutOfDirectMemoryError

  • TODO 产生堆外内存溢出:OutOfDirectMemoryError
  • 1)、springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
  • 2)、lettuce的bug导致netty堆外内存溢出 -Xmx300m;netty如果没有指定堆外内存,默认使用-Xmx300m
  • 可以通过-Dio.netty.maxDirectMemory进行设置
  • 解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存。
  • 1)、升级lettuce客户端。 2)、切换使用jedis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- 解决redis堆外内存溢出问题,升级lettuce的版本 -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>

缓存失效

  1. 缓存穿透
    • 缓存穿透是指查询一个一定不存在的数据, 由于缓存是不命中, 将去查询数据库, 但是数据库也无此记录, 我们没有将这次查询的 null 写入缓存, 这将导致这个不存在的数据每次请求都要到存储层去查询, 失去了缓存的意义。
    • 在流量大时, 可能 DB 就挂掉了, 要是有人利用不存在的 key 频繁攻击我们的应用, 这就是漏洞。
    • 解决:缓存空结果、 并且设置短的过期时间。
  2. 缓存雪崩
    • 缓存雪崩是指在我们设置缓存时采用了相同的过期时间, 导致缓存在某一时刻同时失
      效, 请求全部转发到 DB, DB 瞬时压力过重雪崩。
    • 解决:原有的失效时间基础上增加一个随机值, 比如 1-5 分钟随机, 这样每一个缓存的过期时间的重复率就会降低, 就很难引发集体失效的事件。
  3. 缓存击穿
    • 对于一些设置了过期时间的 key, 如果这些 key 可能会在某些时间点被超高并发地访问,
      是一种非常“热点”的数据
    • 这个时候, 需要考虑一个问题: 如果这个 key 在大量请求同时进来前正好失效, 那么所
      有对这个 key 的数据查询都落到 db, 我们称为缓存击穿。
    • 解决:加锁

Spring Cache

image-20211107144501288

依赖

1
2
3
4
5
<!-- spring cache简化缓存开发 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

配置

CacheAutoConfiguration 自动导入 RedisAutoConfiguration,内含 RedisCacheManager 缓存管理器

1
2
3
4
5
6
7
8
spring:	
# 缓存
cache:
type: redis
# redis
redis:
host: 192.168.128.129
port: 6379

注解

  1. @Cachable:将数据保存到缓存
  2. @CacheEvict:将数据从缓存删除
  3. @CachePut:保存(不影响方法执行)
  4. @Caching:组合多个缓存操作
  5. @CacheConfig:在类级别共享相同配置
  6. @EnableCaching:在启动类上,开启缓存

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 修改(级联更新,连表更新)
*/
@Caching(evict = {
@CacheEvict(cacheNames = "category", key = "'level1Category1'"), // 删除缓存
@CacheEvict(cacheNames = "category", key = "'level1Category2'")
})
@Override
@Transactional
public void updateDetail(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
/**
* 前台:渲染首页,一级分类
*/
@Cacheable({"category"}) // @Cacheable(缓存分区名),如果缓存中有则该方法不会被调用,否则会调用方法并将结果放入缓存
@Override
public List<CategoryEntity> getLevel1List() {
List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntityList;
}

自定义配置

默认

  1. key默认自动生成,名字为 分区名::SimpleKey []
  2. 缓存的value值默认使用jdk序列化机制,将序列化后的数据保存到redis
  3. 默认ttl时间 -1s

自定义

  1. 指定key:接收SpEL表达式(官网文档有解释各种写法)

    1
    @Cacheable(cacheNames = {"category"}, key = "'level1Category'")
  1. 指定存活时间:单位为毫秒

    1
    2
    3
    4
    cache:
    type: redis
    redis:
    time-to-live: 1000
  2. 指定JSON序列化

    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
    /*
    * 自定义配置spring cache
    */
    @Configuration
    @EnableCaching
    @EnableConfigurationProperties(CacheProperties.class)
    public class MyCacheConfig {

    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
    // 设置自定义配置,使用JSON序列化机制保存value
    config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
    config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    // 引入配置文件的配置
    // CacheProperties.Redis redisProperties = cacheProperties.getRedis();
    // if (redisProperties.getTimeToLive() != null) {
    // config = config.entryTtl(redisProperties.getTimeToLive());
    // }
    // if (redisProperties.getKeyPrefix() != null) {
    // config = config.prefixKeysWith(redisProperties.getKeyPrefix());
    // }
    // if (!redisProperties.isCacheNullValues()) {
    // config = config.disableCachingNullValues();
    // }
    // if (!redisProperties.isUseKeyPrefix()) {
    // config = config.disableKeyPrefix();
    // }
    // TODO 这里我一引入就会导致缓存用不了?
    return config;
    }
    }

分布式锁

image-20211107121704131

RedisTemplates实现

分布式锁演进

原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行

image-20211107123619623

image-20211107123526013

image-20211107123842030

image-20211107123853135

image-20211107123902077

代码实现

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
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1、 占分布式锁。 去 redis 占坑String uuid = UUID.randomUUID().toString();
Boolean lock =
redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
//2、 设置过期时间, 必须和加锁是同步的, 原子的
//redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new
DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
} //
获取值对比+对比成功删除=原子操作 lua 脚本解锁
// String lockValue = redisTemplate.opsForValue().get("lock");
// if(uuid.equals(lockValue)){
// //删除我自己的锁
// redisTemplate.delete("lock");//删除锁
// }
return dataFromDb;
}else {
//加锁失败...重试。 synchronized ()
//休眠 100ms 重试
System.out.println("获取分布式锁失败...等待重试");
try{
Thread.sleep(200);
}catch (Exception e){
} r
eturn getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}

Redisson

Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid) 。 充分的利用了 Redis 键值数据库提供的一系列优势, 基于 Java 实用工具包中常用接口, 为使用者提供了一系列具有分布式特性的常用工具类。 使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力, 大大降低了设计和研发大规模分布式系统的难度。 同时结合各富特色的分布式服务, 更进一步简化了分布式环境中程序相互之间的协作。

官方文档: https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

依赖

1
2
3
4
5
6
<!-- reddisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>

程序化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MyRedissonConfig {
/*
* 所有对redisson的使用都是通过client对象
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
// 创建配置
Config config = new Config();
// 单redis节点模式
config.useSingleServer().setAddress("redis://192.168.128.129:6379");
return Redisson.create(config);
}
}

前台-检索服务

搭建页面环境

需要向虚拟机传输文件,所有还是用xshel好使,教程在p122

静态资源

css,img等资源放在nginx的html文件夹下

index.html放在项目resource下的templates文件夹下(注意这里index名字改为list,因为首页搜索发请求地址就是search.gulimall.com/list.html

host配置

使用SwitchHost

192.168.128.129 gulimall.com
192.168.128.129 search.gulimall.com

nginx配置

添加server_name对应host即可

server_name  gulimall.com *.gulimall.com;
#charset koi8-r;
#access_log  /var/log/nginx/log/host.access.log  main;
location /static {
    root   /usr/share/nginx/html;
}
location / {
    proxy_set_header Host $host;
    proxy_pass http://gulimall;
}

gateway配置

1
2
3
4
5
6
7
8
9
# -----------------------nginx,一定要放在最后,要不然/api/下的配置会被覆盖-----------------------
- id: gulimall_host_route
uri: lb://gulimall-product
predicates:
- Host=gulimall.com
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com

根据之前nginx配置中的proxy_set_header Host $host,网关能根据host来选择不同的服务

list.html里面有很多路径错误,记得修改一下

修改一下发起请求的请求路径

检索模型分析

查询参数模型

image-20211111154722613

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 封装页面所有可能传递过来的条件
*/
@Data
public class SearchParamVo {
private String keyword; // 全文匹配关键字
private Long catalog3Id; // 三级分类id
/**
* sort=saleCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
*/
private String sort; // 排序条件
private Integer hasStock; // 是否有货
private String skuPrice; // 价格区间
private List<Long> brandId; // 品牌id 可多选
private List<String> attrs; // 按照属性进行筛选
private Integer pageNum; // 页码
}

返回结果模型

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
/**
* 检索返回数据模型
*/
@Data
public class SearchResultVo {
private List<SkuEsModel> products; //sku在elasticsearch中的模型

// 分页信息
private Integer pageNum; // 当前页码
private Long total; // 总记录数
private Integer totalPages; // 总页码

private List<BrandVo> brands; // 当前查询到的所有涉及到的品牌
private List<CatalogVo> catalogs; // 当前查询到的所有涉及到的分类
private List<AttrVo> attrs; // 当前查询到的所有涉及到的属性

/**
* 内部类,品牌信息
*/
@Data
public static class BrandVo {
private Long brandId;
private String brandName;
private String brandImg;

}

/**
* 内部类,属性信息
*/
@Data
public static class AttrVo {
private Long attrId;
private String attrName;
private List<String> attrValue;
}

/**
* 内部类,分类信息
*/
@Data
public static class CatalogVo {
private Long catalogId;
private String catalogName;
}

}

检索DSL测试

TODO P177、P178

十分劝退,头疼

SearchRequest

查询、排序

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
@Service
public class MallSearchServiceImpl implements MallSearchService {

@Autowired
private RestHighLevelClient client;

/*
* 根据keyword检索所有商品
*/
@Override
public SearchResultVo search(SearchParamVo searchParamVo) {
// 准备检索请求
SearchRequest searchRequest = buildSearchRequest(searchParamVo);
try {
// 执行检索请求
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
} catch (IOException e) {
e.printStackTrace();
}
// 分析响应数据并封装
SearchResult searchResult = buildSearchResult();


return null;
}

/*
* 创建检索结果
*/
private SearchResult buildSearchResult() {
return null;
}

/*
* 创建检索请求
*/
private SearchRequest buildSearchRequest(SearchParamVo param) {
// 指定检索条件,详见dsl.json
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 查询:模糊匹配,过滤(按照属性、分类、品牌、价格区间、库存)
// 1 query -> bool 开始
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 1.1 bool -> must 模糊匹配
// must -> match
if (!StringUtils.isEmpty(param.getKeyword())) {
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("skuTitle", param.getKeyword());
boolQuery.must(matchQuery);
}
// 1.2 bool -> filter 过滤
// filter -> 第一个term 按照分类过滤
if (param.getCatalog3Id() != null) {
TermQueryBuilder termQuery = QueryBuilders.termQuery("catalogId", param.getCatalog3Id());
boolQuery.filter(termQuery);
}
// filter -> 第二个terms 按照品牌过滤
if (param.getBrandId() != null) {
TermsQueryBuilder termsQuery = QueryBuilders.termsQuery("brandId", param.getBrandId());
boolQuery.filter(termsQuery);
}
// filter -> 第三个nested 按照属性过滤
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
// 操作每一个attr(每一个attr都有一个nested)
for (String attrStr : param.getAttrs()) {
// nested -> query -> bool
BoolQueryBuilder nestedBool = QueryBuilders.boolQuery();
// 页面提交格式:attr = 1_5寸:8寸
String[] strings = attrStr.split("_");
String attrId = strings[0];
String[] attrValues = strings[1].split(":");
// bool -> must -> term、terms
nestedBool.must(QueryBuilders.termQuery("attrs.attrId", attrId));
nestedBool.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
boolQuery.filter(QueryBuilders.nestedQuery("attrs", nestedBool, ScoreMode.None));
}
}

// filter -> 第四个term 按照库存过滤
if (param.getHasStock() != null) {
TermQueryBuilder termQuery = QueryBuilders.termQuery("hasStock", param.getHasStock() == 1);
boolQuery.filter(termQuery);
} else {
TermQueryBuilder termQuery = QueryBuilders.termQuery("hasStock", true);
boolQuery.filter(termQuery);
}
// filter -> 第四个range 按照价格区间过滤
if (!StringUtils.isEmpty(param.getSkuPrice())) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] priceRange = param.getSkuPrice().split("_");
if (param.getSkuPrice().startsWith("_")) {
// _xxx, 小于
rangeQuery.lte(priceRange[1]);
} else if (param.getSkuPrice().endsWith("_")) {
// xxx_, 大于
rangeQuery.gte(priceRange[0]);
} else {
// 区间
rangeQuery.gte(priceRange[0]).lte(priceRange[1]);
}
boolQuery.filter(rangeQuery);
}
searchSourceBuilder.query(boolQuery);

// 排序、分页、高亮
// 2 sort
if (!StringUtils.isEmpty(param.getSort())) {
String sort = param.getSort();
String[] sortValue = sort.split("_");
searchSourceBuilder.sort(sortValue[0], SortOrder.fromString(sortValue[1]));
}
// 2 from sieze
if (param.getPageNum() != null) {
searchSourceBuilder.from((param.getPageNum() - 1) * EsConstant.PAGE_SIZE);
} else {
searchSourceBuilder.from(0);
}
searchSourceBuilder.size(EsConstant.PAGE_SIZE);
// 2 highlight
if (!StringUtils.isEmpty(param.getKeyword())) {
searchSourceBuilder.highlighter(new HighlightBuilder().field("skuTitle").preTags("<b style='color:red'>").postTags("</b>"));
}

// TODO 聚合分析
// 3 aggs

String s = searchSourceBuilder.toString();
System.out.println("构建的DSL:" + s);


// 创建request,指定索引、检索条件
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
return searchRequest;
}
}

聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 聚合分析
// 3 aggs
// 3.1 品牌聚合
TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg").field("brandId").size(EsConstant.BRAND_SIZE);
// 3.1 子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
searchSourceBuilder.aggregation(brand_agg);
// 3.2 分类聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(EsConstant.CATALOG_SIZE);
// 3.2 子聚合
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
searchSourceBuilder.aggregation(catalog_agg);
// 3.3 属性聚合
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
// 3.3 子聚合
TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
// 3.3 子子聚合
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
attr_agg.subAggregation(attr_id_agg);
searchSourceBuilder.aggregation(attr_agg);

注意:如果使用文档的es模型映射规则,会有问题,详见本文档的商品上架的模型分析

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
86
87
88
89
/*
* 创建检索结果
*/
private SearchResultVo buildSearchResult(SearchResponse response, SearchParamVo param) {
SearchResultVo resultVo = new SearchResultVo();
SearchHits hits = response.getHits();

// 1 封装商品
List<SkuEsModel> skuList = new ArrayList<>();
if (hits.getHits() != null && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
// 获得hit中的resource,json转为对象
SkuEsModel sku = JSON.parseObject(hit.getSourceAsString(), SkuEsModel.class);
// 是否高亮
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightField highlightField = hit.getHighlightFields().get("skuTitle");
String highlight = highlightField.getFragments()[0].toString();
sku.setSkuTitle(highlight);
}
skuList.add(sku);
}
}
resultVo.setProducts(skuList);

// 2 封装属性聚合信息
List<SearchResultVo.AttrVo> attrVoList = new ArrayList<>();
// 获得聚合
ParsedNested attr_agg = response.getAggregations().get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResultVo.AttrVo attrVo = new SearchResultVo.AttrVo();
// 封装id,在聚合中
attrVo.setAttrId(Long.valueOf(String.valueOf(bucket.getKeyAsNumber())));
// 封装name,在子聚合中
ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
attrVo.setAttrName(attr_name_agg.getBuckets().get(0).getKeyAsString());
// 封装valueList,在子聚合中
List<String> attrValue = new ArrayList<>();
ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
for (Terms.Bucket aggBucket : attr_value_agg.getBuckets()) {
attrValue.add(aggBucket.getKeyAsString());
}
attrVo.setAttrValue(attrValue);
attrVoList.add(attrVo);
}
resultVo.setAttrs(attrVoList);

// 3 封装品牌聚合信息
List<SearchResultVo.BrandVo> brandVoList = new ArrayList<>();
// 获得聚合
ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
// 根据brandId有许多个bucket
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
// 封装brandId,在聚合中
SearchResultVo.BrandVo brandVo = new SearchResultVo.BrandVo();
brandVo.setBrandId(Long.valueOf(bucket.getKeyAsString()));
// 封装brandName\brandImg,在子聚合中
ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
brandVo.setBrandName(brand_name_agg.getBuckets().get(0).getKeyAsString());
ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
brandVo.setBrandImg(brand_img_agg.getBuckets().get(0).getKeyAsString());
brandVoList.add(brandVo);
}
resultVo.setBrands(brandVoList);

// 4 封装分类聚合信息
List<SearchResultVo.CatalogVo> catalogVoList = new ArrayList<>();
// 获得聚合
ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
// 根据catelogId有许多个bucket
for (Terms.Bucket bucket : catalog_agg.getBuckets()) {
// 封装CatalogId,在聚合中
SearchResultVo.CatalogVo catalogVo = new SearchResultVo.CatalogVo();
catalogVo.setCatalogId(Long.valueOf(bucket.getKeyAsString()));
// 封装CatalogName,在子聚合中
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
catalogVo.setCatalogName(catalog_name_agg.getBuckets().get(0).getKeyAsString());
catalogVoList.add(catalogVo);
}
resultVo.setCatalogs(catalogVoList);

// 5 封装分页信息
long total = hits.getTotalHits().value;
resultVo.setTotal(total);
resultVo.setTotalPages((int) ((total + EsConstant.PRODUCT_PAGE_SIZE - 1) / EsConstant.PRODUCT_PAGE_SIZE));
resultVo.setPageNum(param.getPageNum());

return resultVo;
}

wc,typora有毒,两天没了三次内容,而且未保存内容草稿也没用,东西都没了,我吐了,可能是内容太多了,新开一个

评论




🧡💛💚💙💜🖤🤍