diff --git a/docs/reference/query-dsl/queries.asciidoc b/docs/reference/query-dsl/queries.asciidoc
index e82600a04c7a0..a0fbc832d2927 100644
--- a/docs/reference/query-dsl/queries.asciidoc
+++ b/docs/reference/query-dsl/queries.asciidoc
@@ -81,3 +81,5 @@ include::queries/wildcard-query.asciidoc[]
include::queries/minimum-should-match.asciidoc[]
include::queries/multi-term-rewrite.asciidoc[]
+
+include::queries/template-query.asciidoc[]
diff --git a/docs/reference/query-dsl/queries/template-query.asciidoc b/docs/reference/query-dsl/queries/template-query.asciidoc
new file mode 100644
index 0000000000000..e08b25cfc1df5
--- /dev/null
+++ b/docs/reference/query-dsl/queries/template-query.asciidoc
@@ -0,0 +1,101 @@
+[[query-dsl-template-query]]
+=== Template Query
+
+coming[1.1.0]
+
+A query that accepts a query template and a map of key/value pairs to fill in
+template parameters.
+
+[source,js]
+------------------------------------------
+GET _search
+{
+ "query": {
+ "template": {
+ "query": {"match_{{template}}": {}},
+ "params" : {
+ "template" : "all"
+ }
+ }
+ }
+}
+
+------------------------------------------
+
+
+Alternatively escaping the template works as well:
+
+[source,js]
+------------------------------------------
+GET _search
+{
+ "query": {
+ "template": {
+ "query": "{\"match_{{template}}\": {}}\"",
+ "params" : {
+ "template" : "all"
+ }
+ }
+ }
+}
+------------------------------------------
+
+You register a template by storing it in the conf/scripts directory of
+elasticsearch. In order to execute the stored template reference it in the query parameters:
+
+
+[source,js]
+------------------------------------------
+GET _search
+{
+ "query": {
+ "template": {
+ "query": "storedTemplate",
+ "params" : {
+ "template" : "all"
+ }
+ }
+ }
+}
+
+------------------------------------------
+
+
+Templating is based on Mustache. For simple token substitution all you provide
+is a query containing some variable that you want to substitute and the actual
+values:
+
+
+[source,js]
+------------------------------------------
+GET _search
+{
+ "query": {
+ "template": {
+ "query": {"match_{{template}}": {}},
+ "params" : {
+ "template" : "all"
+ }
+ }
+ }
+}
+
+------------------------------------------
+
+which is then turned into:
+
+[source,js]
+------------------------------------------
+GET _search
+{
+ "query": {
+ "match_all": {}
+ }
+}
+------------------------------------------
+
+
+For more information on how Mustache templating and what kind of templating you
+can do with it check out the [online
+documentation](http://mustache.github.io/mustache.5.html) of the mustache project.
+
diff --git a/pom.xml b/pom.xml
index d5ea3fad15c72..229e23665dfdf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -163,7 +163,14 @@
-
+
+
+ com.github.spullara.mustache.java
+ compiler
+ 0.8.13
+ true
+
+
@@ -496,6 +503,7 @@
org.joda:joda-convert
io.netty:netty
com.ning:compress-lzf
+ com.github.spullara.mustache.java:compiler
diff --git a/rest-api-spec/test/search/30_template_query_execution.yaml b/rest-api-spec/test/search/30_template_query_execution.yaml
new file mode 100644
index 0000000000000..288466389e4ee
--- /dev/null
+++ b/rest-api-spec/test/search/30_template_query_execution.yaml
@@ -0,0 +1,41 @@
+---
+"Template query":
+
+ - do:
+ index:
+ index: test
+ type: testtype
+ id: 1
+ body: { "text": "value1" }
+ - do:
+ index:
+ index: test
+ type: testtype
+ id: 2
+ body: { "text": "value2" }
+ - do:
+ indices.refresh: {}
+
+ - do:
+ search:
+ body: { "query": { "template": { "query": { "term": { "text": { "value": "{{template}}" } } }, "params": { "template": "value1" } } } }
+
+ - match: { hits.total: 1 }
+
+ - do:
+ search:
+ body: { "query": { "template": { "query": {"match_{{template}}": {}}, "params" : { "template" : "all" } } } }
+
+ - match: { hits.total: 2 }
+
+ - do:
+ search:
+ body: { "query": { "template": { "query": "{ \"term\": { \"text\": { \"value\": \"{{template}}\" } } }", "params": { "template": "value1" } } } }
+
+ - match: { hits.total: 1 }
+
+ - do:
+ search:
+ body: { "query": { "template": { "query": "{\"match_{{template}}\": {}}", "params" : { "template" : "all" } } } }
+
+ - match: { hits.total: 2 }
diff --git a/src/main/assemblies/common-bin.xml b/src/main/assemblies/common-bin.xml
index c4a238e1caf7b..bd04eb5253864 100644
--- a/src/main/assemblies/common-bin.xml
+++ b/src/main/assemblies/common-bin.xml
@@ -42,4 +42,4 @@
/
-
\ No newline at end of file
+
diff --git a/src/main/java/org/elasticsearch/index/query/TemplateQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/TemplateQueryBuilder.java
new file mode 100644
index 0000000000000..4d28c58d9632a
--- /dev/null
+++ b/src/main/java/org/elasticsearch/index/query/TemplateQueryBuilder.java
@@ -0,0 +1,52 @@
+/**
+ * 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.index.query;
+
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Facilitates creating template query requests.
+ * */
+public class TemplateQueryBuilder extends BaseQueryBuilder {
+
+ /** Parameters to fill the template with. */
+ private Map vars;
+ /** Template to fill.*/
+ private String template;
+
+ /**
+ * @param template the template to use for that query.
+ * @param vars the parameters to fill the template with.
+ * */
+ public TemplateQueryBuilder(String template, Map vars) {
+ this.template = template;
+ this.vars = vars;
+ }
+
+ @Override
+ protected void doXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject(TemplateQueryParser.NAME);
+ builder.field(TemplateQueryParser.QUERY, template);
+ builder.field(TemplateQueryParser.PARAMS, vars);
+ builder.endObject();
+ }
+}
diff --git a/src/main/java/org/elasticsearch/index/query/TemplateQueryParser.java b/src/main/java/org/elasticsearch/index/query/TemplateQueryParser.java
new file mode 100644
index 0000000000000..91a8fcb651b26
--- /dev/null
+++ b/src/main/java/org/elasticsearch/index/query/TemplateQueryParser.java
@@ -0,0 +1,124 @@
+/**
+ * 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.index.query;
+
+import org.apache.lucene.search.Query;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.elasticsearch.script.ExecutableScript;
+import org.elasticsearch.script.ScriptService;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * In the simplest case, parse template string and variables from the request, compile the template and
+ * execute the template against the given variables.
+ * */
+public class TemplateQueryParser implements QueryParser {
+
+ /** Name to reference this type of query. */
+ public static final String NAME = "template";
+ /** Name of query parameter containing the template string. */
+ public static final String QUERY = "query";
+ /** Name of query parameter containing the template parameters. */
+ public static final String PARAMS = "params";
+ /** This is what we are registered with for query executions. */
+ private final ScriptService scriptService;
+
+ /**
+ * @param scriptService will automatically be wired by Guice
+ * */
+ @Inject
+ public TemplateQueryParser(ScriptService scriptService) {
+ this.scriptService = scriptService;
+ }
+
+ /**
+ * @return a list of names this query is registered under.
+ * */
+ @Override
+ public String[] names() {
+ return new String[] {NAME};
+ }
+
+ @Override
+ @Nullable
+ public Query parse(QueryParseContext parseContext) throws IOException {
+ XContentParser parser = parseContext.parser();
+
+
+ String template = "";
+ Map vars = new HashMap();
+
+ String currentFieldName = null;
+ XContentParser.Token token;
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ } else if (QUERY.equals(currentFieldName)) {
+ if (token == XContentParser.Token.START_OBJECT && ! parser.hasTextCharacters()) {
+ // when called with un-escaped json string
+ XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent);
+ builder.copyCurrentStructure(parser);
+ template = builder.string();
+ } else {
+ // when called with excaped json string or when called with filename
+ template = parser.text();
+ }
+ } else if (PARAMS.equals(currentFieldName)) {
+ XContentParser.Token innerToken;
+ String key = null;
+ while ((innerToken = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ // parsing template parameter map
+ if (innerToken == XContentParser.Token.FIELD_NAME) {
+ key = parser.currentName();
+ } else {
+ if (key != null) {
+ vars.put(key, parser.text());
+ } else {
+ throw new IllegalStateException("Template parameter key must not be null.");
+ }
+ key = null;
+ }
+ }
+ }
+ }
+
+ ExecutableScript executable = this.scriptService.executable("mustache", template, vars);
+ BytesReference querySource = (BytesReference) executable.run();
+
+ XContentParser qSourceParser = XContentFactory.xContent(querySource).createParser(querySource);
+ try {
+ final QueryParseContext context = new QueryParseContext(parseContext.index(), parseContext.indexQueryParser);
+ context.reset(qSourceParser);
+ Query result = context.parseInnerQuery();
+ parser.nextToken();
+ return result;
+ } finally {
+ qSourceParser.close();
+ }
+ }
+}
diff --git a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java
index da7029f6ea3c0..44a9474669af3 100644
--- a/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java
+++ b/src/main/java/org/elasticsearch/indices/query/IndicesQueriesModule.java
@@ -103,6 +103,7 @@ protected void configure() {
qpBinders.addBinding().to(SpanMultiTermQueryParser.class).asEagerSingleton();
qpBinders.addBinding().to(FunctionScoreQueryParser.class).asEagerSingleton();
qpBinders.addBinding().to(SimpleQueryStringParser.class).asEagerSingleton();
+ qpBinders.addBinding().to(TemplateQueryParser.class).asEagerSingleton();
if (ShapesAvailability.JTS_AVAILABLE) {
qpBinders.addBinding().to(GeoShapeQueryParser.class).asEagerSingleton();
diff --git a/src/main/java/org/elasticsearch/script/ScriptModule.java b/src/main/java/org/elasticsearch/script/ScriptModule.java
index bee40a4fb881e..5f2834de4f7f0 100644
--- a/src/main/java/org/elasticsearch/script/ScriptModule.java
+++ b/src/main/java/org/elasticsearch/script/ScriptModule.java
@@ -25,7 +25,9 @@
import org.elasticsearch.common.inject.AbstractModule;
import org.elasticsearch.common.inject.multibindings.MapBinder;
import org.elasticsearch.common.inject.multibindings.Multibinder;
+import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.script.mustache.MustacheScriptEngineService;
import org.elasticsearch.script.mvel.MvelScriptEngineService;
import java.util.List;
@@ -80,6 +82,13 @@ protected void configure() {
} catch (Throwable t) {
// no MVEL
}
+
+ try {
+ multibinder.addBinding().to(MustacheScriptEngineService.class);
+ } catch (Throwable t) {
+ Loggers.getLogger(MustacheScriptEngineService.class).trace("failed to load mustache", t);
+ }
+
for (Class extends ScriptEngineService> scriptEngine : scriptEngines) {
multibinder.addBinding().to(scriptEngine);
}
diff --git a/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngineService.java b/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngineService.java
new file mode 100644
index 0000000000000..22927fc7e3de5
--- /dev/null
+++ b/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngineService.java
@@ -0,0 +1,194 @@
+/**
+ * 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.script.mustache;
+
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.component.AbstractComponent;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.io.FastStringReader;
+import org.elasticsearch.common.io.UTF8StreamWriter;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.script.ExecutableScript;
+import org.elasticsearch.script.ScriptEngineService;
+import org.elasticsearch.script.SearchScript;
+import org.elasticsearch.search.lookup.SearchLookup;
+
+import java.io.IOException;
+import java.lang.ref.SoftReference;
+import java.util.Map;
+
+/**
+ * Main entry point handling template registration, compilation and
+ * execution.
+ *
+ * Template handling is based on Mustache. Template handling is a two step
+ * process: First compile the string representing the template, the resulting
+ * {@link Mustache} object can then be re-used for subsequent executions.
+ */
+public class MustacheScriptEngineService extends AbstractComponent implements ScriptEngineService {
+
+ /** Thread local UTF8StreamWriter to store template execution results in, thread local to save object creation.*/
+ private static ThreadLocal> utf8StreamWriter = new ThreadLocal>();
+
+ /** If exists, reset and return, otherwise create, reset and return a writer.*/
+ private static UTF8StreamWriter utf8StreamWriter() {
+ SoftReference ref = utf8StreamWriter.get();
+ UTF8StreamWriter writer = (ref == null) ? null : ref.get();
+ if (writer == null) {
+ writer = new UTF8StreamWriter(1024 * 4);
+ utf8StreamWriter.set(new SoftReference(writer));
+ }
+ writer.reset();
+ return writer;
+ }
+
+ /**
+ * @param settings automatically wired by Guice.
+ * */
+ @Inject
+ public MustacheScriptEngineService(Settings settings) {
+ super(settings);
+ }
+
+ /**
+ * Compile a template string to (in this case) a Mustache object than can
+ * later be re-used for execution to fill in missing parameter values.
+ *
+ * @param template
+ * a string representing the template to compile.
+ * @return a compiled template object for later execution.
+ * */
+ public Object compile(String template) {
+ /** Factory to generate Mustache objects from. */
+ return (new DefaultMustacheFactory()).compile(new FastStringReader(template), "query-template");
+ }
+
+ /**
+ * Execute a compiled template object (as retrieved from the compile method)
+ * and fill potential place holders with the variables given.
+ *
+ * @param template
+ * compiled template object.
+ * @param vars
+ * map of variables to use during substitution.
+ *
+ * @return the processed string with all given variables substitued.
+ * */
+ public Object execute(Object template, Map vars) {
+ BytesStreamOutput result = new BytesStreamOutput();
+ UTF8StreamWriter writer = utf8StreamWriter().setOutput(result);
+ ((Mustache) template).execute(writer, vars);
+ try {
+ writer.flush();
+ } catch (IOException e) {
+ logger.error("Could not execute query template (failed to flush writer): ", e);
+ } finally {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ logger.error("Could not execute query template (failed to close writer): ", e);
+ }
+ }
+ return result.bytes();
+ }
+
+ @Override
+ public String[] types() {
+ return new String[] {"mustache"};
+ }
+
+ @Override
+ public String[] extensions() {
+ return new String[] {"mustache"};
+ }
+
+ @Override
+ public ExecutableScript executable(Object mustache,
+ @Nullable Map vars) {
+ return new MustacheExecutableScript((Mustache) mustache, vars);
+ }
+
+ @Override
+ public SearchScript search(Object compiledScript, SearchLookup lookup,
+ @Nullable Map vars) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object unwrap(Object value) {
+ return value;
+ }
+
+ @Override
+ public void close() {
+ // Nothing to do here
+ }
+
+ /**
+ * Used at query execution time by script service in order to execute a query template.
+ * */
+ private class MustacheExecutableScript implements ExecutableScript {
+ /** Compiled template object. */
+ private Mustache mustache;
+ /** Parameters to fill above object with. */
+ private Map vars;
+
+ /**
+ * @param mustache the compiled template object
+ * @param vars the parameters to fill above object with
+ **/
+ public MustacheExecutableScript(Mustache mustache,
+ Map vars) {
+ this.mustache = mustache;
+ this.vars = vars;
+ }
+
+ @Override
+ public void setNextVar(String name, Object value) {
+ this.vars.put(name, value);
+ }
+
+ @Override
+ public Object run() {
+ BytesStreamOutput result = new BytesStreamOutput();
+ UTF8StreamWriter writer = utf8StreamWriter().setOutput(result);
+ ((Mustache) mustache).execute(writer, vars);
+ try {
+ writer.flush();
+ } catch (IOException e) {
+ logger.error("Could not execute query template (failed to flush writer): ", e);
+ } finally {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ logger.error("Could not execute query template (failed to close writer): ", e);
+ }
+ }
+ return result.bytes();
+ }
+
+ @Override
+ public Object unwrap(Object value) {
+ return value;
+ }
+ }
+}
diff --git a/src/test/java/org/elasticsearch/index/query/TemplateQueryBuilderTest.java b/src/test/java/org/elasticsearch/index/query/TemplateQueryBuilderTest.java
new file mode 100644
index 0000000000000..f4edc46c0b5ec
--- /dev/null
+++ b/src/test/java/org/elasticsearch/index/query/TemplateQueryBuilderTest.java
@@ -0,0 +1,47 @@
+/**
+ * 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.index.query;
+
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Test building and serialising a template search request.
+ * */
+public class TemplateQueryBuilderTest extends ElasticsearchTestCase {
+
+ @Test
+ public void testJSONGeneration() throws IOException {
+ Map vars = new HashMap();
+ vars.put("template", "filled");
+ TemplateQueryBuilder builder = new TemplateQueryBuilder("I am a $template string", vars);
+ XContentBuilder content = XContentFactory.jsonBuilder();
+ content.startObject();
+ builder.doXContent(content, null);
+ content.endObject();
+ content.close();
+ assertEquals(content.string(), "{\"template\":{\"query\":\"I am a $template string\",\"params\":{\"template\":\"filled\"}}}");
+ }
+}
diff --git a/src/test/java/org/elasticsearch/index/query/TemplateQueryParserTest.java b/src/test/java/org/elasticsearch/index/query/TemplateQueryParserTest.java
new file mode 100644
index 0000000000000..0d95cf9c65df6
--- /dev/null
+++ b/src/test/java/org/elasticsearch/index/query/TemplateQueryParserTest.java
@@ -0,0 +1,108 @@
+/**
+ * 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.index.query;
+
+import org.apache.lucene.search.ConstantScoreQuery;
+import org.apache.lucene.search.Query;
+import org.elasticsearch.cache.recycler.CacheRecyclerModule;
+import org.elasticsearch.cluster.ClusterService;
+import org.elasticsearch.common.inject.AbstractModule;
+import org.elasticsearch.common.inject.Injector;
+import org.elasticsearch.common.inject.ModulesBuilder;
+import org.elasticsearch.common.inject.util.Providers;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.settings.SettingsModule;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.index.IndexNameModule;
+import org.elasticsearch.index.analysis.AnalysisModule;
+import org.elasticsearch.index.cache.IndexCacheModule;
+import org.elasticsearch.index.codec.CodecModule;
+import org.elasticsearch.index.engine.IndexEngineModule;
+import org.elasticsearch.index.query.functionscore.FunctionScoreModule;
+import org.elasticsearch.index.settings.IndexSettingsModule;
+import org.elasticsearch.index.similarity.SimilarityModule;
+import org.elasticsearch.indices.fielddata.breaker.CircuitBreakerService;
+import org.elasticsearch.indices.fielddata.breaker.DummyCircuitBreakerService;
+import org.elasticsearch.indices.query.IndicesQueriesModule;
+import org.elasticsearch.script.ScriptModule;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.elasticsearch.threadpool.ThreadPoolModule;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Test parsing and executing a template request.
+ * */
+public class TemplateQueryParserTest extends ElasticsearchTestCase {
+
+ private Injector injector;
+ private QueryParseContext context;
+
+ @Before
+ public void setup() {
+ Settings settings = ImmutableSettings.Builder.EMPTY_SETTINGS;
+
+ Index index = new Index("test");
+ injector = new ModulesBuilder().add(
+ new SettingsModule(settings),
+ new CacheRecyclerModule(settings),
+ new CodecModule(settings),
+ new ThreadPoolModule(settings),
+ new IndicesQueriesModule(),
+ new ScriptModule(settings),
+ new IndexSettingsModule(index, settings),
+ new IndexCacheModule(settings),
+ new AnalysisModule(settings),
+ new IndexEngineModule(settings),
+ new SimilarityModule(settings),
+ new IndexNameModule(index),
+ new IndexQueryParserModule(settings),
+ new FunctionScoreModule(),
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ClusterService.class).toProvider(Providers.of((ClusterService) null));
+ bind(CircuitBreakerService.class).to(DummyCircuitBreakerService.class);
+ }
+ }
+ ).createInjector();
+
+ IndexQueryParserService queryParserService = injector.getInstance(IndexQueryParserService.class);
+ context = new QueryParseContext(index, queryParserService);
+ }
+
+ @Test
+ public void testParser() throws IOException {
+ String templateString = "{\"template\": {"
+ + "\"query\":{\"match_{{template}}\": {}},"
+ + "\"params\":{\"template\":\"all\"}}" + "}";
+
+ XContentParser templateSourceParser = XContentFactory.xContent(templateString).createParser(templateString);
+ context.reset(templateSourceParser);
+
+ TemplateQueryParser parser = injector.getInstance(TemplateQueryParser.class);
+ Query query = parser.parse(context);
+ assertTrue("Parsing template query failed.", query instanceof ConstantScoreQuery);
+ }
+}
diff --git a/src/test/java/org/elasticsearch/index/query/TemplateQueryTest.java b/src/test/java/org/elasticsearch/index/query/TemplateQueryTest.java
new file mode 100644
index 0000000000000..b99fec6ea76ff
--- /dev/null
+++ b/src/test/java/org/elasticsearch/index/query/TemplateQueryTest.java
@@ -0,0 +1,124 @@
+/**
+ * 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.index.query;
+
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Full integration test of the template query plugin.
+ * */
+@ElasticsearchIntegrationTest.ClusterScope(scope = ElasticsearchIntegrationTest.Scope.SUITE)
+public class TemplateQueryTest extends ElasticsearchIntegrationTest {
+
+ @Before
+ public void setup() {
+ createIndex("test");
+ ensureGreen();
+
+ client().prepareIndex("test", "testtype").setId("1")
+ .setSource("text", "value1").get();
+ client().prepareIndex("test", "testtype").setId("2")
+ .setSource("text", "value2").get();
+ refresh();
+ }
+
+ @Test
+ public void testTemplateInBody() throws IOException {
+ Map vars = new HashMap();
+ vars.put("template", "all");
+
+ TemplateQueryBuilder builder = new TemplateQueryBuilder(
+ "{\"match_{{template}}\": {}}\"", vars);
+ SearchResponse sr = client().prepareSearch().setQuery(builder)
+ .execute().actionGet();
+ ElasticsearchAssertions.assertHitCount(sr, 2);
+ }
+
+ @Test
+ public void testTemplateWOReplacementInBody() throws IOException {
+ Map vars = new HashMap();
+
+ TemplateQueryBuilder builder = new TemplateQueryBuilder(
+ "{\"match_all\": {}}\"", vars);
+ SearchResponse sr = client().prepareSearch().setQuery(builder)
+ .execute().actionGet();
+ ElasticsearchAssertions.assertHitCount(sr, 2);
+ }
+
+ @Test
+ public void testTemplateInFile() {
+ Map vars = new HashMap();
+ vars.put("template", "all");
+
+ TemplateQueryBuilder builder = new TemplateQueryBuilder(
+ "storedTemplate", vars);
+ SearchResponse sr = client().prepareSearch().setQuery(builder)
+ .execute().actionGet();
+ ElasticsearchAssertions.assertHitCount(sr, 2);
+
+ }
+
+ @Test
+ public void testRawEscapedTemplate() throws IOException {
+ String query = "{\"template\": {\"query\": \"{\\\"match_{{template}}\\\": {}}\\\"\",\"params\" : {\"template\" : \"all\"}}}";
+
+ SearchResponse sr = client().prepareSearch().setQuery(query)
+ .execute().actionGet();
+ ElasticsearchAssertions.assertHitCount(sr, 2);
+ }
+
+ @Test
+ public void testRawTemplate() throws IOException {
+ String query = "{\"template\": {\"query\": {\"match_{{template}}\": {}},\"params\" : {\"template\" : \"all\"}}}";
+ SearchResponse sr = client().prepareSearch().setQuery(query)
+ .execute().actionGet();
+ ElasticsearchAssertions.assertHitCount(sr, 2);
+ }
+
+ @Test
+ public void testRawFSTemplate() throws IOException {
+ String query = "{\"template\": {\"query\": \"storedTemplate\",\"params\" : {\"template\" : \"all\"}}}";
+
+ SearchResponse sr = client().prepareSearch().setQuery(query)
+ .execute().actionGet();
+ ElasticsearchAssertions.assertHitCount(sr, 2);
+ }
+
+ @Override
+ public Settings nodeSettings(int nodeOrdinal) {
+ String scriptPath = this.getClass()
+ .getResource("config").getPath();
+
+ Settings settings = ImmutableSettings
+ .settingsBuilder()
+ .put("path.conf", scriptPath).build();
+
+ return settings;
+ }
+}
diff --git a/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTest.java b/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTest.java
new file mode 100644
index 0000000000000..fce98ff45fea3
--- /dev/null
+++ b/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTest.java
@@ -0,0 +1,55 @@
+/**
+ * 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.script.mustache;
+
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Mustache based templating test
+ * */
+public class MustacheScriptEngineTest extends ElasticsearchTestCase {
+ private MustacheScriptEngineService qe;
+
+ private static String TEMPLATE = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ + "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
+
+ @Before
+ public void setup() {
+ qe = new MustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
+ }
+
+ @Test
+ public void testSimpleParameterReplace() {
+ Map vars = new HashMap();
+ vars.put("boost_val", "0.3");
+ BytesReference o = (BytesReference) qe.execute(qe.compile(TEMPLATE), vars);
+ assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ + "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.3 } }}",
+ new String(o.toBytes(), Charset.forName("UTF-8")));
+ }
+
+}
diff --git a/src/test/java/org/elasticsearch/script/mustache/MustacheTest.java b/src/test/java/org/elasticsearch/script/mustache/MustacheTest.java
new file mode 100644
index 0000000000000..84cc7e42e7a36
--- /dev/null
+++ b/src/test/java/org/elasticsearch/script/mustache/MustacheTest.java
@@ -0,0 +1,56 @@
+/**
+ * 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.script.mustache;
+
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.HashMap;
+
+/**
+ * Figure out how Mustache works for the simplest use case. Leaving in here for now for reference.
+ * */
+public class MustacheTest extends ElasticsearchTestCase {
+
+ @Test
+ public void test() {
+ HashMap scopes = new HashMap();
+ scopes.put("boost_val", "0.2");
+
+ String template = "GET _search {\"query\": " + "{\"boosting\": {"
+ + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ + "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}"
+ + "}}, \"negative_boost\": {{boost_val}} } }}";
+ MustacheFactory f = new DefaultMustacheFactory();
+ Mustache mustache = f.compile(new StringReader(template), "example");
+ StringWriter writer = new StringWriter();
+ mustache.execute(writer, scopes);
+ writer.flush();
+ assertEquals(
+ "Mustache templating broken",
+ "GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ + "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.2 } }}",
+ writer.toString());
+ }
+}
diff --git a/src/test/resources/org/elasticsearch/index/query/config/scripts/storedTemplate.mustache b/src/test/resources/org/elasticsearch/index/query/config/scripts/storedTemplate.mustache
new file mode 100644
index 0000000000000..a779da7c467c5
--- /dev/null
+++ b/src/test/resources/org/elasticsearch/index/query/config/scripts/storedTemplate.mustache
@@ -0,0 +1,3 @@
+{
+ "match_{{template}}": {}
+}