谷粒商城--整合Elasticsearch和商品的上架-3

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 谷粒商城--整合Elasticsearch和商品的上架

二、整合商品上架


需求:


  • 在后台选择上架的商品才能在网站展示
  • 上架的商品也可以被ES检索


sku在es中存储

商品上架在es中应该是存储spu


  • 检索的时候输入名字,这样就是需要用sku的title去全文检索
  • 检索使用商品规格,规格是spu的公共属性,每个spu是一样的


方案1:


缺点:如果每个sku都存储规格参数(如尺寸),会有冗余存储,因为每个spu对应的sku的规格参数都一样


{
    skuId:1
    spuId:11
    skyTitile:华为xx
    price:999
    saleCount:99
    attr:[
        {尺寸:5},
        {CPU:高通945},
        {分辨率:全高清}
  ]
}

方案2:


先找到4000个符合要求的spu,再根据4000个spu查询对应的属性,封装了4000个id,long 8B*4000=32000B=32KB

1K个人检索,就是32MB


sku索引
{
    spuId:1
    skuId:11
}
attr索引
{
    skuId:11
    attr:[
        {尺寸:5},
        {CPU:高通945},
        {分辨率:全高清}
  ]
}

结论:如果将规格参数单独建立索引,会出现检索时出现大量数据传输的问题,会引起网络网络


所以我们选方案一,用空间换时间


最终选用的数据模型

{ “type”: “keyword” }, 保持数据精度问题,可以检索,但不分词

“analyzer”: “ik_smart” 中文分词器

“index”: false, 不可被检索,不生成index

“doc_values”: false 默认为true,不可被聚合,es就不会维护一些聚合的信息


这个数据模型要先在es中建立


PUT product
{
    "mappings":{
        "properties": {
            "skuId":{ "type": "long" },
            "spuId":{ "type": "keyword" },  
            "skuTitle": {
                "type": "text",
                "analyzer": "ik_smart"  
            },
            "skuPrice": { "type": "keyword" },  
            "skuImg"  : { "type": "keyword" },  
            "saleCount":{ "type":"long" },
            "hasStock": { "type": "boolean" },
            "hotScore": { "type": "long"  },
            "brandId":  { "type": "long" },
            "catalogId": { "type": "long"  },
            "brandName": {"type": "keyword"}, 
            "brandImg":{
                "type": "keyword",
                "index": false,  
                "doc_values": false 
            },
            "catalogName": {"type": "keyword" }, 
            "attrs": {
                "type": "nested",
                "properties": {
                    "attrId": {"type": "long"  },
                    "attrName": {
                        "type": "keyword",
                        "index": false,
                        "doc_values": false
                    },
                    "attrValue": {"type": "keyword" }
                }
            }
        }
    }
}

3130e74ee9aeaafee777015bf0265246_9490112ad04fc121918a93b510f9e32b.png


构造基本数据

商品上架需要在es中保存spu信息并更新spu状态信息,所以我们就建立专门的vo来接收


SkuEsModel


写在common模块


@Data
public class SkuEsModel { //common中
    private Long skuId;
    private Long spuId;
    private String skuTitle;
    private BigDecimal skuPrice;
    private String skuImg;
    private Long saleCount;
    private boolean hasStock;
    private Long hotScore;
    private Long brandId;
    private Long catalogId;
    private String brandName;
    private String brandImg;
    private String catalogName;
    private List<Attr> attrs;
    @Data
    public static class Attr{
        private Long attrId;
        private String attrName;
        private String attrValue;
    }
}

库存量查询

上架的话需要确定库存,所以调用ware微服务来检测是否有库存


@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;
    @PostMapping(value = "/hasStock")
    public R getSkuHasStock(@RequestBody List<Long> skuIds) {
        List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
        return R.ok().setData(vos);
    }
}

impl


实现也比较好理解,就是先用自定义的mapper查有没有库存


有的话,给库存赋值,并收集成集合


@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
    List<SkuHasStockVo> skuHasStockVos = skuIds.stream().map(item -> {
        Long count = this.baseMapper.getSkuStock(item);
        SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
        skuHasStockVo.setSkuId(item);
        skuHasStockVo.setHasStock(count == null ? false : count > 0);
        return skuHasStockVo;
    }).collect(Collectors.toList());
    return skuHasStockVos;
}

自定义mapper


这里的库存并不是简单查一下库存表,所以不能用mp自带的api去查询,需要自定义一个简单的sql


就是用库存减去锁定的库存即可得出!


<select id="getSkuStock" resultType="java.lang.Long">
    SELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id = #{skuId}
</select>

商品上架

完整代码

controller


/**
 * 商品上架
 */
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
    spuInfoService.up(spuId);
    return R.ok();
}

impl

@Override
    public void up(Long spuId) {
        //1.获得spu对应的sku集合
        List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
        //2.获得spu的基础属性实体集合
        List<ProductAttrValueEntity> baseAttrs = productAttrValueService.baseAttrListforSpu(spuId);
        //3.获得基本属性中可搜索的属性id
        //3.1获得spu基础属性实体集合中的属性id集合
        List<Long> attrIds = baseAttrs.stream().map(attr -> {
            return attr.getAttrId();
        }).collect(Collectors.toList());
        //3.2获得可搜索属性实体类对象
        List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);
        //3.3将它们转化为set集合
        Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());
        //3.4对所有基础属性实体过滤,第一步是只保留可搜索属性实体类对象,第二步是给这些对象中的Attrs对象赋值,最后收集为attrsList
        List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
            return idSet.contains(item.getAttrId());
        }).map(item -> {
            SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
            BeanUtils.copyProperties(item, attrs);
            return attrs;
        }).collect(Collectors.toList());
        //收集所有skuId的集合
        List<Long> skuIdList = skuInfoEntities.stream()
                .map(SkuInfoEntity::getSkuId)
                .collect(Collectors.toList());
        //TODO 1、发送远程调用,库存系统查询是否有库存
        Map<Long, Boolean> stockMap = null;
        try {
            R skuHasStock = wareFeignService.getSkuHasStock(skuIdList);
            TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
            stockMap = skuHasStock.getData(typeReference).stream()
                    .collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
        } catch (Exception e) {
            log.error("库存服务查询异常:原因{}",e);
        }
//2、封装每个sku的信息
        Map<Long, Boolean> finalStockMap = stockMap;
        List<SkuEsModel> collect = skuInfoEntities.stream().map(sku -> {
            //组装需要的数据
            SkuEsModel esModel = new SkuEsModel();
            esModel.setSkuPrice(sku.getPrice());
            esModel.setSkuImg(sku.getSkuDefaultImg());
            //设置库存信息
            if (finalStockMap == null) {
                esModel.setHasStock(true);
            } else {
                esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
            }
            //TODO 2、热度评分。0
            esModel.setHotScore(0L);
            //TODO 3、查询品牌和分类的名字信息
            BrandEntity brandEntity = brandService.getById(sku.getBrandId());
            esModel.setBrandName(brandEntity.getName());
            esModel.setBrandId(brandEntity.getBrandId());
            esModel.setBrandImg(brandEntity.getLogo());
            CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
            esModel.setCatalogId(categoryEntity.getCatId());
            esModel.setCatalogName(categoryEntity.getName());
            //设置检索属性
            esModel.setAttrs(attrsList);
            BeanUtils.copyProperties(sku,esModel);
            return esModel;
        }).collect(Collectors.toList());
        //TODO 5、将数据发给es进行保存:mall-search
        R r = searchFeignService.productStatusUp(collect);
        if (r.getCode() == 0) {
            //远程调用成功
            //TODO 6、修改当前spu的状态
            this.baseMapper.updaSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
        } else {
            //远程调用失败
            //TODO 7、重复调用?接口幂等性:重试机制
        }
    }

详解

获得spu对应的sku集合

其中skuInfoService.getSkusBySpuId中getSkusBySpuId就是写一个简单的query因为是单表查询


e0276d366a857fc957d2cc77e2265ba4_9dc8b0f47b8140b6f29049930bf91f92.png


所以应该这样写


@Override
public List<SkuInfoEntity> getSkusBySpuId(Long spuId) {
    List<SkuInfoEntity> list = this.list(
            new QueryWrapper<SkuInfoEntity>().eq("spu_id",spuId));
    return list;
}

获得spu的基础属性实体集合

785a3a87ca839c551aea15ff420b6fef_c1823c8da28604de58760b291bfcca66.png


@Override
public List<ProductAttrValueEntity> baseAttrListforSpu(Long spuId) {
    return this.list(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));
}

给SkuEsModel.Attrs对象赋值

找出所有属性的id集合


List<Long> attrIds = baseAttrs.stream().map(attr -> {
    return attr.getAttrId();
}).collect(Collectors.toList());

在上面的结果中进行筛选,得到可搜索ID


 List<Long> searchAttrIds = attrService.selectSearchAttrs(attrIds);

