Browse Source

Add _render_query API call for Search Applications (#95513)

Kathleen DeRusso 2 years ago
parent
commit
ba93ddb11d
20 changed files with 910 additions and 153 deletions
  1. 85 0
      docs/reference/search-application/apis/search-application-render-query.asciidoc
  2. 69 0
      docs/reference/search-application/apis/search-application-search.asciidoc
  3. 38 0
      rest-api-spec/src/main/resources/rest-api-spec/api/search_application.render_query.json
  4. 1 0
      x-pack/plugin/ent-search/qa/rest/roles.yml
  5. 9 0
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/20_search_application_put.yml
  6. 154 0
      x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/52_search_application_render_query.yml
  7. 6 1
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearch.java
  8. 3 2
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/SearchApplicationTemplate.java
  9. 73 0
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/SearchApplicationTemplateService.java
  10. 0 97
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/QuerySearchApplicationAction.java
  11. 72 0
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RenderSearchApplicationQueryAction.java
  12. 4 5
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RestQuerySearchApplicationAction.java
  13. 47 0
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RestRenderSearchApplicationQueryAction.java
  14. 105 0
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/SearchApplicationSearchRequest.java
  15. 9 41
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/TransportQuerySearchApplicationAction.java
  16. 71 0
      x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/TransportRenderSearchApplicationQueryAction.java
  17. 69 0
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/action/RenderQueryResponseSerializingTests.java
  18. 6 7
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/action/SearchApplicationSearchRequestSerializingTests.java
  19. 88 0
      x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/action/TransportRenderQueryActionTests.java
  20. 1 0
      x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java

+ 85 - 0
docs/reference/search-application/apis/search-application-render-query.asciidoc

@@ -0,0 +1,85 @@
+[role="xpack"]
+[[search-application-render-query]]
+=== Render Search Application Query
+
+preview::[]
+
+++++
+<titleabbrev>Render Search Application Query</titleabbrev>
+++++
+
+Given specified query parameters, creates an Elasticsearch query to run. Any unspecified template parameters will be
+assigned their default values if applicable. Returns the specific Elasticsearch query that would be generated and
+run by calling <<search-application-search,search application search>>.
+
+[[search-application-render-query-request]]
+==== {api-request-title}
+
+`POST _application/search_application/<name>/_render_query`
+
+[[search-application-render-query-prereqs]]
+==== {api-prereq-title}
+
+Requires read privileges on the backing alias of the search application.
+
+[[search-application-render-query-request-body]]
+==== {api-request-body-title}
+
+`params`::
+(Optional, map of strings to objects)
+Query parameters specific to this request, which will override any defaults specified in the template.
+
+[[search-application-render-query-response-codes]]
+==== {api-response-codes-title}
+
+`404`::
+Search Application `<name>` does not exist.
+
+[[search-application-render-query-example]]
+==== {api-examples-title}
+
+The following example renders a query for a search application called `my-app`. In this case, the `from` and `size`
+parameters are not specified, so default values are pulled from the search application template.
+
+[source,console]
+----
+POST _application/search_application/my-app/_render_query
+{
+  "params": {
+    "value": "my first query",
+    "text_fields": [
+        {
+            "name": "title",
+            "boost": 10
+        },
+        {
+            "name": "text",
+            "boost": 1
+        }
+    ]
+  }
+}
+----
+// TEST[skip:TBD]
+
+A sample response:
+
+[source,console-result]
+----
+{
+    "size": 10,
+    "from": 0,
+    "query": {
+        "multi_match": {
+            "query": "my first query",
+            "fields": [
+                "title^10",
+                "text^1"
+            ]
+        }
+    }
+}
+
+----
+
+

+ 69 - 0
docs/reference/search-application/apis/search-application-search.asciidoc

@@ -0,0 +1,69 @@
+[role="xpack"]
+[[search-application-search]]
+=== Search Application Search
+
+preview::[]
+
+++++
+<titleabbrev>Search Application Search</titleabbrev>
+++++
+
+Given specified query parameters, creates an Elasticsearch query to run. Any unspecified template parameters will be
+assigned their default values if applicable.
+
+[[search-application-search-request]]
+==== {api-request-title}
+
+`POST _application/search_application/<name>/_search`
+
+[[search-application-search-prereqs]]
+==== {api-prereq-title}
+
+Requires read privileges on the backing alias of the search application.
+
+[[search-application-search-path-params]]
+
+[[search-application-search-request-body]]
+==== {api-request-body-title}
+
+`params`::
+(Optional, map of strings to objects)
+Query parameters specific to this request, which will override any defaults specified in the template.
+
+[[search-application-search-response-codes]]
+==== {api-response-codes-title}
+
+`404`::
+Search Application `<name>` does not exist.
+
+[[search-application-search-example]]
+==== {api-examples-title}
+
+The following example performs a search against a search application called `my-app`:
+
+[source,console]
+----
+POST _application/search_application/my-app/_search
+{
+  "params": {
+    "value": "my first query",
+    "size": 10,
+    "from": 0,
+    "text_fields": [
+        {
+            "name": "title",
+            "boost": 10
+        },
+        {
+            "name": "text",
+            "boost": 1
+        }
+    ]
+  }
+}
+----
+// TEST[skip:TBD]
+
+The expected results are search results from the query that was run.
+
+

+ 38 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/search_application.render_query.json

@@ -0,0 +1,38 @@
+{
+  "search_application.render_query": {
+    "documentation": {
+      "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-application-render-query.html",
+      "description": "Renders a query for given search application search parameters"
+    },
+    "stability": "experimental",
+    "visibility": "public",
+    "headers": {
+      "accept": [
+        "application/json"
+      ],
+      "content_type": [
+        "application/json"
+      ]
+    },
+    "url": {
+      "paths": [
+        {
+          "path": "/_application/search_application/{name}/_render_query",
+          "methods": [
+            "POST"
+          ],
+          "parts": {
+            "name": {
+              "type": "string",
+              "description": "The name of the search application to render the query for"
+            }
+          }
+        }
+      ]
+    },
+    "body": {
+      "description": "Search parameters, which will override any default search parameters defined in the search application template",
+      "required": false
+    }
+  }
+}

+ 1 - 0
x-pack/plugin/ent-search/qa/rest/roles.yml

@@ -22,6 +22,7 @@ entsearch:
         "test-re-creating-search-application",
         "test-search-application-to-delete",
         "test-nonexistent-search-application",
+        "test-search-application-with-invalid-template"
     ]
       privileges: [ "manage" ]
     - names: [

+ 9 - 0
x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/20_search_application_put.yml

@@ -242,3 +242,12 @@ teardown:
             script:
               id: test-script
   - match: { error.type: "x_content_parse_exception" }
+---
+"Create Search Application - Script missing from template":
+  - do:
+      catch: bad_request
+      search_application.put:
+        name: test-search-application-with-invalid-template
+        body:
+          indices: [ "test-index1" ]
+          template: {}

+ 154 - 0
x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/52_search_application_render_query.yml

@@ -0,0 +1,154 @@
+setup:
+  - do:
+      indices.create:
+        index: test-search-index1
+        body:
+          settings:
+            index:
+              number_of_shards: 1
+              number_of_replicas: 0
+
+  - do:
+      indices.create:
+        index: test-search-index2
+        body:
+          settings:
+            index:
+              number_of_shards: 1
+              number_of_replicas: 0
+
+  - do:
+      indices.create:
+        index: test-index
+        body:
+          settings:
+            index:
+              number_of_shards: 1
+              number_of_replicas: 0
+
+  - do:
+      search_application.put:
+        name: test-search-application
+        body:
+          indices: [ "test-search-index1", "test-search-index2" ]
+          analytics_collection_name: "test-analytics"
+          template:
+            script:
+              source:
+                query:
+                  term:
+                    "{{field_name}}": "{{field_value}}"
+              params:
+                field_name: field1
+                field_value: value1
+
+  - do:
+      index:
+        index: test-search-index1
+        id: doc1
+        body:
+          field1: value1
+          field2: value1
+        refresh: true
+
+  - do:
+      index:
+        index: test-search-index2
+        id: doc2
+        body:
+          field1: value1
+          field3: value3
+        refresh: true
+
+---
+teardown:
+  - do:
+      search_application.delete:
+        name: test-search-application
+        ignore: 404
+
+  - do:
+      indices.delete:
+        index: test-search-index1
+        ignore: 404
+
+  - do:
+      indices.delete:
+        index: test-search-index2
+        ignore: 404
+
+  - do:
+      indices.delete:
+        index: test-index
+        ignore: 404
+
+---
+"Render Query for Search Application with default parameters":
+
+  - do:
+      search_application.render_query:
+        name: test-search-application
+
+  - match: {
+    query: {
+      term: {
+        field1: {
+          value: "value1"
+        }
+      }
+    }
+  }
+
+---
+"Render query for search application overriding part of the parameters":
+
+  - do:
+      search_application.render_query:
+        name: test-search-application
+        body:
+          params:
+            field_name: field2
+
+
+  - match: {
+    query: {
+      term: {
+        field2: {
+          value: "value1"
+        }
+      }
+    }
+  }
+
+---
+"Render query for search application overriding all parameters":
+
+  - do:
+      search_application.render_query:
+        name: test-search-application
+        body:
+          params:
+            field_name: field3
+            field_value: value3
+
+
+  - match: {
+    query: {
+      term: {
+        field3: {
+          value: "value3"
+        }
+      }
+    }
+  }
+---
+"Render query for search application - not found":
+
+  - do:
+      catch: "missing"
+      search_application.render_query:
+        name: nonexisting-test-search-application
+        body:
+          params:
+            field_name: field3
+            field_value: value3

+ 6 - 1
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearch.java

@@ -58,16 +58,19 @@ import org.elasticsearch.xpack.application.search.action.GetSearchApplicationAct
 import org.elasticsearch.xpack.application.search.action.ListSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.PutSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.QuerySearchApplicationAction;
+import org.elasticsearch.xpack.application.search.action.RenderSearchApplicationQueryAction;
 import org.elasticsearch.xpack.application.search.action.RestDeleteSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.RestGetSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.RestListSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.RestPutSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.RestQuerySearchApplicationAction;
+import org.elasticsearch.xpack.application.search.action.RestRenderSearchApplicationQueryAction;
 import org.elasticsearch.xpack.application.search.action.TransportDeleteSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.TransportGetSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.TransportListSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.TransportPutSearchApplicationAction;
 import org.elasticsearch.xpack.application.search.action.TransportQuerySearchApplicationAction;
+import org.elasticsearch.xpack.application.search.action.TransportRenderSearchApplicationQueryAction;
 import org.elasticsearch.xpack.core.XPackPlugin;
 import org.elasticsearch.xpack.core.XPackSettings;
 import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
@@ -117,6 +120,7 @@ public class EnterpriseSearch extends Plugin implements ActionPlugin, SystemInde
             new ActionHandler<>(ListSearchApplicationAction.INSTANCE, TransportListSearchApplicationAction.class),
             new ActionHandler<>(PutSearchApplicationAction.INSTANCE, TransportPutSearchApplicationAction.class),
             new ActionHandler<>(QuerySearchApplicationAction.INSTANCE, TransportQuerySearchApplicationAction.class),
+            new ActionHandler<>(RenderSearchApplicationQueryAction.INSTANCE, TransportRenderSearchApplicationQueryAction.class),
             usageAction,
             infoAction
         );
@@ -145,7 +149,8 @@ public class EnterpriseSearch extends Plugin implements ActionPlugin, SystemInde
             new RestPutAnalyticsCollectionAction(),
             new RestGetAnalyticsCollectionAction(),
             new RestDeleteAnalyticsCollectionAction(),
-            new RestPostAnalyticsEventAction()
+            new RestPostAnalyticsEventAction(),
+            new RestRenderSearchApplicationQueryAction()
         );
     }
 

+ 3 - 2
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/SearchApplicationTemplate.java

@@ -18,7 +18,7 @@ import org.elasticsearch.xcontent.ToXContentObject;
 import org.elasticsearch.xcontent.XContentBuilder;
 import org.elasticsearch.xcontent.XContentParser;
 import org.elasticsearch.xpack.application.search.action.QuerySearchApplicationAction;
-import org.elasticsearch.xpack.application.search.action.QuerySearchApplicationAction.Request;
+import org.elasticsearch.xpack.application.search.action.SearchApplicationSearchRequest;
 
 import java.io.IOException;
 import java.util.Map;
@@ -28,7 +28,8 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstr
 
 /**
  * Search template included in a {@link SearchApplication}. It will be used for searching using the
- * {@link QuerySearchApplicationAction}, overriding the parameters included on it via {@link Request}
+ * {@link QuerySearchApplicationAction}, overriding the parameters included on it via
+ * {@link SearchApplicationSearchRequest}
  */
 public class SearchApplicationTemplate implements ToXContentObject, Writeable {
 

+ 73 - 0
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/SearchApplicationTemplateService.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.application.search;
+
+import org.elasticsearch.common.ValidationException;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+import org.elasticsearch.script.Script;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.script.TemplateScript;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xcontent.XContentFactory;
+import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xcontent.XContentParserConfiguration;
+import org.elasticsearch.xcontent.XContentType;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SearchApplicationTemplateService {
+
+    private final ScriptService scriptService;
+    private final NamedXContentRegistry xContentRegistry;
+
+    private final Logger logger = LogManager.getLogger(SearchApplicationTemplateService.class);
+
+    public SearchApplicationTemplateService(ScriptService scriptService, NamedXContentRegistry xContentRegistry) {
+        this.scriptService = scriptService;
+        this.xContentRegistry = xContentRegistry;
+    }
+
+    public SearchSourceBuilder renderQuery(SearchApplication searchApplication, Map<String, Object> templateParams) throws IOException {
+        final Map<String, Object> renderedTemplateParams = renderTemplate(searchApplication, templateParams);
+        final Script script = searchApplication.searchApplicationTemplate().script();
+        TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(renderedTemplateParams);
+        String requestSource = compiledTemplate.execute();
+        XContentParserConfiguration parserConfig = XContentParserConfiguration.EMPTY.withRegistry(xContentRegistry)
+            .withDeprecationHandler(LoggingDeprecationHandler.INSTANCE);
+        try (XContentParser parser = XContentFactory.xContent(XContentType.JSON).createParser(parserConfig, requestSource)) {
+            SearchSourceBuilder builder = SearchSourceBuilder.searchSource();
+            builder.parseXContent(parser, false);
+            return builder;
+        }
+    }
+
+    /**
+     * Renders the search application's associated template with the provided request parameters.
+     *
+     * @param searchApplication The SearchApplication we're accessing, to access the associated template
+     * @param queryParams Specified parameters, which is expected to contain a subset of the parameters included in the template.
+     * @return Map of all template parameters including template defaults for non-specified parameters
+     * @throws ValidationException on invalid template parameters
+     */
+    public Map<String, Object> renderTemplate(SearchApplication searchApplication, Map<String, Object> queryParams)
+        throws ValidationException {
+
+        final SearchApplicationTemplate template = searchApplication.searchApplicationTemplate();
+        final Script script = template.script();
+
+        Map<String, Object> mergedTemplateParams = new HashMap<>(script.getParams());
+        mergedTemplateParams.putAll(queryParams);
+
+        return mergedTemplateParams;
+    }
+}

+ 0 - 97
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/QuerySearchApplicationAction.java

@@ -7,113 +7,16 @@
 
 package org.elasticsearch.xpack.application.search.action;
 
-import org.elasticsearch.action.ActionRequest;
-import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.ActionType;
 import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.io.stream.StreamInput;
-import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.xcontent.ConstructingObjectParser;
-import org.elasticsearch.xcontent.ParseField;
-import org.elasticsearch.xcontent.XContentParser;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.Objects;
-
-import static org.elasticsearch.action.ValidateActions.addValidationError;
-import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
 
 public class QuerySearchApplicationAction extends ActionType<SearchResponse> {
 
     public static final QuerySearchApplicationAction INSTANCE = new QuerySearchApplicationAction();
     public static final String NAME = "cluster:admin/xpack/application/search_application/search";
 
-    private static final ParseField QUERY_PARAMS_FIELD = new ParseField("params");
-
     public QuerySearchApplicationAction() {
         super(NAME, SearchResponse::new);
     }
 
-    public static class Request extends ActionRequest {
-        private final String name;
-
-        private static final ConstructingObjectParser<Request, String> PARSER = new ConstructingObjectParser<>(
-            "query_params",
-            false,
-            (params, searchAppName) -> {
-                @SuppressWarnings("unchecked")
-                final Map<String, Object> queryParams = (Map<String, Object>) params[0];
-                return new Request(searchAppName, queryParams);
-            }
-        );
-
-        static {
-            PARSER.declareObject(constructorArg(), (p, c) -> p.map(), QUERY_PARAMS_FIELD);
-        }
-
-        private final Map<String, Object> queryParams;
-
-        public Request(StreamInput in) throws IOException {
-            super(in);
-            this.name = in.readString();
-            this.queryParams = in.readMap();
-        }
-
-        public Request(String name) {
-            this(name, Map.of());
-        }
-
-        public Request(String name, Map<String, Object> queryParams) {
-            Objects.requireNonNull(name, "Application name must be specified");
-            this.name = name;
-
-            Objects.requireNonNull(queryParams, "Query parameters must be specified");
-            this.queryParams = queryParams;
-        }
-
-        public static Request fromXContent(String name, XContentParser contentParser) {
-            return PARSER.apply(contentParser, name);
-        }
-
-        public String name() {
-            return name;
-        }
-
-        public Map<String, Object> queryParams() {
-            return queryParams;
-        }
-
-        @Override
-        public ActionRequestValidationException validate() {
-            ActionRequestValidationException validationException = null;
-
-            if (Strings.isEmpty(name)) {
-                validationException = addValidationError("Search Application name is missing", validationException);
-            }
-
-            return validationException;
-        }
-
-        @Override
-        public void writeTo(StreamOutput out) throws IOException {
-            super.writeTo(out);
-            out.writeString(name);
-            out.writeGenericMap(queryParams);
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            QuerySearchApplicationAction.Request request = (QuerySearchApplicationAction.Request) o;
-            return Objects.equals(name, request.name) && Objects.equals(queryParams, request.queryParams);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(name, queryParams);
-        }
-    }
 }

+ 72 - 0
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RenderSearchApplicationQueryAction.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.application.search.action;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.ActionType;
+import org.elasticsearch.common.io.stream.NamedWriteable;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.xcontent.ToXContentObject;
+import org.elasticsearch.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class RenderSearchApplicationQueryAction extends ActionType<RenderSearchApplicationQueryAction.Response> {
+
+    public static final RenderSearchApplicationQueryAction INSTANCE = new RenderSearchApplicationQueryAction();
+    public static final String NAME = "cluster:admin/xpack/application/search_application/render_query";
+
+    public RenderSearchApplicationQueryAction() {
+        super(NAME, RenderSearchApplicationQueryAction.Response::new);
+    }
+
+    public static class Response extends ActionResponse implements ToXContentObject, NamedWriteable {
+
+        private final SearchSourceBuilder searchSourceBuilder;
+
+        public Response(StreamInput in) throws IOException {
+            super(in);
+            this.searchSourceBuilder = new SearchSourceBuilder(in);
+        }
+
+        public Response(String name, SearchSourceBuilder searchSourceBuilder) {
+            this.searchSourceBuilder = searchSourceBuilder;
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            searchSourceBuilder.writeTo(out);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            return searchSourceBuilder.toXContent(builder, params);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Response response = (Response) o;
+            return searchSourceBuilder.equals(response.searchSourceBuilder);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(searchSourceBuilder);
+        }
+
+        @Override
+        public String getWriteableName() {
+            return NAME;
+        }
+    }
+}

+ 4 - 5
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RestQuerySearchApplicationAction.java

@@ -14,7 +14,6 @@ import org.elasticsearch.rest.Scope;
 import org.elasticsearch.rest.ServerlessScope;
 import org.elasticsearch.rest.action.RestToXContentListener;
 import org.elasticsearch.xpack.application.EnterpriseSearch;
-import org.elasticsearch.xpack.application.search.action.QuerySearchApplicationAction.Request;
 
 import java.io.IOException;
 import java.util.List;
@@ -40,13 +39,13 @@ public class RestQuerySearchApplicationAction extends BaseRestHandler {
     @Override
     protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
         final String searchAppName = restRequest.param("name");
-        Request request;
+        SearchApplicationSearchRequest request;
         if (restRequest.hasContent()) {
-            request = Request.fromXContent(searchAppName, restRequest.contentParser());
+            request = SearchApplicationSearchRequest.fromXContent(searchAppName, restRequest.contentParser());
         } else {
-            request = new Request(searchAppName);
+            request = new SearchApplicationSearchRequest(searchAppName);
         }
-        final Request finalRequest = request;
+        final SearchApplicationSearchRequest finalRequest = request;
         return channel -> client.execute(QuerySearchApplicationAction.INSTANCE, finalRequest, new RestToXContentListener<>(channel));
     }
 }

+ 47 - 0
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/RestRenderSearchApplicationQueryAction.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.application.search.action;
+
+import org.elasticsearch.client.internal.node.NodeClient;
+import org.elasticsearch.rest.BaseRestHandler;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.xpack.application.EnterpriseSearch;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.elasticsearch.rest.RestRequest.Method.POST;
+
+public class RestRenderSearchApplicationQueryAction extends BaseRestHandler {
+
+    public static final String ENDPOINT_PATH = "/" + EnterpriseSearch.SEARCH_APPLICATION_API_ENDPOINT + "/{name}" + "/_render_query";
+
+    @Override
+    public String getName() {
+        return "search_application_render_query_action";
+    }
+
+    @Override
+    public List<Route> routes() {
+        return List.of(new Route(POST, ENDPOINT_PATH));
+    }
+
+    @Override
+    protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
+        final String searchAppName = restRequest.param("name");
+        SearchApplicationSearchRequest request;
+        if (restRequest.hasContent()) {
+            request = SearchApplicationSearchRequest.fromXContent(searchAppName, restRequest.contentParser());
+        } else {
+            request = new SearchApplicationSearchRequest(searchAppName);
+        }
+        final SearchApplicationSearchRequest finalRequest = request;
+        return channel -> client.execute(RenderSearchApplicationQueryAction.INSTANCE, finalRequest, new RestToXContentListener<>(channel));
+    }
+}

+ 105 - 0
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/SearchApplicationSearchRequest.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.application.search.action;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.core.Nullable;
+import org.elasticsearch.xcontent.ConstructingObjectParser;
+import org.elasticsearch.xcontent.ParseField;
+import org.elasticsearch.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;
+
+public class SearchApplicationSearchRequest extends ActionRequest {
+
+    private static final ParseField QUERY_PARAMS_FIELD = new ParseField("params");
+    private final String name;
+
+    private static final ConstructingObjectParser<SearchApplicationSearchRequest, String> PARSER = new ConstructingObjectParser<>(
+        "query_params",
+        false,
+        (params, searchAppName) -> {
+            @SuppressWarnings("unchecked")
+            final Map<String, Object> queryParams = (Map<String, Object>) params[0];
+            return new SearchApplicationSearchRequest(searchAppName, queryParams);
+        }
+    );
+
+    static {
+        PARSER.declareObject(constructorArg(), (p, c) -> p.map(), QUERY_PARAMS_FIELD);
+    }
+
+    private final Map<String, Object> queryParams;
+
+    public SearchApplicationSearchRequest(StreamInput in) throws IOException {
+        super(in);
+        this.name = in.readString();
+        this.queryParams = in.readMap();
+    }
+
+    public SearchApplicationSearchRequest(String name) {
+        this(name, Map.of());
+    }
+
+    public SearchApplicationSearchRequest(String name, @Nullable Map<String, Object> queryParams) {
+        this.name = Objects.requireNonNull(name, "Application name must be specified");
+        this.queryParams = Objects.requireNonNullElse(queryParams, Map.of());
+    }
+
+    public static SearchApplicationSearchRequest fromXContent(String name, XContentParser contentParser) {
+        return PARSER.apply(contentParser, name);
+    }
+
+    public String name() {
+        return name;
+    }
+
+    public Map<String, Object> queryParams() {
+        return queryParams;
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        ActionRequestValidationException validationException = null;
+
+        if (Strings.isEmpty(name)) {
+            validationException = addValidationError("Search Application name is missing", validationException);
+        }
+
+        return validationException;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        out.writeString(name);
+        out.writeGenericMap(queryParams);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SearchApplicationSearchRequest request = (SearchApplicationSearchRequest) o;
+        return Objects.equals(name, request.name) && Objects.equals(queryParams, request.queryParams);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, queryParams);
+    }
+}

+ 9 - 41
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/TransportQuerySearchApplicationAction.java

@@ -17,35 +17,26 @@ import org.elasticsearch.cluster.service.ClusterService;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.util.BigArrays;
-import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.logging.LogManager;
 import org.elasticsearch.logging.Logger;
-import org.elasticsearch.script.Script;
 import org.elasticsearch.script.ScriptService;
-import org.elasticsearch.script.TemplateScript;
 import org.elasticsearch.search.builder.SearchSourceBuilder;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xcontent.NamedXContentRegistry;
-import org.elasticsearch.xcontent.XContentFactory;
-import org.elasticsearch.xcontent.XContentParser;
-import org.elasticsearch.xcontent.XContentParserConfiguration;
-import org.elasticsearch.xcontent.XContentType;
+import org.elasticsearch.xpack.application.search.SearchApplicationTemplateService;
 
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
 
 public class TransportQuerySearchApplicationAction extends SearchApplicationTransportAction<
-    QuerySearchApplicationAction.Request,
+    SearchApplicationSearchRequest,
     SearchResponse> {
 
     private static final Logger logger = LogManager.getLogger(TransportQuerySearchApplicationAction.class);
 
     private final Client client;
-    private final ScriptService scriptService;
 
-    private final NamedXContentRegistry xContentRegistry;
+    private final SearchApplicationTemplateService templateService;
 
     @Inject
     public TransportQuerySearchApplicationAction(
@@ -54,16 +45,16 @@ public class TransportQuerySearchApplicationAction extends SearchApplicationTran
         Client client,
         ClusterService clusterService,
         NamedWriteableRegistry namedWriteableRegistry,
-        NamedXContentRegistry xContentRegistry,
         BigArrays bigArrays,
+        XPackLicenseState licenseState,
         ScriptService scriptService,
-        XPackLicenseState licenseState
+        NamedXContentRegistry xContentRegistry
     ) {
         super(
             QuerySearchApplicationAction.NAME,
             transportService,
             actionFilters,
-            QuerySearchApplicationAction.Request::new,
+            SearchApplicationSearchRequest::new,
             client,
             clusterService,
             namedWriteableRegistry,
@@ -71,17 +62,14 @@ public class TransportQuerySearchApplicationAction extends SearchApplicationTran
             licenseState
         );
         this.client = client;
-        this.scriptService = scriptService;
-        this.xContentRegistry = xContentRegistry;
+        this.templateService = new SearchApplicationTemplateService(scriptService, xContentRegistry);
     }
 
     @Override
-    protected void doExecute(QuerySearchApplicationAction.Request request, ActionListener<SearchResponse> listener) {
+    protected void doExecute(SearchApplicationSearchRequest request, ActionListener<SearchResponse> listener) {
         systemIndexService.getSearchApplication(request.name(), listener.delegateFailure((l, searchApplication) -> {
-            final Script script = searchApplication.searchApplicationTemplate().script();
-
             try {
-                final SearchSourceBuilder sourceBuilder = renderTemplate(script, mergeTemplateParams(request, script));
+                SearchSourceBuilder sourceBuilder = templateService.renderQuery(searchApplication, request.queryParams());
                 SearchRequest searchRequest = new SearchRequest(searchApplication.indices()).source(sourceBuilder);
 
                 client.execute(
@@ -94,24 +82,4 @@ public class TransportQuerySearchApplicationAction extends SearchApplicationTran
             }
         }));
     }
-
-    private static Map<String, Object> mergeTemplateParams(QuerySearchApplicationAction.Request request, Script script) {
-        Map<String, Object> mergedTemplateParams = new HashMap<>(script.getParams());
-        mergedTemplateParams.putAll(request.queryParams());
-
-        return mergedTemplateParams;
-    }
-
-    private SearchSourceBuilder renderTemplate(Script script, Map<String, Object> templateParams) throws IOException {
-        TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(templateParams);
-        String requestSource = compiledTemplate.execute();
-
-        XContentParserConfiguration parserConfig = XContentParserConfiguration.EMPTY.withRegistry(xContentRegistry)
-            .withDeprecationHandler(LoggingDeprecationHandler.INSTANCE);
-        try (XContentParser parser = XContentFactory.xContent(XContentType.JSON).createParser(parserConfig, requestSource)) {
-            SearchSourceBuilder builder = SearchSourceBuilder.searchSource();
-            builder.parseXContent(parser, false);
-            return builder;
-        }
-    }
 }

+ 71 - 0
x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/search/action/TransportRenderSearchApplicationQueryAction.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.application.search.action;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.client.internal.Client;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.util.BigArrays;
+import org.elasticsearch.license.XPackLicenseState;
+import org.elasticsearch.logging.LogManager;
+import org.elasticsearch.logging.Logger;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xpack.application.search.SearchApplicationTemplateService;
+
+import java.util.Map;
+
+public class TransportRenderSearchApplicationQueryAction extends SearchApplicationTransportAction<
+    SearchApplicationSearchRequest,
+    RenderSearchApplicationQueryAction.Response> {
+
+    private static final Logger logger = LogManager.getLogger(TransportRenderSearchApplicationQueryAction.class);
+
+    private final SearchApplicationTemplateService templateService;
+
+    @Inject
+    public TransportRenderSearchApplicationQueryAction(
+        TransportService transportService,
+        ActionFilters actionFilters,
+        Client client,
+        ClusterService clusterService,
+        NamedWriteableRegistry namedWriteableRegistry,
+        BigArrays bigArrays,
+        XPackLicenseState licenseState,
+        ScriptService scriptService,
+        NamedXContentRegistry xContentRegistry
+    ) {
+        super(
+            RenderSearchApplicationQueryAction.NAME,
+            transportService,
+            actionFilters,
+            SearchApplicationSearchRequest::new,
+            client,
+            clusterService,
+            namedWriteableRegistry,
+            bigArrays,
+            licenseState
+        );
+        this.templateService = new SearchApplicationTemplateService(scriptService, xContentRegistry);
+    }
+
+    @Override
+    protected void doExecute(SearchApplicationSearchRequest request, ActionListener<RenderSearchApplicationQueryAction.Response> listener) {
+        systemIndexService.getSearchApplication(request.name(), ActionListener.wrap(searchApplication -> {
+            final Map<String, Object> renderedMetadata = templateService.renderTemplate(searchApplication, request.queryParams());
+            final SearchSourceBuilder sourceBuilder = templateService.renderQuery(searchApplication, renderedMetadata);
+            listener.onResponse(new RenderSearchApplicationQueryAction.Response(request.name(), sourceBuilder));
+        }, listener::onFailure));
+    }
+
+}

+ 69 - 0
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/action/RenderQueryResponseSerializingTests.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.application.search.action;
+
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.TermQueryBuilder;
+import org.elasticsearch.search.aggregations.AggregationBuilder;
+import org.elasticsearch.search.aggregations.Aggregator;
+import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.test.AbstractNamedWriteableTestCase;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class RenderQueryResponseSerializingTests extends AbstractNamedWriteableTestCase<RenderSearchApplicationQueryAction.Response> {
+
+    @Override
+    protected RenderSearchApplicationQueryAction.Response createTestInstance() {
+        return new RenderSearchApplicationQueryAction.Response(randomAlphaOfLengthBetween(5, 15), randomSearchSourceBuilder());
+    }
+
+    @Override
+    protected RenderSearchApplicationQueryAction.Response mutateInstance(RenderSearchApplicationQueryAction.Response instance)
+        throws IOException {
+        return randomValueOtherThan(instance, this::createTestInstance);
+    }
+
+    protected SearchSourceBuilder randomSearchSourceBuilder() {
+        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+
+        if (randomBoolean()) {
+            searchSourceBuilder.query(new TermQueryBuilder(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10)));
+        }
+        if (randomBoolean()) {
+            searchSourceBuilder.aggregation(
+                new TermsAggregationBuilder(randomAlphaOfLengthBetween(1, 10)).field(randomAlphaOfLengthBetween(1, 10))
+                    .collectMode(randomFrom(Aggregator.SubAggCollectionMode.values()))
+            );
+        }
+        return searchSourceBuilder;
+    }
+
+    @Override
+    protected NamedWriteableRegistry getNamedWriteableRegistry() {
+        return new NamedWriteableRegistry(
+            Arrays.asList(
+                new NamedWriteableRegistry.Entry(
+                    RenderSearchApplicationQueryAction.Response.class,
+                    RenderSearchApplicationQueryAction.NAME,
+                    RenderSearchApplicationQueryAction.Response::new
+                ),
+                new NamedWriteableRegistry.Entry(AggregationBuilder.class, TermsAggregationBuilder.NAME, TermsAggregationBuilder::new),
+                new NamedWriteableRegistry.Entry(QueryBuilder.class, TermQueryBuilder.NAME, TermQueryBuilder::new)
+            )
+        );
+    }
+
+    @Override
+    protected Class<RenderSearchApplicationQueryAction.Response> categoryClass() {
+        return RenderSearchApplicationQueryAction.Response.class;
+    }
+}

+ 6 - 7
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/action/QuerySearchApplicationActionRequestSerializingTests.java → x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/action/SearchApplicationSearchRequestSerializingTests.java

@@ -13,24 +13,23 @@ import org.elasticsearch.xpack.application.search.SearchApplicationTestUtils;
 
 import java.io.IOException;
 
-public class QuerySearchApplicationActionRequestSerializingTests extends AbstractWireSerializingTestCase<
-    QuerySearchApplicationAction.Request> {
+public class SearchApplicationSearchRequestSerializingTests extends AbstractWireSerializingTestCase<SearchApplicationSearchRequest> {
 
     @Override
-    protected Writeable.Reader<QuerySearchApplicationAction.Request> instanceReader() {
-        return QuerySearchApplicationAction.Request::new;
+    protected Writeable.Reader<SearchApplicationSearchRequest> instanceReader() {
+        return SearchApplicationSearchRequest::new;
     }
 
     @Override
-    protected QuerySearchApplicationAction.Request createTestInstance() {
-        return new QuerySearchApplicationAction.Request(
+    protected SearchApplicationSearchRequest createTestInstance() {
+        return new SearchApplicationSearchRequest(
             randomAlphaOfLengthBetween(1, 10),
             SearchApplicationTestUtils.randomSearchApplicationQueryParams()
         );
     }
 
     @Override
-    protected QuerySearchApplicationAction.Request mutateInstance(QuerySearchApplicationAction.Request instance) throws IOException {
+    protected SearchApplicationSearchRequest mutateInstance(SearchApplicationSearchRequest instance) throws IOException {
         return randomValueOtherThan(instance, this::createTestInstance);
     }
 

+ 88 - 0
x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/search/action/TransportRenderQueryActionTests.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+package org.elasticsearch.xpack.application.search.action;
+
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.client.internal.Client;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
+import org.elasticsearch.common.util.BigArrays;
+import org.elasticsearch.license.MockLicenseState;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.transport.TransportService;
+import org.elasticsearch.xcontent.NamedXContentRegistry;
+import org.elasticsearch.xpack.application.search.SearchApplication;
+import org.elasticsearch.xpack.application.search.SearchApplicationTestUtils;
+import org.elasticsearch.xpack.application.utils.LicenseUtils;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class TransportRenderQueryActionTests extends ESTestCase {
+    public void testWithUnsupportedLicense() {
+        MockLicenseState licenseState = mock(MockLicenseState.class);
+
+        when(licenseState.isAllowed(LicenseUtils.LICENSED_ENT_SEARCH_FEATURE)).thenReturn(false);
+        when(licenseState.isActive()).thenReturn(false);
+        when(licenseState.statusDescription()).thenReturn("invalid license");
+
+        TransportRenderSearchApplicationQueryAction transportAction = new TransportRenderSearchApplicationQueryAction(
+            mock(TransportService.class),
+            mock(ActionFilters.class),
+            mock(Client.class),
+            mock(ClusterService.class),
+            mock(NamedWriteableRegistry.class),
+            mock(BigArrays.class),
+            licenseState,
+            mock(ScriptService.class),
+            mock(NamedXContentRegistry.class)
+        );
+
+        SearchApplication app = new SearchApplication(
+            "my-search-app",
+            new String[] { "index1" },
+            "my-analytics-collection",
+            System.currentTimeMillis(),
+            SearchApplicationTestUtils.getRandomSearchApplicationTemplate()
+        );
+        SearchApplicationSearchRequest request = new SearchApplicationSearchRequest("my-search-app", Map.of("field1", "value1"));
+
+        final AtomicReference<Throwable> throwableRef = new AtomicReference<>();
+        final AtomicReference<RenderSearchApplicationQueryAction.Response> responseRef = new AtomicReference<>();
+
+        transportAction.doExecute(mock(Task.class), request, new ActionListener<>() {
+            @Override
+            public void onResponse(RenderSearchApplicationQueryAction.Response response) {
+                responseRef.set(response);
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                throwableRef.set(e);
+            }
+        });
+
+        assertThat(responseRef.get(), is(nullValue()));
+        assertThat(throwableRef.get(), instanceOf(ElasticsearchSecurityException.class));
+        assertThat(
+            throwableRef.get().getMessage(),
+            containsString("Search Applications and behavioral analytics require an active trial, platinum or enterprise license.")
+        );
+    }
+}

+ 1 - 0
x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java

@@ -102,6 +102,7 @@ public class Constants {
         "cluster:admin/xpack/application/search_application/get",
         "cluster:admin/xpack/application/search_application/list",
         "cluster:admin/xpack/application/search_application/put",
+        "cluster:admin/xpack/application/search_application/render_query",
         "cluster:admin/xpack/application/search_application/search",
         "cluster:admin/xpack/ccr/auto_follow_pattern/activate",
         "cluster:admin/xpack/ccr/auto_follow_pattern/delete",