title: 乐优商城学习笔记十二-搜素微服务
date: 2019-04-19 14:30:36
tags:
- 乐优商城
- java
- springboot
categories:
- 乐优商城
0.学习目标
- 独立编写数据导入功能
- 独立实现基本搜索
- 独立实现页面分页
- 独立实现结果排序
1.索引库数据导入
昨天我们学习了Elasticsearch的基本应用。今天就学以致用,搭建搜索微服务,实现搜索功能。
1.1.创建搜索服务
创建module:
[图片上传失败...(image-c343c2-1555657109674)]
Pom文件:
leyou
com.leyou.parent
1.0.0-SNAPSHOT
4.0.0
com.leyou.service
ly-search
1.0.0-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-elasticsearch
org.springframework.cloud
spring-cloud-starter-openfeign
application.yml:
server:
port: 8083
spring:
application:
name: search-service
main:
allow-bean-definition-overriding: true
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 182.254.227.85: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}
Feign报错'xx.FeignClientSpecification', defined in null, could not be registered.
在SpringBoot 2.1之前,这个配置默认就是true,而在2.1做了更改。
设置为true后,因为FeignClientSpecification的原因,FeignClient注解的configuration参数会被覆盖
解决方案
spring:
main:
allow-bean-definition-overriding: true
启动类:
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchService {
public static void main(String[] args) {
SpringApplication.run(LySearchService.class, args);
}
}
1.2.2.需要什么数据
再来看看页面中有什么数据:
直观能看到的:图片、价格、标题、副标题
暗藏的数据:spu的id,sku的id
另外,页面还有过滤条件:
这些过滤条件也都需要存储到索引库中,包括:
商品分类、品牌、可用来搜索的规格参数等
综上所述,我们需要的数据格式有:
spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数
1.2.3.最终的数据结构
我们创建一个类,封装要保存到索引库的数据,并设置映射属性:
@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;// 卖点
private Long brandId;// 品牌id
private Long cid1;// 1级分类id
private Long cid2;// 2级分类id
private Long cid3;// 3级分类id
private Date createTime;// 创建时间
private List price;// 价格
@Field(type = FieldType.keyword, index = false)
private String skus;// sku信息的json结构
private Map specs;// 可搜索的规格参数,key是参数名,值是参数值
}
一些特殊字段解释:
all:用来进行全文检索的字段,里面包含标题、商品分类信息
price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤
skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段
-
specs:所有规格参数的集合。key是参数名,值是参数值。
例如:我们在specs中存储 内存:4G,6G,颜色为红色,转为json就是:
{ "specs":{ "内存":[4G,6G], "颜色":"红色" } }
当存储到索引库时,elasticsearch会处理为两个字段:
- specs.内存 : [4G,6G]
- specs.颜色:红色
另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。
- specs.颜色.keyword:红色
1.3.商品微服务提供接口
索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。
先思考我们需要的数据:
SPU信息
SKU信息
SPU的详情
商品分类名称(拼接all字段)
规格参数
再思考我们需要哪些服务:
- 第一:分批查询spu的服务,已经写过。
- 第二:根据spuId查询sku的服务,已经写过
- 第三:根据spuId查询SpuDetail的服务,已经写过
- 第四:根据商品分类id,查询商品分类名称,没写过
因此我们需要额外提供一个查询商品分类名称的接口。
1.3.1.商品分类名称查询
controller:
/**
* 根据商品分类id查询名称
* @param ids 要查询的分类id集合
* @return 多个名称的集合
*/
@GetMapping("names")
public ResponseEntity> queryNameByIds(@RequestParam("ids") List ids){
List list = this.categoryService.queryNameByIds(ids);
if (list == null || list.size() < 1) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return ResponseEntity.ok(list);
}
service:
public List queryNameByIds(List ids) {
return this.categoryMapper.selectByIdList(ids).stream()
.map(Category::getName).collect(Collectors.toList());
}
测试:
1.3.2.编写FeignClient
问题展现:
现在,我们要在搜索微服务调用商品微服务的接口。
第一步要引入商品微服务依赖:ly-item-interface
。
com.leyou.service
ly-item-interface
${leyou.latest.version}
第二步,编写FeignClient
商品的FeignClient:
@FeignClient(value = "item-service")
public interface GoodsClient extends GoodsApi {
}
商品分类的FeignClient:
@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {
}
商品服务接口
public interface GoodsApi {
/**
* 分页查询商品
* @param page
* @param rows
* @param saleable
* @param key
* @return
*/
@GetMapping("/spu/page")
PageResult 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 querySkuBySpuId(@RequestParam("id") Long id);
List queryBySkuSpuId(@RequestParam("id")Long id);
}
public interface CategoryApi {
@GetMapping("category/list/ids")
List queryCategoryByIds(@RequestParam("ids") List ids);
}
需要在ly-item-interface中引入一些依赖:
org.springframework
spring-webmvc
com.leyou.common
ly-common
1.0.0-SNAPSHOT
项目结构:
测试
引入springtest依赖:
org.springframework.boot
spring-boot-starter-test
test
创建测试类:
在接口上按快捷键:Ctrl + Shift + T
测试代码:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LySearchService.class)
public class CategoryClientTest {
@Autowired
private CategoryClient categoryClient;
@Test
public void testQueryCategories() {
List names = this.categoryClient.queryNameByIds(Arrays.asList(1L, 2L, 3L));
names.forEach(System.out::println);
}
}
结果:
[图片上传失败...(image-d9c9e7-1555657109674)]
1.4.导入数据
1.4.1.创建GoodsRepository
java代码:
public interface GoodsRepository extends ElasticsearchRepository {
}
1.4.2.创建索引
我们新建一个测试类,在里面进行数据的操作:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LySearchService.class)
public class ElasticsearchTest {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Test
public void createIndex(){
// 创建索引
this.elasticsearchTemplate.createIndex(Goods.class);
// 配置映射
this.elasticsearchTemplate.putMapping(Goods.class);
}
}
查询结果
GET /goods
{
"goods": {
"aliases": {},
"mappings": {
"docs": {
"properties": {
"all": {
"type": "text",
"analyzer": "ik_max_word"
},
"skus": {
"type": "keyword",
"index": false
},
"subTitle": {
"type": "keyword",
"index": false
}
}
}
},
"settings": {
"index": {
"refresh_interval": "1s",
"number_of_shards": "1",
"provided_name": "goods",
"creation_date": "1555572720690",
"store": {
"type": "fs"
},
"number_of_replicas": "0",
"uuid": "10qY2kDdTbq6EO8SEqfexA",
"version": {
"created": "6020499"
}
}
}
}
}
1.4.3.导入数据
@Service
public class SearchService {
@Autowired
private CategoryClient categoryClient;
@Autowired
private BrandClient brandClient;
@Autowired
private GoodClient goodClient;
@Autowired
private SpecificationClient specificationClient;
public Goods bulidGoods(Spu spu){
Long supId = spu.getId();
//查询分类
List categories = categoryClient.queryCategoryByIds(
Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
if (CollectionUtils.isEmpty(categories)){
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
}
List names =categories.stream().map(Category::getName).collect(Collectors.toList());
//查询品牌
Brand brand = brandClient.queryBrandById(spu.getBrandId());
if (brand == null){
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
//搜索字段
String all = spu.getTitle()+ StringUtils.join(names,"")+brand.getName();
//查询sku
List skuList = goodClient.queryBySkuSpuId(spu.getId());
if (CollectionUtils.isEmpty(skuList)){
throw new LyException(ExceptionEnum.GOOD_SKU_NOT_FOND);
}
//对SKU进行处理
List
因为过滤参数中有一类比较特殊,就是数值区间:
所以我们在存入时要进行处理:
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,然后写入索引库:
@Test
public void loadData(){
int page = 1;
int rows = 100;
int size = 0;
do {
// 查询spu
PageResult result = this.goodClient.querySpuByPage(page, rows, true, null);
List spus = result.getItem();
// spu转为goods
List goods = spus.stream().map(searchService::bulidGoods)
.collect(Collectors.toList());
// 把goods放入索引库
this.goodsRepository.saveAll(goods);
size = spus.size();
//翻页
page++;
}while (size == 100);
}
通过kibana查询, 可以看到数据成功导入: