Skip to content

Commit 3f92995

Browse files
Add documentation for plugin-defined custom variants (#371) (#374)
Co-authored-by: Sylvain Wallez <[email protected]>
1 parent a18cb67 commit 3f92995

File tree

3 files changed

+161
-3
lines changed

3 files changed

+161
-3
lines changed

docs/api-conventions/variant-types.asciidoc

+46
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,50 @@ include-tagged::{doc-tests-src}/api_conventions/ApiConventionsTest.java[variant-
5757
<2> Test a larger set of variant kinds.
5858
<3> Get the kind and value held by the variant object.
5959

60+
[discrete]
61+
[[variant-types-custom]]
62+
==== Custom extensions provided by {es} plugins
63+
64+
{es} accepts plugins that can extend the available variants for a number of types. This includes queries, aggregations, text analyzers and tokenizers, ingest processors, etc.
65+
66+
The {java-client} classes for these types accept a `_custom` variant in addition to the builtin ones. This allows you to use these plugin-defined extensions by providing arbitrary JSON in requests, and also receive arbitrary JSON produced by the plugins in responses.
67+
68+
In the examples below we use a hypothetical plugin that adds a `sphere-distance` aggregation that groups documents containing 3D coordinates according to their distance to a reference location.
69+
70+
To create a custom aggregation, use the `_custom()` aggregation type and provide its identifier, defined by the plugin, and parameters. The parameters can be any object or value that can be serialized to JSON. In the example below we use a simple map:
71+
72+
["source","java"]
73+
--------------------------------------------------
74+
include-tagged::{doc-tests-src}/api_conventions/ApiConventionsTest.java[custom-variant-creation]
75+
--------------------------------------------------
76+
<1> Parameters for the custom aggregation.
77+
<2> Create a custom aggregation named `neighbors` of kind `sphere-distance` with its parameters.
78+
79+
The results of custom variants are returned as raw JSON represented by a `JsonData` object. You can then traverse the JSON tree to get the data. Since this is not always convenient, you can also define classes that represent that JSON data and deserialize them from the raw JSON.
80+
81+
Traversing the JSON tree:
82+
83+
["source","java"]
84+
--------------------------------------------------
85+
include-tagged::{doc-tests-src}/api_conventions/ApiConventionsTest.java[custom-variant-navigation-json]
86+
--------------------------------------------------
87+
<1> Use `Void` if you're only interested in aggregation results, not search hits (see also <<aggregations>>).
88+
<2> Get the `neighbors` aggregation result as custom JSON result.
89+
<3> Traverse the JSON tree to extract the result data.
90+
91+
Using a class that represents the custom aggregation results:
92+
93+
["source","java"]
94+
--------------------------------------------------
95+
include-tagged::{doc-tests-src}/api_conventions/ApiConventionsTest.java[custom-variant-navigation-typed]
96+
--------------------------------------------------
97+
<1> Deserialize the custom JSON to a dedicated `SphereDistanceAggregate` class.
98+
99+
Where `SphereDistanceAggregate` can be defined as follows:
100+
["source","java"]
101+
--------------------------------------------------
102+
include-tagged::{doc-tests-src}/api_conventions/ApiConventionsTest.java[custom-variant-types]
103+
--------------------------------------------------
104+
105+
60106
{doc-tests-blurb}

java-client/src/test/java/co/elastic/clients/documentation/DocTestsTransport.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import co.elastic.clients.transport.ElasticsearchTransport;
2525
import co.elastic.clients.transport.Endpoint;
2626
import co.elastic.clients.transport.TransportOptions;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
2729

2830
import javax.annotation.Nullable;
2931
import java.io.IOException;
@@ -40,7 +42,9 @@
4042
*/
4143
public class DocTestsTransport implements ElasticsearchTransport {
4244

43-
private final JsonpMapper mapper = new JacksonJsonpMapper();
45+
private final JsonpMapper mapper = new JacksonJsonpMapper(
46+
new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
47+
);
4448

4549
private final ThreadLocal<Object> result = new ThreadLocal<>();
4650

java-client/src/test/java/co/elastic/clients/documentation/api_conventions/ApiConventionsTest.java

+110-2
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@
3131
import co.elastic.clients.elasticsearch.indices.Alias;
3232
import co.elastic.clients.elasticsearch.indices.CreateIndexRequest;
3333
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
34-
import co.elastic.clients.transport.ElasticsearchTransport;
34+
import co.elastic.clients.json.JsonData;
3535
import co.elastic.clients.util.ApiTypeHelper;
36+
import com.fasterxml.jackson.annotation.JsonCreator;
37+
import com.fasterxml.jackson.annotation.JsonProperty;
38+
import com.fasterxml.jackson.databind.ObjectMapper;
39+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
40+
import jakarta.json.JsonArray;
41+
import jakarta.json.JsonObject;
42+
import jakarta.json.JsonValue;
3643
import org.junit.jupiter.api.Assertions;
3744
import org.junit.jupiter.api.Test;
3845
import org.slf4j.Logger;
3946
import org.slf4j.LoggerFactory;
4047

48+
import java.io.StringReader;
4149
import java.util.Arrays;
4250
import java.util.HashMap;
4351
import java.util.List;
@@ -47,7 +55,7 @@ public class ApiConventionsTest extends Assertions {
4755

4856
private static class SomeApplicationData {}
4957

50-
private ElasticsearchTransport transport = new DocTestsTransport();
58+
private DocTestsTransport transport = new DocTestsTransport();
5159
Logger logger = LoggerFactory.getLogger(this.getClass());
5260

5361
public void blockingAndAsync() throws Exception {
@@ -121,6 +129,8 @@ public void builderLambdasShort() throws Exception {
121129
}
122130

123131
public void builderIntervals() throws Exception {
132+
ObjectMapper mapper = new ObjectMapper();
133+
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
124134
ElasticsearchClient client = new ElasticsearchClient(transport);
125135

126136
//tag::builder-intervals
@@ -193,6 +203,104 @@ public void variantCreation() {
193203
//end::variant-kind
194204
}
195205

206+
//tag::custom-variant-types
207+
public static class SphereDistanceAggregate {
208+
private final List<Bucket> buckets;
209+
@JsonCreator
210+
public SphereDistanceAggregate(
211+
@JsonProperty("buckets") List<Bucket> buckets
212+
) {
213+
this.buckets = buckets;
214+
}
215+
public List<Bucket> buckets() {
216+
return buckets;
217+
};
218+
}
219+
220+
public static class Bucket {
221+
private final double key;
222+
private final double docCount;
223+
@JsonCreator
224+
public Bucket(
225+
@JsonProperty("key") double key,
226+
@JsonProperty("doc_count") double docCount) {
227+
this.key = key;
228+
this.docCount = docCount;
229+
}
230+
public double key() {
231+
return key;
232+
}
233+
public double docCount() {
234+
return docCount;
235+
}
236+
}
237+
//end::custom-variant-types
238+
239+
@Test
240+
public void customVariants() throws Exception {
241+
242+
ElasticsearchClient esClient = new ElasticsearchClient(transport);
243+
244+
String json = "{\"took\":1,\"timed_out\":false,\"_shards\":{\"failed\":0.0,\"successful\":1.0,\"total\":1.0},\n" +
245+
"\"hits\":{\"total\":{\"relation\":\"eq\",\"value\":0},\"hits\":[]},\n" +
246+
"\"aggregations\":{\"sphere-distance#neighbors\":{\"buckets\":[{\"key\": 1.0,\"doc_count\":1}]}}}";
247+
248+
transport.setResult(SearchResponse.of(b -> b.withJson(
249+
transport.jsonpMapper().jsonProvider().createParser(new StringReader(json)),
250+
transport.jsonpMapper())
251+
));
252+
253+
//tag::custom-variant-creation
254+
Map<String, Object> params = new HashMap<>(); // <1>
255+
params.put("interval", 10);
256+
params.put("scale", "log");
257+
params.put("origin", new Double[]{145.0, 12.5, 1649.0});
258+
259+
SearchRequest request = SearchRequest.of(r -> r
260+
.index("stars")
261+
.aggregations("neighbors", agg -> agg
262+
._custom("sphere-distance", params) // <2>
263+
)
264+
);
265+
//end::custom-variant-creation
266+
267+
{
268+
//tag::custom-variant-navigation-json
269+
SearchResponse<Void> response = esClient.search(request, Void.class); // <1>
270+
271+
JsonData neighbors = response
272+
.aggregations().get("neighbors")
273+
._custom(); // <2>
274+
275+
JsonArray buckets = neighbors.toJson() // <3>
276+
.asJsonObject()
277+
.getJsonArray("buckets");
278+
279+
for (JsonValue item : buckets) {
280+
JsonObject bucket = item.asJsonObject();
281+
double key = bucket.getJsonNumber("key").doubleValue();
282+
double docCount = bucket.getJsonNumber("doc_count").longValue();
283+
doSomething(key, docCount);
284+
}
285+
//end::custom-variant-navigation-json
286+
}
287+
288+
{
289+
//tag::custom-variant-navigation-typed
290+
SearchResponse<Void> response = esClient.search(request, Void.class);
291+
292+
SphereDistanceAggregate neighbors = response
293+
.aggregations().get("neighbors")
294+
._custom()
295+
.to(SphereDistanceAggregate.class); // <1>
296+
297+
for (Bucket bucket : neighbors.buckets()) {
298+
doSomething(bucket.key(), bucket.docCount());
299+
}
300+
//end::custom-variant-navigation-typed
301+
}
302+
}
303+
196304
@Test
197305
public void collections() {
198306
//tag::collections-list

0 commit comments

Comments
 (0)