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


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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容