10.搜索微服务

总结:

一、为搜索功能新建的Goods的类需要设计那些字段:

eg: 搜索框中输入“手机”关键字,显示搜索结果页面上显示的数据就是我们要设计的字段:
10.搜索微服务_第1张图片
0.有很多不同商品的图片,图片里还有很多不同颜色的小图片,说明这个大图片是一个Spu,一个Spu有一个List。得到:Goods—Spu

1.spuId:点击图片,会跳转到商品详情页,需要spuId来查询Spu对应的商品详情页。

2.List:sku的小图片,sku:用户点击相应的小图片sku,显示对应sku的图片到大图,sku的价格,sku的标题.String 把List序列化为Json字符串,可以提高查询效率。elasticsearch对集合支持不好。

3.Spu_subtitle: 卖点
10.搜索微服务_第2张图片
4.cid1,cid2,cid3

5.brandId: 根据搜索的商品结果,再根据brandId进行聚合,点击品牌,就可以显示一个品牌下的所有商品

6.Map:不同分类,对应的规格参数不一样,规格参数个数也不一样,所以要用Map, name是规格参数名,value是规格参数值。这样就可以根据spuId, 得到规格参数放入Map中。

7.价格,spu下的不同sku,不同sku有不同的价格,只要几个价格满足条件,就可以搜索出来。所以要把所有价格加到集合中。
10.搜索微服务_第3张图片
8.createdTime: 新品排序,根据创建时间排序

9.all: 查询字段,有可能根据品牌名称,分类名称,spu标题查询, 所以这些字段都要有,有字段才能查询,为了简化,将这些字段加入到all

搜索"手机",展示搜索页面

请求路径: http://api.leyou.com/api/search/page
请求参数: POST表单请求(因为有可能会是标题查询,再加上过滤字段,请求页面。搜索条件比较长):Form Data key: 手机
返回结果: ResponseEntity

1.用对象接收请求数据:
SearchRequest {

  • private String key;// 搜索条件
  • private Integer page;// 当前页
  • private static final Integer DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
  • private static final Integer DEFAULT_PAGE = 1;// 默认页
    }

2.SearchController

根据用户搜索条件查询对应商品集
public ResponseEntity search(@RequestBody SearchRequest request)

3.SearchService

3.1 将Spu对象转换成Goods对象
public Goods buildGoods(Spu spu) throws IOException

3.2 根据用户搜索条件查询对应商品集
public PageResult search(SearchRequest request)

1.索引库数据导入

1.1.创建搜索服务

创建module:

1.2.索引库数据格式分析

接下来,我们需要商品数据导入索引库,便于用户搜索。

那么问题来了,我们有SPU和SKU,到底如何保存到索引库?

1.2.1.以结果为导向

大家来看下搜索结果页:
10.搜索微服务_第4张图片
可以看到,每一个搜索结果都有至少1个商品,当我们选择大图下方的小图,商品会跟着变化。

因此,搜索的结果是SPU,即多个SKU的集合

既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。

1.2.2.需要什么数据

再来看看页面中有什么数据:
10.搜索微服务_第5张图片
直观能看到的:图片、价格、标题、副标题

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

另外,页面还有过滤条件:
10.搜索微服务_第6张图片
这些过滤条件也都需要存储到索引库中,包括:

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

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

spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数

1.2.3.最终的数据结构

我们创建一个类,封装要保存到索引库的数据,并设置映射属性:

不加注解的原因是:这些会自动判断,因为String类型有两种类型,分词或不分词。
private Long brandId;// 品牌id
private Long cid1;// 1级分类id
private Long cid2;// 2级分类id
private Long cid3;// 3级分类id
private Date createTime;// 创建时间
private List price;// 价格

@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;// List信息的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,查询商品分类名称,没写过
  • 第五:根据商品品牌id,查询商品的品牌,没写过
  • 第六:规格参数接口

因此我们需要额外提供一个查询商品分类名称的接口。

1.3.1.商品分类名称查询

在CategoryController中添加接口:

@GetMapping("names")
public ResponseEntity<List<String>> queryNamesByIds(@RequestParam("ids")List<Long> ids){
   

    List<String> names = this.categoryService.queryNamesByIds(ids);
    if (CollectionUtils.isEmpty(names)) {
   
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(names);
}

测试:
10.搜索微服务_第7张图片

1.3.2.编写FeignClient

10.搜索微服务_第8张图片

1.3.2.1.问题展现

操作leyou-search工程

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

第一步要在leyou-search工程中,引入商品微服务依赖:leyou-item-interface

第二步,编写FeignClient
10.搜索微服务_第9张图片

@FeignClient(value = "item-service")
public interface GoodsClient {
   

    /**
     * 分页查询商品
     * @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);
}

以上的这些代码直接从商品微服务中拷贝而来,完全一致。差别就是没有方法的具体实现。大家觉得这样有没有问题?

而FeignClient代码遵循SpringMVC的风格,因此与商品微服务的Controller完全一致。这样就存在一定的问题:

  • 代码冗余。尽管不用写实现,只是写接口,但服务调用方要写与服务controller一致的代码,有几个消费者就要写几次。
  • 增加开发成本。调用方还得清楚知道接口的路径,才能编写正确的FeignClient。

1.3.2.2.解决方案

因此,一种比较友好的实践是这样的:

  • 我们的服务提供方不仅提供实体类,还要提供api接口声明
  • 调用方不用自己编写接口方法声明,直接继承提供方给的Api接口即可,

第一步:服务的提供方在leyou-item-interface中提供API接口,并编写接口声明:
10.搜索微服务_第10张图片

商品分类服务接口:

@RequestMapping("category")
public interface CategoryApi {
   

    @GetMapping("names")
    ResponseEntity<List<String>> queryNameByIds(@RequestParam("ids") List<Long> ids);
}

商品服务接口,返回值不再使用ResponseEntity:

@RequestMapping("/goods")
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);
}

品牌的接口:

@RequestMapping("brand")
public interface BrandApi {
   

    @GetMapping("{id}")
    public Brand queryBrandById(@PathVariable("id") Long id);
}

规格参数的接口:

@RequestMapping("spec")
public interface SpecificationApi {
   

    @GetMapping("params")
    public List<SpecParam> queryParams(
            @RequestParam(value = "gid", required = false) Long gid,
            @RequestParam(value = "ci

你可能感兴趣的:(10.搜索微服务)