1. 导入商品数据

1.1. 搭建搜索工程

1567922826266

1584799832037

1567924368985

pom.xml内容如下:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.atguigu</groupId>
<artifactId>gmall</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.atguigu</groupId>
<artifactId>gmall-search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gmall-search</name>
<description>谷粒商城搜索系统</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

bootstrap.yml:

1
2
3
4
5
6
7
spring:
application:
name: search-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848

application.yml:

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
server:
port: 18086
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
zipkin:
base-url: http://localhost:9411
discovery-client-enabled: false
sender:
type: web
sleuth:
sampler:
probability: 1
elasticsearch:
rest:
uris: http://172.16.116.100:9200
feign:
sentinel:
enabled: true
logging:
level:
com.atguigu.gmall: debug

GmallSearchApplication.java引导类:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class GmallSearchApplication {

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

}

网关路由:

1
2
3
4
- id: search-route
uri: lb://search-service
predicates:
- Host=search.gmall.com

1.2. 构建es数据模型

接下来,我们需要商品数据导入索引库,便于用户搜索。

那么问题来了,我们有SPU和SKU,到底如何保存到索引库?

先看京东搜索,输入“小米”搜索:

1567925466441

可以看到每条记录就是一个sku,再来看看每条记录需要哪些字段:

1567925782145

直观能看到:sku的默认图片、sku的价格、标题、skuId

1567926242184

排序及筛选字段:综合、新品、销量、价格、库存(是否有货)等

1567926434794

聚合字段:品牌、分类、搜索规格参数(多个)

最终可以构建一个Goods对象:(注意属性名需要提前和前端约定好,不能随便改)

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
@Document(indexName = "goods", type = "info", shards = 3, replicas = 2)
@Data
public class Goods {

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

@Field(type = FieldType.Long)
private Long sales; // 销量

@Field(type = FieldType.Boolean)
private Boolean store; // 是否有货

@Field(type = FieldType.Date)
private Date createTime; // 新品

@Field(type = FieldType.Long)
private Long brandId;
@Field(type = FieldType.Keyword)
private String brandName;
@Field(type = FieldType.Long)
private Long categoryId;
@Field(type = FieldType.Keyword)
private String categoryName;
@Field(type = FieldType.Nested)
private List<SearchAttrValue> attrs;

}

搜索规格参数:SearchAttrValue

1
2
3
4
5
6
7
8
9
10
@Data
public class SearchAttrValue {

@Field(type = FieldType.Long)
private Long attrId;
@Field(type = FieldType.Keyword)
private String attrName;
@Field(type = FieldType.Keyword)
private String attrValue;
}

“type”: “nested”

嵌套结构,防止数据扁平化问题。

参照官方文档:https://www.elastic.co/guide/cn/elasticsearch/guide/current/nested-objects.html

1.3. 批量导入

1.3.1. 数据接口

索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。

先思考我们需要的数据:

  • 分页查询已上架的SPU信息
  • 根据SpuId查询对应的SKU信息(接口已写好)
  • 根据分类id查询商品分类(逆向工程已自动生成)
  • 根据品牌id查询品牌(逆向工程已自动生成)
  • 根据skuid查询库存(gmall-wms中接口已写好)
  • 根据spuId查询检索规格参数及值
  • 根据skuId查询检索规格参数及值

大部分接口之前已经编写完成,接下来开发接口:

1.3.1.1. 分页查询已上架SPU

在gmall-pms的SpuController中添加方法:

1
2
3
4
5
6
@PostMapping("page")
public ResponseVo<List<SpuEntity>> querySpusByPage(@RequestBody PageParamVo pageParamVo){
PageResultVo page = spuService.queryPage(pageParamVo);
List<SpuEntity> list = (List<SpuEntity>)page.getList();
return ResponseVo.ok(list);
}

1.3.1.2. 根据spuId查询检索属性及值

在gmall-pms的SpuAttrValueController中添加方法:

1
2
3
4
5
6
7
@ApiOperation("根据spuId查询检索属性及值")
@GetMapping("spu/{spuId}")
public ResponseVo<List<SpuAttrValueEntity>> querySearchAttrValueBySpuId(@PathVariable("spuId")Long spuId){
List<SpuAttrValueEntity> attrValueEntities = spuAttrValueService.querySearchAttrValueBySpuId(spuId);

return ResponseVo.ok(attrValueEntities);
}

在SpuAttrValueService中添加接口方法:

