Elasticsearch实现商品搜索

作者: AC编程 | 来源:发表于2021-05-18 09:35 被阅读0次

    准备

    如果您对Elasticsearch还不了解,您可以先阅读我另外一篇文章看完这篇Elasticsearch还不会,你打我,先对Elasticsearch有一个基础的了解。

    一、安装并运行Elasticsearch

    在下载之前你应该确保你的 Java 版本保持在 1.8 及以上,这是 Elasticsearch 的硬性要求,可以自行打开命令行输入 java -version 来查看 Java 的版本

    安装完 Java,就可以跟着Elasticsearch官网下载地址安装 Elasticsearch,直接下载压缩包比较简单。我的开发环境是Windows,因此我选择的是Windows版本。

    选择历史版本

    下载压缩包后对其进行解压,进入Elasticsearch的bin目录,D:\install\elasticsearch-7.12.1\bin,双击elasticsearch.bat 启动Elasticsearch

    elasticsearch.bat

    此时,Elasticsearch运行在本地的9200端口,在浏览器中输入地址http://localhost:9200/如果看到以下信息就说明你的电脑已成功安装Elasticsearch

    Elasticsearch安装成功

    默认情况下,Elasticsearch 只允许本机访问,如果需要远程访问,可以修改 Elastic 安装目录的config/elasticsearch.yml文件,去掉network.host的注释,将它的值改成0.0.0.0,然后重新启动 Elasticsearch。

    network.host: 0.0.0.0
    

    上面代码中,设成0.0.0.0让任何人都可以访问。线上服务不要这样设置,要设成具体的 IP。

    二、Elasticsearch可视化操作平台Kibana

    Kibana 是一个开源的分析和可视化平台,旨在与 Elasticsearch 合作。Kibana 提供搜索、查看和与存储在 Elasticsearch 索引中的数据进行交互的功能。开发者或运维人员可以轻松地执行高级数据分析,并在各种图表、表格和地图中可视化数据。

    你可以从 Elasticsearch 的官网下载Kibana,同样Kibana我们也选择下载版本7.12.1。解压文档后,进入kibana的bin目录D:\install\kibana-7.12.1-windows-x86_64\bin,双击kibana.bat启动Kibana。

    Kibana下载

    kibana启动后运行在5601端口上,我们可以在浏览器中输入http://localhost:5601地址来访问kibana。

    注意:启动kibana前必须先启动Elasticsearch,否则kibana会启动不成功。

    kibana控制台界面

    开发中我们一般用得比较多的是Dev Tools工具

    Dev Tools使用

    三、Elasticsearch中文分词器-IK分词器

    3.1 中文分词

    首先我们通过Postman发送GET请求查询分词效果

    GET http://localhost:9200/_analyze
    {
        "text":"我爱你中国"
    }
    
    Postman发送GET请求查询分词效果

    得到如下结果,可以发现ES的默认分词器无法识别中文:我、我爱你、中国这样的词汇,而是简单的将每个字拆完分为一个词,这显然不符合我们的使用要求,所以我们需要安装中文分词器来解决这个问题。

    {
        "tokens": [
            {
                "token": "我",
                "start_offset": 0,
                "end_offset": 1,
                "type": "<IDEOGRAPHIC>",
                "position": 0
            },
            {
                "token": "爱",
                "start_offset": 1,
                "end_offset": 2,
                "type": "<IDEOGRAPHIC>",
                "position": 1
            },
            {
                "token": "你",
                "start_offset": 2,
                "end_offset": 3,
                "type": "<IDEOGRAPHIC>",
                "position": 2
            },
            {
                "token": "中",
                "start_offset": 3,
                "end_offset": 4,
                "type": "<IDEOGRAPHIC>",
                "position": 3
            },
            {
                "token": "国",
                "start_offset": 4,
                "end_offset": 5,
                "type": "<IDEOGRAPHIC>",
                "position": 4
            }
        ]
    }
    

    或用kibana请求得到效果(用kibana的话就不用再写IP地址和端口了)

    GET _analyze
    {
      "text":"我爱你中国"
    }
    
    用kibana请求

    IK分词器是一款国人开发的相对简单的中文分词器。首先我们访问 https://github.com/medcl/elasticsearch-analysis-ik/releases 下载与ES对应版本的中文分词器。将解压后的后的文件夹放入ES根目录下的plugins/ik目录下(ik目录要手动创建),重启ES即可使用。

    IK下载 ik目录

    IK提供了两个分词算法ik_smart和ik_max_word。其中ik_smart为最少切分;ik_max_word为最细粒度划分。

    • ik_max_word:会将文本做最细粒度的拆分,例如「我是程序员」会被拆分为「我、是、程序员、程序、员」。
    • ik_smart:会将文本做最少切分,例如「我是程序员」会被拆分为「我、是、程序员」
    GET _analyze
    {
      "analyzer":"ik_smart",
      "text":"我爱你中国"
    }
    
    我爱你中国
    GET _analyze
    {
      "analyzer":"ik_smart",
      "text":"我是程序员"
    }
    
    我是程序员

    四、Spring Data Elasticsearch

    4.1 Spring Data Elasticsearch简介

    Spring Data Elasticsearch是Spring Data项目下的一个子模块。查看 Spring Data的官网:http://projects.spring.io/spring-data/

    Spring Data 是的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如MySQL),还是非关系数据库(如Redis),或者类似Elasticsearch这样的索引数据库。从而简化开发人员的代码,提高开发效率。

    Spring Data
    4.2 Spring Data Elasticsearch注解

    Spring Data Elasticsearch通过注解来声明字段的映射属性,有下面的三个注解

    @Document

    @Document作用在类,标记实体类为文档对象,一般有四个属性

    • indexName:对应索引库名称,mysql中数据库的概念
    • type:对应在索引库中的类型,mysql中表的概念,新版已经没有该属性了
    • shards:分片数量,默认5
    • replicas:副本数量,默认1
    @Id

    @Id 作用在成员变量,标记一个字段作为id主键

    @Field

    @Field作用在成员变量,标记为文档的字段,并指定字段映射属性:

    • type:字段类型,是枚举:FieldType,可以是text、long、short、date、integer、object等
    • text:存储数据时候,会自动分词,并生成索引
    • keyword:存储数据时候,不会分词建立索引
    • Numerical:数值类型,分两类
      基本数据类型:long、interger、short、byte、double、float、half_float
      浮点数的高精度类型:scaled_float
      需要指定一个精度因子,比如10或100。elasticsearch会把真实值乘以这个因子后存储,取出时再还原。
    • Date:日期类型,elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间。
    • index:是否建立倒排索引,布尔类型,默认是true
    • store:是否存储,布尔类型,默认是false
    • analyzer:分词器名称,这里的ik_max_word即使用ik分词器

    五、代码实现

    5.1 创建工程[es-product]
    新建项目 项目名称 选择Spring Web 选择Spring Data Elasticsearch
    5.2 配置pom.xml

    导入lombok、mybatis-plus、mysql依赖包,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 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.4.5</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.es</groupId>
        <artifactId>product</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>es-product</name>
        <description>Elasticsearch实现商品搜索</description>
        <properties>
            <java.version>1.8</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
            </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>
    
            <!-- 导入 -->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.10</version>
                <scope>provided</scope>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.2.0</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.17</version>
            </dependency>
    
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    5.3 创建数据和数据表

    1、创建数据

    -- 创建数据库
    create database es_mall_db  character set utf8mb4;
    
    --  创建用户
    create user 'es_mall_u '@'%' identified by 'es_mall_PWD_123';
    
    -- 授权用户
    grant all privileges on seata_db.* to 'es_mall_u'@'%';
    
    -- 刷新
    flush privileges;
    

    2、创建数据表

    CREATE TABLE `brand` (
      `id` char(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'ID',
      `brand_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '品牌名称',
      `deleted` tinyint(1) DEFAULT '0' COMMENT '0正常 1删除',
      PRIMARY KEY (`brand_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='品牌'
    
    CREATE TABLE `category` (
      `id` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ID',
      `category_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '分类名称',
      `deleted` tinyint(1) DEFAULT '0' COMMENT '0正常 1删除',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品分类'
    
    CREATE TABLE `merchant` (
      `id` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ID',
      `merchant_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '店铺名称',
      `address` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
      `phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系电话',
      `star` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '星级',
      `deleted` tinyint(1) DEFAULT '0' COMMENT '0正常 1删除',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='店铺'
    
    CREATE TABLE `product` (
      `id` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ID',
      `merchant_id` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '店铺ID',
      `brand_id` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '品牌ID',
      `category_id` char(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商品分类ID',
      `product_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商品名称',
      `product_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '商品编号(barcode)',
      `images` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图片',
      `original_price` double DEFAULT '0' COMMENT '商品原价',
      `discount_price` double DEFAULT '0' COMMENT '商品折扣价',
      `stock` int(11) DEFAULT '0' COMMENT '库存',
      `sales_volume` int(11) DEFAULT '0' COMMENT '销量',
      `views` int(11) DEFAULT '0' COMMENT '浏览量',
      `synopsis` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '简介',
      `introduction` mediumtext COLLATE utf8mb4_unicode_ci COMMENT '详细介绍',
      `publish_state` tinyint(2) DEFAULT '0' COMMENT '发布状态(0:否;1:是)',
      `deleted` tinyint(1) DEFAULT '0' COMMENT '0正常 1删除',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品'
    

    3、插入测试数据

    insert into brand (id,brand_name) values (REPLACE(UUID(),"-",""),"小米");
    insert into brand (id,brand_name) values (REPLACE(UUID(),"-",""),"华为");
    insert into brand (id,brand_name) values (REPLACE(UUID(),"-",""),"Apple");
    
    insert into category (id,category_name) values (REPLACE(UUID(),"-",""),"手机");
    insert into category (id,category_name) values (REPLACE(UUID(),"-",""),"电脑");
    
    insert into merchant (id,merchant_name,address,star) values (REPLACE(UUID(),"-",""),"华为专卖店","深圳",'5');
    insert into merchant (id,merchant_name,address,star) values (REPLACE(UUID(),"-",""),"AC电子商城","珠海",'5');
    
    insert into product (id,merchant_id,brand_id,category_id,product_name,product_no,original_price,discount_price,stock,synopsis,publish_state) 
    values (REPLACE(UUID(),"-",""),"6022b9dbb3ce11ebaf7000163e066303","968a74aab3cd11ebaf7000163e066303",'bb592145b3cd11ebaf7000163e066303','Apple iPhone 12 (A2404) 128GB 白色','A0001',6799,5800,100,'支持移动联通电信5G 双卡双待手机',1);
    
    insert into product (id,merchant_id,brand_id,category_id,product_name,product_no,original_price,discount_price,stock,synopsis,publish_state) 
    values (REPLACE(UUID(),"-",""),"6022b9dbb3ce11ebaf7000163e066303","968a74aab3cd11ebaf7000163e066303",'bb592145b3cd11ebaf7000163e066303','Apple iPhone 8 (A2404) 256GB 黑色','B0001',4700,3500,50,'支持移动联通电信4G',1);
    
    insert into product (id,merchant_id,brand_id,category_id,product_name,product_no,original_price,discount_price,stock,synopsis,publish_state) 
    values (REPLACE(UUID(),"-",""),"601ae548b3ce11ebaf7000163e066303","9059788ab3cd11ebaf7000163e066303",'bb592145b3cd11ebaf7000163e066303','华为mate40pro 5G手机 亮黑色 8+256G','C0001',7499,7000,200,'全网通(碎屏险套餐)',1);
    
    
    5.4 配置application.yml
    server:
      port: 8080
    
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://47.105.146.74:3306/es_mall_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =GMT%2B8
        username: es_mall_u
        password: es_mall_PWD_123
    
    5.5 配置Elasticsearch信息

    旧版Elasticsearch配置信息都是在application.yml里配置的,但如果用的是新版Elasticsearch,这样配置会提示已过时,新版我需要用配置类

    已过时
    package com.es.product.confg;
    
    import org.elasticsearch.client.RestHighLevelClient;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.elasticsearch.client.ClientConfiguration;
    import org.springframework.data.elasticsearch.client.RestClients;
    import org.springframework.data.elasticsearch.config.AbstractElasticsearchConfiguration;
    import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
    
    /**
     * @author Alan Chen
     * @description
     * @date 2021/5/12
     */
    @Configuration
    @EnableElasticsearchRepositories
    public class EsRestClientConfig extends AbstractElasticsearchConfiguration {
    
        @Override
        @Bean
        public RestHighLevelClient elasticsearchClient() {
    
            final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                    .connectedTo("localhost:9200")
                    .build();
    
            return RestClients.create(clientConfiguration).rest();
        }
    }
    
    5.6 entity类

    创建entity包,并创建对应的实体类,entity类对应数据表

    /**
     * @author Alan Chen
     * @description 品牌
     * @date 2021/5/13
     */
    @Data
    public class Brand {
    
        @TableId(type = IdType.UUID)
        private String id;
    
        //品牌名称
        private String brandName;
    
        //逻辑删除标志 0:未删除;1:已删除
        @TableLogic
        private Integer deleted;
    }
    
    /**
     * @author Alan Chen
     * @description 商品分类
     * @date 2021/5/13
     */
    @Data
    public class Category {
    
        @TableId(type = IdType.UUID)
        private String id;
    
        //分类名称
        private String categoryName;
    
        //逻辑删除标志 0:未删除;1:已删除
        @TableLogic
        private Integer deleted;
    }
    
    /**
     * @author Alan Chen
     * @description 店铺
     * @date 2021/5/13
     */
    @Data
    public class Merchant {
    
        @TableId(type = IdType.UUID)
        private String id;
    
        //店铺名称
        private String merchantName;
    
        //地址
        private String address;
    
        //联系电话
        private String phone;
    
        //星级
        private String star;
    
        //逻辑删除标志 0:未删除;1:已删除
        @TableLogic
        private Integer deleted;
    }
    
    /**
     * @author Alan Chen
     * @description 商品
     * @date 2020-01-04
     */
    @Data
    public class Product{
    
        @TableId(type = IdType.UUID)
        private String id;
    
        //店铺ID
        private String merchantId;
    
        //品牌ID
        private String brandId;
    
        //商品分类ID
        private String categoryId;
    
        //商品名称
        private String productName;
    
        //商品编号(barcode)
        private String productNo;
    
        //图片
        private String images;
    
        //商品原价
        private double originalPrice;
    
        //商品折扣价
        private double discountPrice;
    
        //库存
        private Integer stock;
    
        //销量
        private Integer salesVolume;
    
        //浏览量
        private Integer views;
    
        //简介
        private String synopsis;
    
        //详细介绍
        private String introduction;
    
        //发布状态
        private int publishState;
    
        //逻辑删除标志 0:未删除;1:已删除
        @TableLogic
        private Integer deleted;
    }
    
    5.7 es模块

    新建es包,将Elasticsearch相关的类都放es包里,并在es创建dao、document、repository、service包

    EsProduct 跨表组装需要的数据,存入ES里

    package com.es.product.es.document;
    
    import lombok.Data;
    import org.springframework.data.annotation.Id;
    import org.springframework.data.elasticsearch.annotations.Document;
    import org.springframework.data.elasticsearch.annotations.Field;
    import org.springframework.data.elasticsearch.annotations.FieldType;
    
    /**
     * @author Alan Chen
     * @description ES类-商品
     * @date 2021/5/13
     */
    @Data
    @Document(indexName = "es_product",shards = 1, replicas = 0)
    public class EsProduct {
    
        @Id
        private String id;
    
        //店铺名称
        @Field(type = FieldType.Keyword)
        private String merchantName;
    
        //品牌名称
        @Field(type = FieldType.Keyword)
        private String brandName;
    
        //商品分类名称
        @Field(type = FieldType.Keyword)
        private String categoryName;
    
        //商品名称
        @Field(type = FieldType.Text, analyzer = "ik_max_word")
        private String productName;
    
        //商品编号(barcode)
        @Field(type = FieldType.Keyword)
        private String productNo;
    
        //图片
        @Field(type = FieldType.Keyword)
        private String images;
    
        //商品原价
        @Field(type = FieldType.Double)
        private double originalPrice;
    
        //商品折扣价
        @Field(type = FieldType.Double)
        private double discountPrice;
    
        //库存
        @Field(type = FieldType.Integer)
        private Integer quantity;
    
        //销量
        @Field(type = FieldType.Integer)
        private Integer salesVolume;
    
        //简介
        @Field(type = FieldType.Text, analyzer = "ik_max_word")
        private String synopsis;
    }
    

    EsProductDao 继承mybatis的BaseMapper 类,查询商品数据

    package com.es.product.es.dao;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.es.product.es.document.EsProduct;
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    /**
     * @author Alan Chen
     * @description
     * @date 2021/5/13
     */
    @Repository
    @Mapper
    public interface EsProductDao extends BaseMapper {
    
        /**
         * 获取所有商品数据
         * @return
         */
        List<EsProduct> listAll();
    
        /**
         * 获取信息
         * @param id
         * @return
         */
        EsProduct selectById(String id);
    }
    

    EsProductRepository继承SpringBoot Data Elasticsearch的ElasticsearchRepository,操作ES

    package com.es.product.es.repository;
    
    import com.es.product.es.document.EsProduct;
    import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
    
    import java.util.List;
    
    /**
     * @author Alan Chen
     * @description
     * @date 2021/5/13
     */
    public interface EsProductRepository extends ElasticsearchRepository<EsProduct,String> {
    
        /**
         * 根据价格区间查询(自定义方法-名称约定)
         * @param price1
         * @param price2
         * @return
         */
        List<EsProduct> findByOriginalPriceBetween(double price1, double price2);
    }
    
    
    package com.es.product.es.service;
    
    import com.es.product.es.document.EsProduct;
    import org.springframework.data.domain.Page;
    
    import java.util.List;
    
    /**
     * @author Alan Chen
     * @description
     * @date 2021/5/13
     */
    public interface IEsProductService {
    
        /**
         * 从数据库中导入商品到ES
         * @return
         */
        void importAll();
    
        /**
         * 新增/修改(id存在就是修改,否则就是插入)
         * @param id
         */
        void save(String id);
    
        /**
         * 根据id删除商品
         * @param id
         */
        void delete(String id);
    
        /**
         * 根据id获得商品
         * @param id
         * @return
         */
        EsProduct get(String id);
    
        /**
         * 批量删除
         * @param ids
         */
        void deletes(List<String> ids);
    
        /**
         * 所有商品
         * @return
         */
        List<EsProduct> listAll();
    
        /**
         * 根据价格区间查询(自定义方法)
         * @param price1
         * @param price2
         * @return
         */
        List<EsProduct> queryByPriceBetween(double price1, double price2);
    
        /**
         * 名称查询(高级查询)
         * @param productName
         * @return
         */
        Page<EsProduct> query(String productName);
    
        /**
         * 分页查询
         * @param categoryName
         * @param page
         * @param size
         * @return
         */
        Page<EsProduct> pageSearch(String categoryName,int page ,int size);
    
        /**
         * 分页查询(排序)
         * @param categoryName
         * @param page
         * @param size
         * @return
         */
        Page<EsProduct> pageSortSearch(String categoryName,int page ,int size);
    }
    
    
    package com.es.product.es.service.impl;
    
    import com.es.product.es.dao.EsProductDao;
    import com.es.product.es.document.EsProduct;
    import com.es.product.es.repository.EsProductRepository;
    import com.es.product.es.service.IEsProductService;
    import org.elasticsearch.index.query.QueryBuilders;
    import org.elasticsearch.search.sort.SortBuilders;
    import org.elasticsearch.search.sort.SortOrder;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.PageRequest;
    import org.springframework.data.domain.Sort;
    import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author Alan Chen
     * @description
     * @date 2021/5/13
     */
    @Service
    public class EsProductServiceImpl implements IEsProductService {
    
        @Autowired
        EsProductDao esProductDao;
    
        @Autowired
        EsProductRepository esProductRepository;
    
        /**
         * 从数据库中导入商品到ES
         * @return
         */
        @Override
        public void importAll() {
            List<EsProduct> list = esProductDao.listAll();
            esProductRepository.saveAll(list);
        }
    
        /**
         * 新增/修改(id存在就是修改,否则就是插入)
         * @param id
         */
        @Override
        public void save(String id) {
            EsProduct esProduct = esProductDao.selectById(id);
            esProductRepository.save(esProduct);
        }
    
        /**
         * 根据id删除商品
         * @param id
         */
        @Override
        public void delete(String id) {
            esProductRepository.deleteById(id);
        }
    
        /**
         * 根据id获得商品
         * @param id
         * @return
         */
        @Override
        public EsProduct get(String id) {
            return esProductDao.selectById(id);
        }
    
        /**
         * 批量删除
         * @param ids
         */
        @Override
        public void deletes(List<String> ids) {
            for(String id : ids){
                esProductRepository.deleteById(id);
            }
        }
    
        /**
         * 所有商品(按价格排序)
         * @return
         */
        @Override
        public List<EsProduct> listAll() {
            Iterable<EsProduct> items = this.esProductRepository.findAll(Sort.by(Sort.Direction.DESC, "discountPrice"));
    
            List<EsProduct> list = new ArrayList<>();
            items.forEach(esProduct -> list.add(esProduct));
    
            return list;
        }
    
        /**
         * 根据价格区间查询(自定义方法)
         * @param price1
         * @param price2
         * @return
         */
        @Override
        public List<EsProduct> queryByPriceBetween(double price1, double price2) {
            return esProductRepository.findByOriginalPriceBetween(price1,price2);
        }
    
        /**
         * 名称查询(高级查询)
         * @param productName
         * @return
         */
        @Override
        public Page<EsProduct> query(String productName) {
            // 构建查询条件
            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            // 添加基本的分词查询
            queryBuilder.withQuery(QueryBuilders.matchQuery("productName", productName));
            // 执行搜索,获取结果
            Page<EsProduct> esProducts = esProductRepository.search(queryBuilder.build());
    
            return esProducts;
        }
    
        /**
         * 分页查询
         * @param categoryName
         * @param page
         * @param size
         * @return
         */
        @Override
        public Page<EsProduct> pageSearch(String categoryName, int page, int size) {
            // 构建查询条件
            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            // 添加基本的分词查询
            queryBuilder.withQuery(QueryBuilders.termQuery("categoryName", categoryName));
    
            // 设置分页参数
            queryBuilder.withPageable(PageRequest.of(page, size));
    
            // 执行搜索,获取结果
            Page<EsProduct> esProducts = esProductRepository.search(queryBuilder.build());
    
            return esProducts;
        }
    
        /**
         * 分页查询(排序)
         * @param categoryName
         * @param page
         * @param size
         * @return
         */
        @Override
        public Page<EsProduct> pageSortSearch(String categoryName, int page, int size) {
            // 构建查询条件
            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            // 添加基本的分词查询
            queryBuilder.withQuery(QueryBuilders.termQuery("categoryName", categoryName));
    
            // 设置分页参数
            queryBuilder.withPageable(PageRequest.of(page, size));
    
            // 排序
            queryBuilder.withSort(SortBuilders.fieldSort("originalPrice").order(SortOrder.DESC));
    
            // 执行搜索,获取结果
            Page<EsProduct> esProducts = esProductRepository.search(queryBuilder.build());
    
            return esProducts;
        }
    
    }
    
    5.8 mapper文件
    <?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.es.product.es.dao.EsProductDao">
    
        <select id="listAll" resultType="com.es.product.es.document.EsProduct">
            select
                t.id,
                t2.brand_name,
                t3.category_name,
                t4.merchant_name,
                t.product_name,
                t.product_no,
                t.images,
                t.original_price,
                t.discount_price,
                t.stock,
                t.sales_volume,
                t.synopsis
            from product t
            left join brand t2 on t.brand_id = t2.id
            left join category t3 on t.category_id = t3.id
            left join merchant t4 on t.merchant_id = t4.id
            <where>
                t.deleted = 0
                and t2.deleted = 0
                and t3.deleted = 0
                and t4.deleted = 0
            </where>
        </select>
    
        <select id="selectById" resultType="com.es.product.es.document.EsProduct">
            select
                t.id,
                t2.brand_name,
                t3.category_name,
                t4.merchant_name,
                t.product_name,
                t.product_no,
                t.images,
                t.original_price,
                t.discount_price,
                t.stock,
                t.sales_volume,
                t.synopsis
            from product t
            left join brand t2 on t.brand_id = t2.id
            left join category t3 on t.category_id = t3.id
            left join merchant t4 on t.merchant_id = t4.id
            <where>
                t.deleted = 0
                and t.id = #{id}
            </where>
        </select>
    
    </mapper>
    
    5.9 单元测试类
    package com.es.product;
    
    import com.es.product.es.document.EsProduct;
    import com.es.product.es.service.IEsProductService;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.domain.Page;
    
    import java.util.List;
    
    @SpringBootTest
    class EsProductApplicationTests {
    
        @Autowired
        private IEsProductService esProductService;
    
        /**
         * 从数据库中导入商品到ES
         * @return
         */
        @Test
        public void importAll(){
            esProductService.importAll();
        }
    
        /**
         * 新增/修改(id存在就是修改,否则就是插入)
         */
        @Test
        public void save(){
            String id = "2e01a7b7b3cf11ebaf7000163e066303";
            esProductService.save(id);
        }
    
        /**
         * 根据id删除商品
         */
        @Test
        public void delete(){
            String id = "2e01a7b7b3cf11ebaf7000163e066303";
            esProductService.delete(id);
        }
    
        /**
         * 根据id获得商品
         * @return
         */
        @Test
        public void get(){
            String id = "2e01a7b7b3cf11ebaf7000163e066303";
            EsProduct esProduct = esProductService.get(id);
            System.out.println(esProduct);
        }
    
        /**
         * 批量删除
         */
        @Test
        public void deletes(){
    
        }
    
        /**
         * 所有商品(按价格排序)
         * @return
         */
        @Test
        public void listAll(){
            List<EsProduct> list = esProductService.listAll();
            System.out.println(list);
        }
    
        /**
         * 根据价格区间查询(自定义方法)
         * @return
         */
        @Test
        public void queryByPriceBetween(){
            List<EsProduct> list = esProductService.queryByPriceBetween(4000,5000);
            System.out.println(list);
        }
    
        /**
         * 名称查询
         * @return
         */
        @Test
        public void query(){
            String productName = "Apple";
            Page<EsProduct> page = esProductService.query(productName);
    
            // 打印总条数
            System.out.println(page.getTotalElements());
            // 打印总页数
            System.out.println(page.getTotalPages());
            page.forEach(System.out::println);
        }
    
        /**
         * 分页查询
         * @return
         */
        @Test
        public void pageSearch(){
            String categoryName = "手机";
            int page = 1;
            int size =2;
    
            Page<EsProduct> esProducts = esProductService.pageSearch(categoryName,page,size);
    
            // 打印总条数
            System.out.println(esProducts.getTotalElements());
            // 打印总页数
            System.out.println(esProducts.getTotalPages());
            // 每页大小
            System.out.println(esProducts.getSize());
            // 当前页
            System.out.println(esProducts.getNumber());
            esProducts.forEach(System.out::println);
        }
    
        /**
         * 分页查询(排序)
         * @return
         */
        @Test
        public void pageSortSearch(){
            String categoryName = "手机";
            int page = 1;
            int size =2;
    
            Page<EsProduct> esProducts = esProductService.pageSortSearch(categoryName,page,size);
    
            // 打印总条数
            System.out.println(esProducts.getTotalElements());
            // 打印总页数
            System.out.println(esProducts.getTotalPages());
            // 每页大小
            System.out.println(esProducts.getSize());
            // 当前页
            System.out.println(esProducts.getNumber());
            esProducts.forEach(System.out::println);
        }
    
    }
    
    项目截图

    六、测试接口详解

    6.1 将商品数据全部导入ES
        @Override
        public void importAll() {
            List<EsProduct> list = esProductDao.listAll();
            esProductRepository.saveAll(list);
        }
    
    6.2 新增/修改(id存在就是修改,否则就是插入)
    /**
         * 新增/修改(id存在就是修改,否则就是插入)
         * @param id
         */
        @Override
        public void save(String id) {
            EsProduct esProduct = esProductDao.selectById(id);
            esProductRepository.save(esProduct);
        }
    

    当业务模块对某一商品进行了更新(如改了商品名称、商品价格、销售后库存减少等)就可以调用该接口更新ES里的商品信息。

    6.3 根据id删除商品
    /**
         * 根据id删除商品
         * @param id
         */
        @Override
        public void delete(String id) {
            esProductRepository.deleteById(id);
        }
    

    商品删除或下架后,可以调用该接口更新ES里的商品信息。

    6.4 所有商品(按价格排序)
    /**
         * 所有商品(按价格排序)
         * @return
         */
        @Override
        public List<EsProduct> listAll() {
            Iterable<EsProduct> items = this.esProductRepository.findAll(Sort.by(Sort.Direction.DESC, "discountPrice"));
    
            List<EsProduct> list = new ArrayList<>();
            items.forEach(esProduct -> list.add(esProduct));
    
            return list;
        }
    
    6.5 根据价格区间查询(自定义方法)

    Spring Data 的另一个强大功能,是根据方法名称自动实现功能。比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。当然,方法名称要符合一定的约定:


    image.png

    我们来按照价格区间查询,定义这样的一个方法

    /**
         * 根据价格区间查询(自定义方法)
         * @param price1
         * @param price2
         * @return
         */
        @Override
        public List<EsProduct> queryByPriceBetween(double price1, double price2) {
            return esProductRepository.findByOriginalPriceBetween(price1,price2);
        }
    
    6.6 名称查询(高级查询)

    虽然基本查询和自定义方法已经很强大了,但是如果是复杂查询(模糊、通配符、词条查询等)就显得力不从心了。此时,我们只能使用原生查询。

    /**
         * 名称查询(高级查询)
         * @param productName
         * @return
         */
        @Override
        public Page<EsProduct> query(String productName) {
            // 构建查询条件
            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            // 添加基本的分词查询
            queryBuilder.withQuery(QueryBuilders.matchQuery("productName", productName));
            // 执行搜索,获取结果
            Page<EsProduct> esProducts = esProductRepository.search(queryBuilder.build());
    
            return esProducts;
        }
    
    6.7 分页查询
    /**
         * 分页查询
         * @param categoryName
         * @param page
         * @param size
         * @return
         */
        @Override
        public Page<EsProduct> pageSearch(String categoryName, int page, int size) {
            // 构建查询条件
            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            // 添加基本的分词查询
            queryBuilder.withQuery(QueryBuilders.termQuery("categoryName", categoryName));
    
            // 设置分页参数
            queryBuilder.withPageable(PageRequest.of(page, size));
    
            // 执行搜索,获取结果
            Page<EsProduct> esProducts = esProductRepository.search(queryBuilder.build());
    
            return esProducts;
        }
    
    6.8 分页查询(排序)
    /**
         * 分页查询(排序)
         * @param categoryName
         * @param page
         * @param size
         * @return
         */
        @Override
        public Page<EsProduct> pageSortSearch(String categoryName, int page, int size) {
            // 构建查询条件
            NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
            // 添加基本的分词查询
            queryBuilder.withQuery(QueryBuilders.termQuery("categoryName", categoryName));
    
            // 设置分页参数
            queryBuilder.withPageable(PageRequest.of(page, size));
    
            // 排序
            queryBuilder.withSort(SortBuilders.fieldSort("originalPrice").order(SortOrder.DESC));
    
            // 执行搜索,获取结果
            Page<EsProduct> esProducts = esProductRepository.search(queryBuilder.build());
    
            return esProducts;
        }
    

    七、补充

    7. 1 新版@Document 无type属性

    在新版ES里,@Document注解已经没有type属性了,因此我的EsProduct没有配置type

    @Document(indexName = "es_product",shards = 1, replicas = 0)
    public class EsProduct {}
    
    7.2 新版ES删除索引文档

    以前旧版ES删除索引文档是这样的,通过ID删除索引文档

    DELETE /es_product/_doc/z8qEEHIBZBLFtWo4JEtR
    

    现在新版ES不需要写文档类型_doc了

    DELETE /es_product
    
    删除索引文档

    八、Elasticsearch与Mysql数据同步问题

    Mysql数据同步到ES中分为两种,分别是全量同步和增量同步。全量同步表示第一次建立好ES索引之后,将Mysql中所有数据一次性导入到ES中。增量同步表示Mysql中产生新的数据,这些新的数据包括三种情况,就是新插入Mysql中的数据,更新老的数据,删除的数据,这些数据的变动与新增都要同步到ES中,Elasticsearch与Mysql数据同步有三种方式,分别是:

    1、通过Elasticsearch提供的API进行增删改查。
    2、通过收集日志进行同步,如ES官方数据收集和同步组件logstash。
    3、通过中间件进行数据全量、增量的数据同步,如阿里巴巴canal、go-mysql-elasticsearch等,二者都是基于Mysql的binlog订阅。

    源码地址

    es-product源码

    相关文章

      网友评论

        本文标题:Elasticsearch实现商品搜索

        本文链接:https://www.haomeiwen.com/subject/brhndltx.html