一、Spring Data 介绍

Spring Data 是一个用于简化数据库、非关系型数据库、索引库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持 map-reduce 框架和云计算数据服务。 Spring Data 可以极大的简化 JPA(Elasticsearch…)的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了 CRUD 外,还包括如分页、排序等一些常用的功能。

Spring Data:https://spring.io/projects/spring-data

二、Spring Data Elasticsearch 介绍

Spring Data Elasticsearch 基于 spring data API 简化 Elasticsearch 操作,将原始操作 Elasticsearch 的客户端 API 进行封装 。Spring Data 为 Elasticsearch 项目提供集成搜索引擎。Spring Data Elasticsearch POJO 的关键功能区域为中心的模型与 Elastichsearch 交互文档和轻松地编写一个存储索引库数据访问层。

Spring Data Elasticsearch:https://spring.io/projects/spring-data-elasticsearch

三、演变

Elasticsearch 官网提出的近期版本对 type 概念的演变情况如下:

在 5.X 版本中,一个 index 下可以创建多个 type;

在 6.X 版本中,一个 index 下只能存在一个 type;

在 7.X 版本中,直接去除了 type 的概念,就是说 index 不再会有 type。

三、核心概念

1、document(文档)

es 中的最小数据单元

一个 document 可以是一条商品数据,也可以是一条订单数据,通常用 JSON 格式表示

一个 document 里面有多个 field,每个 field 就是一个数据字段

以下是一个商品 document:

1
2
3
4
5
6
{
"id": "1",
"name": "iPhone 12 Pro",
"desc": "iPhone 12 Pro是苹果公司旗下的智能手机...",
"category_id": "1"
}

2、index(索引)

包含一堆有相似结构的文档 (document) 数据,比如一个商品分类索引,订单索引,索引有一个名称。

一个 index 包含很多 document,一个 index 就代表了一类类似的或者相同的 document。比如说建立一个商品 index,里面存放了所有商品的 document。

3、type(类型)

每个索引里都可以有一个或多个 type,type 是 index 中的一个逻辑数据分类,一个 type 下的 document,都有部分相同的 field。

比如:商品的 index,里面存放了所有的商品 document,而商品分很多种类,每个种类的 document 的 field 可能不太一样,比如说数码产品,可能会包含如保修时间,食品会包含保质期这样的的 field

4、cluster(集群)及 node(节点)

一个集群包含多个节点,每个节点属于哪个集群是通过配置(集群名称,默认是 elasticsearch)来决定的。

节点也有一个名称(默认是随机分配的),节点名称很重要(在执行运维管理操作的时候),默认节点会去加入一个名称为“elasticsearch”的集群,如果直接启动一堆节点,那么它们会自动组成一个 elasticsearch 集群,当然一个节点也可以组成一个 elasticsearch 集群

5、shard

单台机器无法存储大量数据,es 可以将一个索引 (index) 中的数据切分为多个 shard,分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个 shard 都是一个 lucene index。

6、replica

任何一个服务器随时可能故障或宕机,此时 shard 可能就会丢失,因此可以为每个 shard 创建多个 replica 副本。

replica 可以在 shard 故障时提供备用服务,保证数据不丢失,多个 replica 还可以提升搜索操作的吞吐量和性能。

三、SpringData Elasticsearch 入门

创建 SpringBoot 项目 elasticsearch-springdata-es

1、pom.xml 依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

2、增加索引数据

(1)编写实体类

创建Item

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

  • @Document 作用在类,标记实体类为文档对象,一般有四个属性
    1. indexName:对应索引库名称
    2. shards:分片数量,默认 5
    3. replicas:副本数量,默认 1
  • @Id 作用在成员变量,标记一个字段作为 id 主键
  • @Field 作用在成员变量,标记为文档的字段,并指定字段映射属性:
    1. type:字段类型,取值是枚举:FieldType
    2. index:是否索引,布尔类型,默认是 true
    3. store:是否存储,布尔类型,默认是 false
    4. analyzer:分词器名称:ik_max_word
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
package com.atguigu.pojo;

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;

@Document(indexName = "item", shards = 1, replicas = 1)
public class Item {
@Id
private Long id;

@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;

@Field(type = FieldType.Keyword)
private String category;

@Field(type = FieldType.Keyword)
private String brand;

@Field(type = FieldType.Double)
private Double price;

@Field(index = false, type = FieldType.Keyword)
private String images;

// get、set、toString、构造方法 ...
}

3、配置 application.yml

1
2
3
4
5
6
spring:
data:
elasticsearch:
#集群名称
cluster-name: elasticsearch
cluster-nodes: 192.168.88.88:9300

elasticsearch7.x 中 spring.data.elasticsearch.cluster-name,cluster-nodes 等属性过时已经过时,不再推荐使用

官方推荐使用高级 REST 客户端

The Java High Level REST Client is the default client of Elasticsearch, it provides a straight forward replacement for the TransportClient as it accepts and returns the very same request/response objects and therefore depends on the Elasticsearch core project. Asynchronous calls are operated upon a client managed thread pool and require a callback to be notified when the request is done.

4、编写 Repository 类

这是一个接口类,类似我们的 mybatis,平时大多数常用的 curd 操作都可以用这个类来进行

1
2
3
4
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface ItemRepository extends ElasticsearchRepository<Item,Long> {
}

5、编写测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestSpringBootES {

@Autowired
private ElasticsearchTemplate elasticsearchTemplate;

@Test
public void testCreate() {
// 创建索引,会根据Item类的@Document注解信息来创建
elasticsearchTemplate.createIndex(Item.class);
// 配置映射,会根据Item类中的id、Field等字段来自动完成映射
elasticsearchTemplate.putMapping(Item.class);
}

6、增删改操作

Spring Data 的强大之处,就在于你不用写任何 DAO 处理,自动根据方法名或类的信息进行 CRUD 操作。只要你定义一个接口,然后继承 Repository 提供的一些子接口,就能具备各种基本的 CRUD 功能。

① 增加

1
2
3
4
5
6
7
8
@Autowired
private ItemRepository itemRepository;

@Test
public void testAdd() {
Item item = new Item(1L, "iphone12", " 手机", "apple", 5999.00, "https://www.baidu.com/123.jpg");
itemRepository.save(item);
}

② 修改

id 存在就是修改,否则就是插入

1
2
3
4
5
@Test
public void testUpdate() {
Item item = new Item(1L,"iphone12 pro","手机","apple",8399.00,"https://www.baidu.com/123.jpg");
itemRepository.save(item);
}

③ 批量新增

1
2
3
4
5
6
7
8
@Test
public void indexList() {
List<Item> list = new ArrayList<>();
list.add(new Item(2L, "iphone12 pro max", " 手机", "apple", 9899.00, "https://www.baidu.com/123.jpg"));
list.add(new Item(3L, "ipad pro 2021", " 平板", "apple", 12999.00, "https://www.baidu.com/123.jpg"));
// 接收对象集合,实现批量新增
itemRepository.saveAll(list);
}

④ 删除操作

1
2
3
4
@Test
public void testDelete() {
itemRepository.deleteById(1L);
}

⑤ 根据 id 查询

1
2
3
4
5
@Test
public void testQuery() {
Optional<Item> optional = itemRepository.findById(2L);
System.out.println(optional.get());
}

⑥ 查询全部

查询全部,并按照价格降序排序

1
2
3
4
5
6
@Test
public void testFind(){
// 查询全部,并按照价格降序排序
Iterable<Item> items = this.itemRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
items.forEach(item-> System.out.println(item));
}

⑦ 自定义方法

Spring Data 的另一个强大功能,是根据方法名称自动实现功能。

比如:你的方法名叫做:findByTitle,那么它就知道你是根据 title 查询,然后自动帮你完成,无需写实现类。

当然,方法名称要符合一定的约定

KeywordSampleElasticsearch Query String
AndfindByNameAndPrice{“bool” : {“must” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}}
OrfindByNameOrPrice{“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“price” : “?”}} ]}}
IsfindByName{“bool” : {“must” : {“field” : {“name” : “?”}}}}
NotfindByNameNot{“bool” : {“must_not” : {“field” : {“name” : “?”}}}}
BetweenfindByPriceBetween{“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : ?,”include_lower” : true,”include_upper” : true}}}}}
LessThanEqualfindByPriceLessThan{“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}}
GreaterThanEqualfindByPriceGreaterThan{“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}}
BeforefindByPriceBefore{“bool” : {“must” : {“range” : {“price” : {“from” : null,”to” : ?,”include_lower” : true,”include_upper” : true}}}}}
AfterfindByPriceAfter{“bool” : {“must” : {“range” : {“price” : {“from” : ?,”to” : null,”include_lower” : true,”include_upper” : true}}}}}
LikefindByNameLike{“bool” : {“must” : {“field” : {“name” : {“query” : “?*“,”analyze_wildcard” : true}}}}}
StartingWithfindByNameStartingWith{“bool” : {“must” : {“field” : {“name” : {“query” : “?*“,”analyze_wildcard” : true}}}}}
EndingWithfindByNameEndingWith{“bool” : {“must” : {“field” : {“name” : {“query” : “*?”,”analyze_wildcard” : true}}}}}
Contains/ContainingfindByNameContaining{“bool” : {“must” : {“field” : {“name” : {“query” : “?“,”analyze_wildcard” : true}}}}}
InfindByNameIn(Collection<String>names){“bool” : {“must” : {“bool” : {“should” : [ {“field” : {“name” : “?”}}, {“field” : {“name” : “?”}} ]}}}}
NotInfindByNameNotIn(Collection<String>names){“bool” : {“must_not” : {“bool” : {“should” : {“field” : {“name” : “?”}}}}}}
NearfindByStoreNearNot Supported Yet !
TruefindByAvailableTrue{“bool” : {“must” : {“field” : {“available” : true}}}}
FalsefindByAvailableFalse{“bool” : {“must” : {“field” : {“available” : false}}}}
OrderByfindByAvailableTrueOrderByNameDesc{“sort” : [{ “name” : {“order” : “desc”} }],”bool” : {“must” : {“field” : {“available” : true}}}}

⑧ 测试

按照价格区间查询

1
2
3
public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
List<Item> findByPriceBetween(double price1, double price2);
}

测试方法

1
2
3
4
5
6
7
@Test
public void queryByPriceBetween() {
List<Item> list = this.itemRepository.findByPriceBetween(8000.00, 10000.00);
for (Item item : list) {
System.out.println("item = " + item);
}
}