当前位置:   article > 正文

SpringDataElasticSearch - NativeSearchQueryBuilder过滤聚合高亮查询

nativesearchquerybuilder

本文要实现的一个功能,根据品牌、分类、规格、价格过滤查询商品的功能,并对查询结果的关键字进行高亮显示。只做后端功能

本文是以代码驱动,如果看不太懂,可以先复制代码,再慢慢看,注释很详细。

1、引入相关依赖

主要就是fastjsonspring-boot-starter-data-elasticsearch(SpringBoot项目),fastJson的作用是转换对象使用,当然也可以进行时间格式化(本文未作处理)。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.alibaba</groupId>
  7. <artifactId>fastjson</artifactId>
  8. <version>1.2.28</version>
  9. </dependency>

2、ElasticSearch数据测试准备

①创建Goods类,测试类里注入相关对象

  1. @Document(indexName = "goods_sku",type = "goods")
  2. @Data
  3. public class Goods {
  4. @Id
  5. @Field(type = FieldType.Long,store = true)
  6. private Long id; // 主键Id
  7. @Field(type = FieldType.Text,analyzer = "ik_smart",store = true)
  8. private String name; // 商品名称
  9. @Field(type = FieldType.Integer,store = true)
  10. private Integer price; // 商品价格
  11. @Field(type = FieldType.Text,store = true,index = false)
  12. private String image; // 商品图片src
  13. @Field(type = FieldType.Date,store = true,index = false)
  14. private Date createTime; // 商品创建时间
  15. @Field(type = FieldType.Long,store = true,index = false)
  16. private Long spuId; // Spu的Id
  17. @Field(type = FieldType.Keyword,store = true)
  18. private String categoryName;// 分类名称
  19. @Field(type = FieldType.Keyword,store = true)
  20. private String brandName; // 品牌名称
  21. @Field(type = FieldType.Object,store = true)
  22. private Map spec; // 规格Map Map<String,String>,如<"颜色","黑色">
  23. @Field(type = FieldType.Integer,store = true,index = false)
  24. private Integer saleNum; // 销量
  25. public Goods(){
  26. }
  27. public Goods(Long id, String name, Integer price, String image, Date createTime, Long spuId, String categoryName, String brandName, Map spec, Integer saleNum) {
  28. this.id = id;
  29. this.name = name;
  30. this.price = price;
  31. this.image = image;
  32. this.createTime = createTime;
  33. this.spuId = spuId;
  34. this.categoryName = categoryName;
  35. this.brandName = brandName;
  36. this.spec = spec;
  37. this.saleNum = saleNum;
  38. }
  39. }
  1. @Autowired
  2. private ElasticsearchTemplate template;
  3. @Autowired
  4. private GoodsRepository goodsRepository;
  5. @Autowired
  6. private EsResultMapper esResultMapper;

②数据准备 - 尽量多准备一些数据,方便测试查询

  1. @Test
  2. public void createIndex(){
  3. template.createIndex(Goods.class);
  4. }
  5. @Test
  6. public void createDoc(){
  7. Map map1 = new HashMap();
  8. map1.put("颜色","紫色");
  9. map1.put("套餐","标准套餐");
  10. Goods goods1 = new Goods(7L,"小米 Mini9秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
  11. goodsRepository.save(goods1);
  12. // 使用saveAll批量存储
  13. }

3、构建基本查询方法

该方法通过传过来的条件Map,根据条件进行过滤查询,比如分类、品牌、规格、价格区间等(具体取决于需求)。

  1. /**
  2. * 构建基本查询 - 搜索关键字、分类、品牌、规格、价格
  3. * @param searchMap
  4. * @return
  5. */
  6. private BoolQueryBuilder buildBasicQuery(Map searchMap) {
  7. // 构建布尔查询
  8. BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
  9. // 关键字查询
  10. boolQueryBuilder.must(QueryBuilders.matchQuery("name",searchMap.get("keywords")));
  11. // 分类、品牌、规格 都是需要精准查询的,无需分词
  12. // 商品分类过滤
  13. if (searchMap.get("category") != null){
  14. boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("categoryName",searchMap.get("category")));
  15. }
  16. // 商品品牌过滤
  17. if(searchMap.get("brand") != null){
  18. boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("brandName",searchMap.get("brand")));
  19. }
  20. // 规格过滤
  21. if(searchMap.get("spec") != null){
  22. Map<String,String> map = (Map) searchMap.get("spec");
  23. for(Map.Entry<String,String> entry : map.entrySet()){
  24. // 规格查询[spec.xxx],因为规格是不确定的,所以需要精确查找,加上.keyword,如spec.颜色.keyword
  25. boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("spec." + entry.getKey() + ".keyword",entry.getValue()));
  26. }
  27. }
  28. // 价格过滤
  29. if(searchMap.get("price") != null){
  30. // 价格: 0-500 0-*
  31. String[] prices = ((String)searchMap.get("price")).split("-");
  32. if(!prices[0].equals("0")){ // 加两个0是,因为价格转换成分
  33. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(prices[0] + "00"));
  34. }
  35. if(!prices[1].equals("*")){ // 价格有上限
  36. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(prices[1] + "00"));
  37. }
  38. }
  39. return boolQueryBuilder;
  40. }

4、查询分类列表

主要是根据搜索关键字查询查询出来的结果,将其分类,然后把分类查询出来,显示到前端。

  1. /**
  2. * 查询分类列表
  3. * @param searchMap
  4. * @return
  5. */
  6. private List<String> searchCategoryList(Map searchMap) {
  7. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
  8. // 构建查询
  9. BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
  10. nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
  11. // 分类聚合名
  12. String groupName = "sku_category";
  13. // 构建聚合查询
  14. TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(groupName).field("categoryName");
  15. nativeSearchQueryBuilder.addAggregation(termsAggregationBuilder);
  16. // 获取聚合分页结果
  17. AggregatedPage<Goods> goodsList = (AggregatedPage<Goods>) goodsRepository.search(nativeSearchQueryBuilder.build());
  18. // 在查询结果中找到聚合 - 根据聚合名称
  19. StringTerms stringTerms = (StringTerms) goodsList.getAggregation(groupName);
  20. // 获取桶
  21. List<StringTerms.Bucket> buckets = stringTerms.getBuckets();
  22. // 使用流Stream 将分类名存入集合
  23. List<String> categoryList = buckets.stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
  24. // 打印分类名称
  25. categoryList.forEach(System.out::println);
  26. return categoryList;
  27. }

既然有了分类,那么肯定还有对应的品牌、规格。其实品牌和规格与分类是有一个联系的。ElasticSearch查询出分类,每个分类对应一个id,也就是说所有分类和分类的id应该存到Redis中去,这样前端就可以根据返回的分类集合去查询对应的品牌和规格,这里只是提供一个实现思路。

  1. String categoryName = ""; // 分类名
  2. if(searchMap.get("category") == null){ // 如果查询条件没有分类
  3. // 默认取分类列表的第一个
  4. if(categoryList.size() > 0){
  5. categoryName = categoryList.get(0);
  6. }
  7. }else{ // 如果查询条件有分类
  8. // 则取查询条件中的分类
  9. categoryName = searchMap.get("category");
  10. }
  11. // 根据分类名查询品牌 - 实际应该从Redis中查询
  12. if(searchMap.get("brand")==null) {
  13. List<Map> brandList = brandDao.findListByCategoryName(categoryName);
  14. resultMap.put("brandList", brandList);
  15. }
  16. // 根据分类查询规格 - 实际应该从Redis中查询
  17. List<Map> specList = specDao.findListByCategoryName(categoryName);
  18. for(Map spec:specList){
  19. // 规格选项列表 - 选项与选项之间是以,(逗号)分隔的
  20. String[] options = ((String) spec.get("options")).split(",");
  21. // 讲过规格选项放入到规格对象中
  22. spec.put("options",options);
  23. }
  24. // 将规格对象放入到结果集
  25. resultMap.put("specList",specList);

5、重新实现SearchResultMapper - 高亮前奏

因为默认的SearchResultMapper是没有高亮的,我们需要重新实现,重写AggregatedPage方法。

  1. @Component
  2. public class EsResultMapper implements SearchResultMapper {
  3. @Override
  4. public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
  5. // 记录总条数
  6. long totalHits = response.getHits().getTotalHits();
  7. // 记录列表(泛型) - 构建Aggregate使用
  8. List<T> list = Lists.newArrayList();
  9. // 获取搜索结果(真正的的记录)
  10. SearchHits hits = response.getHits();
  11. for (SearchHit hit : hits) {
  12. if(hits.getHits().length <= 0){
  13. return null;
  14. }
  15. // 将原本的JSON对象转换成Map对象
  16. Map<String, Object> map = hit.getSourceAsMap();
  17. // 获取高亮的字段Map
  18. Map<String, HighlightField> highlightFields = hit.getHighlightFields();
  19. for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
  20. // 获取高亮的Key
  21. String key = highlightField.getKey();
  22. // 获取高亮的Value
  23. HighlightField value = highlightField.getValue();
  24. // 实际fragments[0]就是高亮的结果,无需遍历拼接
  25. Text[] fragments = value.getFragments();
  26. StringBuilder sb = new StringBuilder();
  27. for (Text text : fragments) {
  28. sb.append(text);
  29. }
  30. // 因为高亮的字段必然存在于Map中,就是key值
  31. // 可能有一种情况,就是高亮的字段是嵌套Map,也就是说在Map里面还有Map的这种情况,这里没有考虑
  32. map.put(key, sb.toString());
  33. }
  34. // 把Map转换成对象
  35. T item = JSON.parseObject(JSONObject.toJSONString(map),aClass);
  36. list.add(item);
  37. }
  38. // 返回的是带分页的结果
  39. return new AggregatedPageImpl<>(list, pageable, totalHits);
  40. }
  41. }

6、查询商品(sku)列表

  1. /**
  2. * 查询Sku集合 - 商品列表
  3. * @param searchMap 查询条件
  4. * @return
  5. */
  6. private Map searchSkuList(Map searchMap) {
  7. Map resultMap = new HashMap();
  8. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
  9. BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
  10. // 查询
  11. nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
  12. // 排序
  13. String sortField = (String)searchMap.get("sortField"); // 排序字段
  14. String sortRule = (String)searchMap.get("sortRule"); // 排序规则 - 顺序(ASC)/倒序(DESC)
  15. if(sortField!= null && !"".equals(sortField)){
  16. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule)));
  17. }
  18. // 构建分页
  19. nativeSearchQueryBuilder.withPageable(PageRequest.of(0,15));
  20. // 构建高亮查询
  21. HighlightBuilder.Field field = new HighlightBuilder.Field("name").preTags("<font style='color:red'>").postTags("</font>");
  22. nativeSearchQueryBuilder.withHighlightFields(field); // 名字高亮
  23. NativeSearchQuery build = nativeSearchQueryBuilder.build();
  24. // 获取查询结果
  25. AggregatedPage<Goods> goodsPage = template.queryForPage(build, Goods.class, esResultMapper);
  26. long total = goodsPage.getTotalElements(); // 总数据量
  27. long totalPage = goodsPage.getTotalPages(); // 总页数
  28. // ...你还要将是否有上页下页等内容传过去
  29. List<Goods> goodsList = goodsPage.getContent();
  30. goodsList.forEach(System.out::println);
  31. resultMap.put("rows",goodsList);
  32. resultMap.put("total",total);
  33. resultMap.put("totalPage",totalPage);
  34. return resultMap;
  35. }

7、查询

  1. /**
  2. * 搜索方法 - searchMap应该由前端传过来
  3. * searchMap里封装了一些条件,根据条件进行过滤
  4. */
  5. @Test
  6. public void search(){
  7. // 搜索条件Map
  8. Map searchMap = new HashMap();
  9. searchMap.put("keywords","小米");
  10. // searchMap.put("category","手机");
  11. // searchMap.put("brand","小米");
  12. Map map = new HashMap();
  13. map.put("颜色","紫色");
  14. // map.put("",""); // 其他规格类型
  15. searchMap.put("spec",map);
  16. // searchMap.put("price","0-3000");
  17. // 返回结果Map
  18. Map resultMap = new HashMap();
  19. // 查询商品列表
  20. resultMap.putAll(searchSkuList(searchMap));
  21. // 查询分类列表
  22. List<String> categoryList = searchCategoryList(searchMap);
  23. resultMap.put("categoryList",categoryList);
  24. }

