Skip to content

Commit 8e47814

Browse files
Add ES|QL helpers (#762) (#763)
Adds helpers for ES|QL. The ES|QL result format is meant to be compact: it is composed of a metadata part giving field names and their types and a 2D array of values, which isn't easy to use in application code. This PR provides adapters that convert the ES|QL JSON result format into higher level types that are easier to use. Two adapters are provided: * An `ObjectsAdatper` that combines field names and values from the array to build a collection of objects using JSON to object mapping * A `ResultSetAdpater` that provides an implementation of the well-known JDBC `ResultSet`. This is a cursor-based API where the application can inspect at runtime the type and names of the ES|QL results, and is therefore more suited for ad-hoc or dynamic queries where the result structure isn't known in advance. Along with adapters, additional methods in `ElasticsearchEsqlClient` provide simple way to send queries using just a string and optional parameters when you don't need to specify additional request details. Co-authored-by: Sylvain Wallez <sylvain@elastic.co>
1 parent 7bec154 commit 8e47814

25 files changed

+4066
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.elasticsearch._helpers.esql;
21+
22+
import co.elastic.clients.ApiClient;
23+
import co.elastic.clients.elasticsearch.esql.QueryRequest;
24+
import co.elastic.clients.transport.ElasticsearchTransport;
25+
import co.elastic.clients.transport.endpoints.BinaryResponse;
26+
27+
import java.io.IOException;
28+
29+
/**
30+
* A deserializer for ES|QL responses.
31+
*/
32+
public interface EsqlAdapter<Result> {
33+
/**
34+
* ESQL result format this deserializer accepts (text, csv, json, arrow, etc.)
35+
*/
36+
String format();
37+
38+
/**
39+
* For JSON results, whether the result should be organized in rows or columns
40+
*/
41+
boolean columnar();
42+
43+
/**
44+
* Deserialize the raw http response returned by the server
45+
*/
46+
Result deserialize(ApiClient<ElasticsearchTransport, ?> client, QueryRequest request, BinaryResponse response) throws IOException;
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.elasticsearch._helpers.esql;
21+
22+
import co.elastic.clients.json.JsonpDeserializer;
23+
import co.elastic.clients.json.JsonpMapper;
24+
import co.elastic.clients.json.JsonpMappingException;
25+
import co.elastic.clients.json.JsonpUtils;
26+
import jakarta.json.stream.JsonParser;
27+
28+
import java.util.List;
29+
30+
public abstract class EsqlAdapterBase<T> implements EsqlAdapter<T> {
31+
32+
/**
33+
* Reads the header of an ES|QL response, moving the parser at the beginning of the first value row.
34+
* The caller can then read row arrays until finding an end array that closes the top-level array.
35+
*/
36+
public static EsqlMetadata readHeader(JsonParser parser, JsonpMapper mapper) {
37+
JsonpUtils.expectNextEvent(parser, JsonParser.Event.START_OBJECT);
38+
JsonpUtils.expectNextEvent(parser, JsonParser.Event.KEY_NAME);
39+
40+
if (!"columns".equals(parser.getString())) {
41+
throw new JsonpMappingException("Expecting a 'columns' property, but found '" + parser.getString() + "'", parser.getLocation());
42+
}
43+
44+
List<EsqlMetadata.EsqlColumn> columns = JsonpDeserializer
45+
.arrayDeserializer(EsqlMetadata.EsqlColumn._DESERIALIZER)
46+
.deserialize(parser, mapper);
47+
48+
EsqlMetadata result = new EsqlMetadata();
49+
result.columns = columns;
50+
51+
JsonpUtils.expectNextEvent(parser, JsonParser.Event.KEY_NAME);
52+
53+
if (!"values".equals(parser.getString())) {
54+
throw new JsonpMappingException("Expecting a 'values' property, but found '" + parser.getString() + "'", parser.getLocation());
55+
}
56+
57+
JsonpUtils.expectNextEvent(parser, JsonParser.Event.START_ARRAY);
58+
59+
return result;
60+
}
61+
62+
/**
63+
* Checks the footer of an ES|QL response, once the values have been read.
64+
*/
65+
public static void readFooter(JsonParser parser) {
66+
JsonpUtils.expectNextEvent(parser, JsonParser.Event.END_OBJECT);
67+
}
68+
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.elasticsearch._helpers.esql;
21+
22+
import co.elastic.clients.elasticsearch._types.FieldValue;
23+
import co.elastic.clients.elasticsearch.esql.ElasticsearchEsqlAsyncClient;
24+
import co.elastic.clients.elasticsearch.esql.ElasticsearchEsqlClient;
25+
import co.elastic.clients.elasticsearch.esql.QueryRequest;
26+
import co.elastic.clients.json.JsonData;
27+
import co.elastic.clients.transport.endpoints.BinaryResponse;
28+
29+
import java.io.IOException;
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.concurrent.CompletableFuture;
33+
34+
public class EsqlHelper {
35+
36+
//----- Synchronous
37+
38+
public static <T> T query(
39+
ElasticsearchEsqlClient client, EsqlAdapter<T> adapter, String query, Object... params
40+
) throws IOException {
41+
QueryRequest request = buildRequest(adapter, query, params);
42+
BinaryResponse response = client.query(request);
43+
return adapter.deserialize(client, request, response);
44+
}
45+
46+
public static <T> T query(ElasticsearchEsqlClient client, EsqlAdapter<T> adapter, QueryRequest request) throws IOException {
47+
request = buildRequest(adapter, request);
48+
BinaryResponse response = client.query(request);
49+
return adapter.deserialize(client, request, response);
50+
}
51+
52+
//----- Asynchronous
53+
54+
public static <T> CompletableFuture<T> queryAsync(
55+
ElasticsearchEsqlAsyncClient client, EsqlAdapter<T> adapter, String query, Object... params
56+
) {
57+
return doQueryAsync(client, adapter, buildRequest(adapter, query, params));
58+
}
59+
60+
public static <T> CompletableFuture<T> queryAsync(
61+
ElasticsearchEsqlAsyncClient client, EsqlAdapter<T> adapter, QueryRequest request
62+
) {
63+
return doQueryAsync(client, adapter, buildRequest(adapter, request));
64+
}
65+
66+
private static <T> CompletableFuture<T> doQueryAsync(
67+
ElasticsearchEsqlAsyncClient client, EsqlAdapter<T> adapter, QueryRequest request
68+
) {
69+
return client
70+
.query(request)
71+
.thenApply(r -> {
72+
try {
73+
return adapter.deserialize(client, request, r);
74+
} catch (IOException e) {
75+
throw new RuntimeException(e);
76+
}
77+
});
78+
}
79+
80+
//----- Utilities
81+
82+
private static QueryRequest buildRequest(EsqlAdapter<?> adapter, String query, Object... params) {
83+
return QueryRequest.of(esql -> esql
84+
.format(adapter.format())
85+
.columnar(adapter.columnar())
86+
.query(query)
87+
.params(asFieldValues(params))
88+
);
89+
}
90+
91+
private static QueryRequest buildRequest(EsqlAdapter<?> adapter, QueryRequest request) {
92+
return QueryRequest.of(q -> q
93+
// Set/override format and columnar
94+
.format(adapter.format())
95+
.columnar(adapter.columnar())
96+
97+
.delimiter(request.delimiter())
98+
.filter(request.filter())
99+
.locale(request.locale())
100+
.params(request.params())
101+
.query(request.query())
102+
);
103+
}
104+
105+
private static List<FieldValue> asFieldValues(Object... objects) {
106+
107+
List<FieldValue> result = new ArrayList<>(objects.length);
108+
for (Object object: objects) {
109+
result.add(FieldValue.of(JsonData.of(object)));
110+
}
111+
112+
return result;
113+
}
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.elasticsearch._helpers.esql;
21+
22+
import co.elastic.clients.json.JsonpDeserializable;
23+
import co.elastic.clients.json.JsonpDeserializer;
24+
import co.elastic.clients.json.ObjectBuilderDeserializer;
25+
import co.elastic.clients.json.ObjectDeserializer;
26+
import co.elastic.clients.util.ObjectBuilder;
27+
import co.elastic.clients.util.ObjectBuilderBase;
28+
29+
import java.util.List;
30+
31+
public class EsqlMetadata {
32+
33+
@JsonpDeserializable
34+
public static class EsqlColumn {
35+
private String name;
36+
private String type;
37+
38+
public String name() {
39+
return name;
40+
}
41+
42+
public String type() {
43+
return type;
44+
}
45+
46+
public static class Builder extends ObjectBuilderBase implements ObjectBuilder<EsqlColumn> {
47+
EsqlColumn object = new EsqlColumn();
48+
49+
public Builder name(String value) {
50+
object.name = value;
51+
return this;
52+
}
53+
54+
public Builder type(String value) {
55+
object.type = value;
56+
return this;
57+
}
58+
59+
@Override
60+
public EsqlColumn build() {
61+
_checkSingleUse();
62+
return object;
63+
}
64+
}
65+
66+
public static final JsonpDeserializer<EsqlColumn> _DESERIALIZER = ObjectBuilderDeserializer.lazy(
67+
EsqlColumn.Builder::new, EsqlColumn::setupEsqlColumnDeserializer
68+
);
69+
70+
protected static void setupEsqlColumnDeserializer(ObjectDeserializer<EsqlColumn.Builder> op) {
71+
op.add(EsqlColumn.Builder::name, JsonpDeserializer.stringDeserializer(), "name");
72+
op.add(EsqlColumn.Builder::type, JsonpDeserializer.stringDeserializer(), "type");
73+
}
74+
}
75+
76+
public List<EsqlColumn> columns;
77+
}

0 commit comments

Comments
 (0)