【Elasticsearch】7 Spring Boot整

ES在项目中的使用思路

以为ES在处理事务(数据一致性)方面比Database要若很多,所以在项目实战中,ES需要和Database配合使用。

ES中存储的数据相当于Database中对应数据的简略版,只可用于搜索结果展示,真正获取详细的信息还得去Database中获取。

如果不使用ES生态,使用原生ES,那么项目中对于数据的操作如下:

20210818004009.png

数据一致性

在ES和Database数据的同步时,存在两种数据一致性:

  • 数据强一致性
  • 数据最终一致性

数据强一致性表示,Database中的数据和ES中的数据保证实时一致,也就是说Database数据变更后立即同步到ES,数据的同步存在实时性

数据最终一致性表示,Database中的数据和ES中的数据在过了某个特定的时间段之后保证一致,也就是说Database数据变更后隔一段时间再同步到ES,数据的同步存在延时性

1. 数据一致性的实现

数据强一致性的具体实现,可以在Database更新数据时,同时调用Java代码更新ES中的数据,这样做会导致效率低而且不易维护。在企业级解决方案中,会将ES分成一个独立的服务,并配合消息队列实现ES数据的同步更新。

数据最终一致性的具体实现,可以设置定时任务,设置在每天某个并发量低的时刻,参考Database同步ES中的数据。

2. 数据一致性的选择

选择选择强一致性还是最终一致性得看具体的业务,如果该业务的数据实时更新很重要,比如商品价格的调整,那么需要使用强一致性。如果数据的实时更新不那么重要,比如一个商品的日访问量,那么就使用最终一致性。

Spring Boot整合ES

1. Spring Data

Spring Boot整合ES需要另外一个Spring开源的项目,Spring Data。

Spring Data官网:spring.io/projects/sp…

Spring Data项目的目的是为了简化构建基于Spring框架应用的数据访问计数,包括非关系数据库、Map-Reduce 框架、云数据服务等等;另外也包含对关系数据库的访问支持。

20210818115102.png

常见的子项目有:

  • Spring Data JDBC:对JDBC的Spring Data存储库支持。
  • Spring Data JPA:对JPA的Spring Data存储库支持。
  • Spring Data MongoDB:对MongoDB的基于Spring对象文档的存储库支持。
  • Spring Data Redis:从Spring应用程序轻松配置和访问Redis。
  • Spring Data Elasticsearch:从Spring应用程序轻松配置和访问Elasticsearch。

Spring Boot整合ES实际上是Spring Boot整合了Spring Data,通过Spring Data来操作Elasticsearch。

2. 基础环境搭建

  1. Spring Boot版本:2.2.5.RELEASE
  2. 在pom.xml中引入依赖
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
  1. 创建config包
  2. 创建RestClient配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {

@Override
@Bean
public RestHighLevelClient elasticsearchClient() {

// 定义ES客户端对象:ip + port
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();

return RestClients.create(clientConfiguration).rest();
}

}

3. 操作ES两种方式

在Spring data 2.x ~ 3.x 时,推荐使用ElasticTemplate来操作ES,ElasticTemplate底层调用的是TransportClient,使用的是ES的TCP端口9300,但是TransportClient在ES 6.x ~ 7.x 时就已经不推荐使用,在 8.x 已经废弃。所以在最新版的Spring Data 4.x中,已经弃用ElasticTemplate,推荐使用RestHighLevelClient(高等级REST客户端)和ElasticsearchRepository接口来操作ES,使用的是ES的Web端口9200,类似于Kibana。

  • RestHighLevelClient:用来实现ES的复杂检索。
  • ElasticsearchRepository:用来实现ES的常规操作。

通常只用RestHighLevelClient完成高亮检索,剩下的都可以用ElasticsearchRepository完成。

RestHighLevelClient操作ES

1. 注入

1
2
java复制代码@Autowired
private RestHighLevelClient restHighLevelClient;

2. 新增Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void saveDocument() throws IOException {
// 构建索引请求 传入参数为:index名,type名,自定义该Document的_id
IndexRequest indexRequest = new IndexRequest("postilhub", "user", "1");

// 传入参数为:新增Document的数据,数据类型
indexRequest.source("{\"id\":\"1\",\"username\":\"小明\",\"age\":19}", XContentType.JSON);

// 执行新增 RequestOptions.DEFAULT为枚举类型,默认即可
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);

// 查看操作是否成功
System.out.println(indexResponse.status());
}

3. 删除Document

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Test
public void deleteDocument() throws IOException {
// 构建删除请求 传入参数为:index名,type名,该Document的_id
DeleteRequest deleteRequest = new DeleteRequest("postilhub", "user", "1");

// 执行删除 RequestOptions.DEFAULT为枚举类型,默认即可
DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);

// 查看操作是否成功
System.out.println(deleteResponse.status());
}

4. 更新Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void updateDocument() throws IOException {
// 传入参数为:index名,type名,Document的_id
UpdateRequest updateRequest = new UpdateRequest("postilhub", "user", "1");

// 传入参数为:更新后的Document数据,数据类型
updateRequest.doc("{\"id\":\"1\",\"username\":\"小花\",\"age\":19}", XContentType.JSON);

// 执行更新 RequestOptions.DEFAULT为枚举类型,默认即可
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);

// 查看操作是否成功
System.out.println(updateResponse.status());
}

注意:该更新保留原始数据。

5. 查询所有Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Test
public void queryAllDocuments() throws IOException {
// 搜索条件构造器 设置搜索条件为:matchAll
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.matchAllQuery());

// 构建搜索请求 传入参数为:index名
SearchRequest searchRequest = new SearchRequest("postilhub");
// 传入参数为:type名
searchRequest.types("user").source(builder);

// 执行检索
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

// 获取TotalHits和MaxScore
System.out.println("检索出的文档总数:" + searchResponse.getHits().getTotalHits());
System.out.println("检索出的文档最大得分:" + searchResponse.getHits().getMaxScore());
// 检索出的所有文档
for (SearchHit hit : searchResponse.getHits().getHits()) {
System.out.println(hit.getSourceAsMap());
}
}

6. 批量混合操作(插入+删除+更新)

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
java复制代码@Test
public void bulk() throws IOException {
BulkRequest bulkRequest = new BulkRequest();

// 添加Document
IndexRequest indexRequest = new IndexRequest("postilhub", "user", "3");
indexRequest.source("{\"id\":3,\"username\":\"老王\",\"age\":72}", XContentType.JSON);
// 装载indexRequest
bulkRequest.add(indexRequest);

// 删除Document
DeleteRequest deleteRequest = new DeleteRequest("postilhub", "user", "2");
// 装载deleteRequest
bulkRequest.add(deleteRequest);

// 更新Document
UpdateRequest updateRequest = new UpdateRequest("postilhub", "user", "1");
updateRequest.doc("{\"id\":\"1\",\"username\":\"老八\",\"age\":29}", XContentType.JSON);
// 装载updateRequest
bulkRequest.add(updateRequest);

// 执行批量操作
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);

// 分别查看每一个操作是否成功
for (BulkItemResponse item : bulkResponse.getItems()) {
System.out.println(item.status());
}
}

7. 自定义查询Document

可以实现前文《ES高级检索》中所有的查询,支持链式调用组合多个查询模式。

配置不同的查询条件和查询模式,仅仅需要修改SearchSourceBuilder即可,其他地方不变。

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
java复制代码@Test
public void conditionalQueryDocuments() throws IOException, ParseException {
// 搜索构造器 设置组合条件
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(QueryBuilders.termQuery("username", "张三"))
// 分页 从0号document开始 每页容量为10
.from(0).size(10)
// 按照年龄降序排序
.sort("age", SortOrder.DESC)
// 设置关键字高亮 指定关键词匹配为全部字段 不开启字段匹配(如果设置关键词匹配为所有某个字段,则开启字段匹配)
.highlighter(new HighlightBuilder().field("*").requireFieldMatch(false).preTags("\"<span style='color:red'>\"").postTags("\"</span>\""))
// 设置范围过滤 过滤条件为:年龄大于等于15小于等于30
.postFilter(QueryBuilders.rangeQuery("age").gte(15).lte(30));

// 构建搜索请求 传入参数为:index名
SearchRequest searchRequest = new SearchRequest("postilhub");
// 传入参数为:type名
searchRequest.types("user").source(builder);

SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

// 获取TotalHits和MaxScore
System.out.println("检索出的文档总数:" + searchResponse.getHits().getTotalHits());
System.out.println("检索出的文档最大得分:" + searchResponse.getHits().getMaxScore());

// 检索出的所有文档并构建Bean
List<User> userList = new ArrayList<>();
for (SearchHit hit : searchResponse.getHits().getHits()) {
Map<String, Object> sourceMap = hit.getSourceAsMap();

// 将Document转成Bean
User user = new User();
user.setId(hit.getId());
user.setUsername(sourceMap.get("username").toString());
user.setAge(Integer.parseInt(sourceMap.get("age").toString()));
user.setBirth(new SimpleDateFormat("yyyy-MM-dd").parse(sourceMap.get("birth").toString()));
user.setIntro(sourceMap.get("intro").toString());

// 关键字高亮替换
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
// 判断分词字段(username,intro)数据中是否包含关键词,包含则将原关键词替换成高亮关键词
if (highlightFields.containsKey("username")) {
user.setUsername(highlightFields.get("username").fragments()[0].toString());
}
if (highlightFields.containsKey("intro")) {
user.setIntro(highlightFields.get("username").fragments()[0].toString());
}

userList.add(user);
}

// 打印所有Document构成的Bean
userList.forEach(System.out::println);
}

ElasticsearchRepository操作ES

上述案例描述了如何使用RestHighLevelClient来操作ES,我们会发现一个问题,如果我们需要通过RestHighLevelClient来新增和更新Document,那么我们需要必须将Bean转换成JSON格式。当然RestHighLevelClient这种方式更强大,更灵活,但是在处理一些简单检索时,就显得有些麻烦。

ElasticsearchRepository更具有面向对象的思想,配合注解可以将Bean自动JSON序列化,不需要再把Bean手动转换成JSON格式。所以在对ES进行一些常规操作时,推荐使用ElasticsearchRepository

1. 配置

  1. 配置需要存储进ES的Bean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Data
@Document(indexName = "postilhub", type = "user")
public class User {

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

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

@Field(type = FieldType.Integer)
private Integer age;

@Field(type = FieldType.Date)
@JsonFormat(pattern="yyyy-MM-dd", timezone = "GMT+8")
private Date birth;

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

}
* @Document:构建指定index和type,并能够自动将该类构建的Bean通过JSON序列化成Document存入ES的该index的type中。
* @Field:构建指定mapping,给某些必要的字段设置分词器。
* @id:创建Document的同时将Bean中的id赋值给 \_id。注意:


1. 在第一次向ES存储该Document时,ES就会自动去ES中创建该index,type和mapping,因此指定的index和type在原ES中不能已经存在。
2. 在开发中,**ES的Bean和业务中的Bean可以共用同一个类**,ES中使用的Bean只是业务中使用的Bean的一部分,所以我们只需要在需要存入ES的字段上构建mapping即可(上文构建了id,username,age三个字段)。
  1. 创建repository包
  2. 创建该Bean对应的Repository并继承ElasticsearchRepository
1
2
3
java复制代码public interface UserRepository extends ElasticsearchRepository<User, String> {

}

注意:泛型中第一个参数是该Repository对应的是哪一个Bean,第二个参数为Bean中id的类型。
4. 在使用处注入

1
2
java复制代码@Autowired
private UserRepository userRepository;

2. 新增Document

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Test
public void saveDocument() {
// 模拟新增的User
User user = new User();
user.setId("1");
user.setUsername("张三");
user.setAge(18);
user.setBirth(new Date());
user.setIntro("我是张三");

userRepository.save(user);
}

3. 删除Document

1
2
3
4
java复制代码@Test
public void deleteDocument() {
userRepository.deleteById("1");
}

4. 更新Document

save方法不仅能完成新增,还能完成更新。

当Bean的id在ES中不存在时,为新增;当Bean的id在ES中存在时,为更新。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Test
public void updateDocument() {
// 模拟已经更新username后的User
User user = new User();
user.setId("1");
user.setUsername("小明");
user.setAge(18);
user.setBirth(new Date());
user.setIntro("我是张三");

userRepository.save(user);
}

5. 查询指定Document

1
2
3
4
5
6
7
java复制代码@Test
public void queryDocument() {
// 传入Document的id
User user = userRepository.findById("1").get();

System.out.println(user);
}

6. 查询所有Document

1
2
3
4
5
6
7
8
java复制代码@Test
public void queryAllDocuments() {
Iterable<User> users = userRepository.findAll();

users.forEach(user -> {
System.out.println(user);
});
}

7. 查询所有Document并排序

1
2
3
4
5
6
7
8
9
java复制代码@Test
public void queryAllDocumentsBySort() {
// 按照age降序排序
Iterable<User> users = userRepository.findAll(Sort.by(Sort.Order.desc("age")));

users.forEach(user -> {
System.out.println(user);
});
}

8. 分页查询Document

1
2
3
4
5
6
7
8
9
java复制代码@Test
public void queryDocumentsByPage() {
// 页号从0算起
Page<User> userPage = userRepository.search(QueryBuilders.matchAllQuery(), PageRequest.of(0, 10));

userPage.forEach(user -> {
System.out.println(user);
});
}

9. 模糊查询Document

模糊查询规则参考前文。

1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void queryDocumentsByFuzzy() {
FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("intro", "李四");

Iterable<User> users = userRepository.search(fuzzyQueryBuilder);

users.forEach(user -> {
System.out.println(user);
});
}

10. 自定义查询Document

因为ElasticsearchRepository只提供了基本的ES操作接口,所以如果我们要使用ElasticsearchRepository完成更灵活的操作,比如我要根据某个字段关键词进行查询。

因此ElasticsearchRepository提供了一种DIY操作接口的功能,我们只需要在Repository中按照规定对接口进行命名和设计,ElasticsearchRepository会根据我们命名的接口,自动判断其功能并将其实现。

例如:我们需要根据查询出所有username字段数据中包含”张三”的Document(假设username的类型为text)

  1. 在Repository中设计接口

接口命名规则为:findBy + 字段名

1
2
3
4
5
java复制代码public interface UserRepository extends ElasticsearchRepository<User, String> {

List<User> findByUsername(String username);

}
  1. 调用接口完成操作
1
2
3
4
5
6
7
8
java复制代码@Test
public void queryDocumentsByUsername() {
List<User> users = userRepository.findByUsername("张三");

users.forEach(user -> {
System.out.println(user);
});
}

该接口的实现等价于QueryDSL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码GET /postilhub/user/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"username": {
"value": "张三"
}
}
}
]
}
}
}

针对各个接口的设计规则,Spring Data Elasticsearch给开发人员提供了一张表格:

注意:表格中 ? 表示参数,参数类型需要和ES中匹配。返回值统一为:List

命名关键词 命名示例 QueryDSL示例
Is findByUsername {“query”:{“bool”:{“must”:[{“term”:{“username”:{“value”:”?“}}}]}}}
And findByUsernameAndAge {“query”:{“bool”:{“must”:[{“term”:{“username”:{“value”:”?“}}},{“term”:{“age”:{“value”:?}}}]}}}
Or findByUsernameOrAge {“query”:{“bool”:{“should”:[{“term”:{“username”:{“value”:”?“}}},{“term”:{“age”:{“value”:?}}}]}}}
Not findByUsernameNot {“query”:{“bool”:{“must_not”:[{“term”:{“username”:{“value”:”?“}}}]}}}
Between findByAgeBetween {“query”:{“bool”:{“must”:[{“range”:{“age”:{“gt”:?,”lt”:?}}}]}}}
LessThanEqual findByAgeLessThanEqual {“query”:{“bool”:{“must”:[{“range”:{“age”:{“gte”:null,”lte”:?}}}]}}}
GreaterThanEqual findByAgeGreaterThanEqual {“query”:{“bool”:{“must”:[{“range”:{“age”:{“gte”:?,”lte”:null}}}]}}}
LessThan findByAgeLessThan {“query”:{“bool”:{“must”:[{“range”:{“age”:{“gt”:null,”lt”:?}}}]}}}
GreaterThan findByAgeGreaterThan {“query”:{“bool”:{“must”:[{“range”:{“age”:{“gt”:?,”lt”:null}}}]}}}
Like findByUsernameLike {“query”:{“wildcard”:{“username”:{“value”:”*?*“}}}}
StartingWith findByUsernameStartingWith {“query”:{“wildcard”:{“username”:{“value”:”?*“}}}}
EndingWith findByUsernameEndingWith {“query”:{“wildcard”:{“username”:{“value”:”*?“}}}}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%