Day 6 商品搜索

Day 6 商品搜索

这里会总结构建项目过程中遇到的问题、主要流程,以及一些个人思考!!

学习方法:

1 github源码 + 文档 + 官网

2 内容复现 ,实际操作

项目源码同步更新到github 欢迎大家star~ 后期会更新并上传前端项目

搜索相关实体

在ES中存储的商品实体类与数据库中的商品实体类不同,且商品的搜索条件搜索结果都有相应的实体类。

Q: 为什么这两种实体类不同?//todo

/**
 * 在ES中存储的商品实体类
 */
@Document(indexName = "goods",createIndex = false)
@Data
public class GoodsES implements Serializable {
  @Field
  private Long id; // 商品id
  @Field
  private String goodsName; // 商品名称
  @Field
  private String caption; // 副标题
  @Field
  private BigDecimal price; // 价格
  @Field
  private String headerPic; // 头图
  @Field
  private String brand; // 品牌名称
  @CompletionField
  private List<String> tags; // 关键字
  @Field
  private List<String> productType; // 类目名
  @Field
  private Map<String,List<String>> specification; // 规格,键为规格项,值为规格值
}
/**
 * 商品搜索条件
 */
@Data
public class GoodsSearchParam implements Serializable {
  private String keyword; // 关键字
  private String brand; // 品牌名
  private Double highPrice; //最高价
  private Double lowPrice; //最低价
  private Map<String,String> specificationOption; // 规格map, 键:规格名,值:规格值
  private String sortFiled; //排序字段 NEW:新品 PRICE:价格
  private String sort; //排序方式 ASC:升序 DESC:降序
  private Integer page; //页码
  private Integer size; //每页条数
}
/**
 * 商品搜索结果
 */
@Data
public class GoodsSearchResult implements Serializable {
  private Page<GoodsES> goodsPage; // 页面商品信息
  private GoodsSearchParam goodsSearchParam; // 搜索条件回显
  private Set<String> brands; // 和商品有关的品牌列表
  private Set<String> productType; // 和商品有关的类别列表
  // 和商品有关的规格列表,键:规格名,值:规格集合
  private Map<String, Set<String>> specifications;
}

创建创建商品索引

Kinbana是一个es的开源分析与可视化平台

image-20240423232717129

PUT /goods
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "ik_pinyin": {
          "tokenizer": "ik_smart",
          "filter": "pinyin_filter"
         },
        "tag_pinyin": {
          "tokenizer": "keyword",
          "filter": "pinyin_filter"
         }
       },
      "filter": {
        "pinyin_filter": {
          "type": "pinyin",
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "remove_duplicated_term": true
         }
       }
     }
   },
  "mappings": {
    "properties": {
      "id": {
        "type": "long",
        "index": true
       },
      "goodsName": {
        "type": "text",
        "index": true,
        "analyzer": "ik_pinyin",
        "search_analyzer": "ik_pinyin"
       },
      "caption": {
        "type": "text",
        "index": true,
        "analyzer": "ik_pinyin",
        "search_analyzer": "ik_pinyin"
       },
      "tags": {
        "type": "completion",
        "analyzer": "tag_pinyin",
        "search_analyzer": "tag_pinyin"
       },
      "price": {
        "type": "double",
        "index": true
       },
      "headerPic": {
        "type": "keyword",
        "index": true
       },
      "brand": {
        "type": "keyword",
        "index": true
       },
      "productType": {
        "type": "keyword",
        "index": true
       },
      "specification":{
        "properties": {
         "specificationName":{
          "type": "keyword",
          "index": true
          },
         "specificationOption":{
          "type": "keyword",
          "index": true
          }
         } 
       }
     }
   }
}

创建搜索模块

# 端口号
server:
  port: 8004


# 日志格式
logging:
  pattern:
   console: '%d{HH:mm:ss.SSS} %clr(%-5level) ---  [%-15thread] %cyan(%-50logger{50}):%msg%n'


# Nacos
spring:
  application:
   name: shopping_search_customer_api
  cloud:
   nacos:
    discovery:
     server-addr: 192.168.66.100:8848


dubbo:
  application:
  #项目名字
   name: shopping_search_customer_api
  protocol:
   name: dubbo
   port: -1
  registry:
  # 注册地址
   address: nacos://192.168.66.100:8848

# 端口号
server:
  port: 9008


# 日志格式
logging:
  pattern:
   console: '%d{HH:mm:ss.SSS} %clr(%-5level) ---  [%-15thread] %cyan(%-50logger{50}):%msg%n'


spring:
 # elasticsearch
  elasticsearch:
   uris: http://192.168.66.100:9200
  application:
   name: shopping_search_service #服务名
  cloud:
   nacos:
    discovery:
     server-addr: 192.168.66.100:8848 # 注册中心地址
  
dubbo:
  application:
   name: shopping_search_service # 项目名
   serialize-check-status: DISABLE
   check-serializable: false
  protocol:
   name: dubbo # 通讯协议
   port: -1 # 端口号,-1表示自动扫描可用端口。
  registry:
   address: nacos://192.168.66.100:8848 # 注册中心

服务的启动类需要配置分页插件

@EnableDiscoveryClient
@EnableDubbo
@RefreshScope
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class ShoppingSearchServiceApplication {
  public static void main(String[] args) {
    SpringApplication.run(ShoppingSearchServiceApplication.class, args);
   }
  
  // 分页插件
  @Bean
  public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
   }
}

编写搜索相关的接口

public interface SearchService {
  /**
   * 自动补齐关键字
   * @param keyword 被补齐的词
   * @return 补齐的关键词集合
   */
  List<String> autoSuggest(String keyword);


  /**
   * 搜索商品
   * @param goodsSearchParam 搜索条件
   * @return 搜索结果
   */
  GoodsSearchResult search(GoodsSearchParam goodsSearchParam);
  
  /**
   * 向ES同步数据库中的商品数据
   * @param goodsDesc 商品详情
   */
  void syncGoodsToES(GoodsDesc goodsDesc);


  /**
   * 删除ES中的商品数据
   * @param id 商品id
   */
  void delete(Long id);
}

查询详情

/**
 * 商品详情
 */
@Data
public class GoodsDesc implements Serializable {
  private Long id; // 商品id
  private String goodsName; // 商品名称
  private String caption; // 副标题
  private BigDecimal price; // 价格
  private String headerPic; // 头图
  private Boolean isMarketable; // 是否上架
  private String introduction; // 商品介绍


  private Brand brand; // 品牌
  private ProductType productType1; // 一级类目
  private ProductType productType2; // 二级类目id
  private ProductType productType3; // 三级类目id
  private List<GoodsImage> images; // 商品图片
  private List<Specification> specifications; // 商品规格
}

在商品服务Mapper中添加

List<GoodsDesc> findAll();

