乐优商城day11(搜索微服务的搭建,elastic的复杂查询)

所有代码发布在 [https://github.com/hades0525/leyou]

Day11

elastic自定义查询

  1. 排序+分页
@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);
}
 
}
  1. 聚合
@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());
}
}

搭建搜索微服务

  1. 创建搜索微服务
    a. pom依赖



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);
}
}
  1. 索引数据结构
@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是参数名,值是参数值。

  1. 需要的接口
    a. 索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。
    b. 需要的数据:
    • SPU信息
    • SKU信息
    • SPU的详情
    • 商品分类名称(拼接all字段)
    • 规格参数key
    • 品牌
    c. 再思考我们需要哪些服务:
    • 第一:分批查询spu的服务,已经写过。
    • 第二:根据spuId查询sku的服务,已经写过
    • 第三:根据spuId查询SpuDetail的服务,已经写过
    • 第四:根据商品分类id,查询商品分类名称,没写过
    • 第五:规格参数,写过
    • 第六:品牌,没写
    d. 接口编写
    • 查询分类
/**
*根据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{
}
  1. 导入数据
    a. 创建goodsrepository
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>>() {
        });
 
        //定义spec对应的map
        HashMap map = new HashMap<>();
        //对规格进行遍历,并封装spec,其中spec的key是规格参数的名称,value是商品详情中的值
        for (SpecParam param : params) {
            //key是规格参数的名称
            String key = param.getName();
            Object value = "";
 
            if (param.getGeneric()) {
                //参数是通用属性,通过规格参数的ID从商品详情存储的规格参数中查出值
                value = genericSpec.get(param.getId());
                if (param.getNumeric()) {
                    //参数是数值类型,处理成段,方便后期对数值类型进行范围过滤
                    value = chooseSegment(value.toString(), param);
                }
            } else {
                //参数不是通用类型
                value = specialSpec.get(param.getId());
            }
            value = (value == null ? "其他" : value);
            //存入map
            map.put(key, value);
        }
        return map;
    }
 
    /**
     * 对数值进行分段
     * @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;
    }

• 在测试类中调用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处理时忽略空值
  1. 页面渲染
    a. JavaScript部分
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. 商品展示

                       
    ¥
    { {goods.subTitle.substring(0,15)+'...'}}
  • 你可能感兴趣的:(乐优,elasticsearch,乐优商城,feignclient,vue,axios)