ElasticSearch8 - SpringBoot整合ElasticSearch

王谷雨發表於2024-03-25

前言

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

相關文章