Skip to content

Commit ba890cb

Browse files
DATAES-504 - Add ReactiveElasticsearchOperations & ReactiveElasticsearchTemplate
ReactiveElasticsearchOperations is the gateway to executing high level commands against an Elasticsearch cluster using the ReactiveElasticsearchClient. The ReactiveElasticsearchTemplate is the default implementation of ReactiveElasticsearchOperations and offers the following set of features. * Read/Write mapping support for domain types. * A rich query and criteria api. * Resource management and Exception translation. To get started the ReactiveElasticsearchTemplate needs to know about the actual client to work with. The easiest way of setting up the ReactiveElasticsearchTemplate is via AbstractReactiveElasticsearchConfiguration providing dedicated configuration method hooks for base package, the initial entity set etc. @configuration public class Config extends AbstractReactiveElasticsearchConfiguration { @bean @OverRide public ReactiveElasticsearchClient reactiveElasticsearchClient() { // ... } } NOTE: If applicable set default HttpHeaders via the ClientConfiguration of the ReactiveElasticsearchClient. TIP: If needed the ReactiveElasticsearchTemplate can be configured with default RefreshPolicy and IndicesOptions that get applied to the related requests by overriding the defaults of refreshPolicy() and indicesOptions(). The ReactiveElasticsearchTemplate lets you save, find and delete your domain objects and map those objects to documents stored in Elasticsearch. @document(indexName = "marvel", type = "characters") public class Person { private @id String id; private String name; private int age; // Getter/Setter omitted... } template.save(new Person("Bruce Banner", 42)) // save a new document .doOnNext(System.out::println) .flatMap(person -> template.findById(person.id, Person.class)) // then go find it .doOnNext(System.out::println) .flatMap(person -> template.delete(person)) // just to remove remove it again .doOnNext(System.out::println) .flatMap(id -> template.count(Person.class)) // so we've got nothing at the end .doOnNext(System.out::println) .subscribe(); // yeah :) The above outputs the following sequence on the console. > Person(id=QjWCWWcBXiLAnp77ksfR, name=Bruce Banner, age=42) > Person(id=QjWCWWcBXiLAnp77ksfR, name=Bruce Banner, age=42) > QjWCWWcBXiLAnp77ksfR > 0 Original Pull Request: spring-projects#229
1 parent a39c340 commit ba890cb

22 files changed

+2638
-223
lines changed

src/main/asciidoc/index.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ include::{spring-data-commons-docs}/repositories.adoc[]
3030
:leveloffset: +1
3131
include::reference/elasticsearch-clients.adoc[]
3232
include::reference/data-elasticsearch.adoc[]
33+
include::reference/reactive-elasticsearch-operations.adoc[]
3334
include::reference/elasticsearch-misc.adoc[]
3435
:leveloffset: -1
3536

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
[[elasticsearch.reactive.operations]]
2+
= Reactive Elasticsearch Operations
3+
4+
`ReactiveElasticsearchOperations` is the gateway to executing high level commands against an Elasticsearch cluster using the `ReactiveElasticsearchClient`.
5+
6+
The `ReactiveElasticsearchTemplate` is the default implementation of `ReactiveElasticsearchOperations` and offers the following set of features.
7+
8+
* Read/Write mapping support for domain types.
9+
* A rich query and criteria api.
10+
* Resource management and Exception translation.
11+
12+
[[elasticsearch.reactive.template]]
13+
== Reactive Elasticsearch Template
14+
15+
To get started the `ReactiveElasticsearchTemplate` needs to know about the actual client to work with.
16+
Please see <<elasticsearch.clients.reactive>> for details on the client.
17+
18+
[[elasticsearch.reactive.template.configuration]]
19+
=== Reactive Template Configuration
20+
21+
The easiest way of setting up the `ReactiveElasticsearchTemplate` is via `AbstractReactiveElasticsearchConfiguration` providing
22+
dedicated configuration method hooks for `base package`, the `initial entity set` etc.
23+
24+
.The AbstractReactiveElasticsearchConfiguration
25+
====
26+
[source,java]
27+
----
28+
@Configuration
29+
public class Config extends AbstractReactiveElasticsearchConfiguration {
30+
31+
@Bean <1>
32+
@Override
33+
public ReactiveElasticsearchClient reactiveElasticsearchClient() {
34+
// ...
35+
}
36+
}
37+
----
38+
<1> Configure the client to use. This can be done by `ReactiveRestClients` or directly via `DefaultReactiveElasticsearchClient`.
39+
====
40+
41+
NOTE: If applicable set default `HttpHeaders` via the `ClientConfiguration` of the `ReactiveElasticsearchClient`.
42+
43+
TIP: If needed the `ReactiveElasticsearchTemplate` can be configured with default `RefreshPolicy` and `IndicesOptions` that get applied to the related requests by overriding the defaults of `refreshPolicy()` and `indicesOptions()`.
44+
45+
However one might want to be more in control over the actual components and use a more verbose approach.
46+
47+
.Configure the ReactiveElasticsearchTemplate
48+
====
49+
[source,java]
50+
----
51+
@Configuration
52+
public class Config {
53+
54+
@Bean <1>
55+
public ReactiveElasticsearchClient reactiveElasticsearchClient() {
56+
// ...
57+
}
58+
59+
@Bean <2>
60+
public ElasticsearchConverter elasticsearchConverter() {
61+
return new MappingElasticsearchConverter(elasticsearchMappingContext());
62+
}
63+
64+
@Bean <3>
65+
public SimpleElasticsearchMappingContext elasticsearchMappingContext() {
66+
return new SimpleElasticsearchMappingContext();
67+
}
68+
69+
@Bean <4>
70+
public ReactiveElasticsearchOperations reactiveElasticsearchOperations() {
71+
return new ReactiveElasticsearchTemplate(reactiveElasticsearchClient(), elasticsearchConverter());
72+
}
73+
}
74+
----
75+
<1> Configure the client to use. This can be done by `ReactiveRestClients` or directly via `DefaultReactiveElasticsearchClient`.
76+
<2> Set up the `ElasticsearchConverter` used for domain type mapping utilizing metadata provided by the mapping context.
77+
<3> The Elasticsearch specific mapping context for domain type metadata.
78+
<4> The actual template based on the client and conversion infrastructure.
79+
====
80+
81+
[[elasticsearch.reactive.template.usage]]
82+
=== Reactive Template Usage
83+
84+
`ReactiveElasticsearchTemplate` lets you save, find and delete your domain objects and map those objects to documents stored in Elasticsearch.
85+
86+
Consider the following:
87+
88+
.Use the ReactiveElasticsearchTemplate
89+
====
90+
[source,java]
91+
----
92+
@Document(indexName = "marvel", type = "characters")
93+
public class Person {
94+
95+
private @Id String id;
96+
private String name;
97+
private int age;
98+
99+
// Getter/Setter omitted...
100+
}
101+
----
102+
103+
[source,java]
104+
----
105+
template.save(new Person("Bruce Banner", 42)) <1>
106+
.doOnNext(System.out::println)
107+
.flatMap(person -> template.findById(person.id, Person.class)) <2>
108+
.doOnNext(System.out::println)
109+
.flatMap(person -> template.delete(person)) <3>
110+
.doOnNext(System.out::println)
111+
.flatMap(id -> template.count(Person.class)) <4>
112+
.doOnNext(System.out::println)
113+
.subscribe(); <5>
114+
----
115+
116+
The above outputs the following sequence on the console.
117+
118+
[source,text]
119+
----
120+
> Person(id=QjWCWWcBXiLAnp77ksfR, name=Bruce Banner, age=42)
121+
> Person(id=QjWCWWcBXiLAnp77ksfR, name=Bruce Banner, age=42)
122+
> QjWCWWcBXiLAnp77ksfR
123+
> 0
124+
----
125+
<1> Insert a new `Person` document into the _marvel_ index under type _characters_. The `id` is generated on server side and set into the instance returned.
126+
<2> Lookup the `Person` with matching `id` in the _marvel_ index under type _characters_.
127+
<3> Delete the `Person` with matching `id`, extracted from the given instance, in the _marvel_ index under type _characters_.
128+
<4> Count the total number of documents in the _marvel_ index under type _characters_.
129+
<5> Don't forget to _subscribe()_.
130+
====
131+
132+

src/main/java/org/springframework/data/elasticsearch/client/reactive/DefaultReactiveElasticsearchClient.java

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@
5656
import org.elasticsearch.common.xcontent.XContentParser;
5757
import org.elasticsearch.common.xcontent.XContentType;
5858
import org.elasticsearch.index.get.GetResult;
59+
import org.elasticsearch.index.reindex.BulkByScrollResponse;
60+
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
5961
import org.elasticsearch.rest.BytesRestResponse;
6062
import org.elasticsearch.rest.RestStatus;
6163
import org.elasticsearch.search.SearchHit;
6264
import org.reactivestreams.Publisher;
63-
import org.springframework.dao.DataAccessResourceFailureException;
65+
import org.springframework.data.elasticsearch.ElasticsearchException;
6466
import org.springframework.data.elasticsearch.client.ClientConfiguration;
6567
import org.springframework.data.elasticsearch.client.ElasticsearchHost;
6668
import org.springframework.data.elasticsearch.client.NoReachableHostException;
@@ -249,7 +251,9 @@ public Mono<UpdateResponse> update(HttpHeaders headers, UpdateRequest updateRequ
249251
*/
250252
@Override
251253
public Mono<DeleteResponse> delete(HttpHeaders headers, DeleteRequest deleteRequest) {
252-
return sendRequest(deleteRequest, RequestCreator.delete(), DeleteResponse.class, headers).publishNext();
254+
255+
return sendRequest(deleteRequest, RequestCreator.delete(), DeleteResponse.class, headers) //
256+
.publishNext();
253257
}
254258

255259
/*
@@ -264,6 +268,16 @@ public Flux<SearchHit> search(HttpHeaders headers, SearchRequest searchRequest)
264268
.flatMap(Flux::fromIterable);
265269
}
266270

271+
/*
272+
* (non-Javadoc)
273+
* @see org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient#ping(org.springframework.http.HttpHeaders, org.elasticsearch.index.reindex.DeleteByQueryRequest)
274+
*/
275+
public Mono<BulkByScrollResponse> deleteBy(HttpHeaders headers, DeleteByQueryRequest deleteRequest) {
276+
277+
return sendRequest(deleteRequest, RequestCreator.deleteByQuery(), BulkByScrollResponse.class, headers) //
278+
.publishNext();
279+
}
280+
267281
/*
268282
* (non-Javadoc)
269283
* @see org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient#ping(org.springframework.data.elasticsearch.client.reactive.ReactiveElasticsearchClient.ReactiveElasticsearchClientCallback)
@@ -364,27 +378,22 @@ private static <T> Mono<T> doDecode(ClientResponse response, Class<T> responseTy
364378

365379
try {
366380

367-
XContentParser contentParser = createParser(mediaType, content);
381+
Method fromXContent = ReflectionUtils.findMethod(responseType, "fromXContent", XContentParser.class);
382+
383+
return Mono.justOrEmpty(responseType
384+
.cast(ReflectionUtils.invokeMethod(fromXContent, responseType, createParser(mediaType, content))));
385+
386+
} catch (Exception errorParseFailure) {
368387

369388
try {
389+
return Mono.error(BytesRestResponse.errorFromXContent(createParser(mediaType, content)));
390+
} catch (Exception e) {
370391

371-
Method fromXContent = ReflectionUtils.findMethod(responseType, "fromXContent", XContentParser.class);
372392
return Mono
373-
.justOrEmpty(responseType.cast(ReflectionUtils.invokeMethod(fromXContent, responseType, contentParser)));
374-
375-
} catch (Exception errorParseFailure) {
376-
try {
377-
return Mono.error(BytesRestResponse.errorFromXContent(contentParser));
378-
} catch (Exception e) {
379-
// return Mono.error to avoid ElasticsearchStatusException to be caught by outer catch.
380-
return Mono.error(new ElasticsearchStatusException("Unable to parse response body",
381-
RestStatus.fromCode(response.statusCode().value())));
382-
}
393+
.error(new ElasticsearchStatusException(content, RestStatus.fromCode(response.statusCode().value())));
383394
}
384-
385-
} catch (IOException e) {
386-
return Mono.error(new DataAccessResourceFailureException("Error parsing XContent.", e));
387395
}
396+
388397
}
389398

390399
private static XContentParser createParser(String mediaType, String content) throws IOException {
@@ -437,6 +446,18 @@ static Function<UpdateRequest, Request> update() {
437446
static Function<DeleteRequest, Request> delete() {
438447
return RequestConverters::delete;
439448
}
449+
450+
static Function<DeleteByQueryRequest, Request> deleteByQuery() {
451+
452+
return request -> {
453+
454+
try {
455+
return RequestConverters.deleteByQuery(request);
456+
} catch (IOException e) {
457+
throw new ElasticsearchException("Could not parse request", e);
458+
}
459+
};
460+
}
440461
}
441462

442463
/**

src/main/java/org/springframework/data/elasticsearch/client/reactive/ReactiveElasticsearchClient.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.elasticsearch.action.update.UpdateRequest;
3434
import org.elasticsearch.action.update.UpdateResponse;
3535
import org.elasticsearch.index.get.GetResult;
36+
import org.elasticsearch.index.reindex.BulkByScrollResponse;
37+
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
3638
import org.elasticsearch.search.SearchHit;
3739
import org.springframework.data.elasticsearch.client.ClientConfiguration;
3840
import org.springframework.data.elasticsearch.client.ElasticsearchHost;
@@ -348,6 +350,44 @@ default Flux<SearchHit> search(SearchRequest searchRequest) {
348350
*/
349351
Flux<SearchHit> search(HttpHeaders headers, SearchRequest searchRequest);
350352

353+
/**
354+
* Execute a {@link DeleteByQueryRequest} against the {@literal delete by query} API.
355+
*
356+
* @param consumer never {@literal null}.
357+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html">Delete By
358+
* Query API on elastic.co</a>
359+
* @return a {@link Mono} emitting the emitting operation response.
360+
*/
361+
default Mono<BulkByScrollResponse> deleteBy(Consumer<DeleteByQueryRequest> consumer) {
362+
363+
DeleteByQueryRequest request = new DeleteByQueryRequest();
364+
consumer.accept(request);
365+
return deleteBy(request);
366+
}
367+
368+
/**
369+
* Execute a {@link DeleteByQueryRequest} against the {@literal delete by query} API.
370+
*
371+
* @param deleteRequest must not be {@literal null}.
372+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html">Delete By
373+
* Query API on elastic.co</a>
374+
* @return a {@link Mono} emitting the emitting operation response.
375+
*/
376+
default Mono<BulkByScrollResponse> deleteBy(DeleteByQueryRequest deleteRequest) {
377+
return deleteBy(HttpHeaders.EMPTY, deleteRequest);
378+
}
379+
380+
/**
381+
* Execute a {@link DeleteByQueryRequest} against the {@literal delete by query} API.
382+
*
383+
* @param headers Use {@link HttpHeaders} to provide eg. authentication data. Must not be {@literal null}.
384+
* @param deleteRequest must not be {@literal null}.
385+
* @see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html">Delete By
386+
* Query API on elastic.co</a>
387+
* @return a {@link Mono} emitting operation response.
388+
*/
389+
Mono<BulkByScrollResponse> deleteBy(HttpHeaders headers, DeleteByQueryRequest deleteRequest);
390+
351391
/**
352392
* Compose the actual command/s to run against Elasticsearch using the underlying {@link WebClient connection}.
353393
* {@link #execute(ReactiveElasticsearchClientCallback) Execute} selects an active server from the available ones and
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.elasticsearch.config;
17+
18+
import org.elasticsearch.client.RestHighLevelClient;
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
21+
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
22+
23+
/**
24+
* @author Christoph Strobl
25+
* @since 4.0
26+
* @see ElasticsearchConfigurationSupport
27+
*/
28+
public abstract class AbstractElasticsearchConfiguration extends ElasticsearchConfigurationSupport {
29+
30+
/**
31+
* Return the {@link RestHighLevelClient} instance used to connect to the cluster. <br />
32+
* Annotate with {@link Bean} in case you want to expose a {@link RestHighLevelClient} instance to the
33+
* {@link org.springframework.context.ApplicationContext}.
34+
*
35+
* @return never {@literal null}.
36+
*/
37+
public abstract RestHighLevelClient elasticsearchClient();
38+
39+
/**
40+
* Creates {@link ElasticsearchOperations}.
41+
*
42+
* @return never {@literal null}.
43+
*/
44+
@Bean
45+
public ElasticsearchOperations elasticsearchOperations() {
46+
return new ElasticsearchRestTemplate(elasticsearchClient(), elasticsearchConverter());
47+
}
48+
}

0 commit comments

Comments
 (0)