1
List<SpuAttrValueEntity> querySearchAttrValueBySpuId(Long spuId);

在实现类SpuAttrValueServiceImpl中添加实现方法:

1
2
3
4
5
6
7
8
@Autowired
private SpuAttrValueMapper spuAttrValueMapper;

@Override
public List<SpuAttrValueEntity> querySearchAttrValueBySpuId(Long spuId) {

return this.spuAttrValueMapper.querySearchAttrValueBySpuId(spuId);
}

在SpuAttrValueMapper接口中添加接口方法:

1
2
3
4
5
@Mapper
public interface SpuAttrValueMapper extends BaseMapper<SpuAttrValueEntity> {

List<SpuAttrValueEntity> querySearchAttrValueBySpuId(Long spuId);
}

在SpuAttrValueMapper对应的映射文件中添加配置:

1
2
3
4
5
<select id="querySearchAttrValueBySpuId" resultType="SpuAttrValueEntity">
select a.id,a.attr_id,a.attr_name,a.attr_value,a.spu_id
from pms_spu_attr_value a INNER JOIN pms_attr b on a.attr_id=b.id
where a.spu_id=#{spuId} and b.search_type=1
</select>

1.3.1.3. 根据skuId查询检索属性及值

在gmall-pms的SkuAttrValueController中添加方法:

1
2
3
4
5
6
7
@ApiOperation("根据spuId查询检索属性及值")
@GetMapping("sku/{skuId}")
public ResponseVo<List<SkuAttrValueEntity>> querySearchAttrValueBySkuId(@PathVariable("skuId")Long skuId){
List<SkuAttrValueEntity> attrValueEntities = skuAttrValueService.querySearchAttrValueBySkuId(skuId);

return ResponseVo.ok(attrValueEntities);
}

在SkuAttrValueService中添加接口方法:

1
List<SkuAttrValueEntity> querySearchAttrValueBySkuId(Long skuId);

在实现类SkuAttrValueServiceImpl中添加实现方法:

1
2
3
4
5
6
7
@Autowired
private SkuAttrValueMapper skuAttrValueMapper;

@Override
public List<SkuAttrValueEntity> querySearchAttrValueBySkuId(Long skuId) {
return this.skuAttrValueMapper.querySearchAttrValueBySkuId(skuId);
}

在SkuAttrValueMapper接口中添加接口方法:

1
2
3
4
5
@Mapper
public interface SkuAttrValueMapper extends BaseMapper<SkuAttrValueEntity> {

List<SkuAttrValueEntity> querySearchAttrValueBySkuId(Long skuId);
}

在SpuAttrValueMapper对应的映射文件中添加配置:

1
2
3
4
5
<select id="querySearchAttrValueBySkuId" resultType="SkuAttrValueEntity">
select a.id,a.attr_id,a.attr_name,a.attr_value,a.sku_id
from pms_sku_attr_value a INNER JOIN pms_attr b on a.attr_id=b.id
where a.sku_id=#{skuId} and b.search_type=1
</select>

1.3.2. 编写接口工程

参考gmall-sms-interface,创建gmall-pms-interface及gmall-wms-interface

1584806325178

1584806364604

把gmall-wms和gmall-pms这两个工程中对应的entity都copy过来,方便通用。原先工程对应的entity就不用了,例如gmall-pms:

1567950435754

这时,gmall-pms的pom.xml中需要引入gmall-pms-interface的依赖。gmall-wms也是相同的处理方式。

GmallPmsApi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface GmallPmsApi {

@PostMapping("pms/spu/page")
public ResponseVo<List<SpuEntity>> querySpusByPage(@RequestBody PageParamVo pageParamVo);

@GetMapping("pms/sku/spu/{spuId}")
public ResponseVo<List<SkuEntity>> querySkusBySpuId(@PathVariable("spuId")Long spuId);

@GetMapping("pms/category/{id}")
public ResponseVo<CategoryEntity> queryCategoryById(@PathVariable("id") Long id);

@GetMapping("pms/brand/{id}")
public ResponseVo<BrandEntity> queryBrandById(@PathVariable("id") Long id);

@GetMapping("pms/spuattrvalue/spu/{spuId}")
public ResponseVo<List<SpuAttrValueEntity>> querySearchAttrValueBySpuId(@PathVariable("spuId")Long spuId);

@GetMapping("pms/skuattrvalue/sku/{skuId}")
public ResponseVo<List<SkuAttrValueEntity>> querySearchAttrValueBySkuId(@PathVariable("skuId")Long skuId);
}

GmallWmsApi:

1
2
3
4
5
public interface GmallWmsApi {

@GetMapping("wms/waresku/sku/{skuId}")
public ResponseVo<List<WareSkuEntity>> queryWareSkuBySkuId(@PathVariable("skuId")Long skuId);
}

1.3.3. 使用feign调用远程接口

在gmall-search中引入依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-pms-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>gmall-wms-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

在gmall-search中添加feign接口:

1584807796338

GmallPmsClient:

1
2
3
4
@FeignClient("pms-service")
public interface GmallPmsClient extends GmallPmsApi {

}

GmallWmsClient:

1
2
3
@FeignClient("wms-service")
public interface GmallWmsClient extends GmallWmsApi {
}

1.3.4. 导入数据

编写GoodsRepository:

1577432548337

内容如下:

1
2
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}

由于数据导入只需导入一次,这里就写一个测试用例。后续索引库和数据库的数据同步,通过程序自身来维护。

在测试用例中导入数据:

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
@Test
void importData(){

// 创建索引及映射
this.restTemplate.createIndex(Goods.class);
this.restTemplate.putMapping(Goods.class);

Integer pageNum = 1;
Integer pageSize = 100;

do {
// 分页查询spu
PageParamVo pageParamVo = new PageParamVo();
pageParamVo.setPageNum(pageNum);
pageParamVo.setPageSize(pageSize);
ResponseVo<List<SpuEntity>> spuResp = this.pmsClient.querySpusByPage(pageParamVo);
List<SpuEntity> spus = spuResp.getData();

// 遍历spu,查询sku
spus.forEach(spuEntity -> {
ResponseVo<List<SkuEntity>> skuResp = this.pmsClient.querySkusBySpuId(spuEntity.getId());
List<SkuEntity> skuEntities = skuResp.getData();
if (!CollectionUtils.isEmpty(skuEntities)){
// 把sku转化成goods对象
List<Goods> goodsList = skuEntities.stream().map(skuEntity -> {
Goods goods = new Goods();

// 查询spu搜索属性及值
ResponseVo<List<SpuAttrValueEntity>> attrValueResp = this.pmsClient.querySearchAttrValueBySpuId(spuEntity.getId());
List<SpuAttrValueEntity> attrValueEntities = attrValueResp.getData();
List<SearchAttrValue> searchAttrValues = new ArrayList<>();
if (!CollectionUtils.isEmpty(attrValueEntities)) {
searchAttrValues = attrValueEntities.stream().map(spuAttrValueEntity -> {
SearchAttrValue searchAttrValue = new SearchAttrValue();
searchAttrValue.setAttrId(spuAttrValueEntity.getAttrId());
searchAttrValue.setAttrName(spuAttrValueEntity.getAttrName());
searchAttrValue.setAttrValue(spuAttrValueEntity.getAttrValue());
return searchAttrValue;
}).collect(Collectors.toList());
}
// 查询sku搜索属性及值
ResponseVo<List<SkuAttrValueEntity>> skuAttrValueResp = this.pmsClient.querySearchAttrValueBySkuId(spuEntity.getId());
List<SkuAttrValueEntity> skuAttrValueEntities = skuAttrValueResp.getData();
List<SearchAttrValue> searchSkuAttrValues = new ArrayList<>();
if (!CollectionUtils.isEmpty(skuAttrValueEntities)) {
searchSkuAttrValues = skuAttrValueEntities.stream().map(skuAttrValueEntity -> {
SearchAttrValue searchAttrValue = new SearchAttrValue();
searchAttrValue.setAttrId(skuAttrValueEntity.getAttrId());
searchAttrValue.setAttrName(skuAttrValueEntity.getAttrName());
searchAttrValue.setAttrValue(skuAttrValueEntity.getAttrValue());
return searchAttrValue;
}).collect(Collectors.toList());
}
searchAttrValues.addAll(searchSkuAttrValues);
goods.setAttrs(searchAttrValues);

// 查询品牌
ResponseVo<BrandEntity> brandEntityResp = this.pmsClient.queryBrandById(skuEntity.getBrandId());
BrandEntity brandEntity = brandEntityResp.getData();
if (brandEntity != null){
goods.setBrandId(skuEntity.getBrandId());
goods.setBrandName(brandEntity.getName());
}

// 查询分类
ResponseVo<CategoryEntity> categoryEntityResp = this.pmsClient.queryCategoryById(skuEntity.getCatagoryId());
CategoryEntity categoryEntity = categoryEntityResp.getData();
if (categoryEntity != null) {
goods.setCategoryId(skuEntity.getCatagoryId());
goods.setCategoryName(categoryEntity.getName());
}

goods.setCreateTime(spuEntity.getCreateTime());
goods.setPic(skuEntity.getDefaultImage());
goods.setPrice(skuEntity.getPrice().doubleValue());
goods.setSales(0l);
goods.setSkuId(skuEntity.getId());

// 查询库存信息
ResponseVo<List<WareSkuEntity>> listResp = this.wmsClient.queryWareSkuBySkuId(skuEntity.getId());
List<WareSkuEntity> wareSkuEntities = listResp.getData();
if (!CollectionUtils.isEmpty(wareSkuEntities)) {
boolean flag = wareSkuEntities.stream().anyMatch(wareSkuEntity -> wareSkuEntity.getStock() > 0);
goods.setStore(flag);
}
goods.setTitle(skuEntity.getTitle());
return goods;
}).collect(Collectors.toList());

this.goodsRepository.saveAll(goodsList);
}
});

// 导入索引库

pageSize = spus.size();
pageNum++;

} while (pageSize == 100);

}

