目录
一、索引库数据导入
1.1 创建搜索微服务
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>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>
<!-- eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8083
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.19.121:9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true #当你获取host时,返回的不是主机名,而是ip
ip-address: 127.0.0.1
lease-expiration-duration-in-seconds: 10 #10秒不发送九过期
lease-renewal-interval-in-seconds: 5 #每隔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;
/**
* @Author: 98050
* Time: 2018-10-11 16:43
* Feature: 启动器,开启fegin功能
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchService {
public static void main(String[] args) {
SpringApplication.run(LySearchService.class,args);
}
}
1.2 索引库数据格式分析
目前已有的数据是SKU和SPU,那么如何保存在索引库中?
1.2.1 以结果为导向
可以看到,每一个搜索结果都有至少1个商品,当选择大图下方的小图,商品会跟着变化。
因此,搜索的结果是SPU,即多个SKU的集合。
既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。
1.2.2 需要什么数据
直观能看到的:图片、价格、标题、副标题
暗藏的数据:spu的id,sku的id
另外,页面还有过滤条件:
这些过滤条件也都需要存储到索引库中,包括:
商品分类、品牌、可用来搜索的规格参数等
综上所述,需要的数据格式有:
spuId、skuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数
1.2.3 最终的数据结构
package com.leyou.pojo;
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;
/**
* @Author: 98050
* Time: 2018-10-11 17:21
* Feature:搜索时对应的实体类
*/
@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {
@Id
/**
* spuId
*/
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
/**
* 所有需要被搜索的信息,包含标题,分类,甚至品牌
*/
private String all;
@Field(type = FieldType.Keyword, index = false)
/**
* 卖点
*/
private String subTitle;
/**
* 品牌id
*/
private Long brandId;
/**
* 1级分类id
*/
private Long cid1;
/**
* 2级分类id
*/
private Long cid2;
/**
* 3级分类id
*/
private Long cid3;
/**
* 创建时间
*/
private Date createTime;
/**
* 价格
*/
private List<Long> price;
@Field(type = FieldType.Keyword, index = false)
/**
* sku信息的json结构
*/
private String skus;
/**
* 可搜索的规格参数,key是参数名,值是参数值
*/
private Map<String, Object> specs;
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,颜色为红色,转为json就是:
{
"specs":{
"内存":[4G,6G],
"颜色":"红色"
}
}
当存储到索引库时,elasticsearch会处理为两个字段:
-
specs.内存:[4G,6G]
-
specs.颜色:红色
另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。
-
specs.颜色.keyword:红色
1.3 商品微服务提供接口
索引库中的数据来自于数据库,不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以只能调用商品微服务提供的接口服务。
先确定需要的数据:
-
SPU信息
-
SKU信息
-
SPU的详情
-
商品分类名称(拼接all字段)
再确定需要哪些服务:
-
第一:分批查询spu的服务,已有。
-
第二:根据spuId查询sku的服务,已有
-
第三:根据spuId查询SpuDetail的服务,已有
-
第四:根据商品分类id,查询商品分类名称,没有
-
第五:根据商品品牌id,查询商品的品牌,没有
因此我们需要额外提供一个查询商品分类名称的接口。
1.3.1 商品分类名称查询
@GetMapping("names")
public ResponseEntity<List<String>> queryNameByIds(@RequestParam("ids")List<Long> ids){
List<String> list = categoryService.queryNameByIds(ids);
if (list == null || list.size() < 1){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}else {
return ResponseEntity.ok(list);
}
}
1.3.2 编写FigenClient
存在的问题
在编写FeignClient时,代码遵循SpringMVC的风格,因此与商品微服务的Controller完全一致。如下所示:
@FeignClient(value = "item-service")
@RequestMapping("/goods")
public interface GoodsClient {
/**
* 分页查询商品
* @param page
* @param rows
* @param saleable
* @param key
* @return
*/
@GetMapping("/spu/page")
ResponseEntity<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}")
ResponseEntity<SpuDetail> querySpuDetailById(@PathVariable("id") Long id);
/**
* 根据spu的id查询sku
* @param id
* @return
*/
@GetMapping("sku/list")
ResponseEntity<List<Sku>> querySkuBySpuId(@RequestParam("id") Long id);
}
以上的这些代码直接从商品微服务中拷贝而来,完全一致。差别就是没有方法的具体实现。但是这样就存在一定的问题:
-
代码冗余。尽管不用写实现,只是写接口,但服务调用方要写与服务controller一致的代码,有几个消费者就要写几次。
-
增加开发成本。调用方还得清楚知道接口的路径,才能编写正确的FeignClient。
解决方案
-
服务提供方不仅提供实体类,还要提供api接口声明
-
调用方不用字自己编写接口方法声明,直接继承提供方给的Api接口即可
第一步
服务的提供方在leyou-item-interface
中提供API接口,并编写接口声明
添加依赖
需要引入springMVC及leyou-common的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>leyou-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
商品分类服务接口
package com.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;
/**
* @Author: 98050
* Time: 2018-10-11 20:05
* Feature:商品分类服务接口
*/
@RequestMapping("category")
public interface CategoryApi {
/**
* 根据id,查询分类名称
* @param ids
* @return
*/
@GetMapping("names")
ResponseEntity<List<String>> queryNameByIds(@RequestParam("ids")List<Long> ids);
}
商品服务接口
返回值不再使用ResponseEntity
package com.leyou.item.api;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.bo.SpuBo;
import com.leyou.item.pojo.Sku;
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;
/**
* @Author: 98050
* Time: 2018-10-11 20:05
* Feature:商品服务接口
*/
public interface GoodsApi {
/**
* 分页查询
* @param page
* @param rows
* @param sortBy
* @param desc
* @param key
* @param saleable
* @return
*/
@GetMapping("/spu/page")
PageResult<SpuBo> querySpuByPage(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "rows", defaultValue = "5") Integer rows,
@RequestParam(value = "sortBy", required = false) String sortBy,
@RequestParam(value = "desc", defaultValue = "false") Boolean desc,
@RequestParam(value = "key", required = false) String key,
@RequestParam(value = "saleable",defaultValue = "true") Boolean saleable);
/**
* 根据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(Long id);
}
第二步
在调用方leyou-search
中编写FeignClient,但不用写方法声明了,直接继承leyou-item-interface
提供的api接口:
商品的FeignClient
package com.leyou.client;
import com.leyou.item.api.GoodsApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* @Author: 98050
* Time: 2018-10-11 20:50
* Feature:商品FeignClient
*/
@FeignClient(value = "item-service")
public interface GoodsClient extends GoodsApi {
}
商品分类的FeignClient
package com.leyou.client;
import com.leyou.item.api.CategoryApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* @Author: 98050
* Time: 2018-10-11 20:49
* Feature:商品分类FeignClient
*/
@FeignClient(value = "item-service")
public interface CategoryClient extends CategoryApi {
}
项目结构
测试:
package com.leyou.client;
import com.leyou.LySearchService;
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.*;
/**
* Author: 98050
* Time: 2018-10-11 21:15
* Feature:
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LySearchService.class)
public class CategoryClientTest {
@Autowired
private CategoryClient categoryClient;
@Test
public void testQueryCategories() {
List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(1L, 2L, 3L)).getBody();
names.forEach(System.out::println);
}
}
结果:
1.4 导入数据
1.4.1 创建GoodsRepository
package com.leyou.repository;
import com.leyou.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
/**
* @Author: 98050
* Time: 2018-10-11 22:17
* Feature:
*/
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long> {
}
1.4.2 创建索引
新建一个测试类,在里面进行数据的操作:
package com.leyou.client;
import com.leyou.LySearchService;
import com.leyou.pojo.Goods;
import com.leyou.repository.GoodsRepository;
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;
/**
* @Author: 98050
* Time: 2018-10-11 22:13
* Feature:elasticsearch goods索引创建
*/
@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);
}
}
结果:
1.4.3 导入数据
导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存,因此先编写一个SearchService,然后在里面定义一个方法, 把Spu转为Goods
package com.leyou.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.leyou.client.CategoryClient;
import com.leyou.client.GoodsClient;
import com.leyou.client.SpecClient;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import com.leyou.pojo.Goods;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.*;
/**
* @Author: 98050
* Time: 2018-10-11 22:59
* Feature: 数据导入
*/
@Service
public class SearchService {
@Autowired
private CategoryClient categoryClient;
@Autowired
private GoodsClient goodsClient;
private ObjectMapper mapper = new ObjectMapper();
public Goods buildGoods(Spu spu) throws IOException {
Goods goods = new Goods();
//1.查询商品分类名称
List<String> names = this.categoryClient.queryNameByIds(Arrays.asList(spu.getCid1(),spu.getCid2(),spu.getCid3())).getBody();
//2.查询sku
List<Sku> skus = this.goodsClient.querySkuBySpuId(spu.getId());
//3.查询详情
SpuDetail spuDetail = this.goodsClient.querySpuDetailBySpuId(spu.getId());
//4.处理sku,仅封装id,价格、标题、图片、并获得价格集合
List<Long> prices = new ArrayList<>();
List<Map<String,Object>> skuLists = new ArrayList<>();
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());
//取第一张图片
skuMap.put("image", StringUtils.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(),",")[0]);
skuLists.add(skuMap);
});
//提取公共属性
List<Map<String,Object>> genericSpecs = mapper.readValue(spuDetail.getSpecifications(),new TypeReference<List<Map<String,Object>>>(){});
//过滤规格模板,把所有可搜索的信息保存到Map中
Map<String,Object> specMap = new HashMap<>();
String searchable = "searchable";
String v = "v";
String k = "k";
String options = "options";
genericSpecs.forEach(m -> {
List<Map<String, Object>> params = (List<Map<String, Object>>) m.get("params");
params.forEach(spe ->{
if ((boolean)spe.get(searchable)){
if (spe.get(v) != null){
specMap.put(spe.get(k).toString(), spe.get(v));
}else if (spe.get(options) != null){
specMap.put(spe.get(k).toString(), spe.get(options));
}
}
});
});
goods.setId(spu.getId());
goods.setSubTitle(spu.getSubTitle());
goods.setBrandId(spu.getBrandId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setCreateTime(spu.getCreateTime());
goods.setAll(spu.getTitle() + " " + StringUtils.join(names, " "));
goods.setPrice(prices);
goods.setSkus(mapper.writeValueAsString(skuLists));
goods.setSpecs(specMap);
return goods;
}
}
然后编写一个测试类,循环查询Spu,然后调用SearchService中的方法,把SPU变为Goods,然后写入索引库:
package com.leyou.client;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.bo.SpuBo;
import com.leyou.LySearchService;
import com.leyou.pojo.Goods;
import com.leyou.repository.GoodsRepository;
import com.leyou.service.impl.SearchServiceImpl;
import org.elasticsearch.index.query.QueryBuilders;
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.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.IOException;
import java.util.*;
/**
* @Author: 98050
* Time: 2018-10-11 22:13
* Feature:elasticsearch goods索引创建
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LySearchService.class)
public class ElasticsearchTest {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpuClient spuClient;
@Autowired
private SearchServiceImpl searchService;
@Test
public void createIndex(){
// 创建索引
this.elasticsearchTemplate.createIndex(Goods.class);
// 配置映射
this.elasticsearchTemplate.putMapping(Goods.class);
}
@Test
public void loadData() throws IOException {
List<SpuBo> list = new ArrayList<>();
int page = 1;
int row = 100;
int size;
do {
//分页查询数据
PageResult<SpuBo> result = this.goodsClient.querySpuByPage(page, row, null, true, null, true);
List<SpuBo> spus = result.getItems();
size = spus.size();
page ++;
list.addAll(spus);
}while (size == 100);
//创建Goods集合
List<Goods> goodsList = new ArrayList<>();
//遍历spu
for (SpuBo spu : list) {
try {
System.out.println("spu id" + spu.getId());
Goods goods = this.searchService.buildGoods(spu);
goodsList.add(goods);
} catch (IOException e) {
System.out.println("查询失败:" + spu.getId());
}
}
this.goodsRepository.saveAll(goodsList);
}
@Test
public void testAgg(){
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果
queryBuilder.withQuery(QueryBuilders.termQuery("cid3",76)).withSourceFilter(new FetchSourceFilter(new String[]{""},null)).withPageable(PageRequest.of(0,1));
Page<Goods> goodsPage = this.goodsRepository.search(queryBuilder.build());
goodsPage.forEach(System.out::println);
}
@Test
public void testDelete(){
this.goodsRepository.deleteById((long) 2);
}
}
通过kibana查询, 可以看到数据成功导入: