美文网首页
乐优商城学习笔记十三-搜素微服务

乐优商城学习笔记十三-搜素微服务

作者: smallmartial | 来源:发表于2019-04-19 14:58 被阅读0次

    title: 乐优商城学习笔记十二-搜素微服务
    date: 2019-04-19 14:30:36
    tags:
    - 乐优商城
    - java
    - springboot
    categories:
    - 乐优商城


    0.学习目标

    • 独立编写数据导入功能
    • 独立实现基本搜索
    • 独立实现页面分页
    • 独立实现结果排序

    1.索引库数据导入

    昨天我们学习了Elasticsearch的基本应用。今天就学以致用,搭建搜索微服务,实现搜索功能。

    1.1.创建搜索服务

    创建module:

    1526603473533

    [图片上传失败...(image-c343c2-1555657109674)]

    Pom文件:

    <?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>
        <version>1.0.0-SNAPSHOT</version>
    
        <dependencies>
            <!--eureka-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <!--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>
            <!--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
      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.需要什么数据

    再来看看页面中有什么数据:

    1526607712207

    直观能看到的:图片、价格、标题、副标题

    暗藏的数据:spu的id,sku的id

    另外,页面还有过滤条件:

    1526608095471

    这些过滤条件也都需要存储到索引库中,包括:

    商品分类、品牌、可用来搜索的规格参数等

    综上所述,我们需要的数据格式有:

    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<Long> price;// 价格
        @Field(type = FieldType.keyword, index = false)
        private String skus;// sku信息的json结构
        private Map<String, Object> 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<List<String>> queryNameByIds(@RequestParam("ids") List<Long> ids){
        List<String > list = this.categoryService.queryNameByIds(ids);
        if (list == null || list.size() < 1) {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(list);
    }
    

    service:

    public List<String> queryNameByIds(List<Long> ids) {
        return this.categoryMapper.selectByIdList(ids).stream()
                    .map(Category::getName).collect(Collectors.toList());
    }
    

    测试:

    1526611720402

    1.3.2.编写FeignClient

    问题展现:

    现在,我们要在搜索微服务调用商品微服务的接口。

    第一步要引入商品微服务依赖:ly-item-interface

    <!--商品微服务-->
    <dependency>
        <groupId>com.leyou.service</groupId>
        <artifactId>ly-item-interface</artifactId>
        <version>${leyou.latest.version}</version>
    </dependency>
    

    第二步,编写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<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}")
            SpuDetail querySpuDetailById(@PathVariable("id") Long id);
    
            /**
             * 根据spu的id查询sku
             * @param id
             * @return
             */
            @GetMapping("sku/list")
           // List<Sku> querySkuBySpuId(@RequestParam("id") Long id);
            List<Sku> queryBySkuSpuId(@RequestParam("id")Long id);
        }
    
    
    
    public interface CategoryApi {
        @GetMapping("category/list/ids")
        List<Category> queryCategoryByIds(@RequestParam("ids") List<Long> ids);
    }
    
    

    需要在ly-item-interface中引入一些依赖:

    <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>
    </dependency>
    

    项目结构:

    1526614742882

    测试

    引入springtest依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    

    创建测试类:

    在接口上按快捷键:Ctrl + Shift + T
    测试代码:

    
    @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));
            names.forEach(System.out::println);
        }
    }
    

    结果:

    [图片上传失败...(image-d9c9e7-1555657109674)]

    1.4.导入数据

    1.4.1.创建GoodsRepository

    java代码:

    public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
    }
    

    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<Category> categories = categoryClient.queryCategoryByIds(
                    Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
            if (CollectionUtils.isEmpty(categories)){
                throw new LyException(ExceptionEnum.CATEGORY_NOT_FOND);
            }
            List<String> 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<Sku> skuList = goodClient.queryBySkuSpuId(spu.getId());
            if (CollectionUtils.isEmpty(skuList)){
                throw new LyException(ExceptionEnum.GOOD_SKU_NOT_FOND);
            }
            //对SKU进行处理
            List<Map<String,Object>> skus = new ArrayList<>();
            //价格集合
            List<Long> priceList = new ArrayList<>();
            for (Sku sku : skuList) {
                Map<String,Object> map = new HashMap<>();
                map.put("id",sku.getId());
                map.put("title",sku.getTitle());
                map.put("price",sku.getPrice());
                map.put("image",StringUtils.substringBefore(sku.getImages(),","));
                skus.add(map);
                //处理价格
                priceList.add(sku.getPrice());
            }
          //  List<Long> priceList = skuList.stream().map(Sku::getPrice).collect(Collectors.toList());
    
            //查询规格产数
            List<SpecParam> params = specificationClient.querySpecSpecParam(null, spu.getCid3(), true, null);
            if (CollectionUtils.isEmpty(params)){
                throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FIND);
            }
            //查询商品详情
            SpuDetail spuDetail = goodClient.querySpuDetailById(supId);
            //String json = spuDetail.getGenericSpec();
            Map<Long,String> genericSpec = JsonUtils.parseMap(spuDetail.getGenericSpec(),Long.class,String.class);
            //规格参数
            String json =spuDetail.getSpecialSpec();
            Map<Long, List<String>> specialSpec = JsonUtils.nativeRead(json, new TypeReference<Map<Long, List<String>>>() {
            });
            //规格参数,key是规格参数的名字,值是规格参数的值
            Map<String,Object> specs = new HashMap<>();
            for (SpecParam param : params) {
                //规格名称
                String key = param.getName();
                Object value = "";
                if (param.getGeneric()){
                    value =genericSpec.get(param.getId());
                    //判断是否是数值类型
                    if (param.getNumeric()){
                        //处理成段
                        value = chooseSegment(value.toString(),param);
                    }
                }else {
                    value =specialSpec.get(param.getId());
                }
                //存入map
                specs.put(key,value);
            }
            //构建goods对象
            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(spu.getId());
            goods.setAll(spu.getTitle() + " " + StringUtils.join(names, " "));
            goods.setPrice(priceList);
            goods.setSkus(JsonUtils.serialize(skus));
            goods.setSpecs(specs);
            goods.setSubTitle(spu.getSubTitle());
            return goods;
        }
    
    

    因为过滤参数中有一类比较特殊,就是数值区间:

    所以我们在存入时要进行处理:

    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<SpuBo> result = this.goodClient.querySpuByPage(page, rows, true, null);
               List<SpuBo> spus = result.getItem();
    
               // spu转为goods
               List<Goods> goods = spus.stream().map(searchService::bulidGoods)
                       .collect(Collectors.toList());
    
               // 把goods放入索引库
               this.goodsRepository.saveAll(goods);
    
               size = spus.size();
               //翻页
               page++;
           }while (size == 100);
       }
    
    

    通过kibana查询, 可以看到数据成功导入:

    1526628384103

    相关文章

      网友评论

          本文标题:乐优商城学习笔记十三-搜素微服务

          本文链接:https://www.haomeiwen.com/subject/pngqgqtx.html