SpringCloud使用ElasticSearch
搜索微服务模块结构
配置文件
-
pom.xml----->elasticsearch依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
-
pom.xml----->该搜索微服务全部依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>leyou</artifactId> <groupId>com.leyou.parent</groupId> <version>1.0.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.search</groupId> <artifactId>ly-search</artifactId> <dependencies> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- elasticsearch --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <!-- eureka --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- feign 因为每一个微服务都是独立的, 所以我们这里要用feign来远程调用商品微服务 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- 测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!-- 需要用到商品实体类, 所以这里需要引用 --> <dependency> <groupId>com.leyou.upload.service</groupId> <artifactId>ly-item-interface</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency> </dependencies> </project>
-
application.yml----->配置elasticsearch
data: elasticsearch: cluster-name: elasticsearch # 集群名称 cluster-nodes: 192.168.79.128:9300 # 集群地址
-
application.yml----->该搜索微服务全部配置
server: port: 8084 spring: application: name: search-service data: elasticsearch: cluster-name: elasticsearch # 集群名称 cluster-nodes: 192.168.79.128:9300 # 集群地址 Jackson: default-property-inclusion: non_null # 返回的结果是null的就排除 eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka registry-fetch-interval-seconds: 5 # 每5秒拉一次注册信息 instance: prefer-ip-address: true ip-address: 127.0.0.1
启动类
-
LySearchApplication
package com.leyou.search; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @program: leyou * @description: * @author: Mr.Xiao * @create: 2020-06-11 14:39 **/ @SpringBootApplication @EnableDiscoveryClient // eureka @EnableFeignClients // feign public class LySearchApplication { public static void main(String[] args) { SpringApplication.run(LySearchApplication.class); } }
实体类
-
Goods
package com.leyou.search.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; @NoArgsConstructor @AllArgsConstructor @Data @Document(indexName = "goods", type = "docs", shards = 1, replicas = 0) public class Goods { @Id private Long id; // spuId @Field(type = FieldType.Text, analyzer = "ik_max_word") private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌 @Field(type = FieldType.Keyword, index = false) private String subTitle;// 卖点 // 过滤字段 不加注解, spring会自动映射进去 private Long brandId;// 品牌id private Long cid1;// 1级分类id private Long cid2;// 2级分类id private Long cid3;// 3级分类id private Date createTime;// 创建时间 private Set<Long> price;// 价格 @Field(type = FieldType.Keyword, index = false) private String skus;// sku信息的json结构 private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值 }
-
SearchRequest
package com.leyou.search.pojo; import lombok.Data; import org.bouncycastle.jcajce.provider.symmetric.IDEA; import java.util.Map; /** * @program: leyou * @description: * @author: Mr.Xiao * @create: 2020-06-12 17:49 **/ public class SearchRequest { // 当前页码 private Integer page; // 搜索字段 private String key; // 因为是给用户看的页面, 所以每页大小必须定死, 不可修改, 要是给用户输入的话如果是1千万那不搜索服务器直接挂了 private static final int DEFAULT_PAGE = 1; private static final int DEFAULT_SIZE = 20; // 过滤字段 private Map<String, String> filter; public Integer getPage() { if (page == null) { return DEFAULT_PAGE; } // 比较两个数的大小, page 大于 DEFAULT_PAGE用page, 小于DEFAULT_PAGE用DEFAULT_PAGE return Math.max(DEFAULT_PAGE, page); } public void setPage(Integer page) { this.page = page; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public int getSize() { return DEFAULT_SIZE; } public Map<String, String> getFilter() { return filter; } public void setFilter(Map<String, String> filter) { this.filter = filter; } }
-
SearchResult
package com.leyou.search.pojo; import com.leyou.common.vo.PageResult; import com.leyou.item.pojo.Brand; import com.leyou.item.pojo.Category; import lombok.Data; import java.util.List; import java.util.Map; @Data public class SearchResult extends PageResult<Goods> { private List<Category> categories; // 分类待选项 private List<Brand> brands; // 品牌待选项 private List<Map<String, Object>> specs; // 规格参数 key及待选项 public SearchResult() { } public SearchResult(Long total, Long totalPage, List<Goods> items, List<Category> categories, List<Brand> brands, List<Map<String, Object>> specs) { super(total, totalPage, items); this.categories = categories; this.brands = brands; this.specs = specs; } }
client---->继承的是别的微服务下的接口, 接口中提供了对应的可以远程调用的方法
-
BrandClient
package com.leyou.search.client; import com.leyou.item.api.BrandApi; import org.springframework.cloud.openfeign.FeignClient; /** * @program: leyou * @description: 告诉feign 请求服务 请求方式 请求路径 请求参数 返回结果 * @author: Mr.Xiao * @create: 2020-06-11 17:00 **/ @FeignClient("item-service") // 参数是服务名称 public interface BrandClient extends BrandApi { }
-
CategoryClient
package com.leyou.search.client; import com.leyou.item.api.CategoryApi; import org.springframework.cloud.openfeign.FeignClient; /** * @program: leyou * @description: * @author: Mr.Xiao * @create: 2020-06-11 17:00 **/ @FeignClient("item-service") public interface CategoryClient extends CategoryApi { }
-
GoodsClient
package com.leyou.search.client; import com.leyou.item.api.GoodsApi; import org.springframework.cloud.openfeign.FeignClient; /** * @program: leyou * @description: * @author: Mr.Xiao * @create: 2020-06-11 16:59 **/ @FeignClient("item-service") public interface GoodsClient extends GoodsApi { }
-
SpecificationClient
package com.leyou.search.client; import com.leyou.item.api.SpecificationApi; import org.springframework.cloud.openfeign.FeignClient; /** * @program: leyou * @description: * @author: Mr.Xiao * @create: 2020-06-11 17:01 **/ @FeignClient("item-service") public interface SpecificationClient extends SpecificationApi { }
repository---->(操作ElasticSearch接口, 提供了各种方法)
-
GoodsRepository
package com.leyou.search.repository; import com.leyou.search.pojo.Goods; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; /** * @program: leyou * @description: ElasticsearchRepository 第一个参数实体类类型, 第二个参数id类型 * @author: Mr.Xiao * @create: 2020-06-12 12:13 **/ // ElasticsearchRepository 跟 通用mapper一样, 里面包含了各种增删改查 public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> { }
Controller
-
SearchController
package com.leyou.search.web; import com.leyou.common.vo.PageResult; import com.leyou.search.pojo.Goods; import com.leyou.search.pojo.SearchRequest; import com.leyou.search.pojo.SearchResult; import com.leyou.search.service.SearchService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * @program: leyou * @description: 搜索服务 表现层 * @author: Mr.Xiao * @create: 2020-06-12 17:57 **/ @RestController public class SearchController { @Autowired private SearchService searchService; @PostMapping("/page") public ResponseEntity<SearchResult> search(@RequestBody SearchRequest request) { return ResponseEntity.ok(searchService.search(request)); } }
Service
-
SearchService接口
package com.leyou.search.service; import com.leyou.common.vo.PageResult; import com.leyou.item.pojo.Spu; import com.leyou.search.pojo.Goods; import com.leyou.search.pojo.SearchRequest; import com.leyou.search.pojo.SearchResult; /** * @program: leyou * @description: * @author: Mr.Xiao * @create: 2020-06-12 09:32 **/ public interface SearchService { /** * 封装goods对象 * @param spu * @return */ Goods buildGoods(Spu spu); /** * 获取搜索结果 * @param request * @return */ SearchResult search(SearchRequest request); }
-
SearchServiceImpl实体类
package com.leyou.search.service.impl; import com.fasterxml.jackson.core.type.TypeReference; import com.leyou.common.enums.ExceptionEnum; import com.leyou.common.exception.LyException; import com.leyou.common.utils.JsonUtils; import com.leyou.common.utils.NumberUtils; import com.leyou.common.vo.PageResult; import com.leyou.item.pojo.*; import com.leyou.search.client.BrandClient; import com.leyou.search.client.CategoryClient; import com.leyou.search.client.GoodsClient; import com.leyou.search.client.SpecificationClient; import com.leyou.search.pojo.Goods; import com.leyou.search.pojo.SearchRequest; import com.leyou.search.pojo.SearchResult; import com.leyou.search.repository.GoodsRepository; import com.leyou.search.service.SearchService; import javafx.beans.binding.ObjectExpression; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.asn1.esf.SPUserNotice; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilders; import org.elasticsearch.search.aggregations.bucket.terms.LongTerms; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage; import org.springframework.data.elasticsearch.core.query.FetchSourceFilter; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import javax.swing.plaf.ScrollPaneUI; import java.util.*; import java.util.stream.Collectors; /** * @program: leyou * @description: * @author: Mr.Xiao * @create: 2020-06-12 09:32 **/ @Slf4j @Service("searchService") public class SearchServiceImpl implements SearchService { @Autowired private BrandClient brandClient; @Autowired private CategoryClient categoryClient; @Autowired private GoodsClient goodsClient; @Autowired private SpecificationClient specificationClient; @Autowired private GoodsRepository goodsRepository; @Autowired private ElasticsearchTemplate template; /** * 封装goods对象 * @param spu * @return */ @Override public Goods buildGoods(Spu spu) { // 获取spu id Long spuId = spu.getId(); // 获取all 所有需要被搜索的信息,包含标题,分类,甚至品牌 String all = ""; // 获取分类信息 List<Category> categoryList = categoryClient.queryCategoryListByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3())); if (CollectionUtils.isEmpty(categoryList)) { throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND); } // 获取分类名称 以 空格 隔开 List<String> cnames = categoryList.stream().map(Category::getName).collect(Collectors.toList()); // 获取品牌 Brand brand = brandClient.queryBrandById(spu.getBrandId()); if (brand == null) { throw new LyException(ExceptionEnum.BRAND_NOT_FOUND); } all = spu.getTitle() + StringUtils.join(cnames, " ") + brand.getName(); // 获取sku商品信息 List<Sku> skuList = goodsClient.querySkuListBySpuId(spuId); if (CollectionUtils.isEmpty(skuList)) { throw new LyException(ExceptionEnum.GOODS_NOT_FOUND); } // sku 商品集合 ArrayList<Map<String, Object>> skus = new ArrayList<>(); // sku 价格集合 HashSet<Long> prices = new HashSet<>(); // 优于展示字段只是sku中的几个字段, 所以我们这里不需要全部字段, 需要做抽离 for (Sku sku : skuList) { HashMap<String, Object> map = new HashMap<>(); map.put("id", sku.getId()); map.put("title", sku.getTitle()); map.put("price", sku.getPrice()); // 获取第一张图片信息 map.put("image", StringUtils.substringBefore(sku.getImages(), ",")); // 添加sku商品 skus.add(map); // 添加sku价格 prices.add(sku.getPrice()); } // 规格参数 Map<String, Object> specs = new HashMap<>(); // 获取存储规格参数键对象 // 可搜索字段, 分类id是3级分类 List<SpecParam> specParams = specificationClient.queryParamByList(null, spu.getCid3(), true); if (CollectionUtils.isEmpty(specParams)) { throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND); } // 获取spu详细信息 SpuDetail spuDetail = goodsClient.querySpuDetailBySpuId(spuId); if (spuDetail == null) { throw new LyException(ExceptionEnum.SPU_NOT_FOUND); } // 获取通用规格参数 Map<Long, Object> genericSpec = JsonUtils.parseMap(spuDetail.getGenericSpec(), Long.class, Object.class); // 获取特有规格参数 Map<Long, List<Object>> specialSpec = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() {}); // 遍历处理specs for (SpecParam specParam : specParams) { String key = specParam.getName(); Object value = ""; // 是通用规格参数 if (specParam.getGeneric()) { value = genericSpec.get(specParam.getId()); if (specParam.getNumeric()) { // 是数值类型 if (StringUtils.isNotBlank(specParam.getSegments())) { // 处理成段 value = chooseSegment(value.toString(), specParam); } } } else { value = specialSpec.get(specParam.getId()); } specs.put(key, value); } // 封装对象 Goods goods = new Goods(); goods.setCid1(spu.getCid1()); goods.setCid2(spu.getCid2()); goods.setCid3(spu.getCid3()); goods.setBrandId(spu.getBrandId()); goods.setCreateTime(spu.getCreateTime()); goods.setId(spuId); goods.setSubTitle(spu.getSubTitle()); goods.setAll(all); // 所有需要被搜索的信息,包含标题,分类,甚至品牌 goods.setSkus(JsonUtils.serialize(skus)); // sku商品 goods.setPrice(prices); // sku 价格集合 goods.setSpecs(specs); // 可搜索的规格参数,key是参数名,值是参数值 return goods; } /** * 获取搜索结果 * @param request * @return */ @Override public SearchResult search(SearchRequest request) { // 获取参数 int page = request.getPage() - 1; // 注意: elasticsearch分页是以0开始的 int size = request.getSize(); // 1. 创建查询构建器 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 2. 结果过滤 queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null)); // 3. 分页 queryBuilder.withPageable(PageRequest.of(page, size)); // 4. 创建查询条件 QueryBuilder basicQuery = buildConditions(request); queryBuilder.withQuery(basicQuery); // 5. 聚合 // 5.1品牌聚合 String brandAggName = "brand_agg"; queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId")); // 5.2分类集合 String categoryAggName = "category_agg"; queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3")); // 6. 执行这里用了聚合, 所以只能用template AggregatedPage<Goods> goodsList = template.queryForPage(queryBuilder.build(), Goods.class); // 7. 结果解析 long total = goodsList.getTotalElements(); int totalPages = goodsList.getTotalPages(); List<Goods> content = goodsList.getContent(); // 7. 获取品牌List List<Brand> brandList = parseBrand((LongTerms) goodsList.getAggregation(brandAggName)); // 8. 获取分类List List<Category> categoryList = parseCategory((LongTerms) goodsList.getAggregation(categoryAggName)); // 9. 判断分类不问空, 并且为1才聚合规格参数 List<Map<String, Object>> specs = null; if (categoryList != null && categoryList.size() == 1) { // 构建规格参数 specs = buildSpecificationAgg(categoryList.get(0).getId(), basicQuery); } return new SearchResult(total, Long.valueOf(totalPages), content, categoryList, brandList, specs); } /** * 构建查询过滤条件 * @param request * @return */ private QueryBuilder buildConditions(SearchRequest request) { // 1. 构建布尔条件 BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery(); // 2. 构建查询条件 queryBuilder.must(QueryBuilders.matchQuery("all", request.getKey())); // 3. 构建过滤条件 Map<String, String> filter = request.getFilter(); if (filter != null) { for (Map.Entry<String, String> map : filter.entrySet()) { // 获取, 过滤条件的键 String key = map.getKey(); // 如果不是分类或者品牌id if (!"cid3".equals(key) && !"brandId".equals(key)) { key = "specs." + key + ".keyword"; } queryBuilder.filter(QueryBuilders.termQuery(key, map.getValue())); } } return queryBuilder; } /** * 构建规格参数集合 * @param cid * @param basicQuery * @return */ private List<Map<String, Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQuery) { // 1. 创建查询构建器 NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder(); // 2. 把搜索条件添加进去 queryBuilder.withQuery(basicQuery); // 3. 获取规格参数 List<SpecParam> specParams = specificationClient.queryParamByList(null, cid, true); // 4. 判断是否为空 if (CollectionUtils.isEmpty(specParams)) { throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND); } // 5. 创建对象 List<Map<String, Object>> specs = new ArrayList<>(); // 6. 遍历规格参数, 聚合每一个规格参数 for (SpecParam specParam : specParams) { String name = specParam.getName(); queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs." + name + ".keyword")); } // 在原有的搜索条件上加上现在聚合的规格参数条件进行查询 AggregatedPage<Goods> goods = template.queryForPage(queryBuilder.build(), Goods.class); // 7. 解析结果集 // 7.1 遍历规格参数构造, 规格参数待选项 for (SpecParam specParam : specParams) { String name = specParam.getName(); StringTerms terms = (StringTerms) goods.getAggregation(name); List<StringTerms.Bucket> buckets = terms.getBuckets(); // 构建map对象 Map<String, Object> map = new HashMap<>(); map.put("k", name); map.put("options", buckets.stream().map(b -> b.getKeyAsString()).collect(Collectors.toList())); // 添加进规格参数集合中 specs.add(map); } return specs; } /** * 获取聚合后所有的 品牌 * @param terms * @return */ private List<Brand> parseBrand(LongTerms terms) { try{ List<Long> ids = terms.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList()); List<Brand> brands = brandClient.queryBrandByIds(ids); return brands; } catch (Exception e) { log.error("[搜索服务]查询品牌异常: ", e); return null; } } /** * 获取聚合后所有的分类 * @param terms * @return */ private List<Category> parseCategory(LongTerms terms) { try{ List<Long> ids = terms.getBuckets().stream().map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList()); List<Category> categories = categoryClient.queryCategoryListByIds(ids); return categories; } catch (Exception e) { log.error("[搜索服务]查询品牌分类异常: ", e); return null; } } /** * 拼接规格参数 * @param value * @param p * @return */ private String chooseSegment(String value, SpecParam p) { double val = NumberUtils.toDouble(value); String result = "其它"; // 保存数值段 for (String segment : p.getSegments().split(",")) { String[] segs = segment.split("-"); // 获取数值范围 double begin = NumberUtils.toDouble(segs[0]); double end = Double.MAX_VALUE; if(segs.length == 2){ end = NumberUtils.toDouble(segs[1]); } // 判断是否在范围内 if(val >= begin && val < end){ if(segs.length == 1){ result = segs[0] + p.getUnit() + "以上"; }else if(begin == 0){ result = segs[1] + p.getUnit() + "以下"; }else{ result = segment + p.getUnit(); } break; } } return result; } }
源码地址:https://download.csdn.net/download/zsx1314lovezyf/85302523