商城搜索 elasticSearch基础实战——排序,筛选价格区间·功效·品牌,聚合,分页等功能实现

商城搜索 elasticSearch基础实战 -排序筛选聚合分页等功能

  • 一:在elasticSearch创建mapping(首先确保在LIUNX下安装成功elasticSearch)
  • 二: 将商品数据封装成elasticSearch库中需要的数据结构
    • 2.1. 根据mapping规则创建实体类字段
    • 2.2. 将商品数据进行封装(这里根据自己的业务需求进行更改,不要直接复制)
    • 2.3. 将封装的数据通过openfeign调用elasticSearch微服务中:
  • 三. 创建elasticSearch微服务
    • 3.1. elasticSearch服务目录结构
    • 3.1. 在pom导入jar包
    • 3.2. 在yml中配置elasticSearch路径
    • 3.3. 同样也可以在配置类(SearchConfig )进行配置
    • 3.4. 基础配置类(EsConstant)
  • 四. 将数据保存在elasticSearch中
    • 4.1. 在SearchController中写saveSearch接口:
    • 4.2. 在SearchServiceImpl中实现saveSearch接口
  • 六. 搜索商品数据
    • 6.1. 将搜索搜索条件封装成SearchVo 实体类
    • 6.2. 将渲染数据封装成
    • 6.3. 在SearchController增加searchKeywork接口
    • 6.4. 在SearchServiceImpl中实现searchKeywork接口
      • 6.4.1. 传入搜索条件 querySearch
      • 6.4.2. 返回封装数据格式querySearchResponse
  • 七. 测试
    • 7.1. PostMan测试接口数据 路径:localhost:端口号/search/searchKeywork
      • 7.1.1 返回数据结构
    • 7.2. 测试搜索条件 querySearch是否有问题
    • 7.3. kibana测试查询数据结构:
  • 如果有问题请私信我,对自己有收获请给我点个赞。谢谢!

一:在elasticSearch创建mapping(首先确保在LIUNX下安装成功elasticSearch)

