ES在项目中的使用思路
以为ES在处理事务(数据一致性)方面比Database要若很多,所以在项目实战中,ES需要和Database配合使用。
ES中存储的数据相当于Database中对应数据的简略版,只可用于搜索结果展示,真正获取详细的信息还得去Database中获取。
如果不使用ES生态,使用原生ES,那么项目中对于数据的操作如下:
数据一致性
在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 框架、云数据服务等等;另外也包含对关系数据库的访问支持。
常见的子项目有:
- 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. 基础环境搭建
-
Spring Boot版本:2.2.5.RELEASE
-
在pom.xml中引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> -
创建config包
-
创建RestClient配置类
@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. 注入
@Autowired
private RestHighLevelClient restHighLevelClient;
2. 新增Document
@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
@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
@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
@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. 批量混合操作(插入+删除+更新)
@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即可,其他地方不变。
@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. 配置
-
配置需要存储进ES的Bean
@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。
注意:
- 在第一次向ES存储该Document时,ES就会自动去ES中创建该index,type和mapping,因此指定的index和type在原ES中不能已经存在。
- 在开发中,ES的Bean和业务中的Bean可以共用同一个类,ES中使用的Bean只是业务中使用的Bean的一部分,所以我们只需要在需要存入ES的字段上构建mapping即可(上文构建了id,username,age三个字段)。
-
创建repository包
-
创建该Bean对应的Repository并继承ElasticsearchRepository
public interface UserRepository extends ElasticsearchRepository<User, String> { }注意:泛型中第一个参数是该Repository对应的是哪一个Bean,第二个参数为Bean中id的类型。
-
在使用处注入
@Autowired private UserRepository userRepository;
2. 新增Document
@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
@Test
public void deleteDocument() {
userRepository.deleteById("1");
}
4. 更新Document
save方法不仅能完成新增,还能完成更新。
当Bean的id在ES中不存在时,为新增;当Bean的id在ES中存在时,为更新。
@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
@Test
public void queryDocument() {
// 传入Document的id
User user = userRepository.findById("1").get();
System.out.println(user);
}
6. 查询所有Document
@Test
public void queryAllDocuments() {
Iterable<User> users = userRepository.findAll();
users.forEach(user -> {
System.out.println(user);
});
}
7. 查询所有Document并排序
@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
@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
模糊查询规则参考前文。
@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)
-
在Repository中设计接口
接口命名规则为:findBy + 字段名
public interface UserRepository extends ElasticsearchRepository<User, String> { List<User> findByUsername(String username); } -
调用接口完成操作
@Test public void queryDocumentsByUsername() { List<User> users = userRepository.findByUsername("张三"); users.forEach(user -> { System.out.println(user); }); }
该接口的实现等价于QueryDSL:
GET /postilhub/user/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"username": {
"value": "张三"
}
}
}
]
}
}
}
针对各个接口的设计规则,Spring Data Elasticsearch给开发人员提供了一张表格:
注意:表格中 ? 表示参数,参数类型需要和ES中匹配。返回值统一为:List<Bean>
| 命名关键词 | 命名示例 | 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":"*?"}}}} |