其中attrService.selectSearchAttrs实现如下:


@Override
public List<Long> selectSearchAttrs(List<Long> attrIds) {
    List<Long> searchAttrIds = this.baseMapper.selectSearchAttrIds();
    return searchAttrIds;
}

这里的baseMapper.selectSearchAttrIds,baseMapper是MP提供的类,里面很多常见的CRUD,如果要扩展的话,可以直接写扩展的方法名,让写对应的Mapper就行,很方便,安装插件就可以一键生成!


筛选每个属性,下面sql的意思就是普通的查询加上可搜索id的条件


<select id="selectSearchAttrIds" resultType="java.lang.Long">
    select * from pms_attr where attr_id IN
        <foreach collection="attrIds" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    AND search_type = 1
</select>

给SkuEsModel.Attrs对象赋值


Set<Long> idSet = searchAttrIds.stream().collect(Collectors.toSet());
//3.4对所有基础属性实体过滤,第一步是只保留可搜索属性实体类对象,第二步是给这些对象中的Attrs对象赋值,最后收集为attrsList
List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(item -> {
    return idSet.contains(item.getAttrId());
}).map(item -> {
    SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
    BeanUtils.copyProperties(item, attrs);
    return attrs;
}).collect(Collectors.toList());

查询是否有库存

这里属于跨微服务的调用,所以这里使用了openfeign去远程调用ware微服务去查询是否有库存


ware微服务里的查询库存方法为:


@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;
    @PostMapping(value = "/hasStock")
    public R<List<SkuHasStockVo>> getSkuHasStock(@RequestBody List<Long> skuIds) {
        //skuId stock
        List<SkuHasStockVo> vos = wareSkuService.getSkuHasStock(skuIds);
        R<List<SkuHasStockVo>> ok = R.ok();
        ok.setData(vos);
        return ok;
    }
}

实现如下:


通过skuid来查询该sku的库存数


@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
    List<SkuHasStockVo> skuHasStockVos = skuIds.stream().map(item -> {
        Long count = this.baseMapper.getSkuStock(item);
        SkuHasStockVo skuHasStockVo = new SkuHasStockVo();
        skuHasStockVo.setSkuId(item);
        skuHasStockVo.setHasStock(count == null ? false : count > 0);
        return skuHasStockVo;
    }).collect(Collectors.toList());
    return skuHasStockVos;
}

getSkuStock属于自定义的查询接口,内容如下:


<select id="getSkuStock" resultType="java.lang.Long">
    SELECT SUM(stock - stock_locked) FROM wms_ware_sku WHERE sku_id = #{skuId}
</select>

那么接下来product微服务就可以使用该方法进行查询了,内容如下:


Map<Long, Boolean> stockMap = null;
try {
    R<List<SkuHasStockVo>> skuHasStock = wareFeignService.getSkuHasStock(skuIdList);
    TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>() {};
    stockMap = skuHasStock.getData(typeReference).stream()
        .collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
    log.error("库存服务查询异常:原因{}",e);
}

对所有sku进行封装


就是对要保存的sku封装成功ES的VO模型进行接收


Map<Long, Boolean> finalStockMap = stockMap;
List<SkuEsModel> collect = skuInfoEntities.stream().map(sku -> {
    //组装需要的数据
    SkuEsModel esModel = new SkuEsModel();
    esModel.setSkuPrice(sku.getPrice());
    esModel.setSkuImg(sku.getSkuDefaultImg());
    //设置库存信息
    if (finalStockMap == null) {
        esModel.setHasStock(true);
    } else {
        esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
    }
    //TODO 2、热度评分。0
    esModel.setHotScore(0L);
    //TODO 3、查询品牌和分类的名字信息
    BrandEntity brandEntity = brandService.getById(sku.getBrandId());
    esModel.setBrandName(brandEntity.getName());
    esModel.setBrandId(brandEntity.getBrandId());
    esModel.setBrandImg(brandEntity.getLogo());
    CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
    esModel.setCatalogId(categoryEntity.getCatId());
    esModel.setCatalogName(categoryEntity.getName());
    //设置检索属性
    esModel.setAttrs(attrsList);
    BeanUtils.copyProperties(sku,esModel);
    return esModel;
}).collect(Collectors.toList());

mall-search保存数据

controller


@PostMapping(value = "/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
    boolean status = false;
    try {
        status = productSaveService.productStatusUp(skuEsModels);
    } catch (IOException e) {
        log.error("商品上架错误{}",e);
        return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
    }
    if (status) {
        return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMessage());
    } else {
        return R.ok();
    }
}

