1、创建搜索服务
(1)创建module:
(3)引入依赖
<?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.service</groupId>
<artifactId>ly-search</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>2.5.3</version>
</dependency>
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.leyou.service</groupId>
<artifactId>ly-item-interface</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(4)配置文件
server:
port: 8085
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 165.149.69.135:9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
prefer-ip-address: true
ip-address: 127.0.0.1
instance-id: ${spring.application.name}:${server.port}
(5)启动类
package com.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
public class LySearchApplication {
public static void main(String[] args) {
SpringApplication.run(LySearchApplication.class);
}
}
2、索引库数据格式分析
接下来,我们需要商品数据导入索引库,便于用户搜索。
那么问题来了,我们有SPU和SKU,到底如何保存到索引库?
(1)以结果为导向
可以看到,每一个搜索结果都有至少1个商品,当我们选择大图下方的小图,商品会跟着变化。
因此,搜索的结果是SPU,即多个SKU的集合。
既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。
(2)需要什么数据
再来看看页面中有什么数据: 直观能看到的:图片、价格、标题、副标题
暗藏的数据:spu的id,sku的id
另外,页面还有过滤条件: 这些过滤条件也都需要存储到索引库中,包括:
商品分类、品牌、以及其他可用来搜索的规格参数等
综上所述,我们需要的数据格式有:
spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数
(2)最终的数据结构
我们创建一个类,封装要保存到索引库的数据,并设置映射属性:
package com.leyou.search.pojo;
import lombok.Data;
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;
@Data
@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String all;
@Field(type = FieldType.Keyword, index = false)
private String subTitle;
private Long brandId;
private Long cid1;
private Long cid2;
private Long cid3;
private Date createTime;
private List<Long> price;
@Field(type = FieldType.Keyword, index = false)
private String skus;
private Map<String, Object> specs;
}
一些特殊字段解释:
-
all:用来进行全文检索的字段,里面包含标题、商品分类信息 -
price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤 -
skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段 -
specs:所有可搜索规格参数的集合。key是参数名,值是参数值。 例如:我们在specs中存储 内存:4G,6G,颜色为红色,转为json就是: {
"specs":{
"内存":[4G,6G],
"颜色":"红色"
}
}
当存储到索引库时,elasticsearch会处理为两个字段:
- specs.内存 : [4G,6G]
- specs.颜色:红色
另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。
3、商品微服务提供接口
索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。
先思考我们需要的数据:
-
SPU信息 -
SKU信息 -
SPU的详情 -
商品分类名称(拼接all字段) -
规格参数 -
品牌
再思考我们需要哪些服务:
- 第一:分批查询spu的服务,已经写过。
- 第二:根据spuld查询sku的服务,已经写过
- 第三∶根据spuld查询SpuDetail的服务,已经写过
- 第四:根据商品分类id,查询商品分类,没写过
- 第五:规格参数:写过
- 第六:品牌,没写过
因此我们需要额外提供一个查询商品分类名称的接口。
(1)在ly-item-service当中的CategoryController商品分类名称查询
@GetMapping("list/ids")
public ResponseEntity<List<Category>> queryCategoryByIds(@RequestParam("ids") List<Long> ids){
return ResponseEntity.ok(categoryService.queryByIds(ids));
}
(2)查询品牌在BrandController当中的queryBrandById方法
@GetMapping("{id}")
public ResponseEntity<Brand> queryBrandById(@PathVariable("id")Long id){
return ResponseEntity.ok(brandService.queryById(id));
}
(3)运行测试
http://localhost:8083/category/list/ids?ids=1,2,3
http://localhost:8083/brand/1
4、编写FeignClient(这里接口当中的方法是和Controller相对应的只不过是去掉了ResponseEntity)
(1)创建CategoryClient接口
package com.leyou.search.client;
import com.leyou.item.pojo.Category;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface CategoryClient {
@GetMapping("category/list/ids")
List<Category> queryCategoryByIds(@RequestParam("ids") List<Long> ids);
}
创建单元测试
运行成功
(2)创建GoodsClient
package com.leyou.search.client;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import org.springframework.cloud.openfeign.FeignClient;
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.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface GoodsClient {
@GetMapping("/spu/detail/{id}")
SpuDetail queryDetailById(@PathVariable("id") Long spuId);
@GetMapping("sku/list")
List<Sku> querySkuBySpuId(@RequestParam("id") Long spuId);
@GetMapping("/spu/page")
PageResult<Spu> querySpuByPage(
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "saleable",required = false) Boolean saleable,
@RequestParam(value = "key",required = false) String key
);
}
以上的这些代码直接从商品微服务中拷贝而来,完全一致。差别就是没有方法的具体实现。大家觉得这样有没有问题?
而FeignClient代码遵循SpringMVC的风格,因此与商品微服务的Controller完全一致。这样就存在一定的问题:
- 代码冗余。尽管不用写实现,只是写接口,但服务调用方要写与服务controller一致的代码,有几个消费者就要写几次。
- 增加开发成本。调用方还得清楚知道接口的路径,才能编写正确的FeignClient。
解决方案:
因此,一种比较友好的实践是这样的:(服务提供方,不仅提供实体类,还要提供api接口申明)
- 我们的服务提供方不仅提供实体类,还要提供api接口声明
- 调用方不用字节编写接口方法声明,直接继承提供方给的Api接口即可,
(3)在ly-item-interface当中创建
1)引入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
2)创建GoodsApi
package com.leyou.item.api;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
public interface GoodsApi {
@GetMapping("/spu/detail/{id}")
SpuDetail queryDetailById(@PathVariable("id") Long spuId);
@GetMapping("sku/list")
List<Sku> querySkuBySpuId(@RequestParam("id") Long spuId);
@GetMapping("/spu/page")
PageResult<Spu> querySpuByPage(
@RequestParam(value = "page",defaultValue = "1") Integer page,
@RequestParam(value = "rows",defaultValue = "5") Integer rows,
@RequestParam(value = "saleable",required = false) Boolean saleable,
@RequestParam(value = "key",required = false) String key
);
}
(4)GoodsClient继承GoodsApi
package com.leyou.search.client;
import com.leyou.item.api.GoodsApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
}
(5)完善CategoryClient
1)在ly-item-interface当中创建CategoryApi接口
package com.leyou.item.api;
import com.leyou.item.pojo.Category;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
public interface CategoryApi {
@GetMapping("category/list/ids")
List<Category> queryCategoryByIds(@RequestParam("ids") List<Long> ids);
}
2)完善CategoryClient
package com.leyou.search.client;
import com.leyou.item.api.CategoryApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface CategoryClient extends CategoryApi {
}
(6)创建其他相关Api
1)BrandApi
package com.leyou.item.api;
import com.leyou.item.pojo.Brand;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
public interface BrandApi {
@GetMapping("brand/{id}")
Brand queryBrandById(@PathVariable("id")Long id);
}
2)SpecificationApi
package com.leyou.item.api;
import com.leyou.item.pojo.SpecParam;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
public interface SpecificationApi {
@GetMapping("spec/params")
List<SpecParam> queryParamList(
@RequestParam(value = "gid",required = false) Long gid,
@RequestParam(value = "cid",required = false) Long cid,
@RequestParam(value = "searching",required = false) Boolean searching
);
}
(7)创建其他的client
1)SpecificationClient
package com.leyou.search.client;
import com.leyou.item.api.SpecificationApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface SpecificationClient extends SpecificationApi {
}
2)BrandClient
package com.leyou.search.client;
import com.leyou.item.api.BrandApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface BrandClient extends BrandApi {
}
3)运行测试再次运行刚刚的测试方法
|