赞
踩
本文要实现的一个功能,根据品牌、分类、规格、价格过滤查询商品的功能,并对查询结果的关键字进行高亮显示。只做后端功能
本文是以代码驱动,如果看不太懂,可以先复制代码,再慢慢看,注释很详细。
1、引入相关依赖
主要就是fastjson和spring-boot-starter-data-elasticsearch(SpringBoot项目),fastJson的作用是转换对象使用,当然也可以进行时间格式化(本文未作处理)。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.28</version>
- </dependency>
2、ElasticSearch数据测试准备
①创建Goods类,测试类里注入相关对象
@Document(indexName = "goods_sku",type = "goods") @Data public class Goods { @Id @Field(type = FieldType.Long,store = true) private Long id; // 主键Id @Field(type = FieldType.Text,analyzer = "ik_smart",store = true) private String name; // 商品名称 @Field(type = FieldType.Integer,store = true) private Integer price; // 商品价格 @Field(type = FieldType.Text,store = true,index = false) private String image; // 商品图片src @Field(type = FieldType.Date,store = true,index = false) private Date createTime; // 商品创建时间 @Field(type = FieldType.Long,store = true,index = false) private Long spuId; // Spu的Id @Field(type = FieldType.Keyword,store = true) private String categoryName;// 分类名称 @Field(type = FieldType.Keyword,store = true) private String brandName; // 品牌名称 @Field(type = FieldType.Object,store = true) private Map spec; // 规格Map Map<String,String>,如<"颜色","黑色"> @Field(type = FieldType.Integer,store = true,index = false) private Integer saleNum; // 销量 public Goods(){ } public Goods(Long id, String name, Integer price, String image, Date createTime, Long spuId, String categoryName, String brandName, Map spec, Integer saleNum) { this.id = id; this.name = name; this.price = price; this.image = image; this.createTime = createTime; this.spuId = spuId; this.categoryName = categoryName; this.brandName = brandName; this.spec = spec; this.saleNum = saleNum; } }
- @Autowired
- private ElasticsearchTemplate template;
- @Autowired
- private GoodsRepository goodsRepository;
- @Autowired
- private EsResultMapper esResultMapper;
②数据准备 - 尽量多准备一些数据,方便测试查询
- @Test
- public void createIndex(){
- template.createIndex(Goods.class);
- }
-
- @Test
- public void createDoc(){
- Map map1 = new HashMap();
- map1.put("颜色","紫色");
- map1.put("套餐","标准套餐");
- Goods goods1 = new Goods(7L,"小米 Mini9秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
- goodsRepository.save(goods1);
- // 使用saveAll批量存储
- }
-
3、构建基本查询方法
该方法通过传过来的条件Map,根据条件进行过滤查询,比如分类、品牌、规格、价格区间等(具体取决于需求)。
/** * 构建基本查询 - 搜索关键字、分类、品牌、规格、价格 * @param searchMap * @return */ private BoolQueryBuilder buildBasicQuery(Map searchMap) { // 构建布尔查询 BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 关键字查询 boolQueryBuilder.must(QueryBuilders.matchQuery("name",searchMap.get("keywords"))); // 分类、品牌、规格 都是需要精准查询的,无需分词 // 商品分类过滤 if (searchMap.get("category") != null){ boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("categoryName",searchMap.get("category"))); } // 商品品牌过滤 if(searchMap.get("brand") != null){ boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("brandName",searchMap.get("brand"))); } // 规格过滤 if(searchMap.get("spec") != null){ Map<String,String> map = (Map) searchMap.get("spec"); for(Map.Entry<String,String> entry : map.entrySet()){ // 规格查询[spec.xxx],因为规格是不确定的,所以需要精确查找,加上.keyword,如spec.颜色.keyword boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("spec." + entry.getKey() + ".keyword",entry.getValue())); } } // 价格过滤 if(searchMap.get("price") != null){ // 价格: 0-500 0-* String[] prices = ((String)searchMap.get("price")).split("-"); if(!prices[0].equals("0")){ // 加两个0是,因为价格转换成分 boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(prices[0] + "00")); } if(!prices[1].equals("*")){ // 价格有上限 boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(prices[1] + "00")); } } return boolQueryBuilder; }
4、查询分类列表
主要是根据搜索关键字查询查询出来的结果,将其分类,然后把分类查询出来,显示到前端。
/** * 查询分类列表 * @param searchMap * @return */ private List<String> searchCategoryList(Map searchMap) { NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); // 构建查询 BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap); nativeSearchQueryBuilder.withQuery(boolQueryBuilder); // 分类聚合名 String groupName = "sku_category"; // 构建聚合查询 TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(groupName).field("categoryName"); nativeSearchQueryBuilder.addAggregation(termsAggregationBuilder); // 获取聚合分页结果 AggregatedPage<Goods> goodsList = (AggregatedPage<Goods>) goodsRepository.search(nativeSearchQueryBuilder.build()); // 在查询结果中找到聚合 - 根据聚合名称 StringTerms stringTerms = (StringTerms) goodsList.getAggregation(groupName); // 获取桶 List<StringTerms.Bucket> buckets = stringTerms.getBuckets(); // 使用流Stream 将分类名存入集合 List<String> categoryList = buckets.stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList()); // 打印分类名称 categoryList.forEach(System.out::println); return categoryList; }
既然有了分类,那么肯定还有对应的品牌、规格。其实品牌和规格与分类是有一个联系的。ElasticSearch查询出分类,每个分类对应一个id,也就是说所有分类和分类的id应该存到Redis中去,这样前端就可以根据返回的分类集合去查询对应的品牌和规格,这里只是提供一个实现思路。
String categoryName = ""; // 分类名 if(searchMap.get("category") == null){ // 如果查询条件没有分类 // 默认取分类列表的第一个 if(categoryList.size() > 0){ categoryName = categoryList.get(0); } }else{ // 如果查询条件有分类 // 则取查询条件中的分类 categoryName = searchMap.get("category"); } // 根据分类名查询品牌 - 实际应该从Redis中查询 if(searchMap.get("brand")==null) { List<Map> brandList = brandDao.findListByCategoryName(categoryName); resultMap.put("brandList", brandList); } // 根据分类查询规格 - 实际应该从Redis中查询 List<Map> specList = specDao.findListByCategoryName(categoryName); for(Map spec:specList){ // 规格选项列表 - 选项与选项之间是以,(逗号)分隔的 String[] options = ((String) spec.get("options")).split(","); // 讲过规格选项放入到规格对象中 spec.put("options",options); } // 将规格对象放入到结果集 resultMap.put("specList",specList);
5、重新实现SearchResultMapper - 高亮前奏
因为默认的SearchResultMapper是没有高亮的,我们需要重新实现,重写AggregatedPage方法。
- @Component
- public class EsResultMapper implements SearchResultMapper {
-
- @Override
- public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
- // 记录总条数
- long totalHits = response.getHits().getTotalHits();
- // 记录列表(泛型) - 构建Aggregate使用
- List<T> list = Lists.newArrayList();
- // 获取搜索结果(真正的的记录)
- SearchHits hits = response.getHits();
- for (SearchHit hit : hits) {
- if(hits.getHits().length <= 0){
- return null;
- }
- // 将原本的JSON对象转换成Map对象
- Map<String, Object> map = hit.getSourceAsMap();
- // 获取高亮的字段Map
- Map<String, HighlightField> highlightFields = hit.getHighlightFields();
- for (Map.Entry<String, HighlightField> highlightField : highlightFields.entrySet()) {
- // 获取高亮的Key
- String key = highlightField.getKey();
- // 获取高亮的Value
- HighlightField value = highlightField.getValue();
- // 实际fragments[0]就是高亮的结果,无需遍历拼接
- Text[] fragments = value.getFragments();
- StringBuilder sb = new StringBuilder();
- for (Text text : fragments) {
- sb.append(text);
- }
- // 因为高亮的字段必然存在于Map中,就是key值
- // 可能有一种情况,就是高亮的字段是嵌套Map,也就是说在Map里面还有Map的这种情况,这里没有考虑
- map.put(key, sb.toString());
- }
- // 把Map转换成对象
- T item = JSON.parseObject(JSONObject.toJSONString(map),aClass);
- list.add(item);
- }
- // 返回的是带分页的结果
- return new AggregatedPageImpl<>(list, pageable, totalHits);
- }
-
- }
6、查询商品(sku)列表
- /**
- * 查询Sku集合 - 商品列表
- * @param searchMap 查询条件
- * @return
- */
- private Map searchSkuList(Map searchMap) {
- Map resultMap = new HashMap();
- NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
- BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
- // 查询
- nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
- // 排序
- String sortField = (String)searchMap.get("sortField"); // 排序字段
- String sortRule = (String)searchMap.get("sortRule"); // 排序规则 - 顺序(ASC)/倒序(DESC)
- if(sortField!= null && !"".equals(sortField)){
- nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule)));
- }
- // 构建分页
- nativeSearchQueryBuilder.withPageable(PageRequest.of(0,15));
-
- // 构建高亮查询
- HighlightBuilder.Field field = new HighlightBuilder.Field("name").preTags("<font style='color:red'>").postTags("</font>");
- nativeSearchQueryBuilder.withHighlightFields(field); // 名字高亮
- NativeSearchQuery build = nativeSearchQueryBuilder.build();
- // 获取查询结果
- AggregatedPage<Goods> goodsPage = template.queryForPage(build, Goods.class, esResultMapper);
- long total = goodsPage.getTotalElements(); // 总数据量
- long totalPage = goodsPage.getTotalPages(); // 总页数
- // ...你还要将是否有上页下页等内容传过去
- List<Goods> goodsList = goodsPage.getContent();
- goodsList.forEach(System.out::println);
- resultMap.put("rows",goodsList);
- resultMap.put("total",total);
- resultMap.put("totalPage",totalPage);
- return resultMap;
- }
7、查询
- /**
- * 搜索方法 - searchMap应该由前端传过来
- * searchMap里封装了一些条件,根据条件进行过滤
- */
- @Test
- public void search(){
- // 搜索条件Map
- Map searchMap = new HashMap();
- searchMap.put("keywords","小米");
- // searchMap.put("category","手机");
- // searchMap.put("brand","小米");
- Map map = new HashMap();
- map.put("颜色","紫色");
- // map.put("",""); // 其他规格类型
- searchMap.put("spec",map);
- // searchMap.put("price","0-3000");
-
- // 返回结果Map
- Map resultMap = new HashMap();
- // 查询商品列表
- resultMap.putAll(searchSkuList(searchMap));
- // 查询分类列表
- List<String> categoryList = searchCategoryList(searchMap);
- resultMap.put("categoryList",categoryList);
- }
- @RunWith(SpringRunner.class)
- @SpringBootTest
- public class GoodsTest {
-
- @Autowired
- private ElasticsearchTemplate template;
- @Autowired
- private GoodsRepository goodsRepository;
- @Autowired
- private EsResultMapper esResultMapper;
-
- @Test
- public void createIndex(){
- template.createIndex(Goods.class);
- }
-
- @Test
- public void createDoc(){
- // Map map1 = new HashMap();
- // map1.put("颜色","蓝色");
- // map1.put("套餐","标准套餐");
- // Goods goods1 = new Goods(2L,"Redmi Note7秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
- //
- // Map map2 = new HashMap();
- // map2.put("颜色","蓝色");
- // map2.put("套餐","标准套餐");
- // Goods goods2 = new Goods(3L,"Redmi Note7秘境黑优惠套餐16G+64G",500,"xxxx",new Date(),3L,"手机","小米",map2,100);
- //
- // Map map3 = new HashMap();
- // map3.put("颜色","黑色");
- // map3.put("尺寸","64寸");
- // Goods goods3 = new Goods(4L,"小米电视 黑色 64寸 优惠套餐",1000,"xxxx",new Date(),4L,"电视","小米",map3,100);
- //
- // Map map4 = new HashMap();
- // map4.put("颜色","金色");
- // map4.put("尺寸","46寸");
- // Goods goods4 = new Goods(5L,"华为电视 金色 46寸 优惠套餐",1500,"xxxx",new Date(),5L,"电视","华为",map4,100);
- //
- // Map map5 = new HashMap();
- // map5.put("颜色","白金色");
- // map5.put("网络制式","全网通5G");
- // Goods goods5 = new Goods(6L,"华为P30 金色 全网通5G 优惠套餐",2000,"xxxx",new Date(),6L,"手机","华为",map5,100);
- // List<Goods> list = new ArrayList<>();
- // list.add(goods1);
- // list.add(goods2);
- // list.add(goods3);
- // list.add(goods4);
- // list.add(goods5);
- // goodsRepository.saveAll(list);
- Map map1 = new HashMap();
- map1.put("颜色","紫色");
- map1.put("套餐","标准套餐");
- Goods goods1 = new Goods(7L,"小米 Mini9秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
- goodsRepository.save(goods1);
- // Map map1 = new HashMap();
- // map1.put("颜色","蓝色");
- // map1.put("套餐","标准套餐");
- // Goods goods1 = new Goods(2L,"Redmi Note7秘境黑优惠套餐16G+64G",100,"xxxx",new Date(),2L,"手机","小米",map1,100);
- // goodsRepository.save(goods1);
- }
-
-
- /**
- * 搜索方法 - searchMap应该由前端传过来
- * searchMap里封装了一些条件,根据条件进行过滤
- */
- @Test
- public void search(){
- // 搜索条件Map
- Map searchMap = new HashMap();
- searchMap.put("keywords","小米");
- // searchMap.put("category","手机");
- // searchMap.put("brand","小米");
- Map map = new HashMap();
- map.put("颜色","紫色");
- // map.put("",""); // 其他规格类型
- searchMap.put("spec",map);
- // searchMap.put("price","0-3000");
-
- // 返回结果Map
- Map resultMap = new HashMap();
- // 查询商品列表
- resultMap.putAll(searchSkuList(searchMap));
- // 查询分类列表
- List<String> categoryList = searchCategoryList(searchMap);
- resultMap.put("categoryList",categoryList);
- }
-
- /**
- * 查询Sku集合 - 商品列表
- * @param searchMap 查询条件
- * @return
- */
- private Map searchSkuList(Map searchMap) {
- Map resultMap = new HashMap();
- NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
- BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
- // 查询
- nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
- // 排序
- String sortField = (String)searchMap.get("sortField"); // 排序字段
- String sortRule = (String)searchMap.get("sortRule"); // 排序规则 - 顺序(ASC)/倒序(DESC)
- if(sortField!= null && !"".equals(sortField)){
- nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort(sortField).order(SortOrder.valueOf(sortRule)));
- }
- // 构建分页
- nativeSearchQueryBuilder.withPageable(PageRequest.of(0,15));
-
- // 构建高亮查询
- HighlightBuilder.Field field = new HighlightBuilder.Field("name").preTags("<font style='color:red'>").postTags("</font>");
- nativeSearchQueryBuilder.withHighlightFields(field); // 名字高亮
- NativeSearchQuery build = nativeSearchQueryBuilder.build();
- // 获取查询结果
- AggregatedPage<Goods> goodsPage = template.queryForPage(build, Goods.class, esResultMapper);
- long total = goodsPage.getTotalElements(); // 总数据量
- long totalPage = goodsPage.getTotalPages(); // 总页数
- // ...你还要将是否有上页下页等内容传过去
- List<Goods> goodsList = goodsPage.getContent();
- goodsList.forEach(System.out::println);
- resultMap.put("rows",goodsList);
- resultMap.put("total",total);
- resultMap.put("totalPage",totalPage);
- return resultMap;
- }
-
- /**
- * 查询分类列表
- * @param searchMap
- * @return
- */
- private List<String> searchCategoryList(Map searchMap) {
- NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
- // 构建查询
- BoolQueryBuilder boolQueryBuilder = buildBasicQuery(searchMap);
- nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
- // 分类聚合名
- String groupName = "sku_category";
- // 构建聚合查询
- TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(groupName).field("categoryName");
- nativeSearchQueryBuilder.addAggregation(termsAggregationBuilder);
- // 获取聚合分页结果
- AggregatedPage<Goods> goodsList = (AggregatedPage<Goods>) goodsRepository.search(nativeSearchQueryBuilder.build());
- // 在查询结果中找到聚合 - 根据聚合名称
- StringTerms stringTerms = (StringTerms) goodsList.getAggregation(groupName);
- // 获取桶
- List<StringTerms.Bucket> buckets = stringTerms.getBuckets();
- // 使用流Stream 将分类名存入集合
- List<String> categoryList = buckets.stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
- // 打印分类名称
- categoryList.forEach(System.out::println);
- return categoryList;
- }
-
- /**
- * 构建基本查询 - 搜索关键字、分类、品牌、规格、价格
- * @param searchMap
- * @return
- */
- private BoolQueryBuilder buildBasicQuery(Map searchMap) {
- // 构建布尔查询
- BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
- // 关键字查询
- boolQueryBuilder.must(QueryBuilders.matchQuery("name",searchMap.get("keywords")));
- // 分类、品牌、规格 都是需要精准查询的,无需分词
- // 商品分类过滤
- if (searchMap.get("category") != null){
- boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("categoryName",searchMap.get("category")));
- }
- // 商品品牌过滤
- if(searchMap.get("brand") != null){
- boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("brandName",searchMap.get("brand")));
- }
- // 规格过滤
- if(searchMap.get("spec") != null){
- Map<String,String> map = (Map) searchMap.get("spec");
- for(Map.Entry<String,String> entry : map.entrySet()){
- // 规格查询[spec.xxx],因为规格是不确定的,所以需要精确查找,加上.keyword,如spec.颜色.keyword
- boolQueryBuilder.filter(QueryBuilders.matchPhraseQuery("spec." + entry.getKey() + ".keyword",entry.getValue()));
- }
- }
- // 价格过滤
- if(searchMap.get("price") != null){
- // 价格: 0-500 0-*
- String[] prices = ((String)searchMap.get("price")).split("-");
- if(!prices[0].equals("0")){ // 加两个0是,因为价格转换成分
- boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gt(prices[0] + "00"));
- }
- if(!prices[1].equals("*")){ // 价格有上限
- boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").lt(prices[1] + "00"));
- }
- }
- return boolQueryBuilder;
- }
- }
-
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40885085/article/details/105024625/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。