引言
Elasticsearch(以下簡稱ES)並不像mysql這麼容易上手,很多java程序員會覺得ES的java客戶端比較難以掌握,儘管ES 7.x官方推薦的high level client
已經相對於早期的TransportClient
好用很多了。
Spring Boot通過整合Spring Data ElasticSearch爲我們提供了非常便捷的檢索功能支持,我們幾乎可以像操作數據庫一樣操作ES了。本篇文章是我肝了4個小時寫出的文章,希望能對大家有所幫助。
看這篇文章的小夥伴,我假設是對ES有基本瞭解的。比如瞭解ES中索引(index),文檔(document),文檔的屬性(field)等基本概念。
本篇最後給出代碼示例的下載地址,小夥們可以放心使用,我的示例都是經過驗證的。
環境準備
ES的環境我是本地搭建的7.1.0的版本,你可以啓動集羣模式或者單節點模式。集羣模式是用不同的端口模擬的僞集羣。具體的過程不是本文的重點這裏不表。
spring boot 使用的是2.3.1.RELEASE
版本。
spring data 使用的是 4.0.1.RELEASE
版本。
示例
引入依賴
首先是maven引入依賴,我這裏引入寫這篇文章時最新的spring data,如下:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>4.0.1.RELEASE</version>
</dependency>
這裏有一點需要強調,4.0.1的spring-data-elasticsearch默認引入的ES客戶端版本是7.6.2,而我前面說了集羣是7.1.0版本。
在啓動的時候會有警告信息告訴我們客戶端和服務端版本不一致。因爲我們是演示DEMO,並不影響。但是在實際項目中我還是建議你儘量保持客戶端和服務端版本的一致。
集羣配置
然後我們需要使用配置文件或者配置類注入ES集羣的客戶端,代碼如下:
@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200", "localhost:9201", "localhost:9202", "localhost:9203")
.build();
return RestClients.create(clientConfiguration).rest();
}
}
前面說過,我是用本地的僞集羣搭建的ES環境,這裏大家根據自己的集羣情況配置地址即可。
測試連通性
我寫個單元測試看看集羣連接是否正常,
@SpringBootTest(classes = {DemoApplication.class})
class DemoApplicationTests {
@Autowired
RestHighLevelClient highLevelClient;
@Test
void testESClient() throws IOException {
GetRequest getRequest= new GetRequest("kibana_sample_data_ecommerce", "V5z1f28BdseAsPClo7bC");
GetResponse getResponse = highLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse.getIndex());
System.out.println(getResponse.toString());
}
}
很簡單,我讀取集羣中名爲kibana_sample_data_ecommerce
的索引(這是kibana自帶的一個電商的索引示例),查詢ID等於V5z1f28BdseAsPClo7bC
的文檔,這個測試程序打印的結果如下:
kibana_sample_data_ecommerce
{"_index":"kibana_sample_data_ecommerce","_type":"_doc","_id":"V5z1f28BdseAsPClo7bC","_version":1,"_seq_no":3,"_primary_term":1,"found":true,"_source":{"category":["Women's Shoes","Women's Clothing"],"currency":"EUR","customer_first_name":"Diane","customer_full_name":"Diane Chandler","customer_gender":"FEMALE","customer_id":22,"customer_last_name":"Chandler","customer_phone":"","day_of_week":"Sunday","day_of_week_i":6,"email":"[email protected]","manufacturer":["Primemaster","Oceanavigations"],"order_date":"2020-01-26T22:58:05+00:00","order_id":584093,"products":[{"base_price":74.99,"discount_percentage":0,"quantity":1,"manufacturer":"Primemaster","tax_amount":0,"product_id":12304,"category":"Women's Shoes","sku":"ZO0360303603","taxless_price":74.99,"unit_discount_amount":0,"min_price":34.5,"_id":"sold_product_584093_12304","discount_amount":0,"created_on":"2016-12-25T22:58:05+00:00","product_name":"High heeled sandals - argento","price":74.99,"taxful_price":74.99,"base_unit_price":74.99},{"base_price":99.99,"discount_percentage":0,"quantity":1,"manufacturer":"Oceanavigations","tax_amount":0,"product_id":19587,"category":"Women's Clothing","sku":"ZO0272002720","taxless_price":99.99,"unit_discount_amount":0,"min_price":47,"_id":"sold_product_584093_19587","discount_amount":0,"created_on":"2016-12-25T22:58:05+00:00","product_name":"Classic coat - black","price":99.99,"taxful_price":99.99,"base_unit_price":99.99}],"sku":["ZO0360303603","ZO0272002720"],"taxful_total_price":174.98,"taxless_total_price":174.98,"total_quantity":2,"total_unique_products":2,"type":"order","user":"diane","geoip":{"country_iso_code":"GB","location":{"lon":-0.1,"lat":51.5},"continent_name":"Europe"}}}
ES實體映射
我們知道ES讀寫其實都是json類型的數據,而我們在客戶端通常是對象實體定義。所以對象實體和json之間的映射也是一個核心概念。
Spring Data ES支持兩種實體映射方案:
- Jackson Object Mapping
- Meta Model Object Mapping
早期的版本默認使用的是 jackson
的方案,但是在4.x之後Meta Model
就上位了,而前者已經不再被支持。所以我們這裏使用第二種方案。我們先定義一個實體類,並通過註解來表明它跟ES實體之間的映射關係。
@Document(indexName = "my_user")
@Data
@ToString
public class UserEsEntity implements Persistable<String> {
@Id
@Nullable
private String id;
@Field(value = "last-name", type = FieldType.Keyword)
private String lastName;
@Field(type = FieldType.Keyword)
private String type;
@Field(type = FieldType.Integer)
private Integer age;
@Nullable @Field(name = "birth-date", type = FieldType.Date, format = DateFormat.basic_date)
private LocalDate birthDate;
@Field(type = FieldType.Boolean)
private Boolean isDeleted;
@Field(type = FieldType.Date, format = DateFormat.basic_date)
private LocalDate createTime;
@Field(type = FieldType.Date, format = DateFormat.basic_date)
private LocalDate updateTime;
@Override
public boolean isNew() {
return id == null || (createTime == null);
}
}
這裏我們指定了索引的名字是my_user
, @Field
的value屬性可以指定字段在ES中的字段名。另外我們看到日期格式我們還可以指定日期的顯示格式。更多的選項我建議你查看官方文檔。本篇文章的最後也有給出官方文檔的地址。
讀寫測試
我們先來往索引寫入文檔試試看。
@Autowired
private ElasticsearchOperations elasticsearchOperations;
@Test
public void testSave() {
UserEsEntity user = new UserEsEntity();
user.setLastName("張三");
user.setAge(29);
user.setBirthDate(LocalDate.ofYearDay(1989, 100));
user.setId("1");
user.setIsDeleted(false);
user.setCreateTime(new Date());
user.setUpdateTime(new Date());
IndexCoordinates indexCoordinates = elasticsearchOperations.getIndexCoordinatesFor(user.getClass());
IndexQuery indexQuery = new IndexQueryBuilder()
.withId(user.getId())
.withObject(user)
.build();
String documentId = elasticsearchOperations.index(indexQuery, indexCoordinates);
}
解釋下幾個點:
- ElasticsearchOperations是spring data es操作ES的一個接口,在4.x的版本它的默認實現是
ElasticsearchRestTemplate
,我們可以通過debug模式看到這一點,如下圖:
- indexCoordinates是4.x新增的一個參數,通過這個參數我們可以再操作ES的時候同時指定多個index。
運行這個測試方法,成功後我們去ES裏查看索引已經被正確的寫入了,如下圖所示:
我們繼續來看寫如何查詢文檔。查詢之前我已經用上面的testSave
方法寫入了幾條不同的文檔。
@Test
public void testQuery() {
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(new MatchAllQueryBuilder())
.build();
SearchHits<UserEsEntity> searchHits = searchOperations.search(searchQuery, UserEsEntity.class);
long count = searchHits.getTotalHits();
System.out.println(count);
List<SearchHit<UserEsEntity>> list = searchHits.getSearchHits();
for (SearchHit hit:list) {
System.out.println(hit.getContent());
}
}
我這裏的示例是查詢索引下的所有文檔MatchAllQueryBuilder
。
用過spring data的同學都知道,它裏面有個核心的概念叫 Repository
。
Repository 接口是 Spring Data 的一個核心接口,它不提供任何方法,開發者需要在自己定義的接口中聲明需要的方法
public interface Repository<T, ID extends Serializable> { }
Spring Data可以讓我們只定義接口,只要遵循 Spring Data的規範,就無需寫實現類。Repository 有幾個常用的子接口:
- CrudRepository: 繼承 Repository,實現了一組 CRUD 相關的方法
- PagingAndSortingRepository: 繼承 CrudRepository,實現了一組分頁排序相關的方法
- JpaRepository: 繼承 PagingAndSortingRepository,實現一組 JPA 規範相關的方法
同樣,我們操作ES其實也可以使用這種方式,這樣即使你不瞭解ES的客戶端也能輕鬆的操作ES了。我們可以把上面的例子重新用Repository
的方式來寫一遍。
首先要做的是定義自己的Repository
,代碼如下:
public interface UserEsRepository extends ElasticsearchRepository<UserEsEntity, String> {
}
是的,你沒看錯,只有這些代碼,不需要任何實現類我們就能實現對ES基本的增刪改查。(當然複雜的操作需要自定義實現)
然後我們寫入一條文檔,
@Autowired
private UserEsRepository repository;
@Test
public void testSave() {
UserEsEntity entity = new UserEsEntity();
entity.setId(UUID.randomUUID().toString());
entity.setAge(50);
entity.setLastName("東野圭吾");
entity.setBirthDate(LocalDate.ofYearDay(1964, 200));
entity.setCreateTime(new Date());
entity.setUpdateTime(new Date());
entity.setIsDeleted(false);
repository.save(entity);
}
繼續來看看查詢,
@Test
public void testFindAll() {
long count = repository.count();
System.out.println(count);
Iterable<UserEsEntity> result = repository.findAll();
Iterator<UserEsEntity> data = result.iterator();
while (data.hasNext()) {
System.out.println(data.next());
}
}
@Test
public void testFindById() {
Optional<UserEsEntity> data = repository.findById("5c7ca0b7-4236-48f1-8ed4-8ce9555092d8");
System.out.println(data.get());
}
哈哈,簡直不能再爽!跟操作數據庫一模一樣。
這篇文章只是帶你入門spring data es的,我不打算講解太複雜的操作,後面打算再寫一篇進階行蹤的文章。
本篇文章使用的代碼示例已經上傳github,感興趣的可以下載。
https://github.com/pony-maggie/spring-data-es-demo
參考:
- https://spring.io/blog/2020/05/27/what-s-new-in-spring-data-elasticsearch-4-0
- https://docs.spring.io/spring-data/elasticsearch/docs/4.0.x/reference/html/#preface
- https://github.com/spring-projects/spring-data-elasticsearch