1. 导入商品数据 1.1. 搭建搜索工程
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,到底如何保存到索引库?
先看京东搜索,输入“小米”搜索:
可以看到每条记录就是一个sku,再来看看每条记录需要哪些字段:
直观能看到:sku的默认图片、sku的价格、标题、skuId
排序及筛选字段:综合、新品、销量、价格、库存(是否有货)等
聚合字段:品牌、分类、搜索规格参数(多个)
最终可以构建一个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
把gmall-wms和gmall-pms这两个工程中对应的entity都copy过来,方便通用。原先工程对应的entity就不用了,例如gmall-pms:
这时,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接口:
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:
内容如下:
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 { PageParamVo pageParamVo = new PageParamVo (); pageParamVo.setPageNum(pageNum); pageParamVo.setPageSize(pageSize); ResponseVo<List<SpuEntity>> spuResp = this .pmsClient.querySpusByPage(pageParamVo); List<SpuEntity> spus = spuResp.getData(); spus.forEach(spuEntity -> { ResponseVo<List<SkuEntity>> skuResp = this .pmsClient.querySkusBySpuId(spuEntity.getId()); List<SkuEntity> skuEntities = skuResp.getData(); if (!CollectionUtils.isEmpty(skuEntities)){ List<Goods> goodsList = skuEntities.stream().map(skuEntity -> { Goods goods = new Goods (); 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()); } 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. 封装前端检索条件 参照京东:
参照京东,设计模型如下:
内容:
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 @Data public class SearchParamVo { private String keyword; private List<Long> brandId; private Long cid; private List<String> props; private Integer sort = 0 ; private Double priceFrom; private Double priceTo; private Integer pageNum = 1 ; private final Integer pageSize = 20 ; private Boolean store; }
2.3. 搜索的业务逻辑
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(); } } private SearchSourceBuilder buildDsl (SearchParamVo paramVo) { SearchSourceBuilder sourceBuilder = new SearchSourceBuilder (); String keyword = paramVo.getKeyword(); if (StringUtils.isEmpty(keyword)){ return null ; } BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(QueryBuilders.matchQuery("title" , keyword).operator(Operator.AND)); List<Long> brandId = paramVo.getBrandId(); if (!CollectionUtils.isEmpty(brandId)){ boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId" , brandId)); } Long cid = paramVo.getCid(); if (cid != null ) { boolQueryBuilder.filter(QueryBuilders.termQuery("categoryId" , cid)); } 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); } Boolean store = paramVo.getStore(); if (store != null ) { boolQueryBuilder.filter(QueryBuilders.termQuery("store" , store)); } 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); 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); Integer pageNum = paramVo.getPageNum(); Integer pageSize = paramVo.getPageSize(); sourceBuilder.from((pageNum - 1 ) * pageSize); sourceBuilder.size(pageSize); sourceBuilder.highlighter(new HighlightBuilder ().field("title" ).preTags("<font style='color:red'>" ).postTags("</font>" )); sourceBuilder.aggregation(AggregationBuilders.terms("brandIdAgg" ).field("brandId" ) .subAggregation(AggregationBuilders.terms("brandNameAgg" ).field("brandName" )) .subAggregation(AggregationBuilders.terms("logoAgg" ).field("logo" ))); sourceBuilder.aggregation(AggregationBuilders.terms("categoryIdAgg" ).field("categoryId" ) .subAggregation(AggregationBuilders.terms("categoryNameAgg" ).field("categoryName" ))); 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" )))); 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:
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 ; } 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 -> { String goodsJson = hitsHit.getSourceAsString(); Goods goods = JSON.parseObject(goodsJson, Goods.class); 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(); ParsedLongTerms brandIdAgg = (ParsedLongTerms)aggregationMap.get("brandIdAgg" ); List<? extends Terms .Bucket> buckets = brandIdAgg.getBuckets(); if (!CollectionUtils.isEmpty(buckets)){ List<BrandEntity> brands = buckets.stream().map(bucket -> { BrandEntity brandEntity = new BrandEntity (); 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()); ParsedStringTerms logoAgg = (ParsedStringTerms)brandAggregationMap.get("logoAgg" ); List<? extends Terms .Bucket> logoAggBuckets = logoAgg.getBuckets(); if (!CollectionUtils.isEmpty(logoAggBuckets)){ brandEntity.setLogo(logoAggBuckets.get(0 ).getKeyAsString()); } return brandEntity; }).collect(Collectors.toList()); responseVo.setBrands(brands); } ParsedLongTerms categoryIdAgg = (ParsedLongTerms)aggregationMap.get("categoryIdAgg" ); List<? extends Terms .Bucket> categoryIdAggBuckets = categoryIdAgg.getBuckets(); if (!CollectionUtils.isEmpty(categoryIdAggBuckets)){ List<CategoryEntity> categories = categoryIdAggBuckets.stream().map(bucket -> { CategoryEntity categoryEntity = new CategoryEntity (); 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); } 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 (); 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; } private SearchSourceBuilder buildDsl (SearchParamVo paramVo) { SearchSourceBuilder sourceBuilder = new SearchSourceBuilder (); String keyword = paramVo.getKeyword(); if (StringUtils.isEmpty(keyword)){ return null ; } BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(QueryBuilders.matchQuery("title" , keyword).operator(Operator.AND)); List<Long> brandId = paramVo.getBrandId(); if (!CollectionUtils.isEmpty(brandId)){ boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId" , brandId)); } Long cid = paramVo.getCid(); if (cid != null ) { boolQueryBuilder.filter(QueryBuilders.termQuery("categoryId" , cid)); } 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); } Boolean store = paramVo.getStore(); if (store != null ) { boolQueryBuilder.filter(QueryBuilders.termQuery("store" , store)); } 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); 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); Integer pageNum = paramVo.getPageNum(); Integer pageSize = paramVo.getPageSize(); sourceBuilder.from((pageNum - 1 ) * pageSize); sourceBuilder.size(pageSize); sourceBuilder.highlighter(new HighlightBuilder ().field("title" ).preTags("<font style='color:red'>" ).postTags("</font>" )); sourceBuilder.aggregation(AggregationBuilders.terms("brandIdAgg" ).field("brandId" ) .subAggregation(AggregationBuilders.terms("brandNameAgg" ).field("brandName" )) .subAggregation(AggregationBuilders.terms("logoAgg" ).field("logo" ))); sourceBuilder.aggregation(AggregationBuilders.terms("categoryIdAgg" ).field("categoryId" ) .subAggregation(AggregationBuilders.terms("categoryNameAgg" ).field("categoryName" ))); 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" )))); sourceBuilder.fetchSource(new String []{"skuId" , "title" , "price" , "defaultImage" }, null ); System.out.println(sourceBuilder.toString()); return sourceBuilder; } }