eg: 搜索框中输入“手机”关键字,显示搜索结果页面上显示的数据就是我们要设计的字段:
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: 卖点
4.cid1,cid2,cid3
5.brandId: 根据搜索的商品结果,再根据brandId进行聚合,点击品牌,就可以显示一个品牌下的所有商品
6.Map
7.价格,spu下的不同sku,不同sku有不同的价格,只要几个价格满足条件,就可以搜索出来。所以要把所有价格加到集合中。
8.createdTime: 新品排序,根据创建时间排序
9.all: 查询字段,有可能根据品牌名称,分类名称,spu标题查询, 所以这些字段都要有,有字段才能查询,为了简化,将这些字段加入到all
请求路径: http://api.leyou.com/api/search/page
请求参数: POST表单请求(因为有可能会是标题查询,再加上过滤字段,请求页面。搜索条件比较长):Form Data key: 手机
返回结果: ResponseEntity
1.用对象接收请求数据:
SearchRequest {
2.SearchController
根据用户搜索条件查询对应商品集
public ResponseEntitysearch(@RequestBody SearchRequest request)
3.SearchService
3.1 将Spu对象转换成Goods对象
public Goods buildGoods(Spu spu) throws IOException
3.2 根据用户搜索条件查询对应商品集
public PageResult search(SearchRequest request)
创建module:
接下来,我们需要商品数据导入索引库,便于用户搜索。
那么问题来了,我们有SPU和SKU,到底如何保存到索引库?
大家来看下搜索结果页:
可以看到,每一个搜索结果都有至少1个商品,当我们选择大图下方的小图,商品会跟着变化。
因此,搜索的结果是SPU,即多个SKU的集合。
既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。
再来看看页面中有什么数据:
直观能看到的:图片、价格、标题、副标题
暗藏的数据:spu的id,sku的id
另外,页面还有过滤条件:
这些过滤条件也都需要存储到索引库中,包括:
商品分类、品牌、可用来搜索的规格参数等
综上所述,我们需要的数据格式有:
spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数
我们创建一个类,封装要保存到索引库的数据,并设置映射属性:
不加注解的原因是:这些会自动判断,因为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会处理为两个字段:
另外, 对于字符串类型,还会额外存储一个字段,这个字段不会分词,用作聚合。
索引库中的数据来自于数据库,我们不能直接去查询商品的数据库,因为真实开发中,每个微服务都是相互独立的,包括数据库也是一样。所以我们只能调用商品微服务提供的接口服务。
先思考我们需要的数据:
SPU信息
SKU信息
SPU的详情
商品分类名称(拼接all字段)
品牌名称
规格参数
再思考我们需要哪些服务:
因此我们需要额外提供一个查询商品分类名称的接口。
在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);
}
操作leyou-search工程
现在,我们要在搜索微服务调用商品微服务的接口。
第一步要在leyou-search工程中,引入商品微服务依赖:leyou-item-interface
。
@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完全一致。这样就存在一定的问题:
因此,一种比较友好的实践是这样的:
第一步:服务的提供方在leyou-item-interface
中提供API接口,并编写接口声明:
商品分类服务接口:
@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