尚硅谷2020微服务分布式电商项目《谷粒商城》-商品搜索

    技术2022-07-12  77

    关注公众号:java星星 获取全套课件资料

    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对象:(注意属性名需要提前和前端约定好,不能随便改)

    @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

    @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": "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. 封装前端检索条件

    参照京东:

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

    内容:

    /** * 接受页面传递过来的检索参数 * 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:高通-麒麟,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; } }
    Processed: 0.016, SQL: 9