所有代码发布在 [https://github.com/hades0525/leyou]
Day11
elastic自定义查询
@Test
public void testQuery(){
//创建查询构建器
NativeSearchQueryBuilder queryBuilder=new NativeSearchQueryBuilder();
//结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(newString[]{"id","title","price"},null));
//添加查询条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title","小米手机"));
//排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
//分页
queryBuilder.withPageable(PageRequest.of(0,2));
Page- result=repository.search(queryBuilder.build());
long total=result.getTotalElements();
System.out.println("total="+total);
int totalPages=result.getTotalPages();
System.out.println("totalPages="+totalPages);
List
- content=result.getContent();
for(Itemitem:content){
System.out.println("item="+item);
}
}
@Test
public void testAgg(){
NativeSearchQueryBuilder queryBuilder=new NativeSearchQueryBuilder();
String aggName = "popularBrand";
//聚合
queryBuilder.addAggregation(AggregationBuilders.terms("popularBrand").field("brand"));
//查询并返回聚合结果
AggregatedPage- result = template.queryForPage(queryBuilder.build(),Item.class);
//解析聚合
Aggregations aggregations = result.getAggregations();
//获取指定名称的聚合
StringTerms terms = aggregations.get(aggName);
//获取桶
List
buckets = terms.getBuckets();
for(StringTerms.Bucketbucket:buckets){
System.out.println("bucket.getKeyAsString()="+bucket.getKeyAsString());
System.out.println("bucket.getDocCount()="+bucket.getDocCount());
}
}
搭建搜索微服务
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
com.leyou.service
ly-item-interface
1.0.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-test
b. 配置文件
server:
port: 8083
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.163.128:9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 5
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}
c. 启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchService{
public static void main(String[] args){
SpringApplication.run(LySearchService.class,args);
}
}
@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;//卖点
private Long brandId;//品牌id
private Long cid1;//1级分类id
private Long cid2;//2级分类id
private Long cid3;//3级分类id
private Date createTime;//创建时间
private Set price;//价格
@Field(type=FieldType.Keyword,index=false)
private String skus;//sku信息的json结构
private Map specs;//可搜索的规格参数,key是参数名,值是参数值
}
• 需要的数据格式有:spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数
• all:用来进行全文检索的字段,里面包含标题、商品分类信息
• price:价格数组,是所有sku的价格集合。方便根据价格进行筛选过滤
• skus:用于页面展示的sku信息,不索引,不搜索。包含skuId、image、price、title字段
• specs:所有规格参数的集合。key是参数名,值是参数值。
/**
*根据cid查询商品分类
*@paramids
*@return
*/
@GetMapping("list/ids")
public ResponseEntity> queryCategoryByIds(@RequestParam("ids")Listids){
return ResponseEntity.ok(categoryService.queryByIds(ids));
}
• 查询品牌
/**
*根据id查询品牌
*@paramid
*@return
*/
@GetMapping("{id}")
public ResponseEntity queryBrandById(@PathVariable("id")Longid){
return ResponseEntity.ok(brandService.queryById(id));
}
4.编写feignclient
• 我们要在搜索微服务调用商品微服务的接口
• 我们的服务提供方不仅提供实体类,还要提供api接口声明
• 调用方不用字节编写接口方法声明,直接继承提供方给的Api接口即可
a. 在interface中提供api接口
public interface GoodsApi{
/**
*根据spu的id查询详情detail
*@paramspuId
*@return
*/
@GetMapping("/spu/detail/{id}")
SpuDetail queryDetailById(@PathVariable("id")Long spuId);
/**
*根据spu查询下面所有sku
*@paramspuId
*@return
*/
@GetMapping("/sku/list")
List querySkuBySpuId(@RequestParam("id")Long spuId);
/**
*分页查询SPU
*@parampage
*@paramrows
*@paramsaleable
*@paramkey
*@return
*/
@GetMapping("/spu/page")
PageResult querySpuByPage(
@RequestParam(value="page",defaultValue="1")Integerpage,
@RequestParam(value="rows",defaultValue="5")Integerrows,
@RequestParam(value="saleable",required=false)Booleansaleable,
@RequestParam(value="key",required=false)Stringkey
);
}
• 在search中编写feignclient,继承api接口
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi{
}
public interface GoodsRepository extends ElasticsearchRepository{
}
b. 创建索引
@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsRepositoryTest{
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate template;
@Test
public void testCreateIndex(){
template.createIndex(Goods.class);
template.putMapping(Goods.class);
}
}
c. 导入数据
• 导入数据其实就是查询数据,然后把查询到的Spu转变为Goods来保存,因此我们先编写一个SearchService,然后在里面定义一个方法buildGoods, 把Spu转为Goods
public Goods buildGoods(Spu spu) {
Long spuId = spu.getId();
// 获取all字段的拼接
String all = this.getall(spu);
// 需要对sku过滤把不需要的数据去掉
List skus = goodsClient.querySkuBySpuId(spuId);
List skuVoList = this.getSkuVo(skus);
// 获取sku的价格列表
Set prices = this.getPrices(skus);
// 获取specs
HashMap specs = getSpecs(spu); // 数据不全导致的 bug
Goods goods = new Goods();
goods.setBrandId(spu.getBrandId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setCreateTime(spu.getCreateTime());
goods.setId(spuId);
goods.setSubTitle(spu.getSubTitle());
// 搜索条件 拼接:标题、分类、品牌
goods.setAll(all);
goods.setPrice(prices);
goods.setSkus(JsonUtils.toString(skuVoList));
goods.setSpecs(specs); // 数据不全导致的 bug
return goods;
}
/**
* 对all字段进行拼接
* @param spu
* @return
*/
private String getall(Spu spu) {
// 查询分类
List categories = categoryClient.queryCategoryByIds(
Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
if (CollectionUtils.isEmpty(categories)) {
throw new LyException(ExceptionEnum.CATEGORY_NOT_FIND);
}
// 查询品牌
Brand brand = brandClient.queryBrandById(spu.getBrandId());
if (brand == null) {
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
//品牌名
List names = categories.stream().map(Category::getName).collect(Collectors.toList());
// 搜索字段
String all = spu.getTitle() + StringUtils.join(names, ",") + brand.getName();
return all;
}
/**
* 对sku进行处理,去掉不需要的字段
* @param skus
* @return
*/
private List getSkuVo(List skus) {
List skuVoList = new ArrayList<>();
for (Sku sku : skus) {
SkuVo skuVo = new SkuVo();
skuVo.setId(sku.getId());
skuVo.setPrice(sku.getPrice());
skuVo.setTitle(sku.getTitle());
skuVo.setImage(StringUtils.substringBefore(sku.getImages(), ","));
skuVoList.add(skuVo);
}
return skuVoList;
}
/**
* 获取sku的price
* @param skuList
* @return
*/
private Set getPrices(List skuList) {
// 查询sku
if (CollectionUtils.isEmpty(skuList)) {
throw new LyException(ExceptionEnum.GOODS_SKU_NOT_FOUND);
}
return skuList.stream().map(Sku::getPrice).collect(Collectors.toSet());
}
/**
* 获取规格参数
* @param spu
* @return
*/
private HashMap getSpecs(Spu spu) {
// 获取规格参数
List params = specifictionClient.queryParamByList(null, spu.getCid3(), true);
if (CollectionUtils.isEmpty(params)) {
throw new LyException(ExceptionEnum.SPEC_GROUP_NOT_FOUND);
}
// 查询商品详情
SpuDetail spuDetail = goodsClient.queryDetailById(spu.getId());
// 获取通用规格参数
Map genericSpec = JsonUtils.toMap(
spuDetail.getGenericSpec(), Long.class, String.class);
//获取特有规格参数
Map> specialSpec = JsonUtils.nativeRead(
spuDetail.getSpecialSpec(), new TypeReference
• 在测试类中调用buildGoods方法,把spu变为goods
@Test
public void loadData(){
//查询spu信息
intpage=1;
introws=100;
intsize=0;
do{
PageResult result = goodsClient.querySpuByPage(page,rows,true,null);
List spuList =result.getItems();
if(CollectionUtils.isEmpty(spuList)){
break;
}
//构建成goods
List goodsList = spuList.stream().map(
searchService::buildGoods).collect(Collectors.toList());
//存入索引库
goodsRepository.saveAll(goodsList);
//翻页
page++;
size=spuList.size();
}while(size==100);
}
6.页面跳转
a. 页面参数传递
data: {
ly,
search:{},
goodsList:[],
total:0,
totalPage:0,
selectedSku:{}
},
created(){
//获取请求参数
const search = ly.parse(location.search.substring(1));
this.search = search;
//发送后台
this.loadData();
},
methods:{
loadData(){
//发送到后台
ly.http.post("/search/page",this.search).then(resp => {
//保存分页结果
this.total = resp.data.total;
this.totalPage = resp.data.totalPage;
//保存当前页商品
resp.data.items.forEach(goods => {
//把json处理成js对象
goods.skus = JSON.parse(goods.skus);
//初始化被选中的sku
goods.selectedSku = goods.skus[0];
})
this.goodsList = resp.data.items;
}).catch(error => {
})
}
},
b.后台分析
• 请求方式:Post
• 请求路径:/search/page,不过前面的/search应该是网关的映射路径,因此真实映射路径page,代表分页查询
• 请求参数:json格式,目前只有一个属性:key,搜索关键字,但是搜索结果页一定是带有分页查询的,所以将来肯定会有page属性,因此我们可以用一个对象来接收请求的json数据:
• 返回结果:作为分页结果,一般都两个属性:当前页数据、总条数信息,我们可以使用之前定义的PageResult类
c.后台网关准备
i. 网关配置添加
zuul:
prefix: /api
routes:
item-service: /item/**
search-service: /search/**
• 跨域访问添加 com.leyou.gateway.config.GlobalCorsConfig.corsFilter
config.addAllowedOrigin("http://www.leyou.com");
d. pojo类
public class SearchRequest{
private String key;//搜索条件
private Integer page;//当前页
private static final int DEFAULT_SIZE=20;//每页大小,不从页面接收,而是固定大小
private static final int DEFAULT_PAGE=1;//默认页
public String getKey(){
return key;
}
public void setKey(String key){
this.key=key;
}
public int getPage(){
if(page==null){
return DEFAULT_PAGE;
}
//获取页码时做一些校验,不能小于1
return Math.max(DEFAULT_PAGE,page);
}
public void setPage(Integerpage){
this.page=page;
}
public Integer getSize(){
return DEFAULT_SIZE;
}
}
e. controller
@RestController
public class SearchController{
@Autowired
private SearchService searchService;
/**
*搜索功能
*@paramsearchRequest
*@return
*/
@PostMapping("page")
public ResponseEntity>search(@RequestBodySearchRequestsearchRequest){
return ResponseEntity.ok(searchService.search(searchRequest));
}
}
f. service 要设置SourceFilter,来选择要返回的结果,否则返回一堆没用的数据,影响查询效率。
public PageResult search(SearchRequestsearchRequest){
int page = searchRequest.getPage()-1;
int size = searchRequest.getSize();
//创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//0.结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id","subTitle","skus"},null));
//1.分页,从0开始
queryBuilder.withPageable(PageRequest.of(page,size));
//2.过滤
queryBuilder.withQuery(QueryBuilders.matchQuery("all",searchRequest.getKey()));
//3.查询
Page result = repository.search(queryBuilder.build());
//4.解析结果
long total = result.getTotalElements();
int totalPages = result.getTotalPages();
List goodsList = result.getContent();
return new PageResult<>(total,goodsList,totalPages);
}
g. 优化
• 只查询部分字段,所以结果json 数据中有很多null,这很不优雅。
• 在application.yml中添加一行配置,json处理时忽略空值
spring:
jackson:
default-property-inclusion: non_null # 配置json处理时忽略空值
data: {
ly,
search:{},
goodsList:[],
total:0,
totalPage:0,
selectedSku:{}
},
created(){
//获取请求参数
const search = ly.parse(location.search.substring(1));
this.search = search;
//发送后台
this.loadData();
},
methods:{
loadData(){
//发送到后台
ly.http.post("/search/page",this.search).then(resp => {
//保存分页结果
this.total = resp.data.total;
this.totalPage = resp.data.totalPage;
//保存当前页商品
resp.data.items.forEach(goods => {
//把json处理成js对象
goods.skus = JSON.parse(goods.skus);
//初始化被选中的sku
goods.selectedSku = goods.skus[0];
})
//实现sku图片切换
this.goodsList = resp.data.items;
}).catch(error => {
})
}
},
b. 商品展示