Spring Data 框架集成 Elasticsearch
Spring Data 框架介绍
Spring Data 是一个用于简化数据库、非关系型数据库、索引库访问,并支持云服务的开源框架。
其主要目标是使得对数据的访问变得方便快捷,并支持 map-reduce 框架和云计算数据服务。
Spring Data 是的使命是给各种数据访问提供统一的编程接口,不管是关系型数据库(如 MySQL),还是非关系数据库(如 Redis),或者类似 Elasticsearch 这样的索引数据库。从而简化开发人员的代码,提高开发效率。
Spring Data 的官网:https://spring.io/projects/spring-data
Spring Data 常用的功能模块如下:
![](6-框架集成 Elasticsearch/9d897933d7d057dc52487ef6f6d1bee7.png)
Spring Data Elasticsearch 介绍
- Spring Data Elasticsearch 基于 spring data API 简化 Elasticsearch 操作,将原始操作 Elasticsearch 的客户端 API 进行封装
- 官方网站:https://spring.io/projects/spring-data-elasticsearch
特征
- 支持 Spring 的基于@Configuration 的 java 配置方式,或者 XML 配置方式
- 提供了用于操作 ES 的便捷工具类 ElasticsearchTemplate。包括实现文档到 POJO 之间的自动智能映射。
- 利用 Spring 的数据转换服务实现的功能丰富的对象映射
- 基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
- 根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似 mybatis,根据接口自动得到实现)。当然,也支持人工定制查询
版本对比
Elasticsearch 和 SpringBoot 之间版本关系要适配,否则 springboot 找不到 elasticsearch 的节点
Spring boot2.3.x 一般可以兼容 Elasticsearch7.x
既然我们要用到 Elasticsearch
7.x
版本,如何选择合适的版本,这里有个小技巧分享给大家。首先我们可以在 pom.xml 中修改 SpringBoot 依赖的版本为 2.3.11;
然后在项目的
External Libraries
中搜索elasticsearch
,可以发现elasticsearch-7.6.2.jar
这个依赖;然后打开其中的
MANIFEST.MF
文件,通过 jar 包中的X-Compile-Elasticsearch-Version
属性,我们可以找到兼容的 Elasticsearch 版本号为 7.6.2;
![](6-框架集成 Elasticsearch/4fc41bbad52fe724c3563f7a3152e604.png)
如果使用最新 springboot 或 Elasticsearch,我们排除 spring 对 es 的依赖后,手动引入期望的 es 依赖,通过自己编写 Configuration 类和配置项来得到 HighLevelRestClient 这个 bean, 后续使用的时候直接注入即可
框架集成
创建项目
创建 Maven 项目 es-springdata
修改 pom 文件,增加依赖关系
- 目前最新 springboot 对应 Elasticsearch7.6.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring-boot.version>2.3.11.RELEASE</spring-boot.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>
<exclusions>
<!-- 删除 JUnit 4 的测试引擎-->
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!--防止将此依赖传递到其它模块中:当其它项目通过 pom 引入该项目时,就不会将该项目中的这个依赖被传递依赖引入进去
当你依赖某各工程很庞大或很可能与其他工程的 jar 包冲突的时候建议加上该选项,可以节省开销,同时减少依赖冲突-->
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<!-- 防止将此依赖传递到其它模块中-->
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>SpringBoot 主程序
1
2
3
4# es 服务地址 elasticsearch.host=127.0.0.1 # es 服务端口 elasticsearch.port=9200
# 配置日志级别,开启 debug 日志
logging.level.pers.fulsun.es=debug数据实体类
1
2
3
4
5
6
7
8
9
10
11
12
public class Product {
private Long id; // 商品唯一标识
private String title; // 商品名称
private String category; // 分类名称
private Double price; // 商品价格
private String images; // 图片地址
}
配置文件
增加配置文件:在 resources 目录中增加 application.yml 文件
1
2
3
4
5
6
7spring:
data:
elasticsearch:
# es 的集群名称
cluster-name: myes
# es 的连接地址及端口号
cluster-nodes: zy01:9001,zy02:9002,zy03:9003上面配置 Elasticsearch 访问路径和集群名称的配置已经不建议使用了;取而代之的是直接配置 Elasticsearch 的 rest 访问地址
1
2
3
4spring:
elasticsearch:
rest:
uris: http://localhost:9200
配置类
ElasticsearchRestTemplate 是 spring-data-elasticsearch 项目中的一个类,和其他 spring 项目中的 template 类似。
在新版的 spring-data-elasticsearch 中,ElasticsearchRestTemplate 代替了原来的 ElasticsearchTemplate。
原因是 ElasticsearchTemplate 基于 TransportClient,TransportClient 即将在 8.x 以后的版本中移除。所以,我们推荐使用 ElasticsearchRestTemplate。
ElasticsearchRestTemplate 基于 RestHighLevelClient 客户端的。
自定义配置类,继承 AbstractElasticsearchConfiguration,并实现 elasticsearchClient() 抽象方法,创建 RestHighLevelClient 对象。
1
2
3
4
5
6elasticsearch:
# es 服务地址
host: 127.0.0.1
# es 服务端口
port: 9200
# cluster-nodes: zy01:9001,zy02:9002,zy03:90031
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
private String host;
private Integer port;
// 重写父类方法
public RestHighLevelClient elasticsearchClient() {
RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
return new RestHighLevelClient(builder);
}
}
属性映射字段
Spring Data 通过注解来声明字段的映射属性,有下面的三个注解:
@Document
作用在类,标记实体类为文档对象,一般有两个属性- indexName:对应索引库名称
- type:对应在索引库中的类型
- shards:分片数量,默认 5
- replicas:副本数量,默认 1
@Id
作用在成员变量,标记一个字段作为 id 主键@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 分词器
- type:字段类型,是枚举:FieldType,可以是 text、long、short、date、integer、object 等
实体类映射操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Product {
// 必须有 id, 这里的 id 是全局唯一的标识,等同于 es 中的"_id"
private Long id; // 商品唯一标识
private String title; // 商品名称
private String category; // 分类名称
private Double price; // 商品价格
private String images; // 图片地址
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43# 对应的映射为
{
"mappings": {
"_doc": {
"properties": {
"images": {
"index": false,
"type": "keyword"
},
"price": {
"type": "double"
},
"_class": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"id": {
"type": "long"
},
"category": {
"type": "keyword"
},
"title": {
"analyzer": "ik_max_word",
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
},
"inner": {
"type": "keyword"
}
}
}
}
}
}
}
DAO 访问
DAO 数据访问对象
1
2
3
4
public interface ProductDao extends ElasticsearchRepository<Product,Long> {
}
elasticsearchRestTemplate
索引操作
1 | // 索引创建 |
文档操作
1 | // 判断文档是否存在 |
ElasticsearchRepository
1 | // |
构建方法关键字的使用
关键字 使用示例 等同于的 ES 查询 And findByNameAndPrice {“bool” : {“must” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} Or findByNameOrPrice {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}} Is findByName {“bool” : {“must” : {“field” : {“name” : “?”}}}} Not findByNameNot {“bool” : {“must_not” : {“field” : {“name” : “?”}}}} Between findByPriceBetween {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : ?,”include_lower” : true,”include_upper” : true}}}}} LessThanEqual findByPriceLessThan {“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}} GreaterThanEqual findByPriceGreaterThan {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}} Before findByPriceBefore {“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}} After findByPriceAfter {“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}} Like findByNameLike {“bool” : {“must” : {“field” : {“name” : {“query” : “? *”,”analyze_wildcard” : true}}}}} StartingWith findByNameStartingWith {“bool” : {“must” : {“field” : {“name” : {“query” : “? *”,”analyze_wildcard” : true}}}}} EndingWith findByNameEndingWith {“bool” : {“must” : {“field” : {“name” : {“query” : “*?”,”analyze_wildcard” : true}}}}} Contains/Containing findByNameContaining {“bool” : {“must” : {“field” : {“name” : {“query” : “?”,”analyze_wildcard” : true}}}}} In findByNameIn(Collectionnames) {“bool” : {“must” : {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“name” : “?”}} ]}}}} NotIn findByNameNotIn(Collectionnames) {“bool” : {“must_not” : {“bool” : {“should” : {“field” : {“name” : “?”}}}}}} True findByAvailableTrue {“bool” : {“must” : {“field” : {“available” : true}}}} False findByAvailableFalse {“bool” : {“must” : {“field” : {“available” : false}}}} OrderBy findByAvailableTrueOrderByNameDesc {“sort” : [{ “name” : {“order” : “desc”} }],”bool” : {“must” : {“field” : {“available” : true}}}}
使用
索引操作
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package pers.fulsun.es_springdata;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import pers.fulsun.es_springdata.entity.Product;
public class SpringDataESIndexTest {
// 注入 ElasticsearchRestTemplate
private ElasticsearchRestTemplate elasticsearchRestTemplate;
/** 创建索引并增加映射配置 */
// @Disabled
public void createIndex() {
// 创建索引,系统初始化会自动创建索引
System.out.println("创建文档索引");
boolean isCreate = elasticsearchRestTemplate.indexOps(IndexCoordinates.of("template")).create();
System.out.println("创建 template 索引" + isCreate);
}
public void getIndex() {
// 获取索引
boolean exists = elasticsearchRestTemplate.indexOps(IndexCoordinates.of("template")).exists();
System.out.println("template 索引是否存在 = " + exists);
}
public void deleteIndex() {
// 创建索引,系统初始化会自动创建索引
// boolean flg = elasticsearchRestTemplate.deleteIndex(Product.class);
boolean flg = elasticsearchRestTemplate.indexOps(Product.class).delete();
System.out.println("删除索引 = " + flg);
}
}
文档操作
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class SpringDataESProductDaoTest {
private ProductDao productDao;
/** 新增 */
public void save() {
Product product = new Product();
product.setId(1L);
product.setTitle(" 华 为 手 机 ");
product.setCategory(" 手 机 ");
product.setPrice(2999.0);
product.setImages("http://www.shouji.com/hw.jpg");
productDao.save(product);
}
// 修改:不存在会插入
public void update() {
Product product = new Product();
product.setId(1L);
product.setTitle("小米 2 手机");
product.setCategory(" 手 机 ");
product.setPrice(9999.0);
product.setImages("http://www.shouji.com/xm.jpg");
productDao.save(product);
}
// 根据 id 查询
public void findById() {
Product product = productDao.findById(1L).get();
System.out.println(product);
}
// 查询所有
public void findAll() {
Iterable<Product> products = productDao.findAll();
for (Product product : products) {
System.out.println(product);
}
}
// 删除
public void delete() {
Product product = new Product();
product.setId(1L);
productDao.delete(product);
}
// 批量新增
public void saveAll() {
List<Product> productList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Product product = new Product();
product.setId(Long.valueOf(i));
product.setTitle("[" + i + "] 小 米 手 机 ");
product.setCategory(" 手 机 ");
product.setPrice(1999.0 + i);
product.setImages("http://www.shouji.com/xm.jpg");
productList.add(product);
}
productDao.saveAll(productList);
}
// 分页查询
public void findByPageable() {
// 设置排序(排序方式,正序还是倒序,排序的 id)
Sort sort = Sort.by(Direction.ASC, "id");
int currentPage = 0; // 当前页,第一页从 0 开始,1 表示第二页
int pageSize = 5; // 每页显示多少条
// 设置查询分页
PageRequest pageRequest = PageRequest.of(currentPage, pageSize, sort);
// 分页查询
Page<Product> productPage = productDao.findAll(pageRequest);
for (Product Product : productPage.getContent()) {
System.out.println(Product);
}
}
}
文档搜索
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class SpringDataESSearchTest {
private ProductDao productDao;
/** term 查询 search(termQueryBuilder) 调用搜索方法,参数查询构建器对象 */
public void termQuery() {
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", "小米手机");
Iterable<Product> products = productDao.search(termQueryBuilder);
for (Product product : products) {
System.out.println(product);
}
}
/** term 查询加分页 */
public void termQueryByPage() {
int currentPage = 0;
int pageSize = 5;
// 设置查询分页
PageRequest pageRequest = PageRequest.of(currentPage, pageSize);
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title", "小米");
Iterable<Product> products = productDao.search(termQueryBuilder, pageRequest);
for (Product product : products) {
System.out.println(product);
}
}
}
NativeSearchQuery
我们发现 ElasticsearchRepository 的
search()
方法也过时了,不建议使用了,我们可以改用 ElasticsearchRestTemplate 的 search() 方法来实现,具体实现对比如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class SpringDataESSearchTest {
private ProductDao productDao;
/** term 查询 search(termQueryBuilder) 调用搜索方法,参数查询构建器对象 */
public void termQuery() {
TermQueryBuilder termQuery = QueryBuilders.termQuery("title.keyword", "小米手机");
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
NativeSearchQuery query = builder.withQuery(termQuery).build();
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
List<Product> searchProductList =
searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
for (Product product : searchProductList) {
System.out.println(product);
}
}
/** match 查询+排序+分页 */
public void termQueryByPage() {
int currentPage = 0;
int pageSize = 5;
// 设置查询分页
PageRequest pageRequest = PageRequest.of(currentPage, pageSize);
NativeSearchQuery nativeSearchQuery =
new NativeSearchQueryBuilder()
// 查询条件
.withQuery(QueryBuilders.matchQuery("title","米"))
// 分页
.withPageable(pageRequest)
// 排序
.withSort(SortBuilders.fieldSort("id").order(SortOrder.DESC))
// 高亮字段显示
.withHighlightFields(new HighlightBuilder.Field("title"))
.build();
log.info("\nDSL: {}\n===========================",nativeSearchQuery.getQuery().toString());
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Product.class);
searchHits.stream().forEach(item -> System.out.println(item.toString()));
}
}
=======执行结果====================
DSL: {
"match" : {
"title" : {
"query" : "米",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
}
===========================
SearchHit{id='9', score=NaN, sortValues=[9], content=Product(id=9, title=[9] 小 米 手 机 , category= 手 机 , price=2008.0, images=http://www.shouji.com/xm.jpg), highlightFields={title=[[9] 小 <em>米</em> 手 机]}}
SearchHit{id='8', score=NaN, sortValues=[8], content=Product(id=8, title=[8] 小 米 手 机 , category= 手 机 , price=2007.0, images=http://www.shouji.com/xm.jpg), highlightFields={title=[[8] 小 <em>米</em> 手 机]}}
SearchHit{id='7', score=NaN, sortValues=[7], content=Product(id=7, title=[7] 小 米 手 机 , category= 手 机 , price=2006.0, images=http://www.shouji.com/xm.jpg), highlightFields={title=[[7] 小 <em>米</em> 手 机]}}
SearchHit{id='6', score=NaN, sortValues=[6], content=Product(id=6, title=[6] 小 米 手 机 , category= 手 机 , price=2005.0, images=http://www.shouji.com/xm.jpg), highlightFields={title=[[6] 小 <em>米</em> 手 机]}}
SearchHit{id='5', score=NaN, sortValues=[5], content=Product(id=5, title=[5] 小 米 手 机 , category= 手 机 , price=2004.0, images=http://www.shouji.com/xm.jpg), highlightFields={title=[[5] 小 <em>米</em> 手 机]}}
CriteriaQuery
- API 实例应用
1 | /** CriteriaQuery */ |