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: 8080spring: 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@EnableElasticsearchRepositoriespublic 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 */@Datapublic 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 */@Datapublic 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 */@Datapublic 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 */@Datapublic 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@Mapperpublic 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 */@Servicepublic 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;@SpringBootTestclass 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订阅。
作者:陈琰AC
链接:https://www.jianshu.com/p/8362de6dd57b