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

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

学习目标:

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

img

1、商品检索功能介绍

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

1.1. 检索两个入口

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

img

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

img

1.2. 检索列表展示页面

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

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.openfeign.EnableFeignClients;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@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;


  /**
     * 快速创建索引库,设置索引库映射信息(根据实体类上ES相关注解获取)
     * @return
     */
    @GetMapping("/inner/createIndex")
    public Result createIndex(){
        //1.创建索引库
        elasticsearchRestTemplate.createIndex(Goods.class);
        //2.设置索引库石映射信息
        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 id
 * @return
 */
@ApiOperation("根据品牌ID查询品牌信息")
@GetMapping("/inner/getTrademark/{tmId}")
public BaseTrademark getTrademark(@PathVariable("tmId") Long id) {
    return baseTrademarkService.getById(id);
}

3.2 在service-product-client添加接口

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

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

服务降级类

@Override
public BaseTrademark getTrademark(Long id) {
    log.error("[商品服务],getTrademark业务远程调用失败,执行了服务降级");
    return null;
}

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

YAPI接口文档:

3.3.1 控制器

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

package com.atguigu.gmall.list.controller;

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.model.Goods;
import com.atguigu.gmall.list.service.SearchService;
import io.swagger.annotations.ApiOperation;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: atguigu
 * @create: 2023-09-08 15:59
 */
@RestController
@RequestMapping("api/list")
public class SearchController {

    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;

    @Autowired
    private SearchService searchService;

    /**
     * 创建索引库,本着实用主义快速创建索引库
     *
     * @return
     */
    @GetMapping("/createIndex")
    public Result createIndex() {
        //1.创建索引库
        elasticsearchRestTemplate.createIndex(Goods.class);
        //2.设置索引库中字段映射
        elasticsearchRestTemplate.putMapping(Goods.class);
        return Result.ok();
    }


    /**
     * 仅用于测试-将指定sku商品新增到索引库中
     *
     * @param skuId
     * @return
     */
    @ApiOperation("仅用于测试-将指定sku商品新增到索引库中")
    @GetMapping("/inner/upperGoods/{skuId}")
    public Result upperGoods(@PathVariable("skuId") Long skuId) {
        searchService.upperGoods(skuId);
        return Result.ok();
    }

    /**
     * 仅用于测试-将指定sku商品新增到索引库中
     *
     * @param skuId
     * @return
     */
    @ApiOperation("仅用于测试-将指定sku商品进行删除")
    @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 {

    /**
     * 商品上架,需要构建索引库文档对象;将文档存入索引库
     * @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.common.constant.RedisConst;
import com.atguigu.gmall.list.model.Goods;
import com.atguigu.gmall.list.model.SearchAttr;
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.BeanUtils;
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.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-09-09 09:12
 */
@Slf4j
@Service
public class SearchServiceImpl implements SearchService {

    private static final String INDEX_NAME = "goods";


    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 商品上架,需要构建索引库文档对象;将文档存入索引库
     *
     * @param skuId
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.构建索引库文档对象Goods-远程调用商品服务多个接口,采用异步+线程池进行并行处理
            Goods goods = new Goods();
            //1.1 封装商品基本信息
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
                if (skuInfo == null) {
                    throw new RuntimeException("商品不存在!");
                }
                goods.setId(skuId);
                goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                goods.setTitle(skuInfo.getSkuName());
                goods.setCreatedDate(skuInfo.getCreateTime());
                goods.setCreateTime(skuInfo.getCreateTime());
                return skuInfo;
            }, threadPoolExecutor);
            //1.2 封装商品价格
            CompletableFuture<Void> priceCompletableFuture = CompletableFuture.runAsync(() -> {
                BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
                goods.setPrice(skuPrice.doubleValue());
            }, threadPoolExecutor);

            //1.3 封装商品品牌信息
            CompletableFuture<Void> trademarkCompletableFuture = 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);


            //1.4 封装商品分类信息
            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);

            //1.5 封装商品平台属性列表
            CompletableFuture<Void> attrListCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrInfoList = productFeignClient.getAttrListBySkuId(skuId);
                if (!CollectionUtils.isEmpty(attrInfoList)) {
                    //将集合泛型从BaseAttrInfo转为SearchAttr
                    List<SearchAttr> searchAttrList = attrInfoList.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);

            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceCompletableFuture,
                    categoryCompletableFuture,
                    trademarkCompletableFuture,
                    attrListCompletableFuture
            ).join();

            //2.调用ES提供JavaClient将文档存入索引库
            //2.1 构建新增文档请求对象
            IndexRequest indexRequest = new IndexRequest(INDEX_NAME);
            //2.1 设置文档ID
            indexRequest.id(skuId.toString());
            //2.3 设置文档请求体参数 JSON
            String goodJsonStr = JSON.toJSONString(goods);
            indexRequest.source(goodJsonStr, XContentType.JSON);

            //3.执行新增文档请求
            restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException("文档新增失败:{}", e);
        }
    }

    /**
     * 商品下架,将商品文档从索引库删除
     *
     * @param skuId
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            //1.构建删除请求对象
            DeleteRequest deleteRequest = new DeleteRequest(
                    INDEX_NAME,
                    skuId.toString()
            );
            //2.执行删除文档
            restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]删除文档失败:文档ID,{},{}\", skuId.toString(), e.getMessage()");
            throw new RuntimeException("删除文档失败");
        }
    }
}

添加数据

通过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

/**
 * 对指定商品热门分值进行增加/减少
 *
 * @param skuId
 * @param incrscore
 */
