1. 导入商品数据
1.1. 搭建搜索工程
pom.xml内容如下:
<?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:
spring:
application:
name: search-service
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
application.yml:
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引导类:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class GmallSearchApplication {
public static void main(String[] args) {
SpringApplication.run(GmallSearchApplication.class, args);
}
}
网关路由:
- id: search-route
uri: lb://search-service
predicates:
- Host=search.gmall.com
1.2. 构建es数据模型
接下来,我们需要商品数据导入索引库,便于用户搜索。
那么问题来了,我们有SPU和SKU,到底如何保存到索引库?
先看京东搜索,输入“小米”搜索:
可以看到每条记录就是一个sku,再来看看每条记录需要哪些字段:
直观能看到:sku的默认图片、sku的价格、标题、skuId
排序及筛选字段:综合、新品、销量、价格、库存(是否有货)等
聚合字段:品牌、分类、搜索规格参数(多个)
最终可以构建一个Goods对象:(注意属性名需要提前和前端约定好,不能随便改)
@Data
@Document(indexName = "goods", type = "info", shards = 3, replicas = 2)
public class Goods {
// 搜索列表字段
@Id
private Long skuId;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
@Field(type = FieldType.Keyword, index = false)
private String subTitle;
@Field(type = FieldType.Keyword, index = false)
private String defaultImage;
@Field(type = FieldType.Double)
private Double price;
// 排序和筛选字段
@Field(type = FieldType.Long)
private Long sales = 0l; // 销量
@Field(type = FieldType.Date)
private Date createTime; // 新品
@Field(type = FieldType.Boolean)
private boolean store = false; // 是否有货
// 聚合字段
@Field(type = FieldType.Long)
private Long brandId;
@Field(type = FieldType.Keyword)
private String brandName;
@Field(type = FieldType.Keyword)
private String logo;
@Field(type = FieldType.Long)
private Long categoryId;
@Field(type = FieldType.Keyword)
private String categoryName;
@Field(type = FieldType.Nested)
private List<SearchAttrValue> searchAttrs;
}
搜索规格参数:SearchAttrValue
@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中添加方法:
@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中添加方法:
@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中添加接口方法:
List<SpuAttrValueEntity> querySearchAttrValueBySpuId(Long spuId);
在实现类SpuAttrValueServiceImpl中添加实现方法:
@Autowired
private SpuAttrValueMapper spuAttrValueMapper;
@Override
public List<SpuAttrValueEntity> querySearchAttrValueBySpuId(Long spuId) {
return this.spuAttrValueMapper.querySearchAttrValueBySpuId(spuId);
}
在SpuAttrValueMapper接口中添加接口方法:
@Mapper
public interface SpuAttrValueMapper extends BaseMapper<SpuAttrValueEntity> {
List<SpuAttrValueEntity> querySearchAttrValueBySpuId(Long spuId);
}
在SpuAttrValueMapper对应的映射文件中添加配置:
<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中添加方法:
@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中添加接口方法:
List<SkuAttrValueEntity> querySearchAttrValueBySkuId(Long skuId);
在实现类SkuAttrValueServiceImpl中添加实现方法:
@Autowired
private SkuAttrValueMapper skuAttrValueMapper;
@Override
public List<SkuAttrValueEntity> querySearchAttrValueBySkuId(Long skuId) {
return this.skuAttrValueMapper.querySearchAttrValueBySkuId(skuId);
}
在SkuAttrValueMapper接口中添加接口方法:
@Mapper
public interface SkuAttrValueMapper extends BaseMapper<SkuAttrValueEntity> {
List<SkuAttrValueEntity> querySearchAttrValueBySkuId(Long skuId);
}
在SpuAttrValueMapper对应的映射文件中添加配置:
<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
把gmall-wms和gmall-pms这两个工程中对应的entity都copy过来,方便通用。原先工程对应的entity就不用了,例如gmall-pms:
这时,gmall-pms的pom.xml中需要引入gmall-pms-interface的依赖。gmall-wms也是相同的处理方式。
GmallPmsApi:
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:
public interface GmallWmsApi {
@GetMapping("wms/waresku/sku/{skuId}")
public ResponseVo<List<WareSkuEntity>> queryWareSkuBySkuId(@PathVariable("skuId")Long skuId);
}
1.3.3. 使用feign调用远程接口
在gmall-search中引入依赖:
<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接口:
GmallPmsClient:
@FeignClient("pms-service")
public interface GmallPmsClient extends GmallPmsApi {
}
GmallWmsClient:
@FeignClient("wms-service")
public interface GmallWmsClient extends GmallWmsApi {
}
1.3.4. 导入数据
编写GoodsRepository:
内容如下:
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}
由于数据导入只需导入一次,这里就写一个测试用例。后续索引库和数据库的数据同步,通过程序自身来维护。
在测试用例中导入数据:
@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
GET /goods/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": {
"query": "手机",
"operator": "and"
}
}
}
],
"filter": [
{
"nested": {
"path": "searchAttrs",
"query": {
"bool": {
"must": [
{
"term": {
"searchAttrs.attrId": {
"value": "9"
}
}
},
{
"terms": {
"searchAttrs.attrValue": ["5","6","7"]
}
}
]
}
}
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"searchAttrs.attrId": {
"value": "4"
}
}
},
{
"terms": {
"searchAttrs.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. 封装前端检索条件
参照京东:
参照京东,设计模型如下:
内容:
/** * 接受页面传递过来的检索参数 * 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. 搜索的业务逻辑
ElasticSearchConfig:
@Configuration
public class ElasticsearchConfig {
@Bean
public RestHighLevelClient restHighLevelClient(){
return new RestHighLevelClient(RestClient.builder(HttpHost.create("172.16.116.100:9200")));
}
}
SearchController:
@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:
@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:高通-麒麟&props=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. 响应的数据模型
虽然实现了搜索的业务过程,但是,还没有对搜索后的结果进行封装。首先响应数据的数据模型参考笔记目录下的
copy到gmall-search工程的vo包下:
2.5. 完成搜索功能
封装之前,先启动搜索微服务,并在RestClient工具(例如:Postman)中访问,看看控制台打印的搜索结果
在地址栏访问:输入一个有数据的关键字条件
打印出的结果:放到jsonviewer
工具查看,发现数据结构跟kibana几乎一模一样。直接参考kibana即可完成对数据的封装。
最终完成代码,如下
SearchController:
@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:
@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;
}
}
今天的文章5.商品搜索分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/65552.html