编写mapper映射xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bootscoder.shopping_goods_service.mapper.GoodsMapper">
    <insert id="addGoodsSpecificationOption">
        INSERT INTO boots_goods_specification_option VALUES (#{gid},#{optionId})
    </insert>
    <delete id="deleteGoodsSpecificationOption" parameterType="long">
        DELETE FROM boots_goods_specification_option WHERE gid = #{gid}
    </delete>
    <update id="putAway">
        UPDATE boots_goods SET isMarketable = #{isMarketable} WHERE id = #{id}
    </update>
    <resultMap id="goodsMapper" type="com.bootscoder.shopping_common.pojo.Goods">
        <id property="id" column="bid"></id>
        <result property="goodsName" column="goodsName"></result>
        <result property="caption" column="caption"></result>
        <result property="price" column="price"></result>
        <result property="headerPic" column="headerPic"></result>
        <result property="isMarketable" column="isMarketable"></result>
        <result property="introduction" column="introduction"></result>
        <result property="brandId" column="brandId"></result>
        <result property="productType1Id" column="productType1Id"></result>
        <result property="productType2Id" column="productType2Id"></result>
        <result property="productType3Id" column="productType3Id"></result>
        <collection property="images" column="bid" ofType="com.bootscoder.shopping_common.pojo.GoodsImage">
            <id property="id" column="imageId"></id>
            <result property="imageTitle" column="imageTitle"></result>
            <result property="imageUrl" column="imageUrl"></result>
        </collection>
        <collection property="specifications" column="bid" ofType="com.bootscoder.shopping_common.pojo.Specification">
            <id property="id" column="specificationId"></id>
            <result property="specName" column="specName"></result>
            <result property="productTypeId" column="productTypeId"></result>
            <collection property="specificationOptions" column="specificationId" ofType="com.bootscoder.shopping_common.pojo.SpecificationOption">
                <id property="id" column="optionId"></id>
                <result property="optionName" column="optionName"></result>
            </collection>
        </collection>
    </resultMap>
    <select id="findById" parameterType="long" resultMap="goodsMapper">
        SELECT
            boots_goods.id AS bid,
            boots_goods.goodsName AS goodsName,
            boots_goods.caption AS caption,
            boots_goods.price AS price,
            boots_goods.headerPic AS headerPic,
            boots_goods.introduction AS introduction,
            boots_goods.isMarketable AS isMarketable,
            boots_goods.brandId AS brandId,
            boots_goods.productType1Id AS productType1Id,
            boots_goods.productType2Id AS productType2Id,
            boots_goods.productType3Id AS productType3Id,
            boots_goods_image.id AS imageId,
            boots_goods_image.imageTitle AS imageTitle,
            boots_goods_image.imageUrl AS imageUrl,
            boots_specification.id AS specificationId,
            boots_specification.specName AS specName,
            boots_specification.productTypeId AS productTypeId,
            boots_specification_option.id AS optionId,
            boots_specification_option.optionName AS optionName
        FROM
            boots_goods
                LEFT JOIN boots_goods_image ON boots_goods.id = boots_goods_image.goodsId
                LEFT JOIN boots_goods_specification_option ON boots_goods.id = boots_goods_specification_option.gid
                LEFT JOIN boots_specification_option ON boots_goods_specification_option.optionId = boots_specification_option.id
                LEFT JOIN boots_specification ON boots_specification_option.specId = boots_specification.id
        WHERE
            boots_goods.id = #{gid}
    </select>


    <resultMap id="goodsDescMapper" type="com.bootscoder.shopping_common.pojo.GoodsDesc">
        <id property="id" column="bid"></id>
        <result property="goodsName" column="goodsName"></result>
        <result property="caption" column="caption"></result>
        <result property="price" column="price"></result>
        <result property="headerPic" column="headerPic"></result>
        <result property="isMarketable" column="isMarketable"></result>
        <result property="introduction" column="introduction"></result>
        <association property="brand" column="brandId" javaType="com.bootscoder.shopping_common.pojo.Brand">
            <id property="id" column="brandId"></id>
            <result property="name" column="brandName"></result>
        </association>
        <association property="productType1" column="type1Id" javaType="com.bootscoder.shopping_common.pojo.ProductType">
            <id property="id" column="type1Id"></id>
            <result property="name" column="type1Name"></result>
            <result property="level" column="type1Level"></result>
            <result property="parentId" column="type1parentId"></result>
        </association>
        <association property="productType2" column="type2Id" javaType="com.bootscoder.shopping_common.pojo.ProductType">
            <id property="id" column="type2Id"></id>
            <result property="name" column="type2Name"></result>
            <result property="level" column="type2Level"></result>
            <result property="parentId" column="type2parentId"></result>
        </association>
        <association property="productType3" column="type3Id" javaType="com.bootscoder.shopping_common.pojo.ProductType">
            <id property="id" column="type3Id"></id>
            <result property="name" column="type3Name"></result>
            <result property="level" column="type3Level"></result>
            <result property="parentId" column="type3parentId"></result>
        </association>
        <collection property="images" column="bid" ofType="com.bootscoder.shopping_common.pojo.GoodsImage">
            <id property="id" column="imageId"></id>
            <result property="imageTitle" column="imageTitle"></result>
            <result property="imageUrl" column="imageUrl"></result>
        </collection>
        <collection property="specifications" column="bid" ofType="com.bootscoder.shopping_common.pojo.Specification">
            <id property="id" column="specificationId"></id>
            <result property="specName" column="specName"></result>
            <result property="productTypeId" column="productTypeId"></result>
            <collection property="specificationOptions" column="specificationId" ofType="com.bootscoder.shopping_common.pojo.SpecificationOption">
                <id property="id" column="optionId"></id>
                <result property="optionName" column="optionName"></result>
            </collection>
        </collection>
    </resultMap>
    <select id="findAll" resultMap="goodsDescMapper">
        SELECT
            boots_goods.id bid,
            boots_goods.goodsName goodsName,
            boots_goods.caption caption,
            boots_goods.price price,
            boots_goods.headerPic headerPic,
            boots_goods.introduction introduction,
            boots_goods.isMarketable isMarketable,
            boots_goods.brandId brandId,
            boots_brand.`name` brandName,
            type1.id type1Id,
            type1.`name` type1Name,
            type1.level type1Level,
            type1.parentId type1parentId,
            type2.id type2Id,
            type2.`name` type2Name,
            type2.level type2Level,
            type2.parentId type2parentId,
            type3.id type3Id,
            type3.`name` type3Name,
            type3.level type3Level,
            type3.parentId type3parentId,
            boots_goods_image.id imageId,
            boots_goods_image.imageTitle imageTitle,
            boots_goods_image.imageUrl imageUrl,
            boots_specification.id specificationId,
            boots_specification.specName specName,
            boots_specification.productTypeId productTypeId,
            boots_specification_option.id optionId,
            boots_specification_option.optionName optionName
        FROM
            boots_goods,
            boots_goods_image,
            boots_brand,
            boots_specification,
            boots_specification_option,
            boots_goods_specification_option,
            boots_product_type AS type1,
            boots_product_type AS type2,
            boots_product_type AS type3
        WHERE boots_goods.id = boots_goods_specification_option.gid
          AND boots_goods_specification_option.optionId = boots_specification_option.id
          AND boots_specification_option.specId = boots_specification.id
          AND boots_goods.brandId = boots_brand.id
          AND boots_goods.id = boots_goods_image.goodsId
          AND boots_goods.productType1Id = type1.id
          AND boots_goods.productType2Id = type2.id
          AND boots_goods.productType3Id = type3.id
    </select>

    <select id="findDesc" resultMap="goodsDescMapper">
        SELECT
            boots_goods.id bid,
            boots_goods.goodsName goodsName,
            boots_goods.caption caption,
            boots_goods.price price,
            boots_goods.headerPic headerPic,
            boots_goods.introduction introduction,
            boots_goods.isMarketable isMarketable,
            boots_goods.brandId brandId,
            boots_brand.`name` brandName,
            type1.id type1Id,
            type1.`name` type1Name,
            type1.level type1Level,
            type1.parentId type1parentId,
            type2.id type2Id,
            type2.`name` type2Name,
            type2.level type2Level,
            type2.parentId type2parentId,
            type3.id type3Id,
            type3.`name` type3Name,
            type3.level type3Level,
            type3.parentId type3parentId,
            boots_goods_image.id imageId,
            boots_goods_image.imageTitle imageTitle,
            boots_goods_image.imageUrl imageUrl,
            boots_specification.id specificationId,
            boots_specification.specName specName,
            boots_specification.productTypeId productTypeId,
            boots_specification_option.id optionId,
            boots_specification_option.optionName optionName
        FROM
            boots_goods,
            boots_goods_image,
            boots_brand,
            boots_specification,
            boots_specification_option,
            boots_goods_specification_option,
            boots_product_type AS type1,
            boots_product_type AS type2,
            boots_product_type AS type3
        WHERE boots_goods.id = boots_goods_specification_option.gid
          AND boots_goods_specification_option.optionId = boots_specification_option.id
          AND boots_specification_option.specId = boots_specification.id
          AND boots_goods.brandId = boots_brand.id
          AND boots_goods.id = boots_goods_image.goodsId
          AND boots_goods.productType1Id = type1.id
          AND boots_goods.productType2Id = type2.id
          AND boots_goods.productType3Id = type3.id
          AND boots_goods.id = #{id}
    </select>
</mapper>

修改商品服务接口和实现类

提问:

//todo

  1. mapper 和Service 层的内容有什么不同? 为什么这样设计
  2. 商品实体和详情有何区别?为什么这样设计

测试查询所有商品详情

@SpringBootTest
class ShoppingGoodsServiceApplicationTests {

    @Autowired
    GoodsService goodsService;
    @Test
    void contextLoads() {
        System.out.println(goodsService.findAll());
    }
}

image-20240424000756724

编写分词方法

在向ES添加数据时,我们需要将数据库的一些字段进行分词作为商品的关键词,方便编写补齐关键词功能,在搜索服务接口实现类编写分词方法:

这里,关键字是 商品名称 + 副标题的分词结果

@DubboService
@Service
public class SearchServiceImpl implements SearchService {
  @Autowired
  private ElasticsearchClient client;
  @Autowired
  private GoodsESRepository goodsESRepository;
  @Autowired
  private ElasticsearchTemplate template;


  /**
   * 分词
   *
   * @param text   被分词的文本
   * @param analyzer 分词器
   * @return 分词结果
   */
  @SneakyThrows // 抛出已检查异常
  public List<String> analyze(String text, String analyzer) {
    // 分词请求
    AnalyzeRequest request = AnalyzeRequest.of(a ->
       a.index("goods").analyzer(analyzer).text(text)
     );
    // 发送分词请求,获取相应结果
    AnalyzeResponse response = client.indices().analyze(request);
    // 处理相应结果
    List<String> words = new ArrayList(); // 分词结果集合
    List<AnalyzeToken> tokens = response.tokens();
    for (AnalyzeToken token : tokens) {
      String term = token.token();// 分出的词
      words.add(term);
     }
    return words;
   }
}

在测试时,发现JAVA项目无法连接ES,这是由于ES默认不允许远程访问。kibana由于和ES在同一台服务器下所以可以访问,JAVA程序在开发电脑中所以无法访问。我们修改es的配置文件,开启远程访问功能:

  1. 打开ES配置文件

    vim /usr/local/elasticsearch/config/elasticsearch.yml
    
  2. 添加如下内容

    # 单体ES环境
    discovery.type: single-node
    # 允许所有路径访问
    network.host: 0.0.0.0
    
  3. 重启ES和kibana。

ps -ef | grep elasticsearch

kill -9 id

./elasticsearch -d

-d 后台运行

分词测试成功

@Test
void contextLoads() {
    List<String> analyze = searchService.analyze("我爱敲代码,我爱读书", "ik_pinyin");
    System.out.println(analyze);
}

image-20240424004011386

向ES中同步数据

搜索服务模块编写操作ES商品文档的Repository接口

//todo 这个接口的作用是什么?

@Repository
public interface GoodsESRepository extends ElasticsearchRepository<GoodsES,Long> {
}

在搜素服务模块编写同步数据的方法

// 向ES同步商品数据
@Override
public void syncGoodsToES(GoodsDesc goodsDesc) {
  // 将商品详情对象转为GoodsES对象
  GoodsES goodsES = new GoodsES();
  goodsES.setId(goodsDesc.getId());
  goodsES.setGoodsName(goodsDesc.getGoodsName());
  goodsES.setCaption(goodsDesc.getCaption());
  goodsES.setPrice(goodsDesc.getPrice());
  goodsES.setHeaderPic(goodsDesc.getHeaderPic());
  goodsES.setBrand(goodsDesc.getBrand().getName());
  // 类型集合
  List<String> productType = new ArrayList();
  productType.add(goodsDesc.getProductType1().getName());
  productType.add(goodsDesc.getProductType2().getName());
  productType.add(goodsDesc.getProductType3().getName());
  goodsES.setProductType(productType);
  // 规格集合
  Map<String,List<String>> map = new HashMap();
  List<Specification> specifications = goodsDesc.getSpecifications();
  // 遍历规格
  for (Specification specification : specifications) {
    // 规格项集合
    List<SpecificationOption> options = specification.getSpecificationOptions();
    // 规格项名集合
    List<String> optionStrList = new ArrayList();
    for (SpecificationOption option : options) {
      optionStrList.add(option.getOptionName());
     }
    map.put(specification.getSpecName(),optionStrList);
   }
  goodsES.setSpecification(map);
  // 关键字
  List<String> tags = new ArrayList();
  tags.add(goodsDesc.getBrand().getName()); //品牌名是关键字
  tags.addAll(analyze(goodsDesc.getGoodsName(),"ik_smart"));//商品名分词后为关键词
  tags.addAll(analyze(goodsDesc.getCaption(),"ik_smart"));//副标题分词后为关键词
    goodsES.setTags(tags);
  
  // 将GoodsES对象存入ES
  goodsESRepository.save(goodsES);
}

测试同步方法

@SpringBootTest
class ShoppingSearchServiceApplicationTests {
  @DubboReference
  private GoodsService goodsService;

  @Test
  void testSyncGoodsToES(){
    List<GoodsDesc> goods = goodsService.findAll();
    for (GoodsDesc goodsDesc : goods) {
      // 如果商品是上架状态
      if (goodsDesc.getIsMarketable()){
        searchService.syncGoodsToES(goodsDesc);
       }
     }
   }
}

先启动商品服务,再执行测试类,同步所有商品。

注:同步成功后需要注释测试类,否则启动搜索服务前必须启动商品服务。因为测试类中注入了另一个服务

image-20240424005253263

GET /goods/_search
{
  "query": {
    "match_all": {}
  }
}

image-20240424005453981

编写es的简单查询语句可以看到成功查到 --> 即同步成功

商品搜索自动补全(关键字)

源码中有不少函数式接口 nice

//todo 了解什么是函数式接口

 // 自动补齐
    @SneakyThrows
    @Override
    public List<String> autoSuggest(String keyword) {
        // 1.自动补齐查询条件
        Suggester suggester = Suggester.of(
                s -> s.suggesters("prefix_suggestion", FieldSuggester.of(
                        fs -> fs.completion(
                                cs -> cs.skipDuplicates(true)
                                        .size(10)
                                        .field("tags")
                        )
                )).text(keyword)
        );
        // 2.自动补齐查询
        SearchResponse<Map> response = client.search(s -> s.index("goods")
                .suggest(suggester), Map.class);

        // 3.处理查询结果
        Map resultMap = response.suggest();
        List<Suggestion> suggestionList = (List) resultMap.get("prefix_suggestion");
        Suggestion suggestion = suggestionList.get(0);
        List<CompletionSuggestOption> resultList = suggestion.completion().options();

        List<String> result = new ArrayList();
        for (CompletionSuggestOption completionSuggestOption : resultList) {
            String text = completionSuggestOption.text();
            result.add(text);
        }

        return result;
    }

在higress 中配置网关

image-20240424010756091

测试自动补齐成功

image-20240424010741561

编写商品搜索功能

@DubboService
@Service
public class SearchServiceImpl implements SearchService {
    @Autowired
    private ElasticsearchClient client;
    @Autowired
    private GoodsESRepository goodsESRepository;
    @Autowired
    private ElasticsearchTemplate template;

    /**
     * 分词
     *
     * @param text     被分词的文本
     * @param analyzer 分词器
     * @return 分词结果
     */
    @SneakyThrows
    public List<String> analyze(String text, String analyzer) {
        // 创建分词请求
        AnalyzeRequest request = AnalyzeRequest.of(a -> a.index("goods").analyzer(analyzer).text(text));
        // 发送分词请求
        AnalyzeResponse response = client.indices().analyze(request);
        // 处理分词结果
        List<String> words = new ArrayList();
        List<AnalyzeToken> tokens = response.tokens();
        for (AnalyzeToken token : tokens) {
            String term = token.token();
            words.add(term);
        }
        return words;
    }

    // 自动补齐
    @SneakyThrows
    @Override
    public List<String> autoSuggest(String keyword) {
        // 1.自动补齐查询条件
        Suggester suggester = Suggester.of(
                s -> s.suggesters("prefix_suggestion", FieldSuggester.of(
                        fs -> fs.completion(
                                cs -> cs.skipDuplicates(true)
                                        .size(10)
                                        .field("tags")
                        )
                )).text(keyword)
        );
        // 2.自动补齐查询
        SearchResponse<Map> response = client.search(s -> s.index("goods")
                .suggest(suggester), Map.class);

        // 3.处理查询结果
        Map resultMap = response.suggest();
        List<Suggestion> suggestionList = (List) resultMap.get("prefix_suggestion");
        Suggestion suggestion = suggestionList.get(0);
        List<CompletionSuggestOption> resultList = suggestion.completion().options();

        List<String> result = new ArrayList();
        for (CompletionSuggestOption completionSuggestOption : resultList) {
            String text = completionSuggestOption.text();
            result.add(text);
        }

        return result;
    }

    @Override
    public GoodsSearchResult search(GoodsSearchParam goodsSearchParam) {
        // 1.构造ES搜索条件
        NativeQuery nativeQuery = buildQuery(goodsSearchParam);
        // 2.搜索
        SearchHits<GoodsES> search = template.search(nativeQuery, GoodsES.class);
        // 3.将查询结果封装为Mybatis-plus的Page对象
        // 3.1 将SearchHits对象转为List
        List<GoodsES> content = new ArrayList();
        for (SearchHit<GoodsES> goodsESSearchHit : search) {
            GoodsES goodsES = goodsESSearchHit.getContent();
            content.add(goodsES);
        }
        // 3.2 将List转为Mybatis-plus的Page对象
        Page<GoodsES> page = new Page();
        page.setCurrent(goodsSearchParam.getPage()) // 当前页
                .setSize(goodsSearchParam.getSize()) // 每页条数
                .setTotal(search.getTotalHits()) // 总条数
                .setRecords(content); // 结果集

        // 4.封装查询结果
        GoodsSearchResult result = new GoodsSearchResult();
        // 4.1 封装商品
        result.setGoodsPage(page);
        // 4.2 封装查询参数
        result.setGoodsSearchParam(goodsSearchParam);
        // 4.3 封装查询面板
        buildSearchPanel(goodsSearchParam,result);
        return result;
    }

    /**
     * 封装查询面板,即根据查询条件,找到查询结果关联度前20名的商品进行封装
     * @param goodsSearchParam 查询条件对象
     * @param goodsSearchResult 查询结果对象
     */
    public void buildSearchPanel(GoodsSearchParam goodsSearchParam,GoodsSearchResult goodsSearchResult){
        // 1.构造查询条件
        goodsSearchParam.setPage(1);
        goodsSearchParam.setSize(20);
        goodsSearchParam.setSort(null);
        goodsSearchParam.setSortFiled(null);
        NativeQuery nativeQuery = buildQuery(goodsSearchParam);
        // 2.搜索
        SearchHits<GoodsES> search = template.search(nativeQuery, GoodsES.class);
        // 3.将结果封装为List对象
        List<GoodsES> content = new ArrayList();
        for (SearchHit<GoodsES> goodsESSearchHit : search) {
            GoodsES goodsES = goodsESSearchHit.getContent();
            content.add(goodsES);
        }

        // 4.遍历集合,封装查询面板
        // 商品相关的品牌列表
        Set<String> brands = new HashSet();
        // 商品相关的类型列表
        Set<String> productTypes = new HashSet();
        // 商品相关的规格列表
        Map<String,Set<String>> specifications = new HashMap();

        for (GoodsES goodsES : content) {
            // 获取品牌
            brands.add(goodsES.getBrand());
            // 获取类型
            List<String> productType = goodsES.getProductType();
            productTypes.addAll(productType);
            // 获取规格
            Map<String, List<String>> specification = goodsES.getSpecification();
            Set<Map.Entry<String, List<String>>> entries = specification.entrySet();
            for (Map.Entry<String, List<String>> entry : entries) {
                // 规格名
                String key = entry.getKey();
                // 规格值
                List<String> value = entry.getValue();
                // 如果specifications有该规格,则像规格中添加规格项,如果没有该规格,新增键值对
                if (!specifications.containsKey(key)){
                    specifications.put(key,new HashSet(value));
                }else {
                    specifications.get(key).addAll(value);
                }
            }
        }
        goodsSearchResult.setBrands(brands);
        goodsSearchResult.setProductType(productTypes);
        goodsSearchResult.setSpecifications(specifications);
    }


    /**
     * 构造搜索条件
     *
     * @param goodsSearchParam 查询条件对象
     * @return 搜索条件对象
     */
    public NativeQuery buildQuery(GoodsSearchParam goodsSearchParam) {
        // 1.创建复杂查询条件对象
        NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder();
        BoolQuery.Builder builder = new BoolQuery.Builder();

        // 2.如果查询条件有关键词,关键词可以匹配商品名、副标题、品牌字段;否则查询所有商品
        if (!StringUtils.hasText(goodsSearchParam.getKeyword())){
            MatchAllQuery matchAllQuery = new MatchAllQuery.Builder().build();
            builder.must(matchAllQuery._toQuery());
        }else{
            String keyword = goodsSearchParam.getKeyword();
            MultiMatchQuery keywordQuery = MultiMatchQuery.of(q ->q.query(keyword).fields("goodsName","caption","brand"));
            builder.must(keywordQuery._toQuery());
        }

        // 3.如果查询条件有品牌,则精准匹配品牌
        String brand = goodsSearchParam.getBrand();
        if (StringUtils.hasText(brand)){
            TermQuery brandQuery = TermQuery.of(q -> q.field("brand").value(brand));
            builder.must(brandQuery._toQuery());
        }

        // 4.如果查询条件有价格,则匹配价格
        Double highPrice = goodsSearchParam.getHighPrice();
        Double lowPrice = goodsSearchParam.getLowPrice();
        if (highPrice != null && highPrice != 0){
            RangeQuery lte = RangeQuery.of(q -> q.field("price").lte(JsonData.of(highPrice)));
            builder.must(lte._toQuery());
        }
        if (lowPrice != null && lowPrice != 0){
            RangeQuery gte = RangeQuery.of(q -> q.field("price").gte(JsonData.of(lowPrice)));
            builder.must(gte._toQuery());
        }

        // 5.如果查询条件有规格项,则精准匹配规格项
        Map<String, String> specificationOptions = goodsSearchParam.getSpecificationOption();
        if (specificationOptions != null && specificationOptions.size() > 0){
            Set<Map.Entry<String, String>> entries = specificationOptions.entrySet();
            for (Map.Entry<String, String> entry : entries) {
                String key = entry.getKey();
                String value = entry.getValue();
                if (StringUtils.hasText(key)){
                    TermQuery termQuery = TermQuery.of(q->q.field("specification."+key+".keyword").value(value));
                    builder.must(termQuery._toQuery());
                }
            }
        }
        nativeQueryBuilder.withQuery(builder.build()._toQuery());

        // 6.添加分页条件
        PageRequest pageable = PageRequest.of(goodsSearchParam.getPage()-1, goodsSearchParam.getSize());
        nativeQueryBuilder.withPageable(pageable);

        // 7.如果查询条件有排序,则添加排序条件
        String sortFiled = goodsSearchParam.getSortFiled();
        String sort = goodsSearchParam.getSort();
        if (StringUtils.hasText(sort) && StringUtils.hasText(sortFiled)){
            Sort sortParam = null;
            // 新品的正序是ID的倒序
            if (sortFiled.equals("NEW")){
                if (sort.equals("ASC")){
                    sortParam = Sort.by(Sort.Direction.DESC,"id");
                }
                if (sort.equals("DESC")){
                    sortParam = Sort.by(Sort.Direction.ASC,"id");
                }
            }
            if (sortFiled.equals("PRICE")){
                if (sort.equals("ASC")){
                    sortParam = Sort.by(Sort.Direction.ASC,"price");
                }
                if (sort.equals("DESC")){
                    sortParam = Sort.by(Sort.Direction.DESC,"price");
                }
            }
            nativeQueryBuilder.withSort(sortParam);
        }

        // 8.返回封装好的搜索条件对象
        return nativeQueryBuilder.build();
    }

    @Override
    public void syncGoodsToES(GoodsDesc goodsDesc) {
        // 将商品详情数据转为GoodsES对象
        GoodsES goodsES = new GoodsES();
        goodsES.setId(goodsDesc.getId());
        goodsES.setGoodsName(goodsDesc.getGoodsName());
        goodsES.setCaption(goodsDesc.getCaption());
        goodsES.setPrice(goodsDesc.getPrice());
        goodsES.setHeaderPic(goodsDesc.getHeaderPic());
        goodsES.setBrand(goodsDesc.getBrand().getName());
        // 商品类型集合
        List<String> productType = new ArrayList();
        productType.add(goodsDesc.getProductType1().getName());
        productType.add(goodsDesc.getProductType2().getName());
        productType.add(goodsDesc.getProductType3().getName());
        goodsES.setProductType(productType);
        // 商品规格集合
        Map<String, List<String>> map = new HashMap();
        List<Specification> specifications = goodsDesc.getSpecifications();
        // 遍历规格集合
        for (Specification specification : specifications) {
            // 规格项
            List<SpecificationOption> specificationOptions = specification.getSpecificationOptions();
            // 拿到规格项名
            List<String> optionStrList = new ArrayList();
            for (SpecificationOption option : specificationOptions) {
                optionStrList.add(option.getOptionName());
            }
            map.put(specification.getSpecName(), optionStrList);
        }
        goodsES.setSpecification(map);
        // 关键字
        List<String> tags = new ArrayList();
        tags.add(goodsDesc.getBrand().getName()); // 品牌名是关键字
        tags.addAll(analyze(goodsDesc.getGoodsName(), "ik_smart")); // 商品名分词后是关键词
        tags.addAll(analyze(goodsDesc.getCaption(), "ik_smart")); // 副标题分词后是关键词
        goodsES.setTags(tags);

        // 将GoodsES对象存入ES
        goodsESRepository.save(goodsES);
    }

    @Override
    public void delete(Long id) {
        goodsESRepository.deleteById(id);
    }
}

image-20240424012058009

//奇怪这里没有图片,怀疑是因为没有同步的ES

image-20240424020007005

同步后修改成功!(ps: 这里还存在一个bug ,//todo 仍然无法添加没有规格项的商品,会报空指针异常)

根据id查询商品详情

image-20240424020518642

访问成功

管理员操作后同步到ES

@DubboService
public class GoodsServiceImpl implements GoodsService {
  @Autowired
  private GoodsMapper goodsMapper;
  @Autowired
  private GoodsImageMapper goodsImageMapper;
  @DubboReference
  private SearchService searchService;


  @Override
  public void add(Goods goods) {
    // 插入商品数据
    goodsMapper.insert(goods);


    // 插入图片数据
    Long goodsId = goods.getId(); // 获取商品主键
    List<GoodsImage> images = goods.getImages(); // 商品图片
    for (GoodsImage image : images) {
      image.setGoodsId(goodsId); // 给图片设置商品id
      goodsImageMapper.insert(image); //插入图片
     }


    // 插入商品_规格项数据
    // 1.获取规格
    List<Specification> specifications = goods.getSpecifications();
    // 2.获取规格项
    List<SpecificationOption> options = new ArrayList(); //规格项集合
    // 遍历规格,获取规格中的所有规格项
    for (Specification specification : specifications) {
      options.addAll(specification.getSpecificationOptions());
     }
    // 3.遍历规格项,插入商品_规格项数据
    for (SpecificationOption option : options) {
      goodsMapper.addGoodsSpecificationOption(goodsId,option.getId());
     }


    // 将商品数据同步到es中
    GoodsDesc goodsDesc = findDesc(goodsId);
    searchService.syncGoodsToES(goodsDesc);
   }


  @Override
  public void update(Goods goods) {
    // 删除旧图片数据
    Long goodsId = goods.getId(); // 商品id
    QueryWrapper<GoodsImage> queryWrapper = new QueryWrapper();
    queryWrapper.eq("goodsId",goodsId);
    goodsImageMapper.delete(queryWrapper);


    // 删除旧规格项数据
    goodsMapper.deleteGoodsSpecificationOption(goodsId);


    // 插入商品数据
    goodsMapper.updateById(goods);


    // 插入图片数据
    List<GoodsImage> images = goods.getImages(); // 商品图片
    for (GoodsImage image : images) {
      image.setGoodsId(goodsId); // 给图片设置商品id
      goodsImageMapper.insert(image); //插入图片
     }


    // 插入商品_规格项数据
    // 1.获取规格
    List<Specification> specifications = goods.getSpecifications();
    // 2.获取规格项
    List<SpecificationOption> options = new ArrayList(); //规格项集合
    // 遍历规格,获取规格中的所有规格项
    for (Specification specification : specifications) {
      options.addAll(specification.getSpecificationOptions());
     }
    // 3.遍历规格项,插入商品_规格项数据
    for (SpecificationOption option : options) {
      goodsMapper.addGoodsSpecificationOption(goodsId,option.getId());
     }


    // 将商品数据同步到es中
    GoodsDesc goodsDesc = findDesc(goodsId);
    searchService.syncGoodsToES(goodsDesc);
   }


  @Override
  public void putAway(Long id, Boolean isMarketable) {
    goodsMapper.putAway(id,isMarketable);
    // 上架时数据同步到ES,下架时删除ES数据
    if (isMarketable){
      GoodsDesc goodsDesc = findDesc(id);
      searchService.syncGoodsToES(goodsDesc);
     }else {
      searchService.delete(id);
     }
   }
}

这里存在耦合 goods 和search类

优化商品服务

rocketmq:
 # nameserver地址
  name-server: 192.168.66.100:9876
  producer:
  # 生产组
   group: my_group1
  # 发送消息超时时间
   send-message-timeout: 3000

出现问题:nacos服务挂了

tmd 重启服务器后发现,nacos好了,但是Higress 挂了……tmd 神经病

我就奇了怪了,感觉应该是我安装的Higress 有点问题…… 出于一个测试版吧应该,感觉不是很稳定

不然按理来说这些环境的配置应该是很稳定才对,这样才会极大的减轻开发人员的负担 --> 后期考虑放到云服务器上了

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值