Skip to content

Commit 89f766e

Browse files
ESQL: Views REST API
This is extracted from the Views Prototype at #134995 and is originally based on the "ESQL: View" prototype by Nik but with the following changes: * No ES|QL language integration, only REST API for adding/removing and managing Views in cluster state * ViewMetadata now inside ProjectMetadata * NodeFeature to ensure metadata only exists in clusters fully upgraded * Wide CRUD support (add, update, delete, list) * View name validation * View count, length and depth restrictions Co-authored-by: Craig Taverner <[email protected]>
1 parent 18968ba commit 89f766e

File tree

33 files changed

+2218
-5
lines changed

33 files changed

+2218
-5
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"esql.delete_view": {
3+
"documentation": {
4+
"url": "https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-view-delete",
5+
"description": "Delete a non-materialized VIEW for ESQL."
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [
11+
"application/json"
12+
],
13+
"content_type": [
14+
"application/json"
15+
]
16+
},
17+
"url": {
18+
"paths": [
19+
{
20+
"path": "/_query/view/{name}",
21+
"methods": [
22+
"DELETE"
23+
],
24+
"parts": {
25+
"name": {
26+
"type": "string",
27+
"description": "The name of the view to delete"
28+
}
29+
}
30+
}
31+
]
32+
}
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"esql.get_view": {
3+
"documentation": {
4+
"url": "https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-view-get",
5+
"description": "Get a non-materialized VIEW for ESQL."
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [
11+
"application/json"
12+
],
13+
"content_type": [
14+
"application/json"
15+
]
16+
},
17+
"url": {
18+
"paths": [
19+
{
20+
"path": "/_query/view/{name}",
21+
"methods": [
22+
"GET"
23+
],
24+
"parts": {
25+
"name": {
26+
"type": "string",
27+
"description": "The name of the view to get"
28+
}
29+
}
30+
}
31+
]
32+
}
33+
}
34+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"esql.list_views": {
3+
"documentation": {
4+
"url": null,
5+
"description": "List all defined non-materialized ES|QL views"
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [
11+
"application/json"
12+
],
13+
"content_type": [
14+
"application/json"
15+
]
16+
},
17+
"url": {
18+
"paths": [
19+
{
20+
"path": "/_query/views",
21+
"methods": [
22+
"GET"
23+
]
24+
}
25+
]
26+
}
27+
}
28+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"esql.put_view": {
3+
"documentation": {
4+
"url": "https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-view-put",
5+
"description": "Creates a non-materialized VIEW for ESQL."
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [
11+
"application/json"
12+
],
13+
"content_type": [
14+
"application/json"
15+
]
16+
},
17+
"url": {
18+
"paths": [
19+
{
20+
"path": "/_query/view/{name}",
21+
"methods": [
22+
"PUT"
23+
],
24+
"parts": {
25+
"name": {
26+
"type": "string",
27+
"description": "The name of the view to create or update"
28+
}
29+
}
30+
}
31+
]
32+
},
33+
"body": {
34+
"description": "Use the `query` element to define the ES|QL query to use as a non-materialized VIEW.",
35+
"required": true
36+
}
37+
}
38+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9216000
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
inference_api_eis_authorization_persistent_task,9215000
1+
esql_views,9216000

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlFeatures.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,15 @@ public class EsqlFeatures implements FeatureSpecification {
3434
*/
3535
public static final NodeFeature METRICS_SYNTAX = new NodeFeature("esql.metrics_syntax");
3636

37+
/**
38+
* Cluster support for view management (create, get, update, list)
39+
* This marks that the cluster state has support for the ViewMetadata CustomProject
40+
*/
41+
public static final NodeFeature ESQL_VIEWS = new NodeFeature("esql.views");
42+
3743
private Set<NodeFeature> snapshotBuildFeatures() {
3844
assert Build.current().isSnapshot() : Build.current();
39-
return Set.of(METRICS_SYNTAX);
45+
return Set.of(METRICS_SYNTAX, ESQL_VIEWS);
4046
}
4147

4248
@Override

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package org.elasticsearch.xpack.esql.plugin;
88

99
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
10+
import org.elasticsearch.cluster.metadata.Metadata;
1011
import org.elasticsearch.cluster.node.DiscoveryNodes;
1112
import org.elasticsearch.common.breaker.CircuitBreaker;
1213
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
@@ -48,6 +49,8 @@
4849
import org.elasticsearch.threadpool.ExecutorBuilder;
4950
import org.elasticsearch.threadpool.FixedExecutorBuilder;
5051
import org.elasticsearch.threadpool.ThreadPool;
52+
import org.elasticsearch.xcontent.NamedXContentRegistry;
53+
import org.elasticsearch.xcontent.ParseField;
5154
import org.elasticsearch.xpack.core.XPackPlugin;
5255
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
5356
import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
@@ -74,6 +77,7 @@
7477
import org.elasticsearch.xpack.esql.enrich.LookupFromIndexOperator;
7578
import org.elasticsearch.xpack.esql.execution.PlanExecutor;
7679
import org.elasticsearch.xpack.esql.expression.ExpressionWritables;
80+
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
7781
import org.elasticsearch.xpack.esql.io.stream.ExpressionQueryBuilder;
7882
import org.elasticsearch.xpack.esql.io.stream.PlanStreamWrapperQueryBuilder;
7983
import org.elasticsearch.xpack.esql.plan.PlanWritables;
@@ -82,6 +86,21 @@
8286
import org.elasticsearch.xpack.esql.querydsl.query.SingleValueQuery;
8387
import org.elasticsearch.xpack.esql.querylog.EsqlQueryLog;
8488
import org.elasticsearch.xpack.esql.session.IndexResolver;
89+
import org.elasticsearch.xpack.esql.view.ClusterViewService;
90+
import org.elasticsearch.xpack.esql.view.DeleteViewAction;
91+
import org.elasticsearch.xpack.esql.view.GetViewAction;
92+
import org.elasticsearch.xpack.esql.view.ListViewsAction;
93+
import org.elasticsearch.xpack.esql.view.PutViewAction;
94+
import org.elasticsearch.xpack.esql.view.RestDeleteViewAction;
95+
import org.elasticsearch.xpack.esql.view.RestGetViewAction;
96+
import org.elasticsearch.xpack.esql.view.RestListViewsAction;
97+
import org.elasticsearch.xpack.esql.view.RestPutViewAction;
98+
import org.elasticsearch.xpack.esql.view.TransportDeleteViewAction;
99+
import org.elasticsearch.xpack.esql.view.TransportGetViewAction;
100+
import org.elasticsearch.xpack.esql.view.TransportListViewsAction;
101+
import org.elasticsearch.xpack.esql.view.TransportPutViewAction;
102+
import org.elasticsearch.xpack.esql.view.ViewMetadata;
103+
import org.elasticsearch.xpack.esql.view.ViewService;
85104

86105
import java.lang.invoke.MethodHandles;
87106
import java.util.ArrayList;
@@ -182,6 +201,14 @@ public Collection<?> createComponents(PluginServices services) {
182201
);
183202
BigArrays bigArrays = services.indicesService().getBigArrays().withCircuitBreaking();
184203
var blockFactoryProvider = blockFactoryProvider(circuitBreaker, bigArrays, maxPrimitiveArrayBlockSize);
204+
EsqlFunctionRegistry functionRegistry = new EsqlFunctionRegistry();
205+
ViewService viewService = new ClusterViewService(
206+
functionRegistry,
207+
services.clusterService(),
208+
services.featureService(),
209+
services.projectResolver(),
210+
ViewService.ViewServiceConfig.fromSettings(settings)
211+
);
185212
setupSharedSecrets();
186213
List<BiConsumer<LogicalPlan, Failures>> extraCheckers = extraCheckerProviders.stream()
187214
.flatMap(p -> p.checkers(services.projectResolver(), services.clusterService()).stream())
@@ -201,7 +228,9 @@ public Collection<?> createComponents(PluginServices services) {
201228
ThreadPool.Names.SEARCH,
202229
blockFactoryProvider.blockFactory()
203230
),
204-
blockFactoryProvider
231+
blockFactoryProvider,
232+
functionRegistry,
233+
viewService
205234
);
206235
}
207236

@@ -264,7 +293,11 @@ public List<ActionHandler> getActions() {
264293
new ActionHandler(EsqlSearchShardsAction.TYPE, EsqlSearchShardsAction.class),
265294
new ActionHandler(EsqlAsyncStopAction.INSTANCE, TransportEsqlAsyncStopAction.class),
266295
new ActionHandler(EsqlListQueriesAction.INSTANCE, TransportEsqlListQueriesAction.class),
267-
new ActionHandler(EsqlGetQueryAction.INSTANCE, TransportEsqlGetQueryAction.class)
296+
new ActionHandler(EsqlGetQueryAction.INSTANCE, TransportEsqlGetQueryAction.class),
297+
new ActionHandler(PutViewAction.INSTANCE, TransportPutViewAction.class),
298+
new ActionHandler(DeleteViewAction.INSTANCE, TransportDeleteViewAction.class),
299+
new ActionHandler(GetViewAction.INSTANCE, TransportGetViewAction.class),
300+
new ActionHandler(ListViewsAction.INSTANCE, TransportListViewsAction.class)
268301
);
269302
}
270303

@@ -286,7 +319,11 @@ public List<RestHandler> getRestHandlers(
286319
new RestEsqlGetAsyncResultAction(),
287320
new RestEsqlStopAsyncAction(),
288321
new RestEsqlDeleteAsyncResultAction(),
289-
new RestEsqlListQueriesAction()
322+
new RestEsqlListQueriesAction(),
323+
new RestPutViewAction(),
324+
new RestDeleteViewAction(),
325+
new RestGetViewAction(),
326+
new RestListViewsAction()
290327
);
291328
}
292329

@@ -315,12 +352,22 @@ public List<NamedWriteableRegistry.Entry> getNamedWriteables() {
315352

316353
entries.add(ExpressionQueryBuilder.ENTRY);
317354
entries.add(PlanStreamWrapperQueryBuilder.ENTRY);
355+
entries.add(ViewMetadata.ENTRY);
318356

319357
entries.addAll(ExpressionWritables.getNamedWriteables());
320358
entries.addAll(PlanWritables.getNamedWriteables());
321359
return entries;
322360
}
323361

362+
@Override
363+
public List<NamedXContentRegistry.Entry> getNamedXContent() {
364+
List<NamedXContentRegistry.Entry> namedXContent = new ArrayList<>();
365+
namedXContent.add(
366+
new NamedXContentRegistry.Entry(Metadata.ProjectCustom.class, new ParseField(ViewMetadata.TYPE), ViewMetadata::fromXContent)
367+
);
368+
return namedXContent;
369+
}
370+
324371
public List<ExecutorBuilder<?>> getExecutorBuilders(Settings settings) {
325372
final int allocatedProcessors = EsExecutors.allocatedProcessors(settings);
326373
return List.of(
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.view;
9+
10+
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.cluster.ClusterState;
12+
import org.elasticsearch.cluster.ClusterStateUpdateTask;
13+
import org.elasticsearch.cluster.metadata.ProjectMetadata;
14+
import org.elasticsearch.cluster.project.ProjectResolver;
15+
import org.elasticsearch.cluster.service.ClusterService;
16+
import org.elasticsearch.core.SuppressForbidden;
17+
import org.elasticsearch.features.FeatureService;
18+
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
19+
import org.elasticsearch.xpack.esql.plugin.EsqlFeatures;
20+
21+
import java.util.Map;
22+
import java.util.function.Function;
23+
24+
/**
25+
* Implementation of {@link ViewService} that keeps the views in the cluster state.
26+
*/
27+
public class ClusterViewService extends ViewService {
28+
private final ClusterService clusterService;
29+
private final FeatureService featureService;
30+
private final ProjectResolver projectResolver;
31+
32+
public ClusterViewService(
33+
EsqlFunctionRegistry functionRegistry,
34+
ClusterService clusterService,
35+
FeatureService featureService,
36+
ProjectResolver projectResolver,
37+
ViewServiceConfig config
38+
) {
39+
super(functionRegistry, config);
40+
this.clusterService = clusterService;
41+
this.featureService = featureService;
42+
this.projectResolver = projectResolver;
43+
}
44+
45+
@Override
46+
protected ViewMetadata getMetadata() {
47+
return getMetadata(clusterService.state());
48+
}
49+
50+
protected ViewMetadata getMetadata(ClusterState clusterState) {
51+
return getProjectMetadata(clusterState).custom(ViewMetadata.TYPE, ViewMetadata.EMPTY);
52+
}
53+
54+
protected ProjectMetadata getProjectMetadata(ClusterState clusterState) {
55+
return projectResolver.getProjectMetadata(clusterService.state());
56+
}
57+
58+
@Override
59+
protected void updateViewMetadata(ActionListener<Void> callback, Function<ViewMetadata, Map<String, View>> function) {
60+
submitUnbatchedTask("update-esql-view-metadata", new ClusterStateUpdateTask() {
61+
@Override
62+
public ClusterState execute(ClusterState currentState) {
63+
var project = getProjectMetadata(currentState);
64+
var views = project.custom(ViewMetadata.TYPE, ViewMetadata.EMPTY);
65+
Map<String, View> policies = function.apply(views);
66+
var metadata = ProjectMetadata.builder(project).putCustom(ViewMetadata.TYPE, new ViewMetadata(policies));
67+
return ClusterState.builder(currentState).putProjectMetadata(metadata).build();
68+
}
69+
70+
@Override
71+
public void clusterStateProcessed(ClusterState oldState, ClusterState newState) {
72+
callback.onResponse(null);
73+
}
74+
75+
@Override
76+
public void onFailure(Exception e) {
77+
callback.onFailure(e);
78+
}
79+
});
80+
}
81+
82+
@SuppressForbidden(reason = "legacy usage of unbatched task") // TODO add support for batching here
83+
private void submitUnbatchedTask(@SuppressWarnings("SameParameterValue") String source, ClusterStateUpdateTask task) {
84+
clusterService.submitUnbatchedStateUpdateTask(source, task);
85+
}
86+
87+
@Override
88+
protected void assertMasterNode() {
89+
assert clusterService.localNode().isMasterNode();
90+
}
91+
92+
@Override
93+
protected boolean viewsFeatureEnabled() {
94+
return featureService.clusterHasFeature(clusterService.state(), EsqlFeatures.ESQL_VIEWS);
95+
}
96+
}

0 commit comments

Comments
 (0)