Browse Source

Search Templates: Adds API endpoint to render search templates as a response

Closes #6821
Colin Goodheart-Smithe 10 years ago
parent
commit
d9ab3cba77
17 changed files with 795 additions and 1 deletions
  1. 0 1
      .settings/org.eclipse.core.resources.prefs
  2. 4 0
      core/src/main/java/org/elasticsearch/action/ActionModule.java
  3. 44 0
      core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateAction.java
  4. 69 0
      core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequest.java
  5. 42 0
      core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequestBuilder.java
  6. 68 0
      core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateResponse.java
  7. 66 0
      core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/TransportRenderSearchTemplateAction.java
  8. 3 0
      core/src/main/java/org/elasticsearch/client/Client.java
  9. 24 0
      core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java
  10. 19 0
      core/src/main/java/org/elasticsearch/client/support/AbstractClient.java
  11. 3 0
      core/src/main/java/org/elasticsearch/rest/action/RestActionModule.java
  12. 105 0
      core/src/main/java/org/elasticsearch/rest/action/admin/indices/validate/template/RestRenderSearchTemplateAction.java
  13. 147 0
      core/src/test/java/org/elasticsearch/validate/RenderSearchTemplateTests.java
  14. 1 0
      core/src/test/resources/org/elasticsearch/validate/config/scripts/file_template_1.mustache
  15. 71 0
      docs/reference/search/search-template.asciidoc
  16. 19 0
      rest-api-spec/api/render_search_template.json
  17. 110 0
      rest-api-spec/src/main/resources/rest-api-spec/test/template/30_render_search_template.yaml

+ 0 - 1
.settings/org.eclipse.core.resources.prefs

@@ -1,7 +1,6 @@
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/main/resources=UTF-8
-encoding//src/test/java=UTF-8
 encoding//src/test/resources=UTF-8
 encoding/<project>=UTF-8
 encoding/rest-api-spec=UTF-8

+ 4 - 0
core/src/main/java/org/elasticsearch/action/ActionModule.java

@@ -21,6 +21,7 @@ package org.elasticsearch.action;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+
 import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction;
 import org.elasticsearch.action.admin.cluster.health.TransportClusterHealthAction;
 import org.elasticsearch.action.admin.cluster.node.hotthreads.NodesHotThreadsAction;
@@ -117,6 +118,8 @@ import org.elasticsearch.action.admin.indices.upgrade.post.UpgradeAction;
 import org.elasticsearch.action.admin.indices.upgrade.post.UpgradeSettingsAction;
 import org.elasticsearch.action.admin.indices.validate.query.TransportValidateQueryAction;
 import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryAction;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateAction;
+import org.elasticsearch.action.admin.indices.validate.template.TransportRenderSearchTemplateAction;
 import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerAction;
 import org.elasticsearch.action.admin.indices.warmer.delete.TransportDeleteWarmerAction;
 import org.elasticsearch.action.admin.indices.warmer.get.GetWarmersAction;
@@ -302,6 +305,7 @@ public class ActionModule extends AbstractModule {
         registerAction(ExplainAction.INSTANCE, TransportExplainAction.class);
         registerAction(ClearScrollAction.INSTANCE, TransportClearScrollAction.class);
         registerAction(RecoveryAction.INSTANCE, TransportRecoveryAction.class);
+        registerAction(RenderSearchTemplateAction.INSTANCE, TransportRenderSearchTemplateAction.class);
 
         //Indexed scripts
         registerAction(PutIndexedScriptAction.INSTANCE, TransportPutIndexedScriptAction.class);

+ 44 - 0
core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateAction.java

@@ -0,0 +1,44 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.action.admin.indices.validate.template;
+
+import org.elasticsearch.action.Action;
+import org.elasticsearch.client.ElasticsearchClient;
+
+public class RenderSearchTemplateAction extends Action<RenderSearchTemplateRequest, RenderSearchTemplateResponse, RenderSearchTemplateRequestBuilder> {
+
+    public static final RenderSearchTemplateAction INSTANCE = new RenderSearchTemplateAction();
+    public static final String NAME = "indices:admin/render/template/search";
+
+    public RenderSearchTemplateAction() {
+        super(NAME);
+    }
+
+    @Override
+    public RenderSearchTemplateRequestBuilder newRequestBuilder(ElasticsearchClient client) {
+        return new RenderSearchTemplateRequestBuilder(client, this);
+    }
+
+    @Override
+    public RenderSearchTemplateResponse newResponse() {
+        return new RenderSearchTemplateResponse();
+    }
+
+}

+ 69 - 0
core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequest.java

@@ -0,0 +1,69 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.action.admin.indices.validate.template;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.script.Template;
+
+import java.io.IOException;
+
+public class RenderSearchTemplateRequest extends ActionRequest<RenderSearchTemplateRequest> {
+
+    private Template template;
+    
+    public void template(Template template) {
+        this.template = template;
+    }
+    
+    public Template template() {
+        return template;
+    }
+    
+    @Override
+    public ActionRequestValidationException validate() {
+        ActionRequestValidationException exception = null;
+        if (template == null) {
+            exception = new ActionRequestValidationException();
+            exception.addValidationError("template must not be null");
+        }
+        return exception;
+    }
+    
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        boolean hasTemplate = template!= null;
+        out.writeBoolean(hasTemplate);
+        if (hasTemplate) {
+            template.writeTo(out);
+        }
+    }
+    
+    @Override
+    public void readFrom(StreamInput in) throws IOException {
+        super.readFrom(in);
+        if (in.readBoolean()) {
+            template = Template.readTemplate(in);
+        }
+    }
+}

+ 42 - 0
core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequestBuilder.java

@@ -0,0 +1,42 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.action.admin.indices.validate.template;
+
+import org.elasticsearch.action.ActionRequestBuilder;
+import org.elasticsearch.client.ElasticsearchClient;
+import org.elasticsearch.script.Template;
+
+public class RenderSearchTemplateRequestBuilder extends ActionRequestBuilder<RenderSearchTemplateRequest, RenderSearchTemplateResponse, RenderSearchTemplateRequestBuilder> {
+
+    public RenderSearchTemplateRequestBuilder(ElasticsearchClient client,
+            RenderSearchTemplateAction action) {
+        super(client, action, new RenderSearchTemplateRequest());
+    }
+    
+    public RenderSearchTemplateRequestBuilder template(Template template) {
+        request.template(template);
+        return this;
+    }
+    
+    public Template template() {
+        return request.template();
+    }
+
+}

+ 68 - 0
core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateResponse.java

@@ -0,0 +1,68 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.action.admin.indices.validate.template;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+
+public class RenderSearchTemplateResponse extends ActionResponse implements ToXContent {
+
+    private BytesReference source;
+
+    public BytesReference source() {
+        return source;
+    }
+    
+    public void source(BytesReference source) {
+        this.source = source;
+    }
+    
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        boolean hasSource = source != null;
+        out.writeBoolean(hasSource);
+        if (hasSource) {
+            out.writeBytesReference(source);
+        }
+    }
+    
+    @Override
+    public void readFrom(StreamInput in) throws IOException {
+        super.readFrom(in);
+        if (in.readBoolean()) {
+            source = in.readBytesReference();
+        }
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.rawField("template_output", source);
+        builder.endObject();
+        return builder;
+    }
+}

+ 66 - 0
core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/TransportRenderSearchTemplateAction.java

@@ -0,0 +1,66 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.action.admin.indices.validate.template;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.HandledTransportAction;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.AbstractRunnable;
+import org.elasticsearch.script.ExecutableScript;
+import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.threadpool.ThreadPool;
+import org.elasticsearch.transport.TransportService;
+
+public class TransportRenderSearchTemplateAction extends HandledTransportAction<RenderSearchTemplateRequest, RenderSearchTemplateResponse> {
+
+    private final ScriptService scriptService;
+
+    @Inject
+    protected TransportRenderSearchTemplateAction(ScriptService scriptService, Settings settings, ThreadPool threadPool,
+            TransportService transportService, ActionFilters actionFilters) {
+        super(settings, RenderSearchTemplateAction.NAME, threadPool, transportService, actionFilters, RenderSearchTemplateRequest.class);
+        this.scriptService = scriptService;
+    }
+
+    @Override
+    protected void doExecute(final RenderSearchTemplateRequest request, final ActionListener<RenderSearchTemplateResponse> listener) {
+        threadPool.generic().execute(new AbstractRunnable() {
+
+            @Override
+            public void onFailure(Throwable t) {
+                listener.onFailure(t);
+            }
+
+            @Override
+            protected void doRun() throws Exception {
+                ExecutableScript executable = scriptService.executable(request.template(), ScriptContext.Standard.SEARCH);
+                BytesReference processedTemplate = (BytesReference) executable.run();
+                RenderSearchTemplateResponse response = new RenderSearchTemplateResponse();
+                response.source(processedTemplate);
+                listener.onResponse(response);
+            }
+        });
+    }
+
+}

+ 3 - 0
core/src/main/java/org/elasticsearch/client/Client.java

@@ -21,6 +21,9 @@ package org.elasticsearch.client;
 
 import org.elasticsearch.action.ActionFuture;
 import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequestBuilder;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse;
 import org.elasticsearch.action.bulk.BulkRequest;
 import org.elasticsearch.action.bulk.BulkRequestBuilder;
 import org.elasticsearch.action.bulk.BulkResponse;

+ 24 - 0
core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java

@@ -102,6 +102,9 @@ import org.elasticsearch.action.admin.indices.upgrade.post.UpgradeResponse;
 import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest;
 import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequestBuilder;
 import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequestBuilder;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse;
 import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequest;
 import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequestBuilder;
 import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerResponse;
@@ -717,6 +720,27 @@ public interface IndicesAdminClient extends ElasticsearchClient {
      */
     ValidateQueryRequestBuilder prepareValidateQuery(String... indices);
 
+    /**
+     * Return the rendered search request for a given search template.
+     *
+     * @param request The request
+     * @return The result future
+     */
+    ActionFuture<RenderSearchTemplateResponse> renderSearchTemplate(RenderSearchTemplateRequest request);
+
+    /**
+     * Return the rendered search request for a given search template.
+     *
+     * @param request  The request
+     * @param listener A listener to be notified of the result
+     */
+    void renderSearchTemplate(RenderSearchTemplateRequest request, ActionListener<RenderSearchTemplateResponse> listener);
+
+    /**
+     * Return the rendered search request for a given search template.
+     */
+    RenderSearchTemplateRequestBuilder prepareRenderSearchTemplate();
+
     /**
      * Puts an index search warmer to be applies when applicable.
      */

+ 19 - 0
core/src/main/java/org/elasticsearch/client/support/AbstractClient.java

@@ -204,6 +204,10 @@ import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryAction
 import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest;
 import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequestBuilder;
 import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateAction;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequestBuilder;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse;
 import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerAction;
 import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequest;
 import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequestBuilder;
@@ -1594,6 +1598,21 @@ public abstract class AbstractClient extends AbstractComponent implements Client
             return new ValidateQueryRequestBuilder(this, ValidateQueryAction.INSTANCE).setIndices(indices);
         }
 
+        @Override
+        public ActionFuture<RenderSearchTemplateResponse> renderSearchTemplate(final RenderSearchTemplateRequest request) {
+            return execute(RenderSearchTemplateAction.INSTANCE, request);
+        }
+
+        @Override
+        public void renderSearchTemplate(final RenderSearchTemplateRequest request, final ActionListener<RenderSearchTemplateResponse> listener) {
+            execute(RenderSearchTemplateAction.INSTANCE, request, listener);
+        }
+
+        @Override
+        public RenderSearchTemplateRequestBuilder prepareRenderSearchTemplate() {
+            return new RenderSearchTemplateRequestBuilder(this, RenderSearchTemplateAction.INSTANCE);
+        }
+
         @Override
         public ActionFuture<PutWarmerResponse> putWarmer(PutWarmerRequest request) {
             return execute(PutWarmerAction.INSTANCE, request);

+ 3 - 0
core/src/main/java/org/elasticsearch/rest/action/RestActionModule.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.rest.action;
 
 import com.google.common.collect.Lists;
+
 import org.elasticsearch.common.inject.AbstractModule;
 import org.elasticsearch.common.inject.multibindings.Multibinder;
 import org.elasticsearch.rest.BaseRestHandler;
@@ -76,6 +77,7 @@ import org.elasticsearch.rest.action.admin.indices.template.head.RestHeadIndexTe
 import org.elasticsearch.rest.action.admin.indices.template.put.RestPutIndexTemplateAction;
 import org.elasticsearch.rest.action.admin.indices.upgrade.RestUpgradeAction;
 import org.elasticsearch.rest.action.admin.indices.validate.query.RestValidateQueryAction;
+import org.elasticsearch.rest.action.admin.indices.validate.template.RestRenderSearchTemplateAction;
 import org.elasticsearch.rest.action.admin.indices.warmer.delete.RestDeleteWarmerAction;
 import org.elasticsearch.rest.action.admin.indices.warmer.get.RestGetWarmerAction;
 import org.elasticsearch.rest.action.admin.indices.warmer.put.RestPutWarmerAction;
@@ -207,6 +209,7 @@ public class RestActionModule extends AbstractModule {
         bind(RestSearchScrollAction.class).asEagerSingleton();
         bind(RestClearScrollAction.class).asEagerSingleton();
         bind(RestMultiSearchAction.class).asEagerSingleton();
+        bind(RestRenderSearchTemplateAction.class).asEagerSingleton();
 
         bind(RestValidateQueryAction.class).asEagerSingleton();
 

+ 105 - 0
core/src/main/java/org/elasticsearch/rest/action/admin/indices/validate/template/RestRenderSearchTemplateAction.java

@@ -0,0 +1,105 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.rest.action.admin.indices.validate.template;
+
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest;
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.rest.BaseRestHandler;
+import org.elasticsearch.rest.BytesRestResponse;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestController;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.RestResponse;
+import org.elasticsearch.rest.action.support.RestActions;
+import org.elasticsearch.rest.action.support.RestBuilderListener;
+import org.elasticsearch.script.Script.ScriptField;
+import org.elasticsearch.script.ScriptService.ScriptType;
+import org.elasticsearch.script.Template;
+import org.elasticsearch.script.mustache.MustacheScriptEngineService;
+
+import java.util.Map;
+
+import static org.elasticsearch.rest.RestRequest.Method.GET;
+import static org.elasticsearch.rest.RestRequest.Method.POST;
+import static org.elasticsearch.rest.RestStatus.OK;
+
+public class RestRenderSearchTemplateAction extends BaseRestHandler {
+
+    @Inject
+    public RestRenderSearchTemplateAction(Settings settings, RestController controller, Client client) {
+        super(settings, controller, client);
+        controller.registerHandler(GET, "/_render/template", this);
+        controller.registerHandler(POST, "/_render/template", this);
+        controller.registerHandler(GET, "/_render/template/{id}", this);
+        controller.registerHandler(POST, "/_render/template/{id}", this);
+    }
+
+    @Override
+    protected void handleRequest(RestRequest request, RestChannel channel, Client client) throws Exception {
+        RenderSearchTemplateRequest renderSearchTemplateRequest;
+        BytesReference source = RestActions.getRestContent(request);
+        XContentParser parser = XContentFactory.xContent(source).createParser(source);
+        String templateId = request.param("id");
+        final Template template;
+        if (templateId == null) {
+            template = Template.parse(parser);
+        } else {
+            Map<String, Object> params = null;
+            String currentFieldName = null;
+            XContentParser.Token token = parser.nextToken();
+            if (token != XContentParser.Token.START_OBJECT) {
+                throw new ElasticsearchParseException("request body must start with [" + XContentParser.Token.START_OBJECT + "] but found [" + token + "]");
+            }
+            while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+                if (token == XContentParser.Token.FIELD_NAME) {
+                    currentFieldName = parser.currentName();
+                } else if (ScriptField.PARAMS.match(currentFieldName)) {
+                    if (token == XContentParser.Token.START_OBJECT) {
+                        params = parser.map();
+                    } else {
+                        throw new ElasticsearchParseException("Expected [" + XContentParser.Token.START_OBJECT + "] for [params] but found [" + token + "]");
+                    }
+                } else {
+                    throw new ElasticsearchParseException("Unknown field [" + currentFieldName + "] of type [" + token + "]");
+                }
+            }
+            template = new Template(templateId, ScriptType.INDEXED, MustacheScriptEngineService.NAME, null, params);
+        }
+        renderSearchTemplateRequest = new RenderSearchTemplateRequest();
+        renderSearchTemplateRequest.template(template);
+        client.admin().indices().renderSearchTemplate(renderSearchTemplateRequest, new RestBuilderListener<RenderSearchTemplateResponse>(channel) {
+
+            @Override
+            public RestResponse buildResponse(RenderSearchTemplateResponse response, XContentBuilder builder) throws Exception {
+                builder.prettyPrint();
+                response.toXContent(builder, ToXContent.EMPTY_PARAMS);
+                return new BytesRestResponse(OK, builder);
+            }});
+    }
+}

+ 147 - 0
core/src/test/java/org/elasticsearch/validate/RenderSearchTemplateTests.java

@@ -0,0 +1,147 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.validate;
+
+import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.script.ScriptService.ScriptType;
+import org.elasticsearch.script.Template;
+import org.elasticsearch.script.mustache.MustacheScriptEngineService;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.elasticsearch.common.settings.Settings.settingsBuilder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+
+@ElasticsearchIntegrationTest.SuiteScopeTest
+public class RenderSearchTemplateTests extends ElasticsearchIntegrationTest {
+
+    private static final String TEMPLATE_CONTENTS = "{\"size\":\"{{size}}\",\"query\":{\"match\":{\"foo\":\"{{value}}\"}},\"aggs\":{\"objects\":{\"terms\":{\"field\":\"{{value}}\",\"size\":\"{{size}}\"}}}}";
+    
+    @Override
+    protected void setupSuiteScopeCluster() throws Exception {
+        client().preparePutIndexedScript(MustacheScriptEngineService.NAME, "index_template_1", "{ \"template\": " + TEMPLATE_CONTENTS + " }").get();
+    }
+    
+    @Override
+    public Settings nodeSettings(int nodeOrdinal) {
+        //Set path so ScriptService will pick up the test scripts
+        return settingsBuilder().put(super.nodeSettings(nodeOrdinal))
+                .put("path.conf", this.getDataPath("config")).build();
+    }
+    
+    @Test
+    public void inlineTemplate() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("value", "bar");
+        params.put("size", 20);
+        Template template = new Template(TEMPLATE_CONTENTS, ScriptType.INLINE, MustacheScriptEngineService.NAME, XContentType.JSON, params);
+        RenderSearchTemplateResponse response = client().admin().indices().prepareRenderSearchTemplate().template(template).get();
+        assertThat(response, notNullValue());
+        BytesReference source = response.source();
+        assertThat(source, notNullValue());
+        Map<String, Object> sourceAsMap = XContentHelper.convertToMap(source, false).v2();
+        assertThat(sourceAsMap, notNullValue());
+        String expected = TEMPLATE_CONTENTS.replace("{{value}}", "bar").replace("{{size}}", "20");
+        Map<String, Object> expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2();
+        assertThat(sourceAsMap, equalTo(expectedMap));
+        
+        params = new HashMap<>();
+        params.put("value", "baz");
+        params.put("size", 100);
+        template = new Template(TEMPLATE_CONTENTS, ScriptType.INLINE, MustacheScriptEngineService.NAME, XContentType.JSON, params);
+        response = client().admin().indices().prepareRenderSearchTemplate().template(template).get();
+        assertThat(response, notNullValue());
+        source = response.source();
+        assertThat(source, notNullValue());
+        sourceAsMap = XContentHelper.convertToMap(source, false).v2();
+        expected = TEMPLATE_CONTENTS.replace("{{value}}", "baz").replace("{{size}}", "100");
+        expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2();
+        assertThat(sourceAsMap, equalTo(expectedMap));
+    }
+    
+    @Test
+    public void indexedTemplate() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("value", "bar");
+        params.put("size", 20);
+        Template template = new Template("index_template_1", ScriptType.INDEXED, MustacheScriptEngineService.NAME, XContentType.JSON, params);
+        RenderSearchTemplateResponse response = client().admin().indices().prepareRenderSearchTemplate().template(template).get();
+        assertThat(response, notNullValue());
+        BytesReference source = response.source();
+        assertThat(source, notNullValue());
+        Map<String, Object> sourceAsMap = XContentHelper.convertToMap(source, false).v2();
+        assertThat(sourceAsMap, notNullValue());
+        String expected = TEMPLATE_CONTENTS.replace("{{value}}", "bar").replace("{{size}}", "20");
+        Map<String, Object> expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2();
+        assertThat(sourceAsMap, equalTo(expectedMap));
+        
+        params = new HashMap<>();
+        params.put("value", "baz");
+        params.put("size", 100);
+        template = new Template("index_template_1", ScriptType.INDEXED, MustacheScriptEngineService.NAME, XContentType.JSON, params);
+        response = client().admin().indices().prepareRenderSearchTemplate().template(template).get();
+        assertThat(response, notNullValue());
+        source = response.source();
+        assertThat(source, notNullValue());
+        sourceAsMap = XContentHelper.convertToMap(source, false).v2();
+        expected = TEMPLATE_CONTENTS.replace("{{value}}", "baz").replace("{{size}}", "100");
+        expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2();
+        assertThat(sourceAsMap, equalTo(expectedMap));
+    }
+    
+    @Test
+    public void fileTemplate() {
+        Map<String, Object> params = new HashMap<>();
+        params.put("value", "bar");
+        params.put("size", 20);
+        Template template = new Template("file_template_1", ScriptType.FILE, MustacheScriptEngineService.NAME, XContentType.JSON, params);
+        RenderSearchTemplateResponse response = client().admin().indices().prepareRenderSearchTemplate().template(template).get();
+        assertThat(response, notNullValue());
+        BytesReference source = response.source();
+        assertThat(source, notNullValue());
+        Map<String, Object> sourceAsMap = XContentHelper.convertToMap(source, false).v2();
+        assertThat(sourceAsMap, notNullValue());
+        String expected = TEMPLATE_CONTENTS.replace("{{value}}", "bar").replace("{{size}}", "20");
+        Map<String, Object> expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2();
+        assertThat(sourceAsMap, equalTo(expectedMap));
+        
+        params = new HashMap<>();
+        params.put("value", "baz");
+        params.put("size", 100);
+        template = new Template("file_template_1", ScriptType.FILE, MustacheScriptEngineService.NAME, XContentType.JSON, params);
+        response = client().admin().indices().prepareRenderSearchTemplate().template(template).get();
+        assertThat(response, notNullValue());
+        source = response.source();
+        assertThat(source, notNullValue());
+        sourceAsMap = XContentHelper.convertToMap(source, false).v2();
+        expected = TEMPLATE_CONTENTS.replace("{{value}}", "baz").replace("{{size}}", "100");
+        expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2();
+        assertThat(sourceAsMap, equalTo(expectedMap));
+    }
+}

+ 1 - 0
core/src/test/resources/org/elasticsearch/validate/config/scripts/file_template_1.mustache

@@ -0,0 +1 @@
+{"size":"{{size}}","query":{"match":{"foo":"{{value}}"}},"aggs":{"objects":{"terms":{"field":"{{value}}","size":"{{size}}"}}}}

+ 71 - 0
docs/reference/search/search-template.asciidoc

@@ -298,3 +298,74 @@ GET /_search/template
 }
 ------------------------------------------
 <1> Name of the the query template stored in the `.scripts` index.
+
+[float]
+==== Validating templates
+
+A template can be rendered in a response with given parameters using
+
+[source,js]
+------------------------------------------
+GET /_render/template
+{
+  "inline": {
+    "query": {
+      "terms": {
+        "status": [
+          "{{#status}}",
+          "{{.}}",
+          "{{/status}}"
+        ]
+      }
+    }
+  },
+  "params": {
+    "status": [ "pending", "published" ]
+  }
+}
+------------------------------------------
+
+This call will return the rendered template:
+
+[source,js]
+------------------------------------------
+{
+  "template_output": {
+    "query": {
+      "terms": {
+        "status": [ <1>
+          "pending",
+          "published"
+        ]
+      }
+    }
+  }
+}
+------------------------------------------
+<1> `status` array has been populated with values from the `params` object.
+
+File and indexed templates can also be rendered by replacing `inline` with 
+`file` or `id` respectively. For example, to render a file template
+
+[source,js]
+------------------------------------------
+GET /_render/template
+{
+  "file": "my_template",
+  "params": {
+    "status": [ "pending", "published" ]
+  }
+}
+------------------------------------------
+
+Pre-registered templates can also be rendered using
+
+[source,js]
+------------------------------------------
+GET /_render/template/<template_name>
+{
+  "params": {
+    "...
+  }
+}
+------------------------------------------

+ 19 - 0
rest-api-spec/api/render_search_template.json

@@ -0,0 +1,19 @@
+{
+  "render_search_template": {
+    "documentation": "http://www.elasticsearch.org/guide/en/elasticsearch/reference/master/search-template.html",
+    "methods": ["GET", "POST"],
+    "url": {
+      "path": "/_render/template",
+      "paths": [ "/_render/template", "/_render/template/{id}" ],
+      "parts": {
+        "id": {
+         "type" : "string",
+         "description" : "The id of the stored search template"
+        }
+      }
+    },
+    "body": {
+      "description": "The search definition template and its params"
+    }
+  }
+}

+ 110 - 0
rest-api-spec/src/main/resources/rest-api-spec/test/template/30_render_search_template.yaml

@@ -0,0 +1,110 @@
+---
+"Indexed Template validate tests":
+
+  - do:
+      put_template:
+        id: "1"
+        body: { "template": { "query": { "match": { "text": "{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } } }
+  - match: { _id: "1" }
+
+  - do:
+      indices.refresh: {}
+
+  - do:
+      render_search_template:
+        body: { "id": "1", "params": { "my_value": "foo", "my_field": "field1" } }
+
+  - match: { template_output.query.match.text: "foo" }
+  - match: { template_output.aggs.my_terms.terms.field: "field1" }
+
+  - do:
+      render_search_template:
+        body: { "id": "1", "params": { "my_value": "bar", "my_field": "my_other_field" } }
+
+  - match: { template_output.query.match.text: "bar" }
+  - match: { template_output.aggs.my_terms.terms.field: "my_other_field" }
+
+  - do:
+      render_search_template:
+        id: "1"
+        body: { "params": { "my_value": "bar", "my_field": "field1" } }
+
+  - match: { template_output.query.match.text: "bar" }
+  - match: { template_output.aggs.my_terms.terms.field: "field1" }
+
+---
+"Inline Template validate tests":
+
+  - do:
+      render_search_template:
+        body: { "inline": { "query": { "match": { "text": "{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } }, "params": { "my_value": "foo", "my_field": "field1" } }
+
+  - match: { template_output.query.match.text: "foo" }
+  - match: { template_output.aggs.my_terms.terms.field: "field1" }
+
+  - do:
+      render_search_template:
+        body: { "inline": { "query": { "match": { "text": "{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } }, "params": { "my_value": "bar", "my_field": "my_other_field" } }
+
+  - match: { template_output.query.match.text: "bar" }
+  - match: { template_output.aggs.my_terms.terms.field: "my_other_field" }
+
+  - do:
+      catch: /Improperly.closed.variable.in.query-template/
+      render_search_template:
+        body: { "inline": { "query": { "match": { "text": "{{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } }, "params": { "my_value": "bar", "my_field": "field1" } }
+---
+"Escaped Indexed Template validate tests":
+
+  - do:
+      put_template:
+        id: "1"
+        body: { "template": "{ \"query\": { \"match\": { \"text\": \"{{my_value}}\" } }, \"size\": {{my_size}} }" }
+  - match: { _id: "1" }
+
+  - do:
+      indices.refresh: {}
+
+  - do:
+      render_search_template:
+        body: { "id": "1", "params": { "my_value": "foo", "my_size": 20 } }
+
+  - match: { template_output.query.match.text: "foo" }
+  - match: { template_output.size: 20 }
+
+  - do:
+      render_search_template:
+        body: { "id": "1", "params": { "my_value": "bar", "my_size": 100 } }
+
+  - match: { template_output.query.match.text: "bar" }
+  - match: { template_output.size: 100 }
+
+  - do:
+      render_search_template:
+        id: "1"
+        body: { "params": { "my_value": "bar", "my_size": 100 } }
+
+  - match: { template_output.query.match.text: "bar" }
+  - match: { template_output.size: 100 }
+
+---
+"Escaped Inline Template validate tests":
+
+  - do:
+      render_search_template:
+        body: { "inline": "{ \"query\": { \"match\": { \"text\": \"{{my_value}}\" } }, \"size\": {{my_size}} }", "params": { "my_value": "foo", "my_size": 20 } }
+
+  - match: { template_output.query.match.text: "foo" }
+  - match: { template_output.size: 20 }
+
+  - do:
+      render_search_template:
+        body: { "inline": "{ \"query\": { \"match\": { \"text\": \"{{my_value}}\" } }, \"size\": {{my_size}} }", "params": { "my_value": "bar", "my_size": 100 } }
+
+  - match: { template_output.query.match.text: "bar" }
+  - match: { template_output.size: 100 }
+
+  - do:
+      catch: /Improperly.closed.variable.in.query-template/
+      render_search_template:
+        body: { "inline": "{ \"query\": { \"match\": { \"text\": \"{{{my_value}}\" } }, \"size\": {{my_size}} }", "params": { "my_value": "bar", "my_size": 100 } }