测试类完整代码

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class GoodsTest {
  4. @Autowired
  5. private ElasticsearchTemplate template;
  6. @Autowired
  7. private GoodsRepository goodsRepository;
  8. @Autowired
  9. private EsResultMapper esResultMapper;
  10. @Test
  11. public void createIndex(){
  12. template.createIndex(Goods.class);
  13. }
  14. @Test
  15. public void createDoc(){
  16. // Map map1 = new HashMap();
  17. // map1.put("颜色","蓝色");
  18. // map1.put("套餐","标准套餐");
  19. // Goods goods1 = new Goods(2L,"Redmi Note7秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
  20. //
  21. // Map map2 = new HashMap();
  22. // map2.put("颜色","蓝色");
  23. // map2.put("套餐","标准套餐");
  24. // Goods goods2 = new Goods(3L,"Redmi Note7秘境黑优惠套餐16G+64G",500,"xxxx",new Date(),3L,"手机","小米",map2,100);
  25. //
  26. // Map map3 = new HashMap();
  27. // map3.put("颜色","黑色");
  28. // map3.put("尺寸","64寸");
  29. // Goods goods3 = new Goods(4L,"小米电视 黑色 64寸 优惠套餐",1000,"xxxx",new Date(),4L,"电视","小米",map3,100);
  30. //
  31. // Map map4 = new HashMap();
  32. // map4.put("颜色","金色");
  33. // map4.put("尺寸","46寸");
  34. // Goods goods4 = new Goods(5L,"华为电视 金色 46寸 优惠套餐",1500,"xxxx",new Date(),5L,"电视","华为",map4,100);
  35. //
  36. // Map map5 = new HashMap();
  37. // map5.put("颜色","白金色");
  38. // map5.put("网络制式","全网通5G");
  39. // Goods goods5 = new Goods(6L,"华为P30 金色 全网通5G 优惠套餐",2000,"xxxx",new Date(),6L,"手机","华为",map5,100);
  40. // List<Goods> list = new ArrayList<>();
  41. // list.add(goods1);
  42. // list.add(goods2);
  43. // list.add(goods3);
  44. // list.add(goods4);
  45. // list.add(goods5);
  46. // goodsRepository.saveAll(list);
  47. Map map1 = new HashMap();
  48. map1.put("颜色","紫色");
  49. map1.put("套餐","标准套餐");
  50. Goods goods1 = new Goods(7L,"小米 Mini9秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
  51. goodsRepository.save(goods1);
  52. // Map map1 = new HashMap();
  53. // map1.put("颜色","蓝色");
  54. // map1.put("套餐","标准套餐");
  55. // Goods goods1 = new Goods(2L,"Redmi Note7秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
  56. // goodsRepository.save(goods1);
  57. }
  58. /**
  59. * 搜索方法 - searchMap应该由前端传过来
  60. * searchMap里封装了一些条件,根据条件进行过滤
  61. */
  62. @Test
  63. public void search(){
  64. // 搜索条件Map
  65. Map searchMap = new HashMap();
  66. searchMap.put("keywords","小米");
  67. // searchMap.put("category","手机");
  68. // searchMap.put("brand","小米");
  69. Map map = new HashMap();
  70. map.put("颜色","紫色");
  71. // map.put("",""); // 其他规格类型
  72. searchMap.put("spec",map);
  73. // searchMap.put("price","0-3000");
  74. // 返回结果Map
  75. Map resultMap = new HashMap();
  76. // 查询商品列表
  77. resultMap.putAll(searchSkuList(searchMap));
  78. // 查询分类列表
  79. List<String> categoryList = searchCategoryList(searchMap);
  80. resultMap.put("categoryList",categoryList);
  81. }
  82. /**
  83. * 查询Sku集合 - 商品列表
  84. * @param searchMap 查询条件
  85. * @return
  86. */
  87. private Map searchSkuList(Map searchMap) {
  88. Map resultMap = new HashMap();
  89. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
  90. BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
  91. // 查询
  92. nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
  93. // 排序
  94. String sortField = (String)searchMap.get("sortField"); // 排序字段
  95. String sortRule = (String)searchMap.get("sortRule"); // 排序规则 - 顺序(ASC)/倒序(DESC)
  96. if(sortField!= null && !"".equals(sortField)){
  97. nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule)));
  98. }
  99. // 构建分页
  100. nativeSearchQueryBuilder.withPageable(PageRequest.of(0,15));
  101. // 构建高亮查询
  102. HighlightBuilder.Field field = new HighlightBuilder.Field("name").preTags("<font style='color:red'>").postTags("</font>");
  103. nativeSearchQueryBuilder.withHighlightFields(field); // 名字高亮
  104. NativeSearchQuery build = nativeSearchQueryBuilder.build();
  105. // 获取查询结果
  106. AggregatedPage<Goods> goodsPage = template.queryForPage(build, Goods.class, esResultMapper);
  107. long total = goodsPage.getTotalElements(); // 总数据量
  108. long totalPage = goodsPage.getTotalPages(); // 总页数
  109. // ...你还要将是否有上页下页等内容传过去
  110. List<Goods> goodsList = goodsPage.getContent();
  111. goodsList.forEach(System.out::println);
  112. resultMap.put("rows",goodsList);
  113. resultMap.put("total",total);
  114. resultMap.put("totalPage",totalPage);
  115. return resultMap;
  116. }
  117. /**
  118. * 查询分类列表
  119. * @param searchMap
  120. * @return
  121. */
  122. private List<String> searchCategoryList(Map searchMap) {
  123. NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
  124. // 构建查询
  125. BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
  126. nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
  127. // 分类聚合名
  128. String groupName = "sku_category";
  129. // 构建聚合查询
  130. TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(groupName).field("categoryName");
  131. nativeSearchQueryBuilder.addAggregation(termsAggregationBuilder);
  132. // 获取聚合分页结果
  133. AggregatedPage<Goods> goodsList = (AggregatedPage<Goods>) goodsRepository.search(nativeSearchQueryBuilder.build());
  134. // 在查询结果中找到聚合 - 根据聚合名称
  135. StringTerms stringTerms = (StringTerms) goodsList.getAggregation(groupName);
  136. // 获取桶
  137. List<StringTerms.Bucket> buckets = stringTerms.getBuckets();
  138. // 使用流Stream 将分类名存入集合
  139. List<String> categoryList = buckets.stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
  140. // 打印分类名称
  141. categoryList.forEach(System.out::println);
  142. return categoryList;
  143. }
  144. /**
  145. * 构建基本查询 - 搜索关键字、分类、品牌、规格、价格
  146. * @param searchMap
  147. * @return
  148. */
  149. private BoolQueryBuilder buildBasicQuery(Map searchMap) {
  150. // 构建布尔查询
  151. BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
  152. // 关键字查询
  153. boolQueryBuilder.must(QueryBuilders.matchQuery("name",searchMap.get("keywords")));
  154. // 分类、品牌、规格 都是需要精准查询的,无需分词
  155. // 商品分类过滤
  156. if (searchMap.get("category") != null){
  157. boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("categoryName",searchMap.get("category")));
  158. }
  159. // 商品品牌过滤
  160. if(searchMap.get("brand") != null){
  161. boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("brandName",searchMap.get("brand")));
  162. }
  163. // 规格过滤
  164. if(searchMap.get("spec") != null){
  165. Map<String,String> map = (Map) searchMap.get("spec");
  166. for(Map.Entry<String,String> entry : map.entrySet()){
  167. // 规格查询[spec.xxx],因为规格是不确定的,所以需要精确查找,加上.keyword,如spec.颜色.keyword
  168. boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("spec." + entry.getKey() + ".keyword",entry.getValue()));
  169. }
  170. }
  171. // 价格过滤
  172. if(searchMap.get("price") != null){
  173. // 价格: 0-500 0-*
  174. String[] prices = ((String)searchMap.get("price")).split("-");
  175. if(!prices[0].equals("0")){ // 加两个0是,因为价格转换成分
  176. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(prices[0] + "00"));
  177. }
  178. if(!prices[1].equals("*")){ // 价格有上限
  179. boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(prices[1] + "00"));
  180. }
  181. }
  182. return boolQueryBuilder;
  183. }
  184. }

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/qq_40885085/article/details/105024625/

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/409893
推荐阅读
相关标签
  

闽ICP备14008679号