学习目标

  • 条件筛选
  • 多条件搜索[品牌、规格条件搜索]
  • 规格过滤
  • 价格区间搜索
  • 搜索分页
  • 搜索排序
  • 搜索高亮

    0.Long类型数据精度丢失

    描述:如图,在代码中显示Long类型数据,在将数据存储到ES中的时候,long数据类型丢失精度导致
    _id 和id的值不一致。
    第6章 商品搜索 - 图1
    第6章 商品搜索 - 图2
    解决方案:
    1. 使用JSON序列化的时候,使用自定义的序列化机制,而不是默认的序列化机制,使其使用我们自定义序列化机制
    步骤:
    1. 1.创建自定义序列化对象继承JsonSerializer
    2. 2.SkuInfo中使用该序列化器
    1. package com.changgou.search.pojo;
    2. public class LongToStringSerializer extends JsonSerializer<Long> {
    3. @Override
    4. public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    5. //将Long类型数据转换成字符串 避免精度丢失的问题
    6. gen.writeString(value.toString());
    7. }
    8. }
    第6章 商品搜索 - 图3
    POJO中
    1. @JsonSerialize(using = LongToStringSerializer.class)
    2. private Long id;
    第6章 商品搜索 - 图4

    1. 品牌统计

    第6章 商品搜索 - 图5
    用户搜索的时候,除了使用分类搜索外,还有可能使用品牌搜索,所以我们还需要显示品牌数据和规格数据,品牌数据和规格数据的显示比较容易,都可以考虑使用分类统计的方式进行分组实现。

    1.1 品牌统计分析

    看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据品牌名字分组查看有多少品牌,大概执行了2个步骤就可以获取数据结果以及品牌统计,我们可以发现他们的搜索条件完全一样。
    1. -- 查询所有
    2. SELECT * FROM tb_sku WHERE name LIKE '%手机%';
    3. -- 根据品牌名字分组查询
    4. SELECT brand_name FROM tb_sku WHERE name LIKE '%手机%' GROUP BY brand_name;
    我们每次执行搜索的时候,需要显示商品品牌名称,这里要显示的品牌名称其实就是符合搜素条件的所有商品的品牌集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询即可实现。

    1.2 品牌分组统计实现

    修改search微服务的com.changgou.search.service.impl.SkuServiceImpl类,添加一个品牌分组搜索,如图:
    第6章 商品搜索 - 图6
    添加的代码如下:
    1. //设置分组条件 商品品牌
    2. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brandName").size(50));
    执行获取分组结果:
    第6章 商品搜索 - 图7
    第6章 商品搜索 - 图8
    整体代码如下:
    1. public Map search(Map<String, String> searchMap) {
    2. //1.获取关键字的值
    3. String keywords = searchMap.get("keywords");
    4. if (StringUtils.isEmpty(keywords)) {
    5. keywords = "华为";//赋值给一个默认的值
    6. }
    7. //2.创建查询对象 的构建对象
    8. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
    9. //3.设置查询的条件
    10. //设置分组条件 商品分类
    11. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
    12. //设置分组条件 商品品牌
    13. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brandName").size(50));
    14. nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
    15. //4.构建查询对象
    16. NativeSearchQuery query = nativeSearchQueryBuilder.build();
    17. //5.执行查询
    18. AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
    19. //获取分组结果 商品分类
    20. StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
    21. //获取分组结果 商品品牌
    22. StringTerms stringTermsBrand = (StringTerms) skuPage.getAggregation("skuBrandgroup");
    23. List<String> categoryList = getStringsCategoryList(stringTermsCategory);
    24. List<String> brandList = getStringsBrandList(stringTermsBrand);
    25. //6.返回结果
    26. Map resultMap = new HashMap<>();
    27. resultMap.put("categoryList", categoryList);
    28. resultMap.put("brandList", brandList);
    29. resultMap.put("rows", skuPage.getContent());
    30. resultMap.put("total", skuPage.getTotalElements());
    31. resultMap.put("totalPages", skuPage.getTotalPages());
    32. return resultMap;
    33. }
    34. /**
    35. * 获取品牌列表
    36. *
    37. * @param stringTermsBrand
    38. * @return
    39. */
    40. private List<String> getStringsBrandList(StringTerms stringTermsBrand) {
    41. List<String> brandList = new ArrayList<>();
    42. if (stringTermsBrand != null) {
    43. for (StringTerms.Bucket bucket : stringTermsBrand.getBuckets()) {
    44. brandList.add(bucket.getKeyAsString());
    45. }
    46. }
    47. return brandList;
    48. }
    49. /**
    50. * 获取分类列表数据
    51. *
    52. * @param stringTerms
    53. * @return
    54. */
    55. private List<String> getStringsCategoryList(StringTerms stringTerms) {
    56. List<String> categoryList = new ArrayList<>();
    57. if (stringTerms != null) {
    58. for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
    59. String keyAsString = bucket.getKeyAsString();//分组的值
    60. categoryList.add(keyAsString);
    61. }
    62. }
    63. return categoryList;
    64. }

    1.3 测试

    使用PostMan请求http://localhost:18085/search 只搜索华为的数据
    第6章 商品搜索 - 图9

    2. 规格统计

    第6章 商品搜索 - 图10
    用户搜索的时候,除了使用分类、品牌搜索外,还有可能使用规格搜索,所以我们还需要显示规格数据,规格数据的显示相比上面2种实现略微较难一些,需要对数据进行处理,我们也可以考虑使用分类统计和品牌统计的方式进行分组实现。

    2.1 规格统计分析

    看下面的SQL语句,我们在执行搜索的时候,第1条SQL语句是执行搜,第2条语句是根据规格分组查看有多少规格,大概执行了2个步骤就可以获取数据结果以及规格统计,我们可以发现他们的搜索条件完全一样。
    1. -- 查询所有
    2. SELECT * FROM tb_sku WHERE name LIKE '%手机%';
    3. -- 根据规格名字分组查询
    4. SELECT spec FROM tb_sku WHERE name LIKE '%手机%' GROUP BY spec;
    上述SQL语句执行后的结果如下图:
    第6章 商品搜索 - 图11
    获取到的规格数据我们发现有重复,不过也可以解决,解决思路如下:
    1. 1.获取所有规格数据
    2. 2.将所有规格数据转换成Map
    3. 3.定义一个Map<String,Set>,key是规格名字,防止重复所以用Mapvalu是规格值,规格值有多个,所以用集合,为了防止规格重复,用Set去除重复
    4. 4.循环规格的Map,将数据填充到定义的Map<String,Set>中
    我们每次执行搜索的时候,需要显示商品规格数据,这里要显示的规格数据其实就是符合搜素条件的所有商品的规格集合,我们可以按照上面的实现思路,使用ES根据分组名称做一次分组查询,并去除重复数据即可实现。

    2.2 规格统计分组实现

    修改search微服务的com.changgou.search.service.impl.SkuServiceImpl类,添加一个规格分组搜索
    如图:添加规格分组条件
    第6章 商品搜索 - 图12
    上图代码如下:
    1. //设置分组条件 商品的规格
    2. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(100));
    如图:获取规格分组结果:
    第6章 商品搜索 - 图13
    封装调用分组结果的方法:
    第6章 商品搜索 - 图14
    上图代码如下:
    1. /**
    2. * 获取规格列表数据
    3. *
    4. * @param stringTermsSpec
    5. * @return
    6. */
    7. private Map<String, Set<String>> getStringSetMap(StringTerms stringTermsSpec) {
    8. Map<String, Set<String>> specMap = new HashMap<String, Set<String>>();
    9. Set<String> specList = new HashSet<>();
    10. if (stringTermsSpec != null) {
    11. for (StringTerms.Bucket bucket : stringTermsSpec.getBuckets()) {
    12. specList.add(bucket.getKeyAsString());
    13. }
    14. }
    15. for (String specjson : specList) {
    16. Map<String, String> map = JSON.parseObject(specjson, Map.class);
    17. for (Map.Entry<String, String> entry : map.entrySet()) {//
    18. String key = entry.getKey(); //规格名字
    19. String value = entry.getValue(); //规格选项值
    20. //获取当前规格名字对应的规格数据
    21. Set<String> specValues = specMap.get(key);
    22. if (specValues == null) {
    23. specValues = new HashSet<String>();
    24. }
    25. //将当前规格加入到集合中
    26. specValues.add(value);
    27. //将数据存入到specMap中
    28. specMap.put(key, specValues);
    29. }
    30. }
    31. return specMap;
    32. }
    整体代码如下:
    1. public Map search(Map<String, String> searchMap) {
    2. //1.获取关键字的值
    3. String keywords = searchMap.get("keywords");
    4. if (StringUtils.isEmpty(keywords)) {
    5. keywords = "华为";//赋值给一个默认的值
    6. }
    7. //2.创建查询对象 的构建对象
    8. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
    9. //3.设置查询的条件
    10. //设置分组条件 商品分类
    11. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
    12. //设置分组条件 商品品牌
    13. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brandName").size(50));
    14. //设置分组条件 商品的规格
    15. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(100));
    16. nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
    17. //4.构建查询对象
    18. NativeSearchQuery query = nativeSearchQueryBuilder.build();
    19. //5.执行查询
    20. AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
    21. //获取分组结果 商品分类
    22. StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
    23. //获取分组结果 商品品牌
    24. StringTerms stringTermsBrand = (StringTerms) skuPage.getAggregation("skuBrandgroup");
    25. //获取分组结果 商品规格数据
    26. StringTerms stringTermsSpec = (StringTerms) skuPage.getAggregation("skuSpecgroup");
    27. List<String> categoryList = getStringsCategoryList(stringTermsCategory);
    28. List<String> brandList = getStringsBrandList(stringTermsBrand);
    29. Map<String, Set<String>> specMap = getStringSetMap(stringTermsSpec);
    30. //6.返回结果
    31. Map resultMap = new HashMap<>();
    32. resultMap.put("specMap", specMap);
    33. resultMap.put("categoryList", categoryList);
    34. resultMap.put("brandList", brandList);
    35. resultMap.put("rows", skuPage.getContent());
    36. resultMap.put("total", skuPage.getTotalElements());
    37. resultMap.put("totalPages", skuPage.getTotalPages());
    38. return resultMap;
    39. }
    40. /**
    41. * 获取品牌列表
    42. *
    43. * @param stringTermsBrand
    44. * @return
    45. */
    46. private List<String> getStringsBrandList(StringTerms stringTermsBrand) {
    47. List<String> brandList = new ArrayList<>();
    48. if (stringTermsBrand != null) {
    49. for (StringTerms.Bucket bucket : stringTermsBrand.getBuckets()) {
    50. brandList.add(bucket.getKeyAsString());
    51. }
    52. }
    53. return brandList;
    54. }
    55. /**
    56. * 获取分类列表数据
    57. *
    58. * @param stringTerms
    59. * @return
    60. */
    61. private List<String> getStringsCategoryList(StringTerms stringTerms) {
    62. List<String> categoryList = new ArrayList<>();
    63. if (stringTerms != null) {
    64. for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
    65. String keyAsString = bucket.getKeyAsString();//分组的值
    66. categoryList.add(keyAsString);
    67. }
    68. }
    69. return categoryList;
    70. }
    71. /**
    72. * 获取规格列表数据
    73. *
    74. * @param stringTermsSpec
    75. * @return
    76. */
    77. private Map<String, Set<String>> getStringSetMap(StringTerms stringTermsSpec) {
    78. Map<String, Set<String>> specMap = new HashMap<String, Set<String>>();
    79. Set<String> specList = new HashSet<>();
    80. if (stringTermsSpec != null) {
    81. for (StringTerms.Bucket bucket : stringTermsSpec.getBuckets()) {
    82. specList.add(bucket.getKeyAsString());
    83. }
    84. }
    85. for (String specjson : specList) {
    86. Map<String, String> map = JSON.parseObject(specjson, Map.class);
    87. for (Map.Entry<String, String> entry : map.entrySet()) {//
    88. String key = entry.getKey(); //规格名字
    89. String value = entry.getValue(); //规格选项值
    90. //获取当前规格名字对应的规格数据
    91. Set<String> specValues = specMap.get(key);
    92. if (specValues == null) {
    93. specValues = new HashSet<String>();
    94. }
    95. //将当前规格加入到集合中
    96. specValues.add(value);
    97. //将数据存入到specMap中
    98. specMap.put(key, specValues);
    99. }
    100. }
    101. return specMap;
    102. }

    2.3 测试

    使用Postman测试访问http://localhost:18086/search 效果如下:
    第6章 商品搜索 - 图15

    3 条件筛选

    第6章 商品搜索 - 图16
    用户有可能会根据分类搜索、品牌搜索,还有可能根据规格搜索,以及价格搜索和排序操作。根据分类和品牌搜索的时候,可以直接根据指定域搜索,而规格搜索的域数据是不确定的,价格是一个区间搜索,所以我们可以分为三段时间,先实现分类、品牌搜素,再实现规格搜索,然后实现价格区间搜索。

    3.1 分类、品牌筛选

    3.1.1 需求分析

    页面每次向后台传入对应的分类和品牌,后台据分类和品牌进行条件过滤即可。

    3.1.2 代码实现

    修改搜索微服务com.changgou.search.service.impl.SkuServiceImpl的search方法,添加分类和品牌过滤,
    添加过滤条件如下:
    第6章 商品搜索 - 图17
    PS说明: 以上,我们建议使用filter ,它的搜索效率要优于must.可以参考官方文档说明:
    https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html
    执行过滤查询如下:
    第6章 商品搜索 - 图18
    上图整体代码如下:
    1. @Override
    2. public Map search(Map<String, String> searchMap) {
    3. //1.获取关键字的值
    4. String keywords = searchMap.get("keywords");
    5. if (StringUtils.isEmpty(keywords)) {
    6. keywords = "华为";//赋值给一个默认的值
    7. }
    8. //2.创建查询对象 的构建对象
    9. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
    10. //3.设置查询的条件
    11. //设置分组条件 商品分类
    12. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
    13. //设置分组条件 商品品牌
    14. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brandName").size(50));
    15. //设置分组条件 商品的规格
    16. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(1000));
    17. //设置主关键字查询
    18. nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("name", keywords));
    19. BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    20. if (!StringUtils.isEmpty(searchMap.get("brand"))) {
    21. boolQueryBuilder.filter(QueryBuilders.termQuery("brandName", searchMap.get("brand")));
    22. }
    23. if (!StringUtils.isEmpty(searchMap.get("category"))) {
    24. boolQueryBuilder.filter(QueryBuilders.termQuery("categoryName", searchMap.get("category")));
    25. }
    26. //构建过滤查询
    27. nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
    28. //4.构建查询对象
    29. NativeSearchQuery query = nativeSearchQueryBuilder.build();
    30. //5.执行查询
    31. AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class);
    32. //获取分组结果 商品分类
    33. StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
    34. //获取分组结果 商品品牌
    35. StringTerms stringTermsBrand = (StringTerms) skuPage.getAggregation("skuBrandgroup");
    36. //获取分组结果 商品规格数据
    37. StringTerms stringTermsSpec = (StringTerms) skuPage.getAggregation("skuSpecgroup");
    38. List<String> categoryList = getStringsCategoryList(stringTermsCategory);
    39. List<String> brandList = getStringsBrandList(stringTermsBrand);
    40. Map<String, Set<String>> specMap = getStringSetMap(stringTermsSpec);
    41. //6.返回结果
    42. Map resultMap = new HashMap<>();
    43. resultMap.put("specMap", specMap);
    44. resultMap.put("categoryList", categoryList);
    45. resultMap.put("brandList", brandList);
    46. resultMap.put("rows", skuPage.getContent());
    47. resultMap.put("total", skuPage.getTotalElements());
    48. resultMap.put("totalPages", skuPage.getTotalPages());
    49. return resultMap;
    50. }

    3.1.3 测试

    测试效果如下:
    访问地址:http://localhost:18085/search
    第6章 商品搜索 - 图19
    此时只能搜到华为手环设备

    3.2 规格过滤

    3.2.1 需求分析

    第6章 商品搜索 - 图20
    规格这一块,需要向后台发送规格名字以及规格值,我们可以按照一定要求来发送数据,例如规格名字以特殊前缀提交到后台:spec_网络制式:电信4G、spec_显示屏尺寸:4.0-4.9英寸
    后台接到数据后,可以根据前缀spec_来区分是否是规格,如果以spec_xxx开始的数据则为规格数据,需要根据指定规格找信息。
    第6章 商品搜索 - 图21
    上图是规格的索引存储格式,真实数据在spechMap.规格名字.keyword中,所以找数据也是按照如下格式去找:
    spechMap.规格名字.keyword

    3.2.2 代码实现

    修改com.changgou.search.service.impl.SkuServiceImpl的search方法,增加规格查询操作,代码如下:
    第6章 商品搜索 - 图22
    1. //规格过滤查询
    2. if (searchMap != null) {
    3. for (String key : searchMap.keySet()) {
    4. if (key.startsWith("spec_")) {
    5. boolQueryBuilder.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", searchMap.get(key)));
    6. }
    7. }
    8. }

    3.2.3 测试

    访问地址:http://localhost:18085/search
    第6章 商品搜索 - 图23

    3.3 价格区间查询

    3.3.1 需求分析

    第6章 商品搜索 - 图24
    价格区间查询,每次需要将价格传入到后台,前端传入后台的价格大概是price=0-500或者price=500-1000依次类推,最后一个是price=3000,后台可以根据-分割,如果分割得到的结果最多有2个,第1个表示x<price,第2个表示price<=y
    1.3.2 代码实现
    修改com.changgou.search.service.impl.SkuServiceImpl的search方法,增加价格区间查询操作,代码如下:
    第6章 商品搜索 - 图25
    上图代码如下:
    1. //价格过滤查询
    2. String price = searchMap.get("price");
    3. if (!StringUtils.isEmpty(price)) {
    4. String[] split = price.split("-");
    5. if (!split[1].equalsIgnoreCase("*")) {
    6. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0], true).to(split[1], true));
    7. } else {
    8. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]));
    9. }
    10. }

    3.3.3 测试

    访问地址:http://localhost:18085/search
    第6章 商品搜索 - 图26
    效果如下(部分数据):
    1. [
    2. {
    3. "id": 1088256019328536576,
    4. "name": "守护宝幼儿安全手环",
    5. "price": 500,
    6. "num": 100,
    7. "image": "http://img10.360buyimg.com/n1/s450x450_jfs/t3457/294/236823024/102048/c97f5825/58072422Ndd7e66c4.jpg",
    8. "status": "1",
    9. "createTime": "2019-01-24T10:03:48.000+0000",
    10. "updateTime": "2019-01-24T10:03:48.000+0000",
    11. "isDefault": null,
    12. "spuId": 1088256019315953664,
    13. "categoryId": 1108,
    14. "categoryName": "户外工具",
    15. "brandName": "守护宝",
    16. "spec": "{\"颜色\":\"红\",\"机身内存\":\"64G\"}",
    17. "specMap": {
    18. "颜色": "红",
    19. "机身内存": "64G"
    20. }
    21. },
    22. {
    23. "id": 1088256014043713536,
    24. "name": "计步器小米手环,适用老人、小孩",
    25. "price": 800,
    26. "num": 100,
    27. "image": "http://img10.360buyimg.com/n1/s450x450_jfs/t3457/294/236823024/102048/c97f5825/58072422Ndd7e66c4.jpg",
    28. "status": "1",
    29. "createTime": "2019-01-24T10:03:47.000+0000",
    30. "updateTime": "2019-01-24T10:03:47.000+0000",
    31. "isDefault": null,
    32. "spuId": 1088256014026936320,
    33. "categoryId": 1192,
    34. "categoryName": "小家电",
    35. "brandName": "小米",
    36. "spec": "{\"颜色\":\"红\",\"机身内存\":\"64G\"}",
    37. "specMap": {
    38. "颜色": "红",
    39. "机身内存": "64G"
    40. }
    41. }
    42. ]

    4 搜索分页

    4.1 分页分析

    第6章 商品搜索 - 图27
    页面需要实现分页搜索,所以我们后台每次查询的时候,需要实现分页。用户页面每次会传入当前页和每页查询多少条数据,当然如果不传入每页显示多少条数据,默认查询30条即可。

    4.2 分页实现

    分页使用PageRequest.of( pageNo- 1, pageSize);实现,第1个参数表示第N页,从0开始,第2个参数表示每页显示多少条,实现代码如下:
    第6章 商品搜索 - 图28
    上图代码如下:
    1. //略
    2. //构建过滤查询
    3. nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
    4. //构建分页查询
    5. Integer pageNum = 1;
    6. if (!StringUtils.isEmpty(searchMap.get("pageNum"))) {
    7. try {
    8. pageNum = Integer.valueOf(searchMap.get("pageNum"));
    9. } catch (NumberFormatException e) {
    10. e.printStackTrace();
    11. pageNum=1;
    12. }
    13. }
    14. Integer pageSize = 3;
    15. nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));
    16. //略
    17. //4.构建查询对象
    18. NativeSearchQuery query = nativeSearchQueryBuilder.build();
    19. //略
    测试如下:
    第6章 商品搜索 - 图29

    5 搜索排序

    5.1 排序分析

    第6章 商品搜索 - 图30
    排序这里总共有根据价格排序、根据评价排序、根据新品排序、根据销量排序,排序要想实现非常简单,只需要告知排序的域以及排序方式即可实现。
    价格排序:只需要根据价格高低排序即可,降序价格高->低,升序价格低->高
    评价排序:评价分为好评、中评、差评,可以在数据库中设计3个列,用来记录好评、中评、差评的量,每次排序的时候,好评的比例来排序,当然还要有条数限制,评价条数需要超过N条。
    新品排序:直接根据商品的发布时间或者更新时间排序。
    销量排序:销量排序除了销售数量外,还应该要有时间段限制。

    5.2 排序实现

    这里我们不单独针对某个功能实现排序,我们只需要在后台接收2个参数,分别是排序域名字和排序方式,代码如下:
    第6章 商品搜索 - 图31
    解释: 前端页面传递要排序的字段(field)和要排序的类型(ASC,DESC),后台接收.
    上图代码如下:
    1. //构建排序查询
    2. String sortRule = searchMap.get("sortRule");
    3. String sortField = searchMap.get("sortField");
    4. if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) {
    5. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));
    6. }
    测试
    根据价格降序:
    1. {"keywords":"手机","pageNum":"1","sortRule":"DESC","sortField":"price"}
    根据价格升序:
    1. {"keywords":"手机","pageNum":"1","sortRule":"ASC","sortField":"price"}

    6 高亮显示

    6.1 高亮分析

    第6章 商品搜索 - 图32
    高亮显示是指根据商品关键字搜索商品的时候,显示的页面对关键字给定了特殊样式,让它显示更加突出,如上图商品搜索中,关键字编程了红色,其实就是给定了红色样式。
    第6章 商品搜索 - 图33

    6.2 高亮搜索实现步骤解析

    将之前的搜索换掉,换成高亮搜索,我们需要做3个步骤:
    1. 1.指定高亮域,也就是设置哪个域需要高亮显示
    2. 设置高亮域的时候,需要指定前缀和后缀,也就是关键词用什么html标签包裹,再给该标签样式
    3. 2.高亮搜索实现
    4. 3.将非高亮数据替换成高亮数据
    第1点,例如在百度中搜索数据的时候,会有2个地方高亮显示,分别是标题和描述,商城搜索的时候,只是商品名称高亮显示了。而高亮显示其实就是添加了样式,例如<span style="color:red;">笔记本</span>,而其中span开始标签可以称为前缀,span结束标签可以称为后缀。
    第2点,高亮搜索使用ElasticsearchTemplate实现。
    第3点,高亮搜索后,会搜出非高亮数据和高亮数据,高亮数据会加上第1点中的高亮样式,此时我们需要将非高亮数据换成高亮数据即可。例如非高亮:华为笔记本性能超强悍 高亮数据:华为<span style="color:red;"笔记本</span>性能超强悍,将非高亮的换成高亮的,到页面就能显示样式了。

    6.3 高亮代码实现

    修改com.changgou.search.service.impl.SkuServiceImpl的search方法搜索代码,添加高亮显示的域:
    第6章 商品搜索 - 图34
    上图代码如下:
    1. //设置高亮条件
    2. nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("name"));
    3. nativeSearchQueryBuilder.withHighlightBuilder(new HighlightBuilder().preTags("<em style=\"color:red\">").postTags("</em>"));
    4. //设置主关键字查询
    5. nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keywords,"name","brandName","categoryName"));
    修改 查询的方法,自定义结果映射器,入下图:
    第6章 商品搜索 - 图35
    上图图片如下:
    1. AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class, new SearchResultMapperImpl());
    自定义一个映射结果类实现接口,作用就是:自定义映射结果集,获取高亮的数据展示,如下图:
    第6章 商品搜索 - 图36
    代码如下:
    1. public class SearchResultMapperImpl implements SearchResultMapper {
    2. @Override
    3. public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
    4. List<T> content = new ArrayList<>();
    5. //如果没有结果返回为空
    6. if (response.getHits() == null || response.getHits().getTotalHits() <= 0) {
    7. return new AggregatedPageImpl<T>(content);
    8. }
    9. for (SearchHit searchHit : response.getHits()) {
    10. String sourceAsString = searchHit.getSourceAsString();
    11. SkuInfo skuInfo = JSON.parseObject(sourceAsString, SkuInfo.class);
    12. Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
    13. HighlightField highlightField = highlightFields.get("name");
    14. //有高亮则设置高亮的值
    15. if (highlightField != null) {
    16. StringBuffer stringBuffer = new StringBuffer();
    17. for (Text text : highlightField.getFragments()) {
    18. stringBuffer.append(text.string());
    19. }
    20. skuInfo.setName(stringBuffer.toString());
    21. }
    22. content.add((T) skuInfo);
    23. }
    24. return new AggregatedPageImpl<T>(content, pageable, response.getHits().getTotalHits(), response.getAggregations(), response.getScrollId());
    25. }
    26. }

    6.4 测试

    第6章 商品搜索 - 图37
    效果如下:
    1. "name": "HTC M8Sd (E8) 波尔多红 电信4G<span style=\"color:red\">手机</span> 双卡双待双通",
    整体代码如下: ```java @Service public class SkuServiceImpl implements SkuService { @Autowired private SkuEsMapper skuEsMapper; @Autowired private SkuFeign skuFeign; @Override public void importSku() {
    1. Result<List<Sku>> listResult = skuFeign.findByStatus("1");
    2. List<Sku> data = listResult.getData();
    3. List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(data), SkuInfo.class);
    4. for (SkuInfo skuInfo : skuInfos) {
    5. String spec = skuInfo.getSpec();
    6. Map map = JSON.parseObject(spec, Map.class);
    7. skuInfo.setSpecMap(map);
    8. }
    9. skuEsMapper.saveAll(skuInfos);
    } @Autowired private ElasticsearchTemplate esTemplate; /**
  • @param searchMap
  • @return */ @Override public Map search(Map searchMap) {
    1. //1.获取关键字的值
    2. String keywords = searchMap.get("keywords");
    3. if (StringUtils.isEmpty(keywords)) {
    4. keywords = "华为";//赋值给一个默认的值
    5. }
    6. //2.创建查询对象 的构建对象
    7. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
    8. //3.设置查询的条件
    9. //设置分组条件 商品分类
    10. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuCategorygroup").field("categoryName").size(50));
    11. //设置分组条件 商品品牌
    12. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuBrandgroup").field("brandName").size(50));
    13. //设置分组条件 商品的规格
    14. nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms("skuSpecgroup").field("spec.keyword").size(500000));
    15. //设置高亮条件
    16. nativeSearchQueryBuilder.withHighlightFields(new HighlightBuilder.Field("name"));
    17. nativeSearchQueryBuilder.withHighlightBuilder(new HighlightBuilder().preTags("<em style=\"color:red\">").postTags("</em>"));
    18. //设置主关键字查询
    19. nativeSearchQueryBuilder.withQuery(QueryBuilders.multiMatchQuery(keywords,"name","brandName","categoryName"));
    20. BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    21. if (!StringUtils.isEmpty(searchMap.get("brand"))) {
    22. boolQueryBuilder.filter(QueryBuilders.termQuery("brandName", searchMap.get("brand")));
    23. }
    24. if (!StringUtils.isEmpty(searchMap.get("category"))) {
    25. boolQueryBuilder.filter(QueryBuilders.termQuery("categoryName", searchMap.get("category")));
    26. }
    27. //规格过滤查询
    28. if (searchMap != null) {
    29. for (String key : searchMap.keySet()) {
    30. if (key.startsWith("spec_")) {
    31. boolQueryBuilder.filter(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", searchMap.get(key)));
    32. }
    33. }
    34. }
    35. //价格过滤查询
    36. String price = searchMap.get("price");
    37. if (!StringUtils.isEmpty(price)) {
    38. String[] split = price.split("-");
    39. if (!split[1].equalsIgnoreCase("*")) {
    40. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(split[0], true).to(split[1], true));
    41. } else {
    42. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(split[0]));
    43. }
    44. }
    45. //构建过滤查询
    46. nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
    47. //构建分页查询
    48. Integer pageNum = 1;
    49. if (!StringUtils.isEmpty(searchMap.get("pageNum"))) {
    50. try {
    51. pageNum = Integer.valueOf(searchMap.get("pageNum"));
    52. } catch (NumberFormatException e) {
    53. e.printStackTrace();
    54. pageNum=1;
    55. }
    56. }
    57. Integer pageSize = 3;
    58. nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum - 1, pageSize));
    59. //构建排序查询
    60. String sortRule = searchMap.get("sortRule");
    61. String sortField = searchMap.get("sortField");
    62. if (!StringUtils.isEmpty(sortRule) && !StringUtils.isEmpty(sortField)) {
    63. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(sortRule.equals("DESC") ? SortOrder.DESC : SortOrder.ASC));
    64. }
    65. //4.构建查询对象
    66. NativeSearchQuery query = nativeSearchQueryBuilder.build();
    67. //5.执行查询
    68. AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(query, SkuInfo.class, new SearchResultMapperImpl());
    69. //获取分组结果 商品分类
    70. StringTerms stringTermsCategory = (StringTerms) skuPage.getAggregation("skuCategorygroup");
    71. //获取分组结果 商品品牌
    72. StringTerms stringTermsBrand = (StringTerms) skuPage.getAggregation("skuBrandgroup");
    73. //获取分组结果 商品规格数据
    74. StringTerms stringTermsSpec = (StringTerms) skuPage.getAggregation("skuSpecgroup");
    75. List<String> categoryList = getStringsCategoryList(stringTermsCategory);
    76. List<String> brandList = getStringsBrandList(stringTermsBrand);
    77. Map<String, Set<String>> specMap = getStringSetMap(stringTermsSpec);
    78. //6.返回结果
    79. Map resultMap = new HashMap<>();
    80. resultMap.put("specMap", specMap);
    81. resultMap.put("categoryList", categoryList);
    82. resultMap.put("brandList", brandList);
    83. resultMap.put("rows", skuPage.getContent());
    84. resultMap.put("total", skuPage.getTotalElements());
    85. resultMap.put("totalPages", skuPage.getTotalPages());
    86. return resultMap;
    } /**
  • 获取品牌列表 *
  • @param stringTermsBrand
  • @return */ private List getStringsBrandList(StringTerms stringTermsBrand) {
    1. List<String> brandList = new ArrayList<>();
    2. if (stringTermsBrand != null) {
    3. for (StringTerms.Bucket bucket : stringTermsBrand.getBuckets()) {
    4. brandList.add(bucket.getKeyAsString());
    5. }
    6. }
    7. return brandList;
    } /**
  • 获取分类列表数据 *
  • @param stringTerms
  • @return */ private List getStringsCategoryList(StringTerms stringTerms) {
    1. List<String> categoryList = new ArrayList<>();
    2. if (stringTerms != null) {
    3. for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
    4. String keyAsString = bucket.getKeyAsString();//分组的值
    5. categoryList.add(keyAsString);
    6. }
    7. }
    8. return categoryList;
    } /**
  • 获取规格列表数据 *
  • @param stringTermsSpec
  • @return */ private Map> getStringSetMap(StringTerms stringTermsSpec) {
    1. Map<String, Set<String>> specMap = new HashMap<String, Set<String>>();
    2. Set<String> specList = new HashSet<>();
    3. if (stringTermsSpec != null) {
    4. for (StringTerms.Bucket bucket : stringTermsSpec.getBuckets()) {
    5. specList.add(bucket.getKeyAsString());
    6. }
    7. }
    8. for (String specjson : specList) {
    9. Map<String, String> map = JSON.parseObject(specjson, Map.class);
    10. for (Map.Entry<String, String> entry : map.entrySet()) {//
    11. String key = entry.getKey(); //规格名字
    12. String value = entry.getValue(); //规格选项值
    13. //获取当前规格名字对应的规格数据
    14. Set<String> specValues = specMap.get(key);
    15. if (specValues == null) {
    16. specValues = new HashSet<String>();
    17. }
    18. //将当前规格加入到集合中
    19. specValues.add(value);
    20. //将数据存入到specMap中
    21. specMap.put(key, specValues);
    22. }
    23. }
    24. return specMap;
    } } ```