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