SpringBoot整合Elasticsearch实现商品搜索

准备

如果您对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源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容