032 搭建搜索微服务01----向ElasticSearch中导入数据--通过Feign实现微服务之间的相互调用
1.创建搜索服务
Pom文件:
<?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>lucky.leyou.parent</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>lucky.leyou.search</groupId> <artifactId>leyou-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> <!-- 因为要leyou-search模块也是一个微服务,必须引入eureka --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- leyou-search模块需要调用其他微服务,需要使用feign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>lucky.leyou.item</groupId> <artifactId>leyou-item-interface</artifactId> </dependency> <dependency> <groupId>lucky.leyou.common</groupId> <artifactId>leyou-common</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> </project>
application.yml:
server:
port: 8083
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: leyou
cluster-nodes: 127.0.0.1:9300 # 程序连接es的端口号是9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 10 #设置拉取服务的时间
instance:
lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
引导类:
package lucky.leyou; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient //能够让注册中心能够发现,扫描到该微服务 @EnableFeignClients // 开启feign客户端 public class LeyouSearchService { public static void main(String[] args) { SpringApplication.run(LeyouSearchService.class, args); } }
2.
因此,搜索的结果是SPU,即多个SKU的集合。
既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。
<2>
商品分类、品牌、可用来搜索的规格参数等
综上所述,我们需要的数据格式有:
package lucky.leyou.domain; 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; /** * elasticsearch索引对应的实体类 * @Document 作用在类,标记实体类为文档对象,一般有四个属性:indexName:对应索引库名称 type:对应在索引库中的类型, * shards:分片数量,默认5,replicas:副本数量,默认1 * @Id 作用在成员变量,标记一个字段作为id主键 * @Field 作用在成员变量,标记为文档的字段,并指定字段映射属性 * 注意:String类型的数据要加注解,因为String有两种类型:keywords、text */ @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;// 卖点,将subTitle注解为keyword,不需要索引即分词 private Long brandId;// 品牌id private Long cid1;// 1级分类id private Long cid2;// 2级分类id private Long cid3;// 3级分类id private Date createTime;// 创建时间 private List<Long> price;// 价格 @Field(type = FieldType.Keyword, index = false) private String skus;// List<sku>信息的json结构 private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值 public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getAll() { return all; } public void setAll(String all) { this.all = all; } public String getSubTitle() { return subTitle; } public void setSubTitle(String subTitle) { this.subTitle = subTitle; } public Long getBrandId() { return brandId; } public void setBrandId(Long brandId) { this.brandId = brandId; } public Long getCid1() { return cid1; } public void setCid1(Long cid1) { this.cid1 = cid1; } public Long getCid2() { return cid2; } public void setCid2(Long cid2) { this.cid2 = cid2; } public Long getCid3() { return cid3; } public void setCid3(Long cid3) { this.cid3 = cid3; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public List<Long> getPrice() { return price; } public void setPrice(List<Long> price) { this.price = price; } public String getSkus() { return skus; } public void setSkus(String skus) { this.skus = skus; } public Map<String, Object> getSpecs() { return specs; } public void setSpecs(Map<String, Object> specs) { this.specs = specs; } }
-
all:用来进行全文检索的字段,里面包含标题、商品分类信息
-
price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤
-
skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段
-
specs:所有规格参数的集合。key是参数名,值是参数值。
{ "specs":{ "内存":[4G,6G], "颜色":"红色" } }
-
specs.内存:[4G,6G]
-
specs.颜色:红色
另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。
-
specs.颜色.keyword:红色
3.
先思考我们需要的数据:
-
SPU信息
-
-
SPU的详情
-
商品分类名称(拼接all字段)
-
品牌名称
-
规格参数
再思考我们需要哪些服务:
-
第一:分批查询spu的服务,已经写过。
-
第二:根据spuId查询sku的服务,已经写过
-
第三:根据spuId查询SpuDetail的服务,已经写过
-
第四:根据商品分类id,查询商品分类名称,没写过
-
第五:根据商品品牌id,查询商品的品牌,没写过
-
第六:规格参数接口
/** * 根据分类id查询分类名称 * @param ids 分类id * @return */ @GetMapping("names") public ResponseEntity<List<String>> queryNamesByIds(@RequestParam("ids")List<Long> ids){ List<String> names = this.iCategoryService.queryNamesByIds(ids); if (CollectionUtils.isEmpty(names)) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(names); }
启动微服务进行测试,访问http://localhost:8081/category/names?ids=1,2,3
(2)通过id查询品牌信息
BrandController.java中添加接口:
/** * 通过id查询品牌信息 * @param id * @return */ @GetMapping("{id}") public ResponseEntity<Brand> queryBrandById(@PathVariable("id") Long id){ Brand brand=this.brandService.queryBrandById(id); if (brand==null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(brand); }
BrandServiceImpl.java中添加如下方法:
/** * 通过id查询品牌信息 * @param id 主键id * @return */ @Override public Brand queryBrandById(Long id) { //selectByPrimaryKey通过主键:id查询品牌信息 return this.brandMapper.selectByPrimaryKey(id); }
重启商品微服务测试:
浏览器中输入:http://localhost:8081/brand/1528
在Navicat中查看真实数据表信息
(3)编写FeignClient
第1步要在leyou-search工程中的pom文件中,引入feign的启动器、分页工具模块和商品微服务依赖:leyou-item-interface
<!-- leyou-search模块需要调用其他微服务,需要使用feign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>lucky.leyou.item</groupId> <artifactId>leyou-item-interface</artifactId> </dependency> <dependency> <groupId>lucky.leyou.common</groupId> <artifactId>leyou-common</artifactId> </dependency>
第2步要在leyou-search工程中的引导类上添加注解:@EnableFeignClients ,开启feign客户端
<2>Feign在代码中使用
问题引出:
@FeignClient(value = "item-service") public interface GoodsClient { /** * 分页查询商品 * @param page * @param rows * @param saleable * @param key * @return */ @GetMapping("/spu/page") PageResult<SpuBo> querySpuByPage( @RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "rows", defaultValue = "5") Integer rows, @RequestParam(value = "saleable", defaultValue = "true") Boolean saleable, @RequestParam(value = "key", required = false) String key); /** * 根据spu商品id查询详情 * @param id * @return */ @GetMapping("/spu/detail/{id}") SpuDetail querySpuDetailById(@PathVariable("id") Long id); /** * 根据spu的id查询sku * @param id * @return */ @GetMapping("sku/list") List<Sku> querySkuBySpuId(@RequestParam("id") Long id); }
以上的这些代码直接从商品微服务中拷贝而来,完全一致。差别就是没有方法的具体实现。大家觉得这样有没有问题?
而FeignClient代码遵循SpringMVC的风格,因此与商品微服务的Controller完全一致。这样就存在一定的问题:
-
代码冗余。尽管不用写实现,只是写接口,但服务调用方要写与服务controller一致的代码,有几个消费者就要写几次。
-
增加开发成本。调用方还得清楚知道接口的路径,才能编写正确的FeignClient。
问题解决:
因此,一种比较友好的实践是这样的:
-
我们的服务提供方不仅提供实体类,还要提供api接口声明
-
调用方不用自己编写接口方法声明,直接继承提供方给的Api接口即可,
CategoryApi:
package lucky.leyou.item.api; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @RequestMapping("category") public interface CategoryApi { @GetMapping("names") List<String> queryNameByIds(@RequestParam("ids") List<Long> ids); }
GoodsApi :
package lucky.leyou.item.api; import lucky.leyou.common.domain.PageResult; import lucky.leyou.item.bo.SpuBo; import lucky.leyou.item.domain.Sku; import lucky.leyou.item.domain.SpuDetail; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; /** * 商品服务的接口:供feign使用 */public interface GoodsApi { /** * 分页查询商品 * @param page * @param rows * @param saleable * @param key * @return */ @GetMapping(path = "/spu/page") public PageResult<SpuBo> querySpuBoByPage( @RequestParam(value = "key", required = false)String key, @RequestParam(value = "saleable", required = false)Boolean saleable, @RequestParam(value = "page", defaultValue = "1")Integer page, @RequestParam(value = "rows", defaultValue = "5")Integer rows ); /** * 根据spu商品id查询详情 * @param id * @return */ @GetMapping("/spu/detail/{id}") SpuDetail querySpuDetailById(@PathVariable("id") Long id); /** * 根据spu的id查询sku * @param id * @return */ @GetMapping("sku/list") List<Sku> querySkuBySpuId(@RequestParam("id") Long id); }
BrandApi:
package lucky.leyou.item.api; import lucky.leyou.item.domain.Brand; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("brand") public interface BrandApi { @GetMapping("{id}") public Brand queryBrandById(@PathVariable("id") Long id); }
SpecificationApi:
package lucky.leyou.item.api; import lucky.leyou.item.domain.SpecParam; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; /** * 规格参数的接口:供feign使用 */ @RequestMapping("spec") public interface SpecificationApi { @GetMapping("params") public List<SpecParam> queryParams( @RequestParam(value = "gid", required = false) Long gid, @RequestParam(value = "cid", required = false) Long cid, @RequestParam(value = "generic", required = false) Boolean generic, @RequestParam(value = "searching", required = false) Boolean searching ); }
商品的FeignClient:
package lucky.leyou.client; import lucky.leyou.item.api.GoodsApi; import org.springframework.cloud.openfeign.FeignClient; @FeignClient(value = "item-service") public interface GoodsClient extends GoodsApi { }
商品分类的FeignClient:
package lucky.leyou.client; import lucky.leyou.item.api.CategoryApi; import org.springframework.cloud.openfeign.FeignClient; @FeignClient(value = "item-service") public interface CategoryClient extends CategoryApi { }
品牌的FeignClient:
package lucky.leyou.client; import lucky.leyou.item.api.BrandApi; import org.springframework.cloud.openfeign.FeignClient; @FeignClient("item-service") public interface BrandClient extends BrandApi { }
规格参数的FeignClient:
package lucky.leyou.client; import lucky.leyou.item.api.SpecificationApi; import org.springframework.cloud.openfeign.FeignClient; @FeignClient("item-service") public interface SpecificationClient extends SpecificationApi { }
第三步:
在leyou-search中引入springtest依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
package lucky.leyou.client; import lucky.leyou.LeyouSearchServiceApplication; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.Arrays; import java.util.List; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest(classes = LeyouSearchServiceApplication.class) public class CategoryClientTest { @Autowired private CategoryClient categoryClient; @Test public void testQueryCategories() { List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(1L, 2L, 3L)); names.forEach(System.out::println); } }
启动工程报错,feign启动报错
***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'pigx-upms-biz.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
Process finished with exit code 1
解决方案:
在模块的application.yml文件中添加
启动成功:
执行结果:
总结:在leyou-search这个微服务中通过Feign调用的leyou-item这个微服务
4.
java代码:
package lucky.leyou.reponsitory; import lucky.leyou.domain.Goods; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> { }
(2)导入数据
导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存,因此我们先编写一个SearchService,然后在里面定义一个方法, 把Spu转为Goods
package lucky.leyou.service; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lucky.leyou.client.BrandClient; import lucky.leyou.client.CategoryClient; import lucky.leyou.client.GoodsClient; import lucky.leyou.client.SpecificationClient; import lucky.leyou.domain.Goods; import lucky.leyou.item.domain.*; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.util.*; /** * 搜索服务 */ @Service public class SearchService { @Autowired private BrandClient brandClient; @Autowired private CategoryClient categoryClient; @Autowired private GoodsClient goodsClient; @Autowired private SpecificationClient specificationClient; private static final ObjectMapper MAPPER = new ObjectMapper(); /** * 把Spu转为Goods * @param spu * @return * @throws IOException */ public Goods buildGoods(Spu spu) throws IOException { // 创建goods对象 Goods goods = new Goods(); // 根据品牌id查询品牌 Brand brand = this.brandClient.queryBrandById(spu.getBrandId()); // 查询分类名称,Arrays.asList该方法能将方法所传参数转为List集合 List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3())); // 根据spuid查询spu下的所有sku List<Sku> skus = this.goodsClient.querySkuBySpuId(spu.getId()); //初始化一个价格集合,收集所有的sku的价格 List<Long> prices = new ArrayList<>(); //收集sku的必要的字段信息 List<Map<String, Object>> skuMapList = new ArrayList<>(); // 遍历skus,获取价格集合 skus.forEach(sku ->{ prices.add(sku.getPrice()); Map<String, Object> skuMap = new HashMap<>(); skuMap.put("id", sku.getId()); skuMap.put("title", sku.getTitle()); skuMap.put("price", sku.getPrice()); //获取sku中的图片,数据库中的图片可能是多张,多张是以,分隔,所以也以逗号进行切割返回图片数组,获取第一张图片 skuMap.put("image", StringUtils.isNotBlank(sku.getImages()) ? StringUtils.split(sku.getImages(), ",")[0] : ""); skuMapList.add(skuMap); }); // 以tb_spec_param表中的分类cid字段和searching字段为查询条件查询出tb_spec_param表中所有的搜索规格参数 //将每一个查询结果封装成SpecParam这个bean对象中,将bean对象放入map中构成查询结果集 List<SpecParam> params = this.specificationClient.queryParams(null, spu.getCid3(), null, true); // 根据spuid查询spuDetail(即数据库表tb_spu_detail中的一行数据)。获取规格参数值 SpuDetail spuDetail = this.goodsClient.querySpuDetailById(spu.getId()); // 获取通用的规格参数,利用jackson工具类json转换为object对象(反序列化),参数1:要转化的json数据,参数2:要转换的数据类型格式 Map<Long, Object> genericSpecMap = MAPPER.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<Long, Object>>() { }); // 获取特殊的规格参数 Map<Long, List<Object>> specialSpecMap = MAPPER.readValue(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<Object>>>() { }); // 定义map接收{规格参数名,规格参数值} Map<String, Object> paramMap = new HashMap<>(); params.forEach(param -> { // 判断是否通用规格参数 if (param.getGeneric()) { // 获取通用规格参数值 String value = genericSpecMap.get(param.getId()).toString(); // 判断是否是数值类型 if (param.getNumeric()){ // 如果是数值的话,判断该数值落在那个区间 value = chooseSegment(value, param); } // 把参数名和值放入结果集中 paramMap.put(param.getName(), value); } else { paramMap.put(param.getName(), specialSpecMap.get(param.getId())); } }); // 设置参数 goods.setId(spu.getId()); goods.setCid1(spu.getCid1()); goods.setCid2(spu.getCid2()); goods.setCid3(spu.getCid3()); goods.setBrandId(spu.getBrandId()); goods.setCreateTime(spu.getCreateTime()); goods.setSubTitle(spu.getSubTitle()); goods.setAll(spu.getTitle() +" "+ StringUtils.join(names, " ")+" "+brand.getName()); //获取spu下的所有sku的价格 goods.setPrice(prices); //获取spu下的所有sku,并使用jackson包下ObjectMapper工具类,将任意的Object对象转化为json字符串 goods.setSkus(MAPPER.writeValueAsString(skuMapList)); //获取所有的规格参数{name:value} goods.setSpecs(paramMap); return goods; } /** * 判断value值所在的区间 * 范例:value=5.2 Segments:0-4.0,4.0-5.0,5.0-5.5,5.5-6.0,6.0- * @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; } }
然后编写一个测试类,循环查询Spu,然后调用IndexService中的方法,把SPU变为Goods,然后写入索引库:
package lucky.leyou.elasticsearch; import lucky.leyou.client.GoodsClient; import lucky.leyou.common.domain.PageResult; import lucky.leyou.domain.Goods; import lucky.leyou.item.bo.SpuBo; import lucky.leyou.reponsitory.GoodsRepository; import lucky.leyou.service.SearchService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.elasticsearch.core.ElasticsearchTemplate; import org.springframework.test.context.junit4.SpringRunner; import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @SpringBootTest @RunWith(SpringRunner.class) public class ElasticSearchTest1 { @Autowired private ElasticsearchTemplate elasticsearchTemplate; @Autowired private GoodsRepository goodsRepository; @Autowired private GoodsClient goodsClient; @Autowired private SearchService searchService; @Test public void test(){ //创建索引,会根据Goods类的@Document注解信息来创建 this.elasticsearchTemplate.createIndex(Goods.class); //配置映射,会根据Goods类中的id、Field等字段来自动完成映射 elasticsearchTemplate.putMapping(Goods.class); Integer page = 1; Integer rows = 100; do { // 分页查询spuBo,获取分页结果集 PageResult<SpuBo> pageResult = this.goodsClient.querySpuBoByPage(null, null, page, rows); // 遍历spubo集合转化为List<Goods> List<Goods> goodsList = pageResult.getItems().stream().map(spuBo -> { try { return this.searchService.buildGoods(spuBo); } catch (IOException e) { e.printStackTrace(); } return null; }).collect(Collectors.toList()); //执行新增数据的方法 this.goodsRepository.saveAll(goodsList); // 获取当前页的数据条数,如果是最后一页,没有100条 rows = pageResult.getItems().size(); // 每次循环页码加1 page++; } while (rows == 100); } }
执行后
打开postman进行测试,输入http://localhost:9200/goods/_search
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)