impl


@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
    //1.在es中建立索引
    //2. 在ES中保存这些数据
    BulkRequest bulkRequest = new BulkRequest();
    for (SkuEsModel skuEsModel : skuEsModels) {
        //构造保存请求
        IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
        indexRequest.id(skuEsModel.getSkuId().toString());
        String jsonString = JSON.toJSONString(skuEsModel);
        indexRequest.source(jsonString, XContentType.JSON);
        bulkRequest.add(indexRequest);
    }
//        BulkRequest bulkRequest, RequestOptions options
    BulkResponse bulk = esRestClient.bulk(bulkRequest, EsConfig.COMMON_OPTIONS);
    //TODO 如果批量错误
    boolean hasFailures = bulk.hasFailures();
    List<String> collect = Arrays.asList(bulk.getItems()).stream().map(item -> {
        return item.getId();
    }).collect(Collectors.toList());
    log.info("商品上架完成:{}",collect);
    return hasFailures;
}

将数据发送给ES保存

  • FeignService
  • 通过FeignService远程调用
@FeignClient("mall-search")
public interface SearchFeignService {
    @PostMapping(value = "/search/save/product")
    R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels);
}
//TODO 5、将数据发给es进行保存:mall-search
R r = searchFeignService.productStatusUp(collect);
if (r.getCode() == 0) {
    //远程调用成功
    //TODO 6、修改当前spu的状态
    this.baseMapper.updaSpuStatus(spuId, ProductConstant.ProductStatusEnum.SPU_UP.getCode());
} else {
    //远程调用失败
    //TODO 7、重复调用?接口幂等性:重试机制
}

DeBug调式

商品上架用到了三个微服务,分别是product、ware、search


那我们分别debug启动它们,然后在这些微服务中使用的方法中打上断点,查看调用流程


获得spu对应的sku集合

f03771a300c3f8df7fd2263ef7014204_01e2417a0cc9d8e0850862a21cdc70b5.png


获得spu的基础属性实体集合

基础属性如下:


333ccb4d2dbc9c47c0547ae12d51b6ad_2808fd8a130fa69d6be2cd8654468936.png


给SkuEsModel.Attrs对象赋值

8f2fde89c7611cd7dc3df253cd9e4202_9eeada0e8ef60aa30a0ee5af10fcc128.png


测试

f4c0dc25b171b8e61c54dbec4f0e9827_59d15d688b6b1b382a65bef979b94335.png

相关实践学习
利用Elasticsearch实现地理位置查询
本实验将分别介绍如何使用Elasticsearch7.10版本进行全文检索、多语言检索和地理位置查询三个Elasticsearch基础检索子场景的实现。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
8月前
|
存储 自然语言处理 应用服务中间件
谷粒商城--整合Elasticsearch和商品的上架-2
谷粒商城--整合Elasticsearch和商品的上架
32 0
|
8月前
|
自然语言处理 关系型数据库 MySQL
谷粒商城--整合Elasticsearch和商品的上架-1
谷粒商城--整合Elasticsearch和商品的上架
76 0
|
14小时前
|
Linux Python
【Elasticsearch】linux使用supervisor常驻Elasticsearch,centos6.10安装 supervisor
【Elasticsearch】linux使用supervisor常驻Elasticsearch,centos6.10安装 supervisor
8 3
|
4天前
|
自然语言处理 搜索推荐
在Elasticsearch 7.9.2中安装IK分词器并进行自定义词典配置
在Elasticsearch 7.9.2中安装IK分词器并进行自定义词典配置
10 1
|
4天前
|
Windows
Windows安装Elasticsearch 7.9.2
Windows安装Elasticsearch 7.9.2
5 0
|
5天前
|
Java API 索引
必知的技术知识:Elasticsearch和Kibana安装
必知的技术知识:Elasticsearch和Kibana安装
|
6天前
|
存储 监控 搜索推荐
在生产环境中部署Elasticsearch:最佳实践和故障排除技巧——安装篇(一)
在生产环境中部署Elasticsearch:最佳实践和故障排除技巧——安装篇(一)
|
19天前
|
网络协议 Java
elasticsearch7.1 安装启动报错
elasticsearch7.1 安装启动报错
16 1
|
19天前
|
自然语言处理 数据可视化 Linux
ElasticSearch安装ik分词器_使用_自定义词典
ElasticSearch安装ik分词器_使用_自定义词典
18 1
|
19天前
Elasticsearch安装配置文件
Elasticsearch安装配置文件
15 0