前言
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
文件: 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 的兩種方案,但均只是簡單提及,更詳細的用法需要自行檢視官方文件。