ElasticSearch8 - SpringBoot整合ElasticSearch
前言
springboot 整合 ES 有两种方案,ES 官方提供的 Elasticsearch Java API Client 和 spring 提供的 [Spring Data Elasticsearch](Spring Data Elasticsearch)
两种方案各有优劣
Spring:高度封装,用着舒服。缺点是更新不及时,有可能无法使用 ES 的新 API
ES 官方:更新及时,灵活,缺点是太灵活了,基本是一比一复制 REST APIs,项目中使用需要二次封装。
Elasticsearch Java API Client
目前最新版本 ES8.12,要求 jdk8 以上,API 里面使用了大量的 builder 和 lambda
官方也提供了 测试用例
兼容
翻了不少博客,大部分都是使用 High Level Rest Client,这是旧版本的 api,新版本使用 Elasticsearch Java API Client,如何兼容旧版本,官方也提供了解决方案)
下文描述的均是新版 API
添加 jar 包
官方文档:[installation](安装| Elasticsearch Java API 客户端 [8.12] |松紧带 --- Installation | Elasticsearch Java API Client [8.12] | Elastic)
使用的是 maven,在 pom.xml 中添加
<dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> <version>8.12.2</version> </dependency> <!-- 如果有添加springmvc,此包可不引入 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version> </dependency>
如果报错 ClassNotFoundException: jakarta.json.spi.JsonProvider
,则还需要添加
<dependency> <groupId>jakarta.json</groupId> <artifactId>jakarta.json-api</artifactId> <version>2.0.1</version> </dependency>
打印请求
在 application. yml 中添加配置,打印 es 的 http 请求(建议在开发调试时使用)
logging: level: tracer: TRACE
连接 ES
配置文件如下,后续所有 ES 操作都通过 ElasticsearchClient 对象
更多配置请看 Common configuration
@Configuration public class ElasticSearchConfig { @Bean public ElasticsearchClient esClient() { // ES服务器URL String serverUrl = "http://127.0.0.1:9200"; // ES用户名和密码 String userName = "xxx"; String password = "xxx"; BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); credsProv.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(userName, password) ); RestClient restClient = RestClient .builder(HttpHost.create(serverUrl)) .setHttpClientConfigCallback(hc -> hc.setDefaultCredentialsProvider(credsProv)) .build(); ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); return new ElasticsearchClient(transport); } }
索引操作
代码中的 esClient 就是 ElasticsearchClient,请自行注入 bean
// 索引名字 String indexName = "student"; // 索引是否存在 BooleanResponse books = esClient.indices().exists(e -> e.index(indexName)); System.out.println("索引是否存在:" + books.value()); // 创建索引 esClient.indices().create(c -> c .index(indexName) .mappings(mappings -> mappings // 映射 .properties("name", p -> p .text(t -> t // text类型,index=false .index(false) ) ) .properties("age", p -> p .long_(t -> t) // long类型 ) ) ); // 删除索引 esClient.indices().delete(d -> d.index(indexName));
文档操作 (CRUD)
下文以官方测试数据 account. json 为例
实体类
首先定义实体类,用于 ES 中的字段
public class Account { private String id; // 解决ES中字段与实体类字段不一致的问题 @JsonProperty("account_number") private Long account_number; private String address; private Integer age; private Long balance; private String city; private String email; private String employer; private String firstname; private String lastname; private String gender; private String state; ... 省略get、set方法 }
新增
String indexName = "account"; // 索引名字 Account account = new Account(); account.setId("1"); account.setLastname("guyu"); // 新增 CreateResponse createResponse = esClient.create(c -> c .index(indexName) // 索引名字 .id(account.getId()) // id .document(account) // 实体类 );
修改
UpdateResponse<Account> updateResp = esClient.update(u -> u .index(indexName) .id(account.getId()) .doc(account), Account.class );
删除
DeleteResponse deleteResp = esClient.delete(d -> d.index(indexName).id("1"));
批量新增
批量操作需要使用到 bulk
List<Account> accountList = ... BulkRequest.Builder br = new BulkRequest.Builder(); for (Account acc : accountList) { br.operations(op -> op .create(c -> c .index(indexName) .id(acc.getId()) .document(acc) ) ); } BulkResponse bulkResp = esClient.bulk(br.build());
有没有觉得批量新增的 .create () 里面的参数很眼熟,批量删除和更新请举一反三
根据 id 查询
// 定义实体类 GetResponse<Account> getResp = esClient.get(g -> g.index(indexName).id("1"), Account.class); if (getResp.found()) { Account source = getResp.source(); // 这就是得到的实体类 source.setId(getResp.id()); } // 不定义实体类 GetResponse<ObjectNode> getResp = esClient.get(g -> g .index(indexName) .id("1"), ObjectNode.class ); if (getResp.found()) { ObjectNode json = getResp.source(); String firstname = json.get("firstname").asText(); System.out.println(firstname); }
搜索
搜索全部
SearchResponse<Account> searchResp = esClient.search(s -> s .index(indexName) .query(q -> q.matchAll(m -> m)) // 搜索全部 , Account.class ); HitsMetadata<Account> hits = searchResp.hits(); long totalValue = hits.total().value(); // 匹配到的数量 hits.hits().forEach(h -> { Account acc = h.source(); // 这就是得到的实体类 acc.setId(h.id()); });
ES API 的对象定义,基本与返回的 json 一一对应的,所以 SearchResponse 就不过多赘述。
搜索 firstname = Amber
SearchResponse<Account> searchResp = esClient.search(s -> s .index(indexName) .query(q -> q // 查询 .match(t -> t .field("firstname") .query("Amber") ) ) , Account.class ); // 也可以这样写 Query firstNameQuery = MatchQuery.of(m -> m.field("firstname").query("Amber"))._toQuery(); SearchResponse<Account> searchResp = esClient.search(s -> s .index(indexName) .query(firstNameQuery) , Account.class );
嵌套查询,比如搜索 firstname = Amber AND age = 32
Query firstNameQuery = MatchQuery.of(m -> m.field("firstname").query("Amber"))._toQuery(); Query ageQuery = MatchQuery.of(m -> m.field("age").query(32))._toQuery(); SearchResponse<Account> searchResp = esClient.search(s -> s .index(indexName) .query(q -> q .bool(b -> b.must(firstNameQuery, ageQuery)) ) , Account.class );
浅分页
from 和 size 参数类似于 mysql 的 limit,详细说明见 Paginate search results
SearchResponse<Account> searchResp = esClient.search(s -> s .index(indexName) .from(0) // 分页参数 .size(20) // 分页参数 , Account.class );
排序
SearchResponse<Account> searchResp = esClient.search(s -> s .index(indexName) .sort(so -> so // 排序字段1 .field(f -> f .field("age") .order(SortOrder.Asc) ) ) .sort(so -> so // 排序字段2 .field(f -> f .field("account_number") .order(SortOrder.Desc) ) ) , Account.class );
Spring Data Elasticsearch
添加 jar 和配置
pom.xml添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
yml 配置
spring: elasticsearch: uris: http://xxx:9200 username: xxx password: xxx logging: level: # 输出es的查询参数(调试用) tracer: TRACE
索引操作
实体类
@Data @Document(indexName = "account") public class Account { @Id private String id; // 解决ES中字段与实体类字段不一致的问题 @Field(name = "account_number", type = FieldType.Long) private Long accountNumber; @Field(type = FieldType.Text) private String address; @Field(type = FieldType.Integer) private Integer age; @Field(type = FieldType.Long) private Long balance; @Field(type = FieldType.Text) private String city; @Field(type = FieldType.Text) private String email; @Field(type = FieldType.Text) private String employer; @Field(type = FieldType.Text) private String firstname; @Field(type = FieldType.Text) private String lastname; @Field(type = FieldType.Text) private String gender; @Field(type = FieldType.Text) private String state; ... 省略get、set 方法 }
IndexOperations idxOpt = template.indexOps(Account.class); // 索引是否存在 boolean idxExist = idxOpt.exists(); // 创建索引 boolean createSuccess = idxOpt.createWithMapping(); System.out.println(createSuccess); // 删除索引 boolean deleted = idxOpt.delete();
文档操作(CRUD)
Account account = new Account(); account.setId("1"); account.setLastname("guyu"); // 这是插入或覆盖,如果id存在了就是覆盖 template.save(account); // 修改,用的是es的_update template.update(account); // 删除 template.delete(account) // 批量新增(用的是es的_bulk) List<Account> accountList = ... template.save(accountList); // 根据id查询 Account account = template.get("1", Account.class);
搜索 + 排序 + 分页
// 搜索 firstname = Amber AND age = 32 Criteria criteria = new Criteria(); criteria.and(new Criteria("firstname").is("Amber")); criteria.and(new Criteria("age").is(32)); // 分页 int pageNum = 1; // 页码 int pageSize = 20; // 每页数量 Query query = new CriteriaQueryBuilder(criteria) .withSort(Sort.by(new Order(Sort.Direction.ASC, "age"))) // 排序字段1 .withSort(Sort.by(new Order(Sort.Direction.DESC, "balance"))) // 排序字段1 .withPageable(PageRequest.of(pageNum - 1, pageSize)) // 浅分页 // 不需要查询的字段 .withSourceFilter(new FetchSourceFilterBuilder().withExcludes("email", "address").build()) .build(); SearchHits<Account> searchHits = template.search(query, Account.class); long totalValue = searchHits.getTotalHits(); // 匹配到的数量 for (SearchHit<Account> searchHit : searchHits.getSearchHits()) { Account account = searchHit.getContent(); // 这就是得到的实体类 }
总结
本文介绍了 SpringBoot 整合 ElasticSearch 的两种方案,但均只是简单提及,更详细的用法需要自行查看官方文档。