@ApiOperation("对指定商品热门分值进行增加/减少")
@GetMapping("/inner/incrHotScore/{skuId}/{incrscore}")
public void incrHotScore(@PathVariable("skuId") String skuId, @PathVariable("incrscore") int incrscore) {
    searchService.incrHotScore(skuId, incrscore);
}

4.1.2 业务层

SearchService

/**
 * 对指定商品分值(热门商品)进行更新
 * 分值
 *
 * @param skuId
 * @param score
 * @return
 */
void incrHotScore(Long skuId, int score);

SearchServiceImpl

@Autowired
private RedisTemplate redisTemplate;


/**
 * 对指定商品热门分值进行增加/减少
 *
 * @param skuId
 * @param incrscore
 */
@Override
public void incrHotScore(String skuId, int incrscore) {
    try {
        //1.先修改Redis中商品热门分值
        String hotKey = "hot:goods:score";
        Double goodsScore = redisTemplate.opsForZSet().incrementScore(hotKey, skuId, incrscore);

        //2.再满足写ES条件后再对ES进行更新-稀释写操作
        if (goodsScore % 10 == 0) {
            UpdateRequest updateRequest = new UpdateRequest(
                    INDEX_NAME,
                    skuId);
            Goods goods = new Goods();
            goods.setHotScore(goodsScore.longValue());
            updateRequest.doc(JSON.toJSONString(goods), XContentType.JSON);
            restHighLevelClient.update(
                    updateRequest, RequestOptions.DEFAULT);
        }
    } catch (IOException e) {
        log.error("[搜索服务]更新热门分值异常:商品ID{},异常信息:{}", skuId, e);
        throw new RuntimeException("更新热门分值异常");
    }
}

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.common.result.Result;
import com.atguigu.gmall.list.client.impl.ListDegradeFeignClient;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

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

    /**
     * 对指定商品分值(热门商品)进行更新
     * 分值
     *
     * @param skuId
     * @param score
     * @return
     */
    @ApiOperation("对指定商品分值(热门商品)进行更新")
    @GetMapping("/inner/incrHotScore/{skuId}/{score}")
    public Result incrHotScore(@PathVariable("skuId") Long skuId, @PathVariable("score") int score);

}

服务降级类

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

import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.list.client.ListFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @author: atguigu
 * @create: 2023-08-04 10:46
 */
@Slf4j
@Component
public class ListDegradeFeignClient implements ListFeignClient {
    @Override
    public Result incrHotScore(Long skuId, int score) {
        log.error("[商品服务]-提供方接口incrHotScore调用异常");
        return null;
    }
}

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

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

import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.item.service.ItemService;
import com.atguigu.gmall.list.client.ListFeignClient;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.*;
import org.apache.commons.lang.StringUtils;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author: atguigu
 * @create: 2023-07-28 16:31
 */
@Service
@SuppressWarnings("all") //去除警告抑制
public class ItemServiceImpl implements ItemService {

    @Autowired
    private ProductFeignClient productFeignClient;


    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private ListFeignClient listFeignClient;

    /**
     * 远程调用商品服务,汇总渲染详情页面所需要数据模型
     * 1. ${skuInfo} 商品SKU基本信息 包含:名称,默认图片,重量,商品SKU图片列表
     * 2. ${categoryView} 商品SKU所属分类 属性:category1Id|name,category2Id|name,category3Id|name
     * 3. ${price} 商品价格-实时
     * 4. ${spuPosterList} 商品海报图片列表
     * 5. ${skuAttrList} 商品SKU所有平台属性以及值
     * 6. ${spuSaleAttrList} 商品Spu所有销售属性,以及销售属性值,带选中效果   根据skuId查销售属性
     * 7. ${valuesSkuJson} 选中一组销售属性实现完成切换SKU详情页面JSON
     *
     * @param skuId
     * @return
     */
    @Override
    public Map<String, Object> getItemInfo(Long skuId) {
        Map<String, Object> mapResult = new HashMap<>();
        //1. ${skuInfo} 商品SKU基本信息 包含:名称,默认图片,重量,商品SKU图片列表
        //2. ${categoryView} 商品SKU所属分类 属性:category1Id|name,category2Id|name,category3Id|name
        //3. ${price} 商品价格-实时
        //4. ${spuPosterList} 商品海报图片列表
        //5. ${skuAttrList} 商品SKU所有平台属性以及值
        //6. ${spuSaleAttrList} 商品Spu所有销售属性,以及销售属性值,带选中效果   根据skuId查销售属性
        //7. ${valuesSkuJson} 选中一组销售属性实现完成切换SKU详情页面JSON
        
        //8.调用搜索服务,更新商品分值
        CompletableFuture.runAsync(() -> {
            listFeignClient.incrHotScore(skuId, 1);
        }).join();

        //9.将以上所有异步任务进行组合 必须所有异步任务都执行完毕
        CompletableFuture.allOf(
                skuInfoCompletableFuture,
                priceCompletableFuture,
                skuAttrListCompletableFuture,
                categoryViewCompletableFuture,
                spuPosterListCompletableFuture,
                spuSaleAttrListCompletableFuture,
                valuesSkuJsonCompletableFuture
        ).join();

        return mapResult;
    }
}