子丹商城整合ElasticSearch检索服务

一、创建一个新的服务模块 zidanmall-search

参考官方文档:

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/index.html

我们选择:Java High Level REST Client 更强大,封装更彻底;

1、pox.xml:

注意:我们虽然添加了 Java High Level REST Client 的依赖,但是 elasticsearch 本身的版本在springboot的版本依赖中默认定义了;版本较低,我这边在 spring-boot-dependencies 中查出来是6.4.3;

需要修改为配套:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.1.8.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.jiguiquan.zidanmall</groupId>
   <artifactId>zidanmall-search</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>zidanmall-search</name>
   <description>全文检索ES服务</description>

   <properties>
      <java.version>1.8</java.version>
      <spring-cloud.version>Greenwich.SR3</spring-cloud.version>
      <elasticsearch.version>7.4.2</elasticsearch.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>com.jiguiquan.zidanmall</groupId>
         <artifactId>zidanmall-common</artifactId>
         <version>1.0-SNAPSHOT</version>
      </dependency>

      <!--es的版本一定要严格和我们安装的软件保持一致-->
      <dependency>
         <groupId>org.elasticsearch.client</groupId>
         <artifactId>elasticsearch-rest-high-level-client</artifactId>
         <version>7.4.2</version>
      </dependency>

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

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

   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>

</project>

2、ElsaticSearch的配置:

如果我们是通过SpringData来使用ES,那么配置将会比较简单,但是我们没有使用spring data,所以需要自己手动配置ES的配置信息:

bootstrap.yml:

server:
  port: 10000
spring:
  application:
    name: zidanmall-product
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yaml
        namespace: c5a52176-88d8-4aaa-ba1d-65caaab513d5
        group: dev

config.ZidanElasticSearchConfig.java:

@Configuration
public class ZidanElasticSearchConfig {
    @Bean
    public RestHighLevelClient esRestClient(){
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("192.168.174.141", 9200, "http")));
        return client;
    }
}

启动项目,一切正常;


二、在ElasticSearch中创建索引product及对应的映射

1、相当于,创建了product数据库,并在一种创建了默认表“_doc”,及对应的表结构(_mapping);

PUT product
{
  "mappings": {
    "properties": {
      "skuId":{
        "type": "long"
      },
      "spuId":{
        "type": "keyword"
      },
      "skuTitle":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "skuPrice":{
        "type": "keyword"
      },
      "skuImg":{
        "type": "keyword",
        "index": false,         //不需要作为索引字段
        "doc_values": false     //不需要进行聚合分析
      },
      "saleCount":{
        "type": "long"
      },
      "hasStock":{
        "type": "boolean"
      },
      "hotScore":{
        "type": "long"
      },
      "brandId":{
        "type": "long"
      },
      "catalogId":{
        "type": "long"
      },
      "brandName":{
        "type": "keyword",
        "index": false
      },
      "brandImg":{
        "type": "keyword",
        "index": false
      },
      "catalogName":{
        "type": "keyword",
        "index": false
      },
      "attrs":{
        "type": "nested",     //嵌套结构
        "properties": {
          "attrId":{
            "type":"long"
          },
          "attrName":{
            "type": "keyword",
            "index": false      
          },
          "attrValue":{
            "type":"keyword"
          }
        }
      }
    }
  }
}
  • nested:嵌入式 数据类型(重要:可以解决ES对数据扁平化处理后的错误查询问题)

  • index:是否需要作为索引字段

  • doc_values:是否需要进行聚合分析

因为当上面2个位true的时候,ES需要做大量的工作,以满足后续使用需求;如果我们不需要,可以置为false,这样可以节约ES的资源!

执行成功:

33.jpg

2、因为嵌入式nested比较重要,所以记录一下:

2.1、有一个user[]数组:

  "user" : [ 
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]}

2.2、如果我们正常存入ES,

PUT my_index/_doc/1{
  "group" : "fans",
  "user" : [ 
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]}

因为ES会对数据进行扁平化处理成:

{
  "group" :        "fans",
  "user.first" : [ "alice", "john" ],
  "user.last" :  [ "smith", "white" ]}

2.3、然后我们搜索:first:Alice,last:Smith的用户

GET my_index/_search{
  "query": {
    "bool": {
      "must": [
        { "match": { "user.first": "Alice" }},
        { "match": { "user.last":  "Smith" }}
      ]
    }
  }}

其实应该是查找不到 Alice Smith这个user的,但是实际结果却是可以查到:

34.jpg

这样的结果显然是由问题的!,该怎么办呢?这时候就需要用到nested嵌入式类型:

3、创建映射(数据类型)为nested

PUT my_index{
  "mappings": {
    "properties": {
      "user": {
        "type": "nested" 
      }
    }
  }}

此时再存入数据:

PUT my_index/_doc/1{
  "group" : "fans",
  "user" : [
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
  ]}

这时候再去查,就查不到数据了:

35.jpg


三、编写spu商品上架功能

36.jpg

spu商品上架:

找出spu下所有的sku,将其存到ES中;(其中要确认当前sku是否有库存,有哪些可以被检索的规格属性等)

1、构造基本数据结构:SkuEsModel.java:

@Data
public class SkuEsModel {
    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 catelogId;
    private String brandName;
    private String brandImg;
    private String catelogName;
    private List<Attrs> attrs;

    @Data
    public static class Attrs{
        private Long attrId;
        private String attrName;
        private String attrValue;
    }
}

后面我们想要往ElasticSearch中存放数据,只需要封装成此entity对象即可;

接口:POST  /product/spuinfo/{spuId}/up

2、Service层核心代码 SpuInfoServiceImpl.java:

@Override
public void up(Long spuId) {
    //组装需要存入ES中的数据模型

    //1、查询出spuId对应的所有sku信息,品牌和名字
    List<SkuInfoEntity> skus = skuInfoService.getSkusBySpuId(spuId);

    //2、调用远程服务 库存系统,查询是否有库存——>远程服务,为了减少请求次数,我们批量执行——————对应4.1
    Map<Long, Boolean> hasStockMap = null;
    try{
        List<Long> skuIds = skus.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
        TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>(){};
        List<SkuHasStockVo> data = wareFeignService.getSkuHasStock(skuIds).getData(typeReference);
        hasStockMap = data.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
        System.out.println("hasStockMap:"+hasStockMap);
    }catch (Exception e){
        log.error("库存服务调用失败,原因:{}", e);
    }

    //3、查询出当前spu的所有“可以被用来检索”的规格属性——>因为属于Spu,所以我们只需要查询一次——————对应4.4
    List<ProductAttrValueEntity> baseAttrs = attrValueService.baseAttrListForSpu(spuId);
    List<Long> attrIds = baseAttrs.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
    //筛选出所有可以被检索的attrIds
    Set<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
    //从baseAttrs中取出在searchAttrIds中的元素
    List<SkuEsModel.Attrs> attrsList = baseAttrs.stream().filter(attr -> searchAttrIds.contains(attr.getAttrId())).map(item -> {
        SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
        BeanUtils.copyProperties(item, attrs);
        return attrs;
    }).collect(Collectors.toList());

    //4、封装每个sku的详细信息
    Map<Long, Boolean> finalHasStockMap = hasStockMap;

    List<SkuEsModel> upProducts = skus.stream().map(sku -> {
        SkuEsModel esModel = new SkuEsModel();
        BeanUtils.copyProperties(sku, esModel);
        //有几个无法拷过来的skuPrice/skuImg/hasStock/hotScore/brandName/brandImg/catalogName /attrs
        esModel.setSkuPrice(sku.getPrice());
        esModel.setSkuImg(sku.getSkuDefaultImg());
        //4.1、调用远程服务 库存系统,查询是否有库存——>远程服务,为了减少请求次数,我们批量执行
        if (finalHasStockMap != null) {
            esModel.setHasStock(finalHasStockMap.get(sku.getSkuId()));
        } else {
            esModel.setHasStock(true);
        }

        //TODO 4.2、热度评分,暂定为0
        esModel.setHotScore(0L);

        //4.3、查询品牌和分类的名字
        BrandEntity brand = brandService.getById(sku.getBrandId());
        esModel.setBrandName(brand.getName());
        esModel.setBrandImg(brand.getLogo());
        CategoryEntity category = categoryService.getById(sku.getCatalogId());
        esModel.setCatelogName(category.getName());

        //4.4、查询出当前sku的所有“可以被用来检索”的规格属性——>因为属于Spu,所以我们只需要查询一次
        esModel.setAttrs(attrsList);
        return esModel;
    }).collect(Collectors.toList());

    //将收集到的需要上架的upProducts通过zidanmall-search服务上传到ES中
    R r = searchFeignService.productStatusUp(upProducts);
    if (r.getCode() == 0){
        //远程调用成功, 修改当前商品的发布状态
        SpuInfoEntity spuInfoEntity = new SpuInfoEntity();
        spuInfoEntity.setId(spuId);
        spuInfoEntity.setPublishStatus(1);
        this.updateById(spuInfoEntity);
    }else {
        //远程调用失败
        //TODO 重复调用问题,接口幂等性————>feign的重试机制
    }
}

其中,除了需要调用本服务中的接口外,还需要使用Feign远程调用:

  • 调用ware库存服务,查询每个sku的库存;

  • 调用search检索服务(整合了ES),将数据存到ES中;

4、zidanmall-ware中的批量查询sku对应的是否有库存接口:

/**
 * 批量查询对应的sku是否有库存
 * @param skuIds
 * @return
 */
@Override
public List<SkuHasStockVo> getSkuHasStock(List<Long> skuIds) {
    List<SkuHasStockVo> collect = skuIds.stream().map(id -> {
        SkuHasStockVo vo = new SkuHasStockVo();
        //select SUM(stock-stock_locked) from `wma_ware_sku` where sku_id = 1
        Long count = baseMapper.getSkuStock(id);
        vo.setSkuId(id);
        vo.setHasStock(count == null ? false : (count > 0));
        return vo;
    }).collect(Collectors.toList());
    return collect;
}

5、zidanmall-search中批量上架SkuEsModel到ES存储中:

ElasticSaveController.java:

/**
 * 商品上架功能
 * @param skuEsModels
 * @return
 */
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels){
    Boolean b = false;
    try {
        b = productSaveService.productStatusUp(skuEsModels);
    } catch (IOException e) {
        log.error("商品上架异常:{}", e);
        return R.error(BizCodeEnum.PRODUCT_UO_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UO_EXCEPTION.getMsg());
    }
    if (b){
        return R.ok();
    }else {
        return R.error(BizCodeEnum.PRODUCT_UO_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UO_EXCEPTION.getMsg());
    }
}

ProductSaveServiceImpl.java:

@Service
@Slf4j
public class ProductSaveServiceImpl implements ProductSaveService {
    @Autowired
    RestHighLevelClient restHighLevelClient;

    /**
     * 上架功能
     * @param skuEsModels
     */
    @Override
    public Boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
        //将数据保存到ES中
        //1、给ES中建立一个索引product,并建好mapping映射,我们已经在ES中手动创建了

        //2、给索引中保存数据——批量操作
        BulkRequest bulkRequest = new BulkRequest();
        skuEsModels.forEach(model -> {
            IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
            indexRequest.id(model.getSkuId().toString());
            String json = JSON.toJSONString(model);
            indexRequest.source(json, XContentType.JSON);
            bulkRequest.add(indexRequest);
        });
        BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, ZidanElasticSearchConfig.COMMON_OPTIONS);
        //可以对结果进行处理
        if (bulk.hasFailures()){
            List<String> collect = Arrays.stream(bulk.getItems()).map(item -> item.getId()).collect(Collectors.toList());
            log.error("下列商品上架不成功:{}", collect);
            return false;
        }else {
            return true;
        }
    }
}

6、其中 RestHighLevelClient 的注入配置:

@Configuration
public class ZidanElasticSearchConfig {
    public static final RequestOptions COMMON_OPTIONS;
    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
        COMMON_OPTIONS = builder.build();
    }

    @Bean
    public RestHighLevelClient esRestClient(){
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("192.168.174.141", 9200, "http")));
        return client;
    }
}

7、补充:为了方便 Feign 调用过程中,获取复杂数据结构,我重新修改了统一返回 R 的内容:

37.jpg

使用方式:

存入:
List<SkuHasStockVo> data =  wareSkuService.getSkuHasStock(skuIds);
return R.ok().setData(data);
取出:
TypeReference<List<SkuHasStockVo>> typeReference = new TypeReference<List<SkuHasStockVo>>(){};
List<SkuHasStockVo> data = wareFeignService.getSkuHasStock(skuIds).getData(typeReference);

四、上架结果:

1、上架成功后:

38.jpg

2、到ES中查询是否已经上架成功——即ES中有没有这部分数据:

39.jpg

到这里,借助ElasticSearch实现商品Sku的上架功能就算是完成了;

当然还有一些瑕疵:比如Feign的重复调用等,后续解决;

3、这里,简单描述一下,Feign的调用过程和重试机制的实现:

//Feign 的调用流程:
/**
 * SynchronousMethodHandler.java类中的invoke()方法:
 * 1、构造请求数据,将对象转为json;
 *      RequestTemplate template = this.buildTemplateFromArgs.create(argv);
 * 2、发送请求,进行执行(执行成功会解码响应数据);
 *      this.executeAndDecode(template);
 * 3、执行请求失败会有重试机制(retryer重试器)
 *      while(true){
 *          try{
 *              this.executeAndDecode(template);
 *          }catch(Exception ex){
 *              try{retryer.continueOrPropagate(e)}catch(Exception ex){throw ex}
 *              continue;
 *          }
 *      }
 */

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