1.导入商品数据

1.1. 搭建搜索工程

6.商品搜索 - 图1

6.商品搜索 - 图2

6.商品搜索 - 图3

pom.xml内容如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <parent>
  6. <groupId>com.atguigu</groupId>
  7. <artifactId>gmall</artifactId>
  8. <version>0.0.1-SNAPSHOT</version>
  9. </parent>
  10. <groupId>com.atguigu</groupId>
  11. <artifactId>gmall-search</artifactId>
  12. <version>0.0.1-SNAPSHOT</version>
  13. <name>gmall-search</name>
  14. <description>谷粒商城搜索系统</description>
  15. <properties>
  16. <java.version>1.8</java.version>
  17. </properties>
  18. <dependencies>
  19. <dependency>
  20. <groupId>com.atguigu</groupId>
  21. <artifactId>gmall-common</artifactId>
  22. <version>0.0.1-SNAPSHOT</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.springframework.boot</groupId>
  26. <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.elasticsearch.client</groupId>
  30. <artifactId>elasticsearch-rest-high-level-client</artifactId>
  31. <version>6.8.1</version>
  32. </dependency>
  33. <dependency>
  34. <groupId>org.springframework.boot</groupId>
  35. <artifactId>spring-boot-starter-web</artifactId>
  36. </dependency>
  37. <dependency>
  38. <groupId>com.alibaba.cloud</groupId>
  39. <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  40. </dependency>
  41. <dependency>
  42. <groupId>com.alibaba.cloud</groupId>
  43. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  44. </dependency>
  45. <dependency>
  46. <groupId>com.alibaba.cloud</groupId>
  47. <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
  48. </dependency>
  49. <dependency>
  50. <groupId>org.springframework.cloud</groupId>
  51. <artifactId>spring-cloud-starter-openfeign</artifactId>
  52. </dependency>
  53. <dependency>
  54. <groupId>org.springframework.cloud</groupId>
  55. <artifactId>spring-cloud-starter-zipkin</artifactId>
  56. </dependency>
  57. <dependency>
  58. <groupId>org.springframework.boot</groupId>
  59. <artifactId>spring-boot-starter-test</artifactId>
  60. <scope>test</scope>
  61. <exclusions>
  62. <exclusion>
  63. <groupId>org.junit.vintage</groupId>
  64. <artifactId>junit-vintage-engine</artifactId>
  65. </exclusion>
  66. </exclusions>
  67. </dependency>
  68. </dependencies>
  69. <build>
  70. <plugins>
  71. <plugin>
  72. <groupId>org.springframework.boot</groupId>
  73. <artifactId>spring-boot-maven-plugin</artifactId>
  74. </plugin>
  75. </plugins>
  76. </build>
  77. </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,到底如何保存到索引库?

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

6.商品搜索 - 图4

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

6.商品搜索 - 图5

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

6.商品搜索 - 图6

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

6.商品搜索 - 图7

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

最终可以构建一个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

6.商品搜索 - 图8

6.商品搜索 - 图9

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

6.商品搜索 - 图10

这时,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接口:

6.商品搜索 - 图11

GmallPmsClient:

@FeignClient("pms-service")
public interface GmallPmsClient extends GmallPmsApi {

}

GmallWmsClient:

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

1.3.4. 导入数据

编写GoodsRepository:

6.商品搜索 - 图12

内容如下:

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

参照京东:

6.商品搜索 - 图13

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

6.商品搜索 - 图14

内容:

/**
 * 接受页面传递过来的检索参数
 * 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. 搜索的业务逻辑

6.商品搜索 - 图15

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. 响应的数据模型

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

6.商品搜索 - 图16

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

6.商品搜索 - 图17

2.5. 完成搜索功能

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

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

6.商品搜索 - 图18

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

6.商品搜索 - 图19

最终完成代码,如下

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;
    }
}