PUT gemme
{
  "mappings": {
    "properties": {
      "productId": { //商品ID
        "type": "long"   
      },
      "price": {   //商品价格
        "type": "keyword"
      },
      "discountPrice": {  //商品折扣价格
        "type": "keyword"
      },
      "productName": { //商品名称
        "type": "text",
        "analyzer": "ik_smart"   //按照ik_smart进行分词
      },
      "productImg": {  //商品图片
        "type": "text",
        "index": false,   //不允许被查询
        "doc_values": false  //不允许被聚合
      },
      "brandId": {  //品牌ID
        "type": "long"
      },
      "brandName": { //品牌名称
        "type": "keyword"
      },
      "oneCategoryId": { //一级分类ID
        "type": "long"
      },
      "oneCategoryName": { //一级分类名称
        "type": "keyword"
      },
      "twoCategoryId": { //二级分类ID
        "type": "long"
      },
      "twoCategoryName": {  //一级分类名称
        "type": "keyword"
      },
      "lockCnt": {   //热销
        "type": "long"
      },
      "publishStatus": { //状态
        "type": "long"
      },
      "productEffect": {  //功效
        "type": "nested",   //防止在es库中被扁平处理
        "properties": {
          "productEffectId": {
            "type": "long"
          },
          "productEffectName": {
            "type": "keyword"
          }
        }
      },
      "skin": {  //肤质
        "type": "nested",
        "properties": {
          "skinId": {
            "type": "long"
          },
          "skinName": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

二: 将商品数据封装成elasticSearch库中需要的数据结构

2.1. 根据mapping规则创建实体类字段

@Data
public class ProductEsVo {
	//商品ID
	private Long productId;
	//商品名称
	private String productName;
	//商品价格
	private BigDecimal price;
	//商品打折后的价格
	private BigDecimal discountPrice;
	// 商品主图
	private String productImg;
	//品牌ID
	private Long brandId;
	//品牌名称
	private  String  brandName;
	//一级分类Id
	private Long oneCategoryId;
	//一级分类名称
	private String oneCategoryName;
	//二级分类Id
	private Long twoCategoryId;
	//二级分类名称
	private String twoCategoryName;
	//出售量
	private Long lockCnt;
	//商品状态
	private Long publishStatus;
	//商品功效
	private List<ProductEffect> productEffect;
	//肤质
	private  List<Skin> skin;

	@Data
	public static  class  ProductEffect{
		//功效ID
		private Long productEffectId;
		//功效名称
		private  String productEffectName;
	}

	@Data
	public static class Skin{
		//肤质ID
		private  Long skinId;
		//肤质名称
		private  String skinName;
	}

}

2.2. 将商品数据进行封装(这里根据自己的业务需求进行更改,不要直接复制)

List<ShopProductInfo> shopList=this.shopProductList();
List<ProductEsVo> esList=shopList.stream().map(pro->{
					ProductEsVo esvo=new ProductEsVo();
					esvo.setProductId(pro.getId());
					esvo.setProductName(pro.getProductName());
					esvo.setPrice(pro.getProductPrice());
					esvo.setBrandId(pro.getBrandId());
					esvo.setOneCategoryId(pro.getOneCategoryId());
					esvo.setTwoCategoryId(pro.getTwoCategoryId());
					esvo.setLockCnt((long) shopWarehouseProductService.getWarehouseProductLockCnt(pro.getId())!=0?(long) shopWarehouseProductService.getWarehouseProductLockCnt(pro.getId()):0);
					esvo.setBrandName(this.shopBrandString(pro.getBrandId()));
					esvo.setPublishStatus(Long.parseLong(publishStatus.toString()));
					esvo.setProductImg(shopProductPictureService.getPrcUrl(pro.getId()));
					esvo.setOneCategoryName(this.shopCategoryName(pro.getOneCategoryId()));
					esvo.setTwoCategoryName(this.shopCategoryName(pro.getTwoCategoryId()));
					String skuList = shopSkuService.skuList(pro.getId());
					if(skuList!=null && skuList.length()>0){
						List<ShopAttributeResponseVo> shopAttributeResponseVos = JSON.parseArray(skuList, ShopAttributeResponseVo.class);
						List<List<ShopAttributeResponseVo.AttributeChildren>> listChildren = shopAttributeResponseVos.stream().filter(item -> {
							return item.getKey().equals(shopAttributeService.getAttributeId(pro.getOneCategoryId()).toString());
						}).map(itemMap -> {
							return itemMap.getChildren();
						}).collect(Collectors.toList());
					if(listChildren.size()>0 && listChildren!=null){
						List<ProductEsVo.ProductEffect> effectList = listChildren.get(0).stream().map(childrenItem -> {
							ProductEsVo.ProductEffect effect = new ProductEsVo.ProductEffect();
							effect.setProductEffectId(Long.parseLong(childrenItem.getId()));
							effect.setProductEffectName(childrenItem.getAttributeName());
							return effect;
						}).collect(Collectors.toList());
						esvo.setProductEffect(effectList);
					}
					}
					return esvo;
				}
		).collect(Collectors.toList());
		return shopSearchFeign.saveSearch(esList); //调用elasticSearch服务的存放接口

2.3. 将封装的数据通过openfeign调用elasticSearch微服务中:

@FeignClient(contextId = "shopSearchFeign", value = "gemme-search”)
public interface ShopSearchFeign {
	@PostMapping("/search/info")
	public R saveSearch(@RequestBody List<ProductEsVo> list);
}

三. 创建elasticSearch微服务

3.1. elasticSearch服务目录结构

商城搜索 elasticSearch基础实战——排序,筛选价格区间·功效·品牌,聚合,分页等功能实现_第1张图片

3.1. 在pom导入jar包

       <dependency>
			<groupId>org.elasticsearch.client</groupId>
			<artifactId>elasticsearch-rest-high-level-client</artifactId>
		</dependency>

首先注意:导入的elasticSearch版本号一定要和安装liunx中elasticSearch版本一致 。如果springboot自带elasticSearch版本号跟导入的jar版本号不一致,需要手动修改springBoot elasticSearch版本号

3.2. 在yml中配置elasticSearch路径

  spring.elasticsearch.rest.uris: ip:9200

3.3. 同样也可以在配置类(SearchConfig )进行配置

(这里是单个elasticSearch,可以根据自己业务配置集群)

@Configuration
public class SearchConfig {

	@Bean
	public RestHighLevelClient getRestHighLevelClient(){
		RestHighLevelClient client = new RestHighLevelClient(
				RestClient.builder(
						new HttpHost("ip", 9200, "http")));
		return client;
	}
}

3.4. 基础配置类(EsConstant)

public class EsConstant {
	public static final  String GEMME_INDEX = "gemme";  //elasticSearch名称
	public static final  int PASE_SIZE = 10;    
}

四. 将数据保存在elasticSearch中

4.1. 在SearchController中写saveSearch接口:

@RestController
@RequestMapping("/search")
public class SearchController {
	private final SearchService searchService;

	@PostMapping("/info")
	public R saveSearch(@RequestBody List<ProductEsVo> list) throws IOException {
		return R.ok(searchService.saveSearch(list));
	}
}

4.2. 在SearchServiceImpl中实现saveSearch接口

@Service
public class SearchServiceImpl implements SearchService {
	@Autowired
	private RestHighLevelClient getRestHighLevelClient;
	@Override  //这里是批量插入
	public boolean saveSearch(List<ProductEsVo> list) throws IOException {
		BulkRequest bulkRequest = new BulkRequest();
		bulkRequest.timeout("10s");
		for (ProductEsVo productEsVo : list) {
			IndexRequest indexRequest = new IndexRequest(EsConstant.GEMME_INDEX);
			indexRequest.id(productEsVo.getProductId().toString());
			indexRequest.source(JSON.toJSONString(productEsVo), XContentType.JSON);
			bulkRequest.add(indexRequest);
		}
		BulkResponse bulk = getRestHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
		return bulk.hasFailures();
	}
}

六. 搜索商品数据

6.1. 将搜索搜索条件封装成SearchVo 实体类

@Data
public class SearchVo {

	//搜索框内容
	private String keyWork;

	//品牌ID
	private Long  brandId;

	//商品分类二级ID
	private Long twoCategoryId;

	//商品价格
	private String price;   //接收数据结构: 1-100 1- -1

	/**
	 * 排序条件
	 * sort=price-asc/desc  按照价格进行排序
	 * sort=lockCnt-asc/desc 按照销售量进行排序
	 */
	private String sort;

	//功效筛选
	private String productEffectId;  //接收数据结构:功效ID1-功效ID2
	//页码
	private Integer pageNumber = 1;

6.2. 将渲染数据封装成

@Data
public class SearchRespon {
	//search服务中的数据
	private List<ProductEsVo> productEsVoList;

	/**
	 * 分页信息
	 */
	private Integer pageNnmber;//当前页码
	private Long total;//总记录数
	private Integer totalPages;//总页码

	//品牌
	private List<BrandVo> brandVoList;
	//功效
	private List<ResponseEffect> responseEffectList;

	@Data
	public static class  BrandVo{
		//品牌ID
		private Long brandId;
		//品牌名称
		private  String  brandName;
	}

	@Data
	public static  class  ResponseEffect{
		//功效ID
		private Long productEffectId;
		//功效名称
		private  String productEffectName;
	}

}

6.3. 在SearchController增加searchKeywork接口

	/**
	 * 查询数据
	 */
	@GetMapping("/searchKeywork")
	public R searchKeywork(SearchVo searchVo){
		return R.ok(searchService.searchKey(searchVo));
	}

6.4. 在SearchServiceImpl中实现searchKeywork接口

	@Override
	public SearchRespon searchKey(SearchVo searchVo) {
		SearchRespon request=null;
		SearchRequest searchRequest=querySearch(searchVo); 
		try {
			//获得返回的数据
			SearchResponse response = getRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
		  //返回封装数据格式
			request= querySearchResponse(response,searchVo);
		} catch (IOException e) {
			e.printStackTrace();
		}
		return request;
	}

6.4.1. 传入搜索条件 querySearch

private SearchRequest querySearch(SearchVo searchVo) {
		//构建搜索条件
		SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
		//查询条件
		//根据搜索查询bool
		BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
		//按照搜索框信息查询
		if(!StringUtils.isEmpty(searchVo.getKeyWork())){
			boolQueryBuilder.must(QueryBuilders.matchQuery("productName",searchVo.getKeyWork()));
		}
		//按照品牌ID查询
		if(searchVo.getBrandId()!=null){
			boolQueryBuilder.filter(QueryBuilders.termQuery("brandId",searchVo.getBrandId()));
		}
		//按照二级分类进行查询
		if(searchVo.getTwoCategoryId()!=null){
			boolQueryBuilder.filter(QueryBuilders.termQuery("twoCategoryId",searchVo.getTwoCategoryId()));
		}
		//按照价格进行筛选    1-1000   1-   -1
		if(!StringUtils.isEmpty(searchVo.getPrice())){
			RangeQueryBuilder priceRange = QueryBuilders.rangeQuery("price");
			String[] priceSplit=searchVo.getPrice().split("-");
			if(priceSplit.length ==2){
				priceRange.gte(priceSplit[0]).lte(priceSplit[1]);
			}else if(priceSplit.length ==1){
				if(searchVo.getPrice().startsWith("-")){
					priceRange.lte(priceSplit[0]);
				}
				if (searchVo.getPrice().endsWith("-")){
					priceRange.gte(priceSplit[0]);
				}
			}
			boolQueryBuilder.filter(QueryBuilders.rangeQuery("price"));
		}
		//通过功效查询  1-2-3
		if(!StringUtils.isEmpty(searchVo.getProductEffectId())){
			String[] pro=searchVo.getProductEffectId().split("-");
			for (int i = 0; i < pro.length; i++) {
				BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
				boolQuery.must(QueryBuilders.termQuery("productEffect.productEffectId",pro[i]));
				// 扁平化处理
				NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("productEffect",boolQuery, ScoreMode.None);
				boolQueryBuilder.filter(nestedQueryBuilder);
			}

		}
		searchSourceBuilder.query(boolQueryBuilder);
		//排序	price-desc /asc      lockCnt-desc/asc
		if(!StringUtils.isEmpty(searchVo.getSort())){
			String[] stringSort=searchVo.getSort().split("-");
			SortOrder order=stringSort[1].equalsIgnoreCase("asc")?SortOrder.ASC:SortOrder.DESC;
			searchSourceBuilder.sort(stringSort[0],order);
		}
		//分页
		searchSourceBuilder.from((searchVo.getPageNumber()-1)*EsConstant.PASE_SIZE);
		searchSourceBuilder.size(EsConstant.PASE_SIZE);
		//构建聚合 (品牌,功效)
		TermsAggregationBuilder brand_agg= AggregationBuilders.terms("brand_agg");
		brand_agg.field("brandId").size(50);
		brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(50));
		searchSourceBuilder.aggregation(brand_agg);
		NestedAggregationBuilder effect_agg = AggregationBuilders.nested("effect_agg","productEffect");
		TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("product_effectId_agg").field("productEffect.productEffectId").size(20);
		termsAggregationBuilder.subAggregation(AggregationBuilders.terms("product_effectName_agg").field("productEffect.productEffectName").size(20));
		effect_agg.subAggregation(termsAggregationBuilder);
		searchSourceBuilder.aggregation(effect_agg);
		SearchRequest searchRequest=new SearchRequest(new String[]{EsConstant.GEMME_INDEX},searchSourceBuilder);
		return  searchRequest;
	}

6.4.2. 返回封装数据格式querySearchResponse

private SearchRespon querySearchResponse(SearchResponse response,SearchVo searchVo) {
		SearchRespon searchRespon = new SearchRespon();
		SearchHits hits = response.getHits();

		List<ProductEsVo> voList=new ArrayList<>();
		//获得商品所有基础信息
		if(hits.getHits() != null && hits.getHits().length>0){
			for (SearchHit hit : hits.getHits()) {
				ProductEsVo esVo = JSON.parseObject(hit.getSourceAsString(), ProductEsVo.class);
				voList.add(esVo);
			}
		}
		searchRespon.setProductEsVoList(voList);
		//获取品牌数据
		ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
		List<SearchRespon.BrandVo> brandVoList = brand_agg.getBuckets().stream().map(item -> {
			SearchRespon.BrandVo brandVo = new SearchRespon.BrandVo();
			brandVo.setBrandId(Long.parseLong(item.getKeyAsString()));
			ParsedStringTerms brand_name_agg = item.getAggregations().get("brand_name_agg");
			brandVo.setBrandName(brand_name_agg.getBuckets().get(0).getKeyAsString());
			return brandVo;
		}).collect(Collectors.toList());
		searchRespon.setBrandVoList(brandVoList);
		//获得功效数据
		ParsedNested effect_agg = response.getAggregations().get("effect_agg");
		ParsedLongTerms e_agg = effect_agg.getAggregations().get("product_effectId_agg");
		List<SearchRespon.ResponseEffect> effectList = e_agg.getBuckets().stream().map(item -> {
			SearchRespon.ResponseEffect responseEffect = new SearchRespon.ResponseEffect();
			responseEffect.setProductEffectId(((Terms.Bucket) item).getKeyAsNumber().longValue());
			ParsedStringTerms product_effectName_agg = item.getAggregations().get("product_effectName_agg");
			responseEffect.setProductEffectName(product_effectName_agg.getBuckets().get(0).getKeyAsString());
			return responseEffect;
		}).collect(Collectors.toList());
		searchRespon.setResponseEffectList(effectList);
		//当前页码
		searchRespon.setPageNnmber(searchVo.getPageNumber());
		//总记录数
		long total = hits.getTotalHits().value;
		searchRespon.setTotal(total);
		//总页码
		searchRespon.setTotalPages((int) (total%EsConstant.PASE_SIZE==0?total/EsConstant.PASE_SIZE:(total/EsConstant.PASE_SIZE+1)));
		return  searchRespon;
	}

七. 测试

7.1. PostMan测试接口数据 路径:localhost:端口号/search/searchKeywork

商城搜索 elasticSearch基础实战——排序,筛选价格区间·功效·品牌,聚合,分页等功能实现_第2张图片

7.1.1 返回数据结构

{
    "code": 0,
    "msg": null,
    "data": {
        "productEsVoList": [
            {
                "productId": 1,
                "productName": "法国Dior/迪奥新款黑管瘾诱超模漆光唇釉口红唇膏740保湿持久正品",
                "price": 279.0,
                "discountPrice": null,
                "productImg": null,
                "brandId": 112359881173,
                "brandName": "Dior/迪奥",
                "oneCategoryId": 40,
                "oneCategoryName": "口红",
                "twoCategoryId": 42,
                "twoCategoryName": "迪奥",
                "lockCnt": 32,
                "publishStatus": 0,
                "productEffect": [
                    {
                        "productEffectId": 1278521450449358849,
                        "productEffectName": "美白"
                    },
                    {
                        "productEffectId": 1278521486050611201,
                        "productEffectName": "保湿"
                    }
                ],
                "skin": null
            },
            {
                "productId": 2,
                "productName": "正品YSL圣罗兰细管纯口红小金条持久哑光21复古红",
                "price": 123.0,
                "discountPrice": null,
                "productImg": null,
                "brandId": 112359881174,
                "brandName": "YSL/圣罗兰",
                "oneCategoryId": 40,
                "oneCategoryName": "口红",
                "twoCategoryId": 43,
                "twoCategoryName": "YSL",
                "lockCnt": 0,
                "publishStatus": 0,
                "productEffect": [
                    {
                        "productEffectId": 1278521450449358849,
                        "productEffectName": "美白"
                    },
                    {
                        "productEffectId": 1278521486050611201,
                        "productEffectName": "保湿"
                    }
                ],
                "skin": null
            }
        ],
        "pageNnmber": 1,
        "total": 2,
        "totalPages": 1,
        "brandVoList": [
            {
                "brandId": 112359881173,
                "brandName": "Dior/迪奥"
            },
            {
                "brandId": 112359881174,
                "brandName": "YSL/圣罗兰"
            }
        ],
        "responseEffectList": [
            {
                "productEffectId": 1278521450449358849,
                "productEffectName": "美白"
            },
            {
                "productEffectId": 1278521486050611201,
                "productEffectName": "保湿"
            }
        ]
    }
}

7.2. 测试搜索条件 querySearch是否有问题

	1.在querySearch中打印System.out.println(searchSourceBuilder.toString());,
	  将打印结构放入Kibana中进行测试

7.3. kibana测试查询数据结构:

GET gemme/_search
{
  "from": 0,
  "size": 10,
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "productName": {
              "query": "正品YSL圣罗兰",
              "operator": "OR",
              "prefix_length": 0,
              "max_expansions": 50,
              "fuzzy_transpositions": true,
              "lenient": false,
              "zero_terms_query": "NONE",
              "auto_generate_synonyms_phrase_query": true,
              "boost": 1
            }
          }
        }
      ],
      "filter": [
        {
          "nested": {
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "productEffect.productEffectId": {
                        "value": "1278521450449358849",
                        "boost": 1
                      }
                    }
                  }
                ],
                "adjust_pure_negative": true,
                "boost": 1
              }
            },
            "path": "productEffect",
            "ignore_unmapped": false,
            "score_mode": "none",
            "boost": 1
          }
        },
        {
          "nested": {
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "productEffect.productEffectId": {
                        "value": "1278521486050611201",
                        "boost": 1
                      }
                    }
                  }
                ],
                "adjust_pure_negative": true,
                "boost": 1
              }
            },
            "path": "productEffect",
            "ignore_unmapped": false,
            "score_mode": "none",
            "boost": 1
          }
        }
      ],
      "adjust_pure_negative": true,
      "boost": 1
    }
  },
  "aggregations": {
    "brand_agg": {
      "terms": {
        "field": "brandId",
        "size": 50,
        "min_doc_count": 1,
        "shard_min_doc_count": 0,
        "show_term_doc_count_error": false,
        "order": [
          {
            "_count": "desc"
          },
          {
            "_key": "asc"
          }
        ]
      },
      "aggregations": {
        "brand_name_agg": {
          "terms": {
            "field": "brandName",
            "size": 50,
            "min_doc_count": 1,
            "shard_min_doc_count": 0,
            "show_term_doc_count_error": false,
            "order": [
              {
                "_count": "desc"
              },
              {
                "_key": "asc"
              }
            ]
          }
        }
      }
    },
    "effect_agg": {
      "nested": {
        "path": "productEffect"
      },
      "aggregations": {
        "product_effectId_agg": {
          "terms": {
            "field": "productEffect.productEffectId",
            "size": 20,
            "min_doc_count": 1,
            "shard_min_doc_count": 0,
            "show_term_doc_count_error": false,
            "order": [
              {
                "_count": "desc"
              },
              {
                "_key": "asc"
              }
            ]
          },
          "aggregations": {
            "product_effectName_agg": {
              "terms": {
                "field": "productEffect.productEffectName",
                "size": 20,
                "min_doc_count": 1,
                "shard_min_doc_count": 0,
                "show_term_doc_count_error": false,
                "order": [
                  {
                    "_count": "desc"
                  },
                  {
                    "_key": "asc"
                  }
                ]
              }
            }
          }
        }
      }
    }
  }
}

如果有问题请私信我,对自己有收获请给我点个赞。谢谢!

你可能感兴趣的:(elasticSearch,elasticsearch,java,搜索引擎)