2. 基本检索

前端根据用户输入的查询、过滤、排序、分页条件等,后台生成DSL,查询出最终结果响应给前端,渲染页面展示给用户。

2.1. 编写完整的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
148
149
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": {
"query": "手机",
"operator": "and"
}
}
}
],
"filter": [
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "9"
}
}
},
{
"terms": {
"attrs.attrValue": ["5","6","7"]
}
}
]
}
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "4"
}
}
},
{
"terms": {
"attrs.attrValue": ["8G", "12G"]
}
}
]
}
}
}
},
{
"terms": {
"brandId": [1,2,3]
}
},
{
"terms": {
"categoryId": [225]
}
},
{
"range": {
"price": {
"gte": 0,
"lte": 10000
}
}
}
]
}
},
"from": 0,
"size": 10,
"highlight": {
"fields": {
"name": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"sort": [
{
"price": {
"order": "desc"
}
}
],
"aggs": {
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggs": {
"attrIdAgg": {
"terms": {
"field": "attrs.attrId"
},
"aggs": {
"attrNameAgg": {
"terms": {
"field": "attrs.attrName"
}
},
"attrValueAgg": {
"terms": {
"field": "attrs.attrValue"
}
}
}
}
}
},
"brandIdAgg": {
"terms": {
"field": "brandId"
},
"aggs": {
"brandNameAgg": {
"terms": {
"field": "brandName"
}
}
}
},
"categoryIdAgg": {
"terms": {
"field": "categoryId"
},
"aggs": {
"categoryNameAgg": {
"terms": {
"field": "categoryName"
}
}
}
}
}
}

2.2. 封装前端检索条件

参照京东:

1568017699170

参照京东,设计模型如下:

1589180479443

内容:

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
/**
* 接受页面传递过来的检索参数
* search?keyword=小米&brandId=1,3&cid=225&props=5:高通-麒麟&props=6:骁龙865-硅谷1000&sort=1&priceFrom=1000&priceTo=6000&pageNum=1&store=true
*
*/
@Data
public class SearchParamVo {

private String keyword; // 检索条件

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

private Long cid; // 分类过滤

// props=5:高通-麒麟,6:骁龙865-硅谷1000
private List<String> props; // 过滤的检索参数

private Integer sort = 0;// 排序字段:0-默认,得分降序;1-按价格升序;2-按价格降序;3-按创建时间降序;4-按销量降序

// 价格区间
private Double priceFrom;
private Double priceTo;

private Integer pageNum = 1; // 页码
private final Integer pageSize = 20; // 每页记录数

private Boolean store; // 是否有货
}

2.3. 搜索的业务逻辑

1577432765142

ElasticSearchConfig:

1
2
3
4
5
6
7
8
@Configuration
public class ElasticsearchConfig {

@Bean
public RestHighLevelClient restHighLevelClient(){
return new RestHighLevelClient(RestClient.builder(HttpHost.create("172.16.116.100:9200")));
}
}

SearchController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("search")
public class SearchController {

@Autowired
private SearchService searchService;

@GetMapping
public Resp<Object> search(SearchParamVo searchParam) throws IOException {

this.searchService.search(searchParam);
return Resp.ok(null);
}
}

SearchService:

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
@Service
public class SearchService {

@Autowired
private RestHighLevelClient restHighLevelClient;

public void search(SearchParamVo paramVo) {
try {
SearchRequest searchRequest = new SearchRequest(new String[]{"goods"}, buildDsl(paramVo));
SearchResponse response = this.restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

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

/**
* 构建查询DSL语句
* @return
*/
private SearchSourceBuilder buildDsl(SearchParamVo paramVo) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

String keyword = paramVo.getKeyword();
if (StringUtils.isEmpty(keyword)){
// 打广告,TODO
return null;
}

// 1. 构建查询条件(bool查询)
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 1.1. 匹配查询
boolQueryBuilder.must(QueryBuilders.matchQuery("title", keyword).operator(Operator.AND));
// 1.2. 过滤
// 1.2.1. 品牌过滤
List<Long> brandId = paramVo.getBrandId();
if (!CollectionUtils.isEmpty(brandId)){
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", brandId));
}
// 1.2.2. 分类过滤
Long cid = paramVo.getCid();
if (cid != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("categoryId", cid));
}

// 1.2.3. 价格区间过滤
Double priceFrom = paramVo.getPriceFrom();
Double priceTo = paramVo.getPriceTo();
if (priceFrom != null || priceTo != null){
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (priceFrom != null){
rangeQuery.gte(priceFrom);
}
if (priceTo != null) {
rangeQuery.lte(priceTo);
}
boolQueryBuilder.filter(rangeQuery);
}

// 1.2.4. 是否有货
Boolean store = paramVo.getStore();
if (store != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("store", store));
}

// 1.2.5. 规格参数的过滤 props=5:高通-麒麟,6:骁龙865-硅谷1000
List<String> props = paramVo.getProps();
if (!CollectionUtils.isEmpty(props)){
props.forEach(prop -> {
String[] attrs = StringUtils.split(prop, ":");
if (attrs != null && attrs.length == 2) {
String attrId = attrs[0];
String attrValueString = attrs[1];
String[] attrValues = StringUtils.split(attrValueString, "-");

BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("searchAttrs.attrId", attrId));
boolQuery.must(QueryBuilders.termsQuery("searchAttrs.attrValue", attrValues));
boolQueryBuilder.filter(QueryBuilders.nestedQuery("searchAttrs", boolQuery, ScoreMode.None));
}
});
}
sourceBuilder.query(boolQueryBuilder);

// 2. 构建排序 0-默认,得分降序;1-按价格升序;2-按价格降序;3-按创建时间降序;4-按销量降序
Integer sort = paramVo.getSort();
String field = "";
SortOrder order = null;
switch (sort){
case 1: field = "price"; order = SortOrder.ASC; break;
case 2: field = "price"; order = SortOrder.DESC; break;
case 3: field = "createTime"; order = SortOrder.DESC; break;
case 4: field = "sales"; order = SortOrder.DESC; break;
default: field = "_score"; order = SortOrder.DESC; break;
}
sourceBuilder.sort(field, order);

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

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

// 5. 构建聚合
// 5.1. 构建品牌聚合
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", "price", "defaultImage"}, null);

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

2.4. 响应的数据模型

虽然实现了搜索的业务过程,但是,还没有对搜索后的结果进行封装。首先响应数据的数据模型参考笔记目录下的

1584880572262

copy到gmall-search工程的vo包下:

1584880536717

2.5. 完成搜索功能

封装之前,先启动搜索微服务,并在RestClient工具(例如:Postman)中访问,看看控制台打印的搜索结果

在地址栏访问:输入一个有数据的关键字条件

1568041357992

打印出的结果:放到jsonviewer工具查看,发现数据结构跟kibana几乎一模一样。直接参考kibana即可完成对数据的封装。

1568047477761

最终完成代码,如下

SearchController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("search")
public class SearchController {

@Autowired
private SearchService searchService;

@GetMapping
public ResponseVo<SearchResponseVo> search(SearchParam searchParam) throws IOException {

SearchResponseVo responseVO = this.searchService.search(searchParam);
return ResponseVo.ok(responseVO);
}
}

SearchService:

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
@Service
public class SearchService {

@Autowired
private RestHighLevelClient restHighLevelClient;

public SearchResponseVo search(SearchParamVo paramVo) {

try {
// 构建查询条件
SearchRequest searchRequest = new SearchRequest(new String[]{"goods"}, this.buildDsl(paramVo));
// 执行查询
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 解析结果集
SearchResponseVo responseVo = this.parseResult(searchResponse);
responseVo.setPageNum(paramVo.getPageNum());
responseVo.setPageSize(paramVo.getPageSize());
return responseVo;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

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

SearchHits hits = response.getHits();
// 总命中的记录数
responseVo.setTotal(hits.getTotalHits());

SearchHit[] hitsHits = hits.getHits();
List<Goods> goodsList = Stream.of(hitsHits).map(hitsHit -> {
// 获取内层hits的_source 数据
String goodsJson = hitsHit.getSourceAsString();
// 反序列化为goods对象
Goods goods = JSON.parseObject(goodsJson, Goods.class);

// 获取高亮的title覆盖掉普通title
Map<String, HighlightField> highlightFields = hitsHit.getHighlightFields();
HighlightField highlightField = highlightFields.get("title");
String highlightTitle = highlightField.getFragments()[0].toString();
goods.setTitle(highlightTitle);
return goods;
}).collect(Collectors.toList());
responseVo.setGoodsList(goodsList);

// 聚合结果集的解析
Map<String, Aggregation> aggregationMap = response.getAggregations().asMap();
// 1. 解析聚合结果集,获取品牌》
// {attrId: null, attrName: "品牌", attrValues: [{id: 1, name: 尚硅谷, logo: http://www.atguigu.com/logo.gif}, {}]}
ParsedLongTerms brandIdAgg = (ParsedLongTerms)aggregationMap.get("brandIdAgg");
List<? extends Terms.Bucket> buckets = brandIdAgg.getBuckets();
if (!CollectionUtils.isEmpty(buckets)){
List<BrandEntity> brands = buckets.stream().map(bucket -> { // {id: 1, name: 尚硅谷, logo: http://www.atguigu.com/logo.gif}
// 为了得到指定格式的json字符串,创建了一个map
BrandEntity brandEntity = new BrandEntity();
// 获取brandIdAgg中的key,这个key就是品牌的id
Long brandId = ((Terms.Bucket) bucket).getKeyAsNumber().longValue();
brandEntity.setId(brandId);
// 解析品牌名称的子聚合,获取品牌名称
Map<String, Aggregation> brandAggregationMap = ((Terms.Bucket) bucket).getAggregations().asMap();
ParsedStringTerms brandNameAgg = (ParsedStringTerms)brandAggregationMap.get("brandNameAgg");
brandEntity.setName(brandNameAgg.getBuckets().get(0).getKeyAsString());
// 解析品牌logo的子聚合,获取品牌 的logo
ParsedStringTerms logoAgg = (ParsedStringTerms)brandAggregationMap.get("logoAgg");
List<? extends Terms.Bucket> logoAggBuckets = logoAgg.getBuckets();
if (!CollectionUtils.isEmpty(logoAggBuckets)){
brandEntity.setLogo(logoAggBuckets.get(0).getKeyAsString());
}
// 把map反序列化为json字符串
return brandEntity;
}).collect(Collectors.toList());
responseVo.setBrands(brands);
}

// 2. 解析聚合结果集,获取分类
ParsedLongTerms categoryIdAgg = (ParsedLongTerms)aggregationMap.get("categoryIdAgg");
List<? extends Terms.Bucket> categoryIdAggBuckets = categoryIdAgg.getBuckets();
if (!CollectionUtils.isEmpty(categoryIdAggBuckets)){
List<CategoryEntity> categories = categoryIdAggBuckets.stream().map(bucket -> { // {id: 225, name: 手机}
CategoryEntity categoryEntity = new CategoryEntity();
// 获取bucket的key,key就是分类的id
Long categoryId = ((Terms.Bucket) bucket).getKeyAsNumber().longValue();
categoryEntity.setId(categoryId);
// 解析分类名称的子聚合,获取分类名称
ParsedStringTerms categoryNameAgg = (ParsedStringTerms)((Terms.Bucket) bucket).getAggregations().get("categoryNameAgg");
categoryEntity.setName(categoryNameAgg.getBuckets().get(0).getKeyAsString());
return categoryEntity;
}).collect(Collectors.toList());
responseVo.setCategories(categories);
}

// 3. 解析聚合结果集,获取规格参数
ParsedNested attrAgg = (ParsedNested)aggregationMap.get("attrAgg");
ParsedLongTerms attrIdAgg = (ParsedLongTerms)attrAgg.getAggregations().get("attrIdAgg");
List<? extends Terms.Bucket> attrIdAggBuckets = attrIdAgg.getBuckets();
if (!CollectionUtils.isEmpty(attrIdAggBuckets)) {
List<SearchResponseAttrVo> filters = attrIdAggBuckets.stream().map(bucket -> {
SearchResponseAttrVo responseAttrVo = new SearchResponseAttrVo();
// 规格参数id
responseAttrVo.setAttrId(((Terms.Bucket) bucket).getKeyAsNumber().longValue());
// 规格参数的名称
ParsedStringTerms attrNameAgg = (ParsedStringTerms)((Terms.Bucket) bucket).getAggregations().get("attrNameAgg");
responseAttrVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString());
// 规格参数值
ParsedStringTerms attrValueAgg = (ParsedStringTerms)((Terms.Bucket) bucket).getAggregations().get("attrValueAgg");
List<? extends Terms.Bucket> attrValueAggBuckets = attrValueAgg.getBuckets();
if (!CollectionUtils.isEmpty(attrValueAggBuckets)){
List<String> attrValues = attrValueAggBuckets.stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());
responseAttrVo.setAttrValues(attrValues);
}
return responseAttrVo;
}).collect(Collectors.toList());
responseVo.setFilters(filters);
}

return responseVo;
}

/**
* 构建查询DSL语句
* @return
*/
private SearchSourceBuilder buildDsl(SearchParamVo paramVo) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

String keyword = paramVo.getKeyword();
if (StringUtils.isEmpty(keyword)){
// 打广告,TODO
return null;
}

// 1. 构建查询条件(bool查询)
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 1.1. 匹配查询
boolQueryBuilder.must(QueryBuilders.matchQuery("title", keyword).operator(Operator.AND));
// 1.2. 过滤
// 1.2.1. 品牌过滤
List<Long> brandId = paramVo.getBrandId();
if (!CollectionUtils.isEmpty(brandId)){
boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", brandId));
}
// 1.2.2. 分类过滤
Long cid = paramVo.getCid();
if (cid != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("categoryId", cid));
}

// 1.2.3. 价格区间过滤
Double priceFrom = paramVo.getPriceFrom();
Double priceTo = paramVo.getPriceTo();
if (priceFrom != null || priceTo != null){
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (priceFrom != null){
rangeQuery.gte(priceFrom);
}
if (priceTo != null) {
rangeQuery.lte(priceTo);
}
boolQueryBuilder.filter(rangeQuery);
}

// 1.2.4. 是否有货
Boolean store = paramVo.getStore();
if (store != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("store", store));
}

// 1.2.5. 规格参数的过滤 props=5:高通-麒麟,6:骁龙865-硅谷1000
List<String> props = paramVo.getProps();
if (!CollectionUtils.isEmpty(props)){
props.forEach(prop -> {
String[] attrs = StringUtils.split(prop, ":");
if (attrs != null && attrs.length == 2) {
String attrId = attrs[0];
String attrValueString = attrs[1];
String[] attrValues = StringUtils.split(attrValueString, "-");

BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.must(QueryBuilders.termQuery("searchAttrs.attrId", attrId));
boolQuery.must(QueryBuilders.termsQuery("searchAttrs.attrValue", attrValues));
boolQueryBuilder.filter(QueryBuilders.nestedQuery("searchAttrs", boolQuery, ScoreMode.None));
}
});
}
sourceBuilder.query(boolQueryBuilder);

// 2. 构建排序 0-默认,得分降序;1-按价格升序;2-按价格降序;3-按创建时间降序;4-按销量降序
Integer sort = paramVo.getSort();
String field = "";
SortOrder order = null;
switch (sort){
case 1: field = "price"; order = SortOrder.ASC; break;
case 2: field = "price"; order = SortOrder.DESC; break;
case 3: field = "createTime"; order = SortOrder.DESC; break;
case 4: field = "sales"; order = SortOrder.DESC; break;
default: field = "_score"; order = SortOrder.DESC; break;
}
sourceBuilder.sort(field, order);

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

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

// 5. 构建聚合
// 5.1. 构建品牌聚合
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", "price", "defaultImage"}, null);

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