第8章-全文检索(上).md 30 KB

第8章-全文检索-站内搜索(上)

学习目标:

  • 能够说出商品检索业务功能
  • 搭建搜索微服务/创建商品索引库
  • 握"nested"类型的应用
  • 完成商品上下架功能
  • 完成及时的更新商品热度

img

1、商品检索功能介绍

无论PC端/移动端,根据用户输入的检索条件,查询出对用的商品

1.1. 检索两个入口

首页的分类,按照三级分类ID进行查询

img

搜索栏,用户录入任意购买意向商品关键字进行检索

img

1.2. 检索列表展示页面

  • 业务数据(SKU商品列表):包含商品ID,商品名称,商品图片.点击检索到商品进入商品详情页面
  • 过滤条件:根据检索到若干件商品动态的聚合(分组)而来,随着过滤条件变化,检索到业务数据也会变化.过滤条件自动跟着变(再次进行聚合)
  • 竞价排名

img

1.3 根据业务搭建数据结构

MySQL ElasticSearch
数据库 database index
table type(已废弃)
字段约束 schema mapping映射
row Document(文档)
字段 column Field

设计索引库方法:

  • 展示业务数据(商品ID,商品默认图片,商品价格,商品标题)
  • 展示过滤条件(品牌,分类,平台属性)
  • 用于排序字段(时间,热度)

1.3.1 建立映射

这时我们要思考三个问题:

  1. 哪些字段需要分词

    • 例如:商品名称
  2. 我们用哪些字段进行过滤(过滤项)

    • 平台属性值

    • 分类Id

    • 品牌Id

  3. 哪些字段我们需要通过搜索查询出来(业务数据)。

    • 商品名称
    • 价格
    • 图片

以上分析的所有显示,以及分词,过滤的字段都应该在es中出现。Es中如何保存这些数据呢?

根据上述的字段描述,应该建立一个mappings对应的存上上述字段描述的信息!

根据以上制定出如下结构:mappings

Index:goods

document: properties

field: id,price,title…

ES中index默认是true。

注意:ik_max_word 中文词库必须有!

attrs:平台属性值的集合,主要用于平台属性值过滤。

字符串类型ES中两种:

  • text:指定分词器IK,会进行分词,需要模糊检索字段
  • keyword:不会分词,用于精确查询

1.3.2 nested 介绍

nested:类型是一种特殊的对象object数据类型(specialised version of the object datatype ),允许对象数组彼此独立地进行索引和查询。

demo: 建立一个普通的index

如果linux 中有这个my_comment_index 先删除!DELETE /my_comment_index

步骤1:建立一个索引( 存储博客文章及其所有评论)

PUT my_comment_index/_doc/1
{
  "title": "狂人日记",
  "body": "《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国国民精神的麻木愚昧颇感痛切。",
  "comments": [
    {
      "name": "张三",
      "age": 34,
      "rating": 8,
      "comment": "非常棒的文章",
      "commented_on": "30 Nov 2023"
    },
    {
      "name": "李四",
      "age": 38,
      "rating": 9,
      "comment": "文章非常好",
      "commented_on": "25 Nov 2022"
    },
    {
      "name": "王五",
      "age": 33,
      "rating": 7,
      "comment": "手动点赞",
      "commented_on": "20 Nov 2021"
    }
  ]
}



#类比项目中商品信息
PUT goods/_doc/1
{
  "title": "HUAWEI Mate 50 直屏旗舰 超光变XMAGE影像 北斗卫星消息 低电量应急模式 128GB冰霜银华为鸿蒙手机",
  "attrInfo": [
    {
      "id": 23,
      "attrName": "运行内存",
      "attrValue": "8G"
    },
    {
     	"id": 114,
        "attrName": "CPU型号",
        "attrValue": "骁龙8+ Gen 1"
    }
  ]
}

如上所示,所以我们有一个文档描述了一个帖子和一个包含帖子上所有评论的内部对象评论。 但是Elasticsearch搜索中的内部对象并不像我们期望的那样工作。

步骤2 : 执行查询

GET /my_comment_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "comments.name": "李四"
          }
        },
        {
          "match": {
            "comments.age": 34
          }
        }
      ]
    }
  }
}

查询结果:居然正常的响应结果了

image-20221205232357129

原因分析:comments字段默认的数据类型是Object,故我们的文档内部存储为:

{ "title": [ 狂人日记], "body": [ 《狂人日记》是一篇象征性和寓意很强的小说,当时... ], "comments.name": [ 张三, 李四, 王五 ], "comments.comment": [ 非常棒的文章,文章非常好,王五,... ], "comments.age": [ 33, 34, 38 ], "comments.rating": [ 7, 8, 9 ] }

{
	"title": "HUAWEI Mate 50 直屏旗舰 超光变XMAGE影像 北斗卫星消息 低电量应急模式 128GB冰霜银华为鸿蒙手机",
  "attrInfo.id":[23,114],
  "attrInfo.attrName":["运行内存","CPU型号"],
  "attrInfo.attrValue":["8G","CPU型号","骁龙8+ Gen 1"],
}

我们可以清楚地看到,comments.name和comments.age之间的关系已丢失。这就是为什么我们的文档匹配李四和34的查询。

步骤3:删除当前索引

DELETE /my_comment_index

步骤4:建立一个nested 类型的(comments字段映射为nested类型,而不是默认的object类型)

PUT my_comment_index
{
  "mappings": {
      "properties": {
        "comments": {
          "type": "nested" 
        }
    }
  }
}


PUT my_comment_index/_doc/1
{
  "title": "狂人日记",
  "body": "《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国国民精神的麻木愚昧颇感痛切。",
  "comments": [
    {
      "name": "张三",
      "age": 34,
      "rating": 8,
      "comment": "非常棒的文章",
      "commented_on": "30 Nov 2023"
    },
    {
      "name": "李四",
      "age": 38,
      "rating": 9,
      "comment": "文章非常好",
      "commented_on": "25 Nov 2022"
    },
    {
      "name": "王五",
      "age": 33,
      "rating": 7,
      "comment": "手动点赞",
      "commented_on": "20 Nov 2021"
    }
  ]
}

重新执行步骤1,使用nested 查询

GET /my_comment_index/_search
{
  "query": {
    "nested": {
      "path": "comments",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "comments.name": "李四"
              }
            },
            {
              "match": {
                "comments.age": 34
              }
            }
          ]
        }
      }
    }
  }
}

结果发现没有返回任何的文档,这是何故?

当将字段设置为nested 嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象。文档的内部表示:

{ {

"comments.name":    [ 张三],
"comments.comment": [ 非常棒的文章 ],
"comments.age":     [ 34 ],
"comments.rating":  [ 9 ]

}, {

"comments.name":    [ 李四],
"comments.comment": [ 文章非常好 ],
"comments.age":     [ 38 ],
"comments.rating":   [ 8 ]

}, {

"comments.name":    [ 王五],
"comments.comment": [手动点赞],
"comments.age":     [ 33 ],
"comments.rating":   [ 7 ]

}, {

"title":            [ 狂人日记 ],
"body":             [ 《狂人日记》是一篇象征性和寓意很强的小说,当时,鲁迅对中国... ]

} }

{
"title": "HUAWEI Mate 50 直屏旗舰 超光变XMAGE影像 北斗卫星消息 低电量应急模式 128GB冰霜银华为鸿蒙手机",
  {
      "attrInfo.id":[23],
      "attrInfo.attrName":["运行内存"],
      "attrInfo.attrValue":["8G"]
  },
   {
      "attrInfo.id":[114],
      "attrInfo.attrName":["CPU型号"],
      "attrInfo.attrValue":["骁龙8 gen 1"]
  }
}

每个内部对象都在内部存储为单独的隐藏文档。 这保持了他们的领域之间的关系。

2、搭建service-list服务

gmall-service模块下搭建搜索模块:service-list

image-20221214212506025

2.1 配置pom.xml

<?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>gmall-service</artifactId>
        <groupId>com.atguigu.gmall</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>service-list</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.atguigu.gmall</groupId>
            <artifactId>service-product-client</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
    </dependencies>
</project>

说明:

  1. 引入service-product-client模块

  2. 引入spring-boot-starter-data-elasticsearch依赖

  3. 在父工程中gmall-parent模块pom.xml中properties节点中指定client版本跟ES服务端版本一致

    <elasticsearch.version>7.8.0</elasticsearch.version>
    

2.2 启动类

package com.atguigu.gmall;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class ListApp {
    public static void main(String[] args) {
        SpringApplication.run(ListApp.class, args);
    }
}

2.3 添加配置文件

bootstrap.properties

spring.application.name=service-list
spring.profiles.active=dev
spring.cloud.nacos.discovery.server-addr=192.168.200.128:8848
spring.cloud.nacos.config.server-addr=192.168.200.128:8848
spring.cloud.nacos.config.prefix=${spring.application.name}
spring.cloud.nacos.config.file-extension=yaml
spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml

说明:Nacos配置文件中添加es配置信息

2.4 实体类

说明:在gmall-model模块中已有商品文档实体类,以及平台属性实体类跟ES索引库mapping简历映射

商品文档实体类

package com.atguigu.gmall.list.model;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

import java.util.Date;
import java.util.List;
import java.util.Objects;

// Index = goods , Type = info  es 7.8.0 逐渐淡化type!  修改!
//  es 的分片,副本是为了保证高可用!
@Data
@Document(indexName = "goods" , shards = 3,replicas = 2)
public class Goods {
    // 商品Id skuId
    @Id
    private Long id;

    @Field(type = FieldType.Keyword, index = false)
    private String defaultImg;

    //  es 中能分词的字段,这个字段数据类型必须是 text!
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String title;

    @Field(type = FieldType.Double)
    private Double price;

    //  @Field(type = FieldType.Date)   6.8.1
    @Field(type = FieldType.Date,format = DateFormat.custom,pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime; // 新品

    @Field(type = FieldType.Long)
    private Long tmId;

    @Field(type = FieldType.Keyword)
    private String tmName;

    @Field(type = FieldType.Keyword)
    private String tmLogoUrl;

    @Field(type = FieldType.Long)
    private Long category1Id;

    @Field(type = FieldType.Keyword)
    private String category1Name;

    @Field(type = FieldType.Long)
    private Long category2Id;

    @Field(type = FieldType.Keyword)
    private String category2Name;

    @Field(type = FieldType.Long)
    private Long category3Id;

    @Field(type = FieldType.Keyword)
    private String category3Name;

    //  商品的热度! 我们将商品被用户点查看的次数越多,则说明热度就越高!
    @Field(type = FieldType.Long)
    private Long hotScore = 0L;

    // 平台属性集合对象
    // Nested 支持嵌套查询 允许对象数组彼此独立检索和查询
    @Field(type = FieldType.Nested)
    private List<SearchAttr> attrs;

}

销售属性实体类

package com.atguigu.gmall.model.list;

import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Data
public class SearchAttr {
    // 平台属性Id
    @Field(type = FieldType.Long)
    private Long attrId;
    // 平台属性值名称
    @Field(type = FieldType.Keyword)
    private String attrValue;
    // 平台属性名
    @Field(type = FieldType.Keyword)
    private String attrName;
}

2.5 创建索引库

package com.atguigu.gmall.list.controller;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.model.Goods;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: atguigu
 * @create: 2023-03-03 10:10
 */
@RestController
@RequestMapping("api/list")
public class ListApiController {


    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;


    /**
     * 创建索引库
     *
     * @return
     */
    @GetMapping("inner/createIndex")
    public Result createIndex() {
        //调用工具类创建索引
        elasticsearchRestTemplate.createIndex(Goods.class);
        //设置索引库映射信息
        elasticsearchRestTemplate.putMapping(Goods.class);
        return Result.ok();
    }


    /**
     * 删除索引库
     * @return
     */
    @GetMapping("inner/deleteIndex")
    public Result deleteIndex() {
        elasticsearchRestTemplate.deleteIndex("goods");
        return Result.ok();
    }

}

在浏览器运行:

http://localhost:8203/api/list/inner/createIndex

通过kibana查看mapping

img

重点:attrs 数据类型必须是nested !

3、商品上架/下架

构建goods数据模型分析

Sku基本信息(详情业务已封装了接口)

Sku分类信息(详情业务已封装了接口)

Sku的品牌信息(无)

Sku对应的平台属性(详情业务已封装了接口)

3.1 在service-product封装接口

3.1.1 Sku的品牌接口

YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/643

service-product商品微服务模块中增加查询品牌RestFul接口实现

控制器ProductApiController

@Autowired
private BaseTrademarkService baseTrademarkService;

/**
 * 根据品牌ID查询品牌信息
 *
 * @param tmId 品牌ID
 * @return
 */
@GetMapping("/inner/getTrademark/{tmId}")
public BaseTrademark getTrademarkById(@PathVariable("tmId") Long tmId) {
    BaseTrademark trademark = baseTrademarkService.getById(tmId);
    return trademark;
}

3.2 在service-product-client添加接口

service-product-client中ProductFeignClient提供Feign API接口

/**
 * 根据品牌ID查询品牌信息
 *
 * @param tmId 品牌ID
 * @return
 */
@GetMapping("/api/product/inner/getTrademark/{tmId}")
public BaseTrademark getTrademarkById(@PathVariable("tmId") Long tmId);

服务降级类

@Override
public BaseTrademark getTrademark(Long tmId) {
    return null;
}

3.3 实现商品上架/下架功能

YAPI接口文档:

3.3.1 控制器

service-list模块ListApiController 处理上下架请求

@Autowired
private SearchService searchService;


/**
 * 测试接口,商品文档对象录入索引
 * @param skuId
 * @return
 */
@GetMapping("/inner/upperGoods/{skuId}")
public Result upperGoods(@PathVariable("skuId") Long skuId){
    searchService.upperGoods(skuId);
    return Result.ok();
}


/**
 * 测试接口,商品文档删除
 * @param skuId
 * @return
 */
@GetMapping("/inner/lowerGoods/{skuId}")
public Result lowerGoods(@PathVariable("skuId") Long skuId){
    searchService.lowerGoods(skuId);
    return Result.ok();
}

3.3.2 业务接口

SearchService

package com.atguigu.gmall.list.service;

public interface SearchService {

    /**
     * 构建商品索引库文档对象Goods,将文档存入索引库
     * @param skuId
     */
    void upperGoods(Long skuId);

    /**
     * 删除索引库文档对象
     * @param skuId
     */
    void lowerGoods(Long skuId);
}

3.3.3. 业务实现类

SearchServiceImpl

package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.Goods;
import com.atguigu.gmall.list.model.SearchAttr;
import com.atguigu.gmall.list.repository.GoodsRepository;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-04-26 14:17
 */
@Slf4j
@Service
@SuppressWarnings("all")
public class SearchServiceImpl implements SearchService {

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private GoodsRepository goodsRepository;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    public static final String index_name = "goods";

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 将商品导入索引库
     *
     * @param skuId
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.构建索引库文档对象Goods
            Goods goods = new Goods();
            goods.setId(skuId);
            //2.远程调用商品微服务-根据商品SkuID查询商品基本信息
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
                if (skuInfo != null) {
                    goods.setCategory3Id(skuInfo.getCategory3Id());
                    goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                    goods.setTitle(skuInfo.getSkuName());
                    goods.setTmId(skuInfo.getTmId());
                }
                return skuInfo;
            }, threadPoolExecutor);
            BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
            if (skuPrice != null) {
                goods.setPrice(skuPrice.doubleValue());
            }
            //3.远程调用商品微服务-根据分类ID查询分类信息.
            CompletableFuture<Void> categoryCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());

                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());

                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }
            }, threadPoolExecutor);

            //4.远程调用商品微服务-根据品牌ID查询品牌信息
            CompletableFuture<Void> priceCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }
            }, threadPoolExecutor);

            //5.远程调用商品微服务-根据skuID查询平台属性列表
            CompletableFuture<Void> attrInfoCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);
                if (!CollectionUtils.isEmpty(attrList)) {
                    List<SearchAttr> searchAttrList = attrList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValue());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(searchAttrList);
                }
            }, threadPoolExecutor);
            goods.setCreateTime(new Date());
            goods.setCreatedDate(new Date());

            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceCompletableFuture,
                    attrInfoCompletableFuture,
                    categoryCompletableFuture).join();

            //6.调用JavaHighLevelRestClient完成文档新增  本质上发起http请求
            //6.1 构建创建文档请求对象 IndexReqeust  封装新增数据对应索引库库名称  文档ID
            IndexRequest request = new IndexRequest(index_name).id(skuId.toString());
            //6.2 构建新增文档请求体参数  JSON
            request.source(JSON.toJSONString(goods), XContentType.JSON);

            //6.3 发起请求 请求ES
            restHighLevelClient.index(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]上架商品:{},失败原因:{}", skuId, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 将指定商品从索引库删除
     *
     * @param skuId
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            DeleteRequest request = new DeleteRequest(
                    index_name,
                    skuId.toString());
            restHighLevelClient.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]下架商品:{},失败原因:{}", skuId, e);
            throw new RuntimeException(e);
        }
    }
}

添加数据

通过kibana查看数据

说明:后期学习了MQ,我们可以根据后台系统添加和修改等操作,发送mq消息自动上下架商品

http://localhost:8203/api/list/inner/upperGoods/21

http://localhost:8203/api/list/inner/lowerGoods/21

4、商品热度排名设值

搜索商品时,后面我们会根据热点排序,何时更新热点?我们在获取商品详情时调用更新

4.1 封装接口与实现类与控制器

YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/739

4.1.1 控制器

ListApiController

package com.atguigu.gmall.list.api;

import com.atguigu.gmall.list.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: atguigu
 * @create: 2023-03-03 14:07
 */
@RestController
@RequestMapping("/api/list")
public class ListApiController {


    @Autowired
    private SearchService searchService;

    /**
     * 提供给详情服务调用:更新商品热度分值
     * @param skuId
     */
    @GetMapping("/inner/incrHotScore/{skuId}")
    public void incrHotScore(@PathVariable("skuId") Long skuId){
        searchService.incrHotScore(skuId);
    }
}

4.1.2 业务层

SearchService

/**
 * 提供给详情服务调用:更新商品热度分值
 * @param skuId
 */
void incrHotScore(Long skuId);

SearchServiceImpl

@Autowired
private RedisTemplate redisTemplate;

/**
 * 更新商品热度分值
 * 1.更新Redis缓冲中商品分值
 * 2.更新ES文档中商品分值(稀释写操作)
 *
 * @param skuId
 */
@Override
public void incrHotScore(Long skuId) {
    try {
        // 1.更新Redis缓冲中商品分值
        String key = "hotScore";
        Double skuHotScore = redisTemplate.opsForZSet().incrementScore(key, skuId.toString(), 1);
        // 2.更新ES文档中商品分值(稀释写操作)
        if (skuHotScore % 10 == 0) {
            //2.1 创建更新文档请求对象 文档ID 文档分值
            UpdateRequest updateRequest = new UpdateRequest(index_name, skuId.toString());
            Goods goods = new Goods();
            goods.setHotScore(skuHotScore.longValue());
            updateRequest.doc(JSON.toJSONString(goods), XContentType.JSON);
            restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
        }
    } catch (IOException e) {
        log.error("[搜索微服务]更新商品分值:{},异常信息:{}", skuId, e);
        throw new RuntimeException(e);
    }
}

4.2 在service-list-client封装接口

4.2.1 搭建service-list-client

gmall-client模块下搭建:service-list-client模块。搭建方式如service-item-client

4.2.2 pom.xml

<?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>gmall-client</artifactId>
        <groupId>com.atguigu.gmall</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-list-client</artifactId>


</project>

4.2.3 添加接口

提供远程调用的Feign接口

package com.atguigu.gmall.list.client;


import com.atguigu.gmall.list.client.impl.ListDegradeFeignClient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "service-list", fallback = ListDegradeFeignClient.class)
public interface ListFeignClient {

    /**
     * 提供给详情服务调用:更新商品热度分值
     * @param skuId
     */
    @GetMapping("/api/list/inner/incrHotScore/{skuId}")
    public void incrHotScore(@PathVariable("skuId") Long skuId);

}

服务降级类

package com.atguigu.gmall.list.client.impl;

import com.atguigu.gmall.list.client.ListFeignClient;
import org.springframework.stereotype.Component;

/**
 * @author: atguigu
 * @create: 2023-03-03 14:36
 */
@Component
public class ListDegradeFeignClient implements ListFeignClient {
    @Override
    public void incrHotScore(Long skuId) {

    }
}

4.3 在service-item模块调用接口

service-item模块pom.xml中引入依赖

<dependency>
    <groupId>com.atguigu.gmall</groupId>
    <artifactId>service-list-client</artifactId>
    <version>1.0</version>
</dependency>

接口调用,更新service-item模块中ItemServiceImpl汇总商品信息方法:getBySkuId


/**
 * 查询商品信息,汇总详情页面需要数据
 * 1.根据SkuID查询商品SKU基本信息包含图片列表-返回sku商品对象
 * 2.根据三级分类ID查询所属分类信息
 * 3.根据SkuID查询价格
 * 4.根据spuId查询商品海报列表
 * 5.根据skuId查询平台属性列表
 * 6.根据spuID+SkuId查询销售属性 选中当前商品销售属性
 * 7.根据spuID查询销售属性跟sku对照关系-选择销售属性组合,切换SKU
 *
 * @param skuId
 * @return
 */
@Override
public Map<String, Object> getItemInfo(Long skuId) {

    //...省略汇总商品七项数据代码

    //8.TODO 远程调用搜索微服务更新商品热度
    CompletableFuture<Void> goodsScoreCompletableFuture = CompletableFuture.runAsync(() -> {
        listFeignClient.incrHotScore(skuId);
    }, executor);

    //x.等待所有异步任务并行执行完毕,主线程继续执行响应结果
    CompletableFuture.allOf(
            skuInfoCompletableFuture,
            categorCompletableFuture,
            priceCompletableFuture,
            spuPosterListCompletableFuture,
            skuAttrListCompletableFuture,
            spuSaleAttrListCompletableFuture,
            valuesSkuJsonCompletableFuture,
            goodsScoreCompletableFuture
    ).join();
    return mapResult;
}