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

Closes #6821
This commit is contained in:
Colin Goodheart-Smithe 2015-06-09 08:31:01 +01:00
parent bbaf4710cb
commit d9ab3cba77
17 changed files with 795 additions and 1 deletions

View File

@ -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

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
});
}
}

View File

@ -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;

View File

@ -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.
*/

View File

@ -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);

View File

@ -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();

View File

@ -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);
}});
}
}

View File

@ -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));
}
}

View File

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

View File

@ -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": {
"...
}
}
------------------------------------------

View File

@ -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"
}
}
}

View File

@ -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 } }