From 3e78138a56b9d07bc1ec8099a0b8a1bf1e546975 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 10 May 2018 11:12:08 +0200 Subject: [PATCH 01/12] [Painless] Add more contexts to painless execute api This change adds two contexts the execute scripts against: * SEARCH_SCRIPT: Allows to run scripts in a search script context. This context is used in `function_score` query's script function, script fields, script sorting and `terms_set` query. * FILTER_SCRIPT: Allows to run scripts in a filter script context. This context is used in the `script` query. In both contexts a index name needs to be specified and a sample document. The document is needed to create an in-memory index that the script can access via the `doc[...]` and other notations. The index name is needed because a mapping is needed to index the document. Examples: ``` POST /_scripts/painless/_execute { "script": { "source": "doc['field'].value.length()" }, "context" : { "search_script": { "document": { "field": "four" }, "index": "my-index" } } } ``` Returns: ``` { "result": 4 } ``` POST /_scripts/painless/_execute { "script": { "source": "doc['field'].value.length() <= params.max_length", "params": { "max_length": 4 } }, "context" : { "filter_script": { "document": { "field": "four" }, "index": "my-index" } } } Returns: ``` { "result": true } ``` --- .../painless/PainlessExecuteAction.java | 224 ++++++++++++++++-- 1 file changed, 206 insertions(+), 18 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index aa650a37c4fa2..64b86ce74cc1c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -18,6 +18,13 @@ */ package org.elasticsearch.painless; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.store.RAMDirectory; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; @@ -26,33 +33,51 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.CheckedBiConsumer; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.mapper.ParsedDocument; +import org.elasticsearch.index.mapper.SourceToParse; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.indices.IndicesService; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.script.FilterScript; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.SearchScript; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -86,9 +111,18 @@ public static class Request extends ActionRequest implements ToXContent { private static final ParseField SCRIPT_FIELD = new ParseField("script"); private static final ParseField CONTEXT_FIELD = new ParseField("context"); - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + private static final ParseField INDEX_FIELD = new ParseField("index"); + private static final ParseField DOCUMENT_FIELD = new ParseField("document"); + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "painless_execute_request", args -> new Request((Script) args[0], (SupportedContext) args[1])); + private static class ParseContext { + + private String index; + private BytesReference document; + + } + static { PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> Script.parse(p), SCRIPT_FIELD); PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> { @@ -96,21 +130,57 @@ public static class Request extends ActionRequest implements ToXContent { XContentParser.Token token = p.nextToken(); assert token == XContentParser.Token.FIELD_NAME; String contextType = p.currentName(); - token = p.nextToken(); - assert token == XContentParser.Token.START_OBJECT; - token = p.nextToken(); - assert token == XContentParser.Token.END_OBJECT; - token = p.nextToken(); - assert token == XContentParser.Token.END_OBJECT; - return SupportedContext.valueOf(contextType.toUpperCase(Locale.ROOT)); + SupportedContext supportedContext = SupportedContext.valueOf(contextType.toUpperCase(Locale.ROOT)); + if (supportedContext == SupportedContext.PAINLESS_TEST) { + token = p.nextToken(); + assert token == XContentParser.Token.START_OBJECT; + token = p.nextToken(); + assert token == XContentParser.Token.END_OBJECT; + token = p.nextToken(); + assert token == XContentParser.Token.END_OBJECT; + } else if (supportedContext == SupportedContext.FILTER_SCRIPT || supportedContext == SupportedContext.SEARCH_SCRIPT) { + for (token = p.nextToken(); token != XContentParser.Token.END_OBJECT; token = p.nextToken()) { + if (token.isValue()) { + if (INDEX_FIELD.match(p.currentName(), p.getDeprecationHandler())) { + c.index = p.textOrNull(); + } else { + throw new IllegalStateException("Unexpected field name [" + p.currentName() + "]"); + } + } else if (token == XContentParser.Token.START_OBJECT) { + if (DOCUMENT_FIELD.match(p.currentName(), p.getDeprecationHandler())) { + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.copyCurrentStructure(p); + builder.flush(); + c.document = BytesReference.bytes(builder); + } + } + } else if (token == XContentParser.Token.FIELD_NAME) { + // ignore + } else { + throw new IllegalStateException("Unexpected token [" + token + "]"); + } + } + } else { + assert false : "Unsupported context"; + } + return supportedContext; }, CONTEXT_FIELD); } private Script script; private SupportedContext context; + private String index; + private BytesReference document; + private XContentType xContentType; + static Request parse(XContentParser parser) throws IOException { - return PARSER.parse(parser, null); + ParseContext parseContext = new ParseContext(); + Request request = PARSER.parse(parser, parseContext); + request.setIndex(parseContext.index); + request.setDocument(parseContext.document); + request.setXContentType(parser.contentType()); + return request; } Request(Script script, SupportedContext context) { @@ -129,12 +199,44 @@ public SupportedContext getContext() { return context; } + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public BytesReference getDocument() { + return document; + } + + public void setDocument(BytesReference document) { + this.document = document; + } + + public XContentType getXContentType() { + return xContentType; + } + + public void setXContentType(XContentType xContentType) { + this.xContentType = xContentType; + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; if (script.getType() != ScriptType.INLINE) { validationException = addValidationError("only inline scripts are supported", validationException); } + if (context == SupportedContext.FILTER_SCRIPT || context == SupportedContext.SEARCH_SCRIPT) { + if (index == null) { + validationException = addValidationError("index is a required parameter for current context", validationException); + } + if (document == null) { + validationException = addValidationError("document is a required parameter for current context", validationException); + } + } return validationException; } @@ -143,6 +245,11 @@ public void readFrom(StreamInput in) throws IOException { super.readFrom(in); script = new Script(in); context = SupportedContext.fromId(in.readByte()); + if (context == SupportedContext.FILTER_SCRIPT) { + index = in.readString(); + document = in.readBytesReference(); + xContentType = XContentType.fromMediaType(in.readString()); + } } @Override @@ -150,6 +257,12 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); script.writeTo(out); out.writeByte(context.id); + out.writeOptionalString(index); + if (context == SupportedContext.FILTER_SCRIPT) { + out.writeString(index); + out.writeBytesReference(document); + out.writeString(xContentType.mediaType()); + } } // For testing only: @@ -159,6 +272,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(CONTEXT_FIELD.getPreferredName()); { builder.startObject(context.name()); + if (context == SupportedContext.FILTER_SCRIPT) { + builder.field(INDEX_FIELD.getPreferredName(), index); + try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, document)) { + parser.nextToken(); + builder.generator().copyCurrentStructure(parser); + } + } builder.endObject(); } builder.endObject(); @@ -171,17 +292,21 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Request request = (Request) o; return Objects.equals(script, request.script) && - context == request.context; + context == request.context && + Objects.equals(index, request.index) && + Objects.equals(document, request.document); } @Override public int hashCode() { - return Objects.hash(script, context); + return Objects.hash(script, context, index, document); } public enum SupportedContext { - PAINLESS_TEST((byte) 0); + PAINLESS_TEST((byte) 0), + FILTER_SCRIPT((byte) 1), + SEARCH_SCRIPT((byte) 2); private final byte id; @@ -193,6 +318,10 @@ public static SupportedContext fromId(byte id) { switch (id) { case 0: return PAINLESS_TEST; + case 1: + return FILTER_SCRIPT; + case 2: + return SEARCH_SCRIPT; default: throw new IllegalArgumentException("unknown context [" + id + "]"); } @@ -283,30 +412,89 @@ public interface Factory { public static class TransportAction extends HandledTransportAction { - private final ScriptService scriptService; + private final ClusterService clusterService; + private final IndicesService indicesServices; @Inject public TransportAction(Settings settings, ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, - ScriptService scriptService) { + ScriptService scriptService, ClusterService clusterService, IndicesService indicesServices) { super(settings, NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, Request::new); this.scriptService = scriptService; + this.clusterService = clusterService; + this.indicesServices = indicesServices; } @Override protected void doExecute(Request request, ActionListener listener) { switch (request.context) { case PAINLESS_TEST: - PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT); - PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams()); - String result = Objects.toString(painlessTestScript.execute()); - listener.onResponse(new Response(result)); + { + PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT); + PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams()); + String result = Objects.toString(painlessTestScript.execute()); + listener.onResponse(new Response(result)); + } + break; + case FILTER_SCRIPT: + { + indexAndOpenIndexReader(request, (context, leafReaderContext) -> { + FilterScript.Factory factory = scriptService.compile(request.script, FilterScript.CONTEXT); + FilterScript.LeafFactory leafFactory = + factory.newFactory(request.getScript().getParams(), context.lookup()); + FilterScript filterScript = leafFactory.newInstance(leafReaderContext); + boolean result = filterScript.execute(); + listener.onResponse(new Response(result)); + }); + } + break; + case SEARCH_SCRIPT: + { + indexAndOpenIndexReader(request, (context, leafReaderContext) -> { + SearchScript.Factory factory = scriptService.compile(request.script, SearchScript.CONTEXT); + SearchScript.LeafFactory leafFactory = + factory.newFactory(request.getScript().getParams(), context.lookup()); + SearchScript searchScript = leafFactory.newInstance(leafReaderContext); + Object result = searchScript.run(); + listener.onResponse(new Response(result)); + }); + } break; default: throw new UnsupportedOperationException("unsupported context [" + request.context + "]"); } } + private void indexAndOpenIndexReader(Request request, + CheckedBiConsumer handler) { + IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed(); + Index[] concreteIndices = + indexNameExpressionResolver.concreteIndices(clusterService.state(), indicesOptions, request.index); + if (concreteIndices.length != 1) { + throw new IllegalArgumentException("[" + request.index + "] does not resolve to a single index"); + } + Index concreteIndex = concreteIndices[0]; + IndexService indexService = indicesServices.indexServiceSafe(concreteIndex); + Analyzer defaultAnalyzer = indexService.getIndexAnalyzers().getDefaultIndexAnalyzer(); + + try (RAMDirectory ramDirectory = new RAMDirectory()) { + try (IndexWriter indexWriter = new IndexWriter(ramDirectory, new IndexWriterConfig(defaultAnalyzer))) { + String index = concreteIndex.getName(); + String type = indexService.mapperService().documentMapper().type(); + SourceToParse sourceToParse = SourceToParse.source(index, type, "_id", request.document, request.xContentType); + ParsedDocument parsedDocument = indexService.mapperService().documentMapper().parse(sourceToParse); + indexWriter.addDocuments(parsedDocument.docs()); + try (IndexReader indexReader = DirectoryReader.open(indexWriter)) { + final long absoluteStartMillis = System.currentTimeMillis(); + QueryShardContext context = + indexService.newQueryShardContext(0, indexReader, () -> absoluteStartMillis, null); + handler.accept(context, indexReader.leaves().get(0)); + } + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } static class RestAction extends BaseRestHandler { From e86764a1c4c31c7a6705964c10b2fcb38e2c5fb6 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 23 May 2018 14:37:55 +0200 Subject: [PATCH 02/12] added tests and docs, changed the parsing code to make more use of ObjectParser --- .../painless/painless-execute-script.asciidoc | 92 +++- .../painless/PainlessExecuteAction.java | 399 +++++++++++------- .../painless/PainlessExecuteRequestTests.java | 38 +- .../painless/70_execute_painless_scripts.yml | 67 ++- 4 files changed, 445 insertions(+), 151 deletions(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index a3ac5b578d781..f99bb05029769 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -25,7 +25,7 @@ The only variable that is available is `params`, which can be used to access use The result of the script is always converted to a string. If no context is specified then this context is used by default. -==== Example +====== Example Request: @@ -52,4 +52,92 @@ Response: "result": "0.1" } -------------------------------------------------- -// TESTRESPONSE \ No newline at end of file +// TESTRESPONSE + +===== Filter script context + +The `filter` context executes scripts as if they were executed inside a `script` query. +For testing purposes a document must be provided that will be indexed temporarily in-memory and +is accessible to the script being tested. Because of this the _source, stored fields and doc values +are available in the script being tested. + +The following parameters are required inside a filter context: + +document:: Contains the document that will be temporarily indexed in-memory and is accessible from the script. +index:: The name of an index containing a mapping that is compatable with the document being indexed. + +====== Example + +[source,js] +---------------------------------------------------------------- +POST /_scripts/painless/_execute +{ + "script": { + "source": "doc['field'].value.length() <= params.max_length", + "params": { + "max_length": 4 + } + "context": { + "index": "my-index", + "document": { + "field": "four" + } + } + } +} +---------------------------------------------------------------- +// CONSOLE + +Response: + +[source,js] +-------------------------------------------------- +{ + "result": true +} +-------------------------------------------------- +// TESTRESPONSE + + +===== Score script context + +The `score` context executes scripts as if they were executed inside a `script_score` function in +`function_score` query. + +Available parameters inside a score context: + +document:: Contains the document that will be temporarily indexed in-memory and is accessible from the script. +index:: The name of an index containing a mapping that is compatable with the document being indexed. +query:: If `_score` is used in the script then a query can specified that will be used the compute a score. + +====== Example + +[source,js] +---------------------------------------------------------------- +POST /_scripts/painless/_execute +{ + "script": { + "source": "doc['rank'].value / params.max_rank", + "params": { + "max_rank": 4 + } + "context": { + "index": "my-index", + "document": { + "rank": 4 + } + } + } +} +---------------------------------------------------------------- +// CONSOLE + +Response: + +[source,js] +-------------------------------------------------- +{ + "result": 0.8 +} +-------------------------------------------------- +// TESTRESPONSE diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index 64b86ce74cc1c..ed74973a8138e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -24,7 +24,12 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; import org.apache.lucene.store.RAMDirectory; +import org.elasticsearch.Version; import org.elasticsearch.action.Action; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; @@ -36,6 +41,7 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.CheckedBiConsumer; @@ -44,14 +50,16 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; @@ -59,6 +67,8 @@ import org.elasticsearch.index.IndexService; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.mapper.SourceToParse; +import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.rest.BaseRestHandler; @@ -77,7 +87,8 @@ import org.elasticsearch.transport.TransportService; import java.io.IOException; -import java.io.UncheckedIOException; +import java.util.Collections; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -111,81 +122,182 @@ public static class Request extends ActionRequest implements ToXContent { private static final ParseField SCRIPT_FIELD = new ParseField("script"); private static final ParseField CONTEXT_FIELD = new ParseField("context"); - private static final ParseField INDEX_FIELD = new ParseField("index"); - private static final ParseField DOCUMENT_FIELD = new ParseField("document"); - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "painless_execute_request", args -> new Request((Script) args[0], (SupportedContext) args[1])); - - private static class ParseContext { - - private String index; - private BytesReference document; - - } + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "painless_execute_request", args -> new Request((Script) args[0], (ExecuteScriptContext) args[1])); static { PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> Script.parse(p), SCRIPT_FIELD); PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> { - // For now only accept an empty json object: XContentParser.Token token = p.nextToken(); assert token == XContentParser.Token.FIELD_NAME; String contextType = p.currentName(); - SupportedContext supportedContext = SupportedContext.valueOf(contextType.toUpperCase(Locale.ROOT)); - if (supportedContext == SupportedContext.PAINLESS_TEST) { - token = p.nextToken(); - assert token == XContentParser.Token.START_OBJECT; - token = p.nextToken(); - assert token == XContentParser.Token.END_OBJECT; - token = p.nextToken(); - assert token == XContentParser.Token.END_OBJECT; - } else if (supportedContext == SupportedContext.FILTER_SCRIPT || supportedContext == SupportedContext.SEARCH_SCRIPT) { - for (token = p.nextToken(); token != XContentParser.Token.END_OBJECT; token = p.nextToken()) { - if (token.isValue()) { - if (INDEX_FIELD.match(p.currentName(), p.getDeprecationHandler())) { - c.index = p.textOrNull(); - } else { - throw new IllegalStateException("Unexpected field name [" + p.currentName() + "]"); - } - } else if (token == XContentParser.Token.START_OBJECT) { - if (DOCUMENT_FIELD.match(p.currentName(), p.getDeprecationHandler())) { - try (XContentBuilder builder = XContentFactory.jsonBuilder()) { - builder.copyCurrentStructure(p); - builder.flush(); - c.document = BytesReference.bytes(builder); - } + ScriptContext scriptContext = fromScriptContextName(contextType.toLowerCase(Locale.ROOT)); + ExecuteScriptContext context = ExecuteScriptContext.PARSER.parse(p, c); + context.scriptContext = scriptContext; + context.xContentType = p.contentType().xContent().type(); + token = p.nextToken(); + assert token == XContentParser.Token.END_OBJECT; + return context; + }, CONTEXT_FIELD); + } + + static final Map> SUPPORTED_CONTEXTS; + + static { + Map> supportedContexts = new HashMap<>(); + supportedContexts.put("painless_test", PainlessTestScript.CONTEXT); + supportedContexts.put("filter", FilterScript.CONTEXT); + supportedContexts.put("score", SearchScript.SCRIPT_SCORE_CONTEXT); + SUPPORTED_CONTEXTS = Collections.unmodifiableMap(supportedContexts); + } + + static ScriptContext fromScriptContextName(String name) { + ScriptContext scriptContext = SUPPORTED_CONTEXTS.get(name); + if (scriptContext == null) { + throw new UnsupportedOperationException("unsupported script context name [" + name + "]"); + } + return scriptContext; + } + + static class ExecuteScriptContext implements Writeable, ToXContentObject { + + private static final ParseField INDEX_FIELD = new ParseField("index"); + private static final ParseField DOCUMENT_FIELD = new ParseField("document"); + private static final ParseField QUERY_FIELD = new ParseField("query"); + private static final ObjectParser PARSER = + new ObjectParser<>("execute_script_context", ExecuteScriptContext::new); + + static { + PARSER.declareString(ExecuteScriptContext::setIndex, INDEX_FIELD); + PARSER.declareObject(ExecuteScriptContext::setDocument, (p, c) -> { + try (XContentBuilder b = XContentBuilder.builder(p.contentType().xContent())) { + b.copyCurrentStructure(p); + return BytesReference.bytes(b); + } + }, DOCUMENT_FIELD); + PARSER.declareObject(ExecuteScriptContext::setQuery, (p, c) -> + AbstractQueryBuilder.parseInnerQueryBuilder(p), QUERY_FIELD); + } + + private ScriptContext scriptContext = PainlessTestScript.CONTEXT; + private String index; + private BytesReference document; + private QueryBuilder query; + + private XContentType xContentType; + + ExecuteScriptContext(StreamInput in) throws IOException { + scriptContext = fromScriptContextName(in.readString()); + index = in.readOptionalString(); + document = in.readOptionalBytesReference(); + String xContentType = in.readOptionalString(); + if (xContentType != null) { + this.xContentType = XContentType.fromMediaType(xContentType); + } + query = in.readOptionalNamedWriteable(QueryBuilder.class); + } + + ExecuteScriptContext() { + } + + void setIndex(String index) { + this.index = index; + } + + void setDocument(BytesReference document) { + this.document = document; + } + + void setQuery(QueryBuilder query) { + this.query = query; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExecuteScriptContext that = (ExecuteScriptContext) o; + return Objects.equals(scriptContext, that.scriptContext) && + Objects.equals(index, that.index) && + Objects.equals(document, that.document) && + Objects.equals(query, that.query); + } + + @Override + public int hashCode() { + return Objects.hash(scriptContext, index, document, query); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(scriptContext.name); + out.writeOptionalString(index); + out.writeOptionalBytesReference(document); + out.writeOptionalString(xContentType != null ? xContentType.mediaType(): null); + out.writeOptionalNamedWriteable(query); + } + + @Override + public String toString() { + return "ExecuteScriptContext{" + + "scriptContext=" + scriptContext.name + + ", index='" + index + '\'' + + ", document=" + document + + ", query=" + query + + ", xContentType=" + xContentType + + '}'; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.startObject(scriptContext.name); + { + if (index != null) { + builder.field(INDEX_FIELD.getPreferredName(), index); + } + if (document != null) { + builder.field(DOCUMENT_FIELD.getPreferredName()); + try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, document, xContentType)) { + builder.generator().copyCurrentStructure(parser); } - } else if (token == XContentParser.Token.FIELD_NAME) { - // ignore - } else { - throw new IllegalStateException("Unexpected token [" + token + "]"); + } + if (query != null) { + builder.field(QUERY_FIELD.getPreferredName(), query); } } - } else { - assert false : "Unsupported context"; + builder.endObject(); } - return supportedContext; - }, CONTEXT_FIELD); + builder.endObject(); + return builder; + } + } private Script script; - private SupportedContext context; - - private String index; - private BytesReference document; - private XContentType xContentType; + private ExecuteScriptContext executeScriptContext = new ExecuteScriptContext(); static Request parse(XContentParser parser) throws IOException { - ParseContext parseContext = new ParseContext(); - Request request = PARSER.parse(parser, parseContext); - request.setIndex(parseContext.index); - request.setDocument(parseContext.document); + Request request = PARSER.parse(parser, null); request.setXContentType(parser.contentType()); return request; } - Request(Script script, SupportedContext context) { + Request(Script script, ExecuteScriptContext context) { + this.script = Objects.requireNonNull(script); + if (context != null) { + this.executeScriptContext = context; + } + } + + Request(Script script, String scriptContextName) { this.script = Objects.requireNonNull(script); - this.context = context != null ? context : SupportedContext.PAINLESS_TEST; + if (scriptContextName != null) { + this.executeScriptContext = new ExecuteScriptContext(); + this.executeScriptContext.scriptContext = fromScriptContextName(scriptContextName); + } } Request() { @@ -195,32 +307,36 @@ public Script getScript() { return script; } - public SupportedContext getContext() { - return context; - } - public String getIndex() { - return index; + return executeScriptContext.index; } public void setIndex(String index) { - this.index = index; + this.executeScriptContext.index = index; } public BytesReference getDocument() { - return document; + return executeScriptContext.document; } public void setDocument(BytesReference document) { - this.document = document; + this.executeScriptContext.document = document; } public XContentType getXContentType() { - return xContentType; + return executeScriptContext.xContentType; } public void setXContentType(XContentType xContentType) { - this.xContentType = xContentType; + this.executeScriptContext.xContentType = xContentType; + } + + public QueryBuilder getQuery() { + return executeScriptContext.query; + } + + public void setQuery(QueryBuilder query) { + this.executeScriptContext.query = query; } @Override @@ -229,11 +345,11 @@ public ActionRequestValidationException validate() { if (script.getType() != ScriptType.INLINE) { validationException = addValidationError("only inline scripts are supported", validationException); } - if (context == SupportedContext.FILTER_SCRIPT || context == SupportedContext.SEARCH_SCRIPT) { - if (index == null) { + if (needDocumentAndIndex(executeScriptContext.scriptContext)) { + if (executeScriptContext.index == null) { validationException = addValidationError("index is a required parameter for current context", validationException); } - if (document == null) { + if (executeScriptContext.document == null) { validationException = addValidationError("document is a required parameter for current context", validationException); } } @@ -244,11 +360,11 @@ public ActionRequestValidationException validate() { public void readFrom(StreamInput in) throws IOException { super.readFrom(in); script = new Script(in); - context = SupportedContext.fromId(in.readByte()); - if (context == SupportedContext.FILTER_SCRIPT) { - index = in.readString(); - document = in.readBytesReference(); - xContentType = XContentType.fromMediaType(in.readString()); + if (in.getVersion().onOrBefore(Version.V_6_4_0)) { + byte scriptContextId = in.readByte(); + assert scriptContextId == 0; + } else { + executeScriptContext = new ExecuteScriptContext(in); } } @@ -256,12 +372,10 @@ public void readFrom(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); script.writeTo(out); - out.writeByte(context.id); - out.writeOptionalString(index); - if (context == SupportedContext.FILTER_SCRIPT) { - out.writeString(index); - out.writeBytesReference(document); - out.writeString(xContentType.mediaType()); + if (out.getVersion().onOrBefore(Version.V_6_4_0)) { + out.writeByte((byte) 0); + } else { + executeScriptContext.writeTo(out); } } @@ -269,20 +383,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.field(SCRIPT_FIELD.getPreferredName(), script); - builder.startObject(CONTEXT_FIELD.getPreferredName()); - { - builder.startObject(context.name()); - if (context == SupportedContext.FILTER_SCRIPT) { - builder.field(INDEX_FIELD.getPreferredName(), index); - try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, document)) { - parser.nextToken(); - builder.generator().copyCurrentStructure(parser); - } - } - builder.endObject(); - } - builder.endObject(); + builder.field(CONTEXT_FIELD.getPreferredName(), executeScriptContext); return builder; } @@ -292,39 +393,29 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Request request = (Request) o; return Objects.equals(script, request.script) && - context == request.context && - Objects.equals(index, request.index) && - Objects.equals(document, request.document); + Objects.equals(executeScriptContext, request.executeScriptContext); } @Override public int hashCode() { - return Objects.hash(script, context, index, document); + return Objects.hash(script, executeScriptContext); } - public enum SupportedContext { - - PAINLESS_TEST((byte) 0), - FILTER_SCRIPT((byte) 1), - SEARCH_SCRIPT((byte) 2); - - private final byte id; - - SupportedContext(byte id) { - this.id = id; - } - - public static SupportedContext fromId(byte id) { - switch (id) { - case 0: - return PAINLESS_TEST; - case 1: - return FILTER_SCRIPT; - case 2: - return SEARCH_SCRIPT; - default: - throw new IllegalArgumentException("unknown context [" + id + "]"); - } + @Override + public String toString() { + return "Request{" + + "script=" + script + + ", executeScriptContext=" + executeScriptContext + + '}'; + } + + static boolean needDocumentAndIndex(ScriptContext scriptContext) { + if (scriptContext == FilterScript.CONTEXT) { + return true; + } else if (scriptContext == SearchScript.SCRIPT_SCORE_CONTEXT) { + return true; + } else { + return false; } } @@ -427,17 +518,24 @@ public TransportAction(Settings settings, ThreadPool threadPool, TransportServic } @Override protected void doExecute(Request request, ActionListener listener) { - switch (request.context) { - case PAINLESS_TEST: - { + // Forking a thread here, because only light weight operations should happen on network thread and + // Creating a in-memory index is not light weight + // TODO: is MANAGEMENT TP the right TP? Right now this is an admin api (see action name). + threadPool.executor(ThreadPool.Names.MANAGEMENT).execute(new AbstractRunnable() { + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + + @Override + protected void doRun() throws Exception { + final ScriptContext scriptContext = request.executeScriptContext.scriptContext; + if (scriptContext == PainlessTestScript.CONTEXT) { PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT); PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams()); String result = Objects.toString(painlessTestScript.execute()); listener.onResponse(new Response(result)); - } - break; - case FILTER_SCRIPT: - { + } else if (scriptContext == FilterScript.CONTEXT) { indexAndOpenIndexReader(request, (context, leafReaderContext) -> { FilterScript.Factory factory = scriptService.compile(request.script, FilterScript.CONTEXT); FilterScript.LeafFactory leafFactory = @@ -446,32 +544,45 @@ protected void doExecute(Request request, ActionListener listener) { boolean result = filterScript.execute(); listener.onResponse(new Response(result)); }); - } - break; - case SEARCH_SCRIPT: - { + } else if (scriptContext == SearchScript.SCRIPT_SCORE_CONTEXT) { indexAndOpenIndexReader(request, (context, leafReaderContext) -> { SearchScript.Factory factory = scriptService.compile(request.script, SearchScript.CONTEXT); SearchScript.LeafFactory leafFactory = factory.newFactory(request.getScript().getParams(), context.lookup()); SearchScript searchScript = leafFactory.newInstance(leafReaderContext); - Object result = searchScript.run(); + + if (request.executeScriptContext.query != null) { + Query luceneQuery = request.executeScriptContext.query.rewrite(context).toQuery(context); + IndexSearcher indexSearcher = new IndexSearcher(leafReaderContext.reader()); + luceneQuery = indexSearcher.rewrite(luceneQuery); + Weight weight = indexSearcher.createWeight(luceneQuery, true, 1f); + Scorer scorer = weight.scorer(indexSearcher.getIndexReader().leaves().get(0)); + // Consume the first (and only) match. + int docID = scorer.iterator().nextDoc(); + assert docID == scorer.docID(); + searchScript.setScorer(scorer); + } + + double result = searchScript.runAsDouble(); listener.onResponse(new Response(result)); }); + } else { + throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]"); } - break; - default: - throw new UnsupportedOperationException("unsupported context [" + request.context + "]"); - } + } + }); } - private void indexAndOpenIndexReader(Request request, - CheckedBiConsumer handler) { + private void indexAndOpenIndexReader(Request request, CheckedBiConsumer handler) throws IOException { + + ClusterState clusterState = clusterService.state(); IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed(); + String indexExpression = request.executeScriptContext.index; Index[] concreteIndices = - indexNameExpressionResolver.concreteIndices(clusterService.state(), indicesOptions, request.index); + indexNameExpressionResolver.concreteIndices(clusterState, indicesOptions, indexExpression); if (concreteIndices.length != 1) { - throw new IllegalArgumentException("[" + request.index + "] does not resolve to a single index"); + throw new IllegalArgumentException("[" + indexExpression + "] does not resolve to a single index"); } Index concreteIndex = concreteIndices[0]; IndexService indexService = indicesServices.indexServiceSafe(concreteIndex); @@ -481,7 +592,9 @@ private void indexAndOpenIndexReader(Request request, try (IndexWriter indexWriter = new IndexWriter(ramDirectory, new IndexWriterConfig(defaultAnalyzer))) { String index = concreteIndex.getName(); String type = indexService.mapperService().documentMapper().type(); - SourceToParse sourceToParse = SourceToParse.source(index, type, "_id", request.document, request.xContentType); + BytesReference document = request.executeScriptContext.document; + XContentType xContentType = request.executeScriptContext.xContentType; + SourceToParse sourceToParse = SourceToParse.source(index, type, "_id", document, xContentType); ParsedDocument parsedDocument = indexService.mapperService().documentMapper().parse(sourceToParse); indexWriter.addDocuments(parsedDocument.docs()); try (IndexReader indexReader = DirectoryReader.open(indexWriter)) { @@ -491,8 +604,6 @@ private void indexAndOpenIndexReader(Request request, handler.accept(context, indexReader.leaves().get(0)); } } - } catch (IOException e) { - throw new UncheckedIOException(e); } } } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java index 488ae0e1643bc..99926316279f4 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java @@ -18,9 +18,17 @@ */ package org.elasticsearch.painless; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.search.SearchModule; import org.elasticsearch.test.AbstractStreamableXContentTestCase; import java.io.IOException; @@ -28,12 +36,34 @@ public class PainlessExecuteRequestTests extends AbstractStreamableXContentTestCase { + @Override + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(new SearchModule(Settings.EMPTY, false, Collections.emptyList()).getNamedWriteables()); + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + return new NamedXContentRegistry(new SearchModule(Settings.EMPTY, false, Collections.emptyList()).getNamedXContents()); + } + @Override protected PainlessExecuteAction.Request createTestInstance() { Script script = new Script(randomAlphaOfLength(10)); - PainlessExecuteAction.Request.SupportedContext context = randomBoolean() ? - PainlessExecuteAction.Request.SupportedContext.PAINLESS_TEST : null; - return new PainlessExecuteAction.Request(script, context); + ScriptContext context = randomBoolean() ? randomFrom(PainlessExecuteAction.Request.SUPPORTED_CONTEXTS.values()) : null; + PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, context != null ? context.name : null); + if (randomBoolean()) { + request.setIndex(randomAlphaOfLength(4)); + } + if (randomBoolean()) { + // TODO: Pass down XContentType here. + // Otherwise the document's xcontent type is incompatible with the that of the XContentParser xcontent type +// request.setDocument(new BytesArray("{}")); +// request.setXContentType(XContentType.JSON); + } + if (randomBoolean()) { + request.setQuery(new MatchAllQueryBuilder()); + } + return request; } @Override @@ -53,7 +83,7 @@ protected boolean supportsUnknownFields() { public void testValidate() { Script script = new Script(ScriptType.STORED, null, randomAlphaOfLength(10), Collections.emptyMap()); - PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, null); + PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, (String) null); Exception e = request.validate(); assertNotNull(e); assertEquals("Validation Failed: 1: only inline scripts are supported;", e.getMessage()); diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml index 7b915cc38dbc0..3fcf0d18bea45 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml +++ b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml @@ -1,3 +1,18 @@ +setup: + - do: + indices.create: + index: my-index + body: + mappings: + doc: + properties: + rank: + type: long + field: + type: keyword + text: + type: text + --- "Execute with defaults": - do: @@ -11,7 +26,7 @@ - match: { result: "0.1" } --- -"Execute with execute_api_script context": +"Execute with painless_test context": - do: scripts_painless_execute: body: @@ -23,3 +38,53 @@ context: painless_test: {} - match: { result: "-90" } + +--- +"Execute with filter context": + - do: + scripts_painless_execute: + body: + script: + source: "doc['field'].value.length() <= params.max_length" + params: + max_length: 4 + context: + filter: + document: + field: "four" + index: "my-index" + - match: { result: true } + +--- +"Execute with score context": + - do: + scripts_painless_execute: + body: + script: + source: "doc['rank'].value / params.max_rank" + params: + max_rank: 5.0 + context: + score: + document: + rank: 4 + index: "my-index" + - match: { result: 0.8 } + + - do: + scripts_painless_execute: + body: + script: + source: "Math.round((_score + (doc['rank'].value / params.max_rank)) * 100.0) / 100.0" + params: + max_rank: 5.0 + context: + score: + document: + rank: 4 + text: quick brown fox + index: "my-index" + query: + match: + text: fox + - match: { result: 1.09 } From d965597e8b14f2d41811219f0aa30b49a138279c Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 23 May 2018 15:47:13 +0200 Subject: [PATCH 03/12] changed PainlessExecuteAction.TransportAction to use TransportSingleShardAction instead of HandledAction, because now in case score or filter contexts are used the request needs to be redirected to a node that has an active IndexService for the index being referenced (a node with a shard copy for that index). --- .../painless/PainlessExecuteAction.java | 147 ++++++++++-------- 1 file changed, 83 insertions(+), 64 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index ed74973a8138e..94e54179dc3ae 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -31,20 +31,21 @@ import org.apache.lucene.store.RAMDirectory; import org.elasticsearch.Version; import org.elasticsearch.action.Action; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.single.shard.SingleShardRequest; +import org.elasticsearch.action.support.single.shard.TransportSingleShardAction; import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.routing.ShardsIterator; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.CheckedBiConsumer; +import org.elasticsearch.common.CheckedBiFunction; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.inject.Inject; @@ -52,7 +53,6 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -70,6 +70,7 @@ import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.BytesRestResponse; @@ -118,7 +119,7 @@ public Response newResponse() { return new Response(); } - public static class Request extends ActionRequest implements ToXContent { + public static class Request extends SingleShardRequest implements ToXContent { private static final ParseField SCRIPT_FIELD = new ParseField("script"); private static final ParseField CONTEXT_FIELD = new ParseField("context"); @@ -289,6 +290,7 @@ static Request parse(XContentParser parser) throws IOException { this.script = Objects.requireNonNull(script); if (context != null) { this.executeScriptContext = context; + index(executeScriptContext.index); } } @@ -313,6 +315,7 @@ public String getIndex() { public void setIndex(String index) { this.executeScriptContext.index = index; + index(executeScriptContext.index); } public BytesReference getDocument() { @@ -501,80 +504,96 @@ public interface Factory { } - public static class TransportAction extends HandledTransportAction { + public static class TransportAction extends TransportSingleShardAction { private final ScriptService scriptService; - private final ClusterService clusterService; private final IndicesService indicesServices; @Inject public TransportAction(Settings settings, ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver, ScriptService scriptService, ClusterService clusterService, IndicesService indicesServices) { - super(settings, NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver, Request::new); + super(settings, NAME, threadPool, clusterService, transportService, actionFilters, indexNameExpressionResolver, + // Forking a thread here, because only light weight operations should happen on network thread and + // Creating a in-memory index is not light weight + // TODO: is MANAGEMENT TP the right TP? Right now this is an admin api (see action name). + Request::new, ThreadPool.Names.MANAGEMENT); this.scriptService = scriptService; - this.clusterService = clusterService; this.indicesServices = indicesServices; } + @Override - protected void doExecute(Request request, ActionListener listener) { - // Forking a thread here, because only light weight operations should happen on network thread and - // Creating a in-memory index is not light weight - // TODO: is MANAGEMENT TP the right TP? Right now this is an admin api (see action name). - threadPool.executor(ThreadPool.Names.MANAGEMENT).execute(new AbstractRunnable() { - @Override - public void onFailure(Exception e) { - listener.onFailure(e); - } + protected Response newResponse() { + return new Response(); + } - @Override - protected void doRun() throws Exception { - final ScriptContext scriptContext = request.executeScriptContext.scriptContext; - if (scriptContext == PainlessTestScript.CONTEXT) { - PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT); - PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams()); - String result = Objects.toString(painlessTestScript.execute()); - listener.onResponse(new Response(result)); - } else if (scriptContext == FilterScript.CONTEXT) { - indexAndOpenIndexReader(request, (context, leafReaderContext) -> { - FilterScript.Factory factory = scriptService.compile(request.script, FilterScript.CONTEXT); - FilterScript.LeafFactory leafFactory = - factory.newFactory(request.getScript().getParams(), context.lookup()); - FilterScript filterScript = leafFactory.newInstance(leafReaderContext); - boolean result = filterScript.execute(); - listener.onResponse(new Response(result)); - }); - } else if (scriptContext == SearchScript.SCRIPT_SCORE_CONTEXT) { - indexAndOpenIndexReader(request, (context, leafReaderContext) -> { - SearchScript.Factory factory = scriptService.compile(request.script, SearchScript.CONTEXT); - SearchScript.LeafFactory leafFactory = - factory.newFactory(request.getScript().getParams(), context.lookup()); - SearchScript searchScript = leafFactory.newInstance(leafReaderContext); - - if (request.executeScriptContext.query != null) { - Query luceneQuery = request.executeScriptContext.query.rewrite(context).toQuery(context); - IndexSearcher indexSearcher = new IndexSearcher(leafReaderContext.reader()); - luceneQuery = indexSearcher.rewrite(luceneQuery); - Weight weight = indexSearcher.createWeight(luceneQuery, true, 1f); - Scorer scorer = weight.scorer(indexSearcher.getIndexReader().leaves().get(0)); - // Consume the first (and only) match. - int docID = scorer.iterator().nextDoc(); - assert docID == scorer.docID(); - searchScript.setScorer(scorer); - } + @Override + protected ClusterBlockException checkRequestBlock(ClusterState state, InternalRequest request) { + if (request.concreteIndex() != null) { + return super.checkRequestBlock(state, request); + } + return null; + } + + @Override + protected boolean resolveIndex(Request request) { + return request.getIndex() != null; + } - double result = searchScript.runAsDouble(); - listener.onResponse(new Response(result)); - }); - } else { - throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]"); + @Override + protected ShardsIterator shards(ClusterState state, InternalRequest request) { + if (request.concreteIndex() == null) { + return null; + } + return state.routingTable().index(request.concreteIndex()).randomAllActiveShardsIt(); + } + + @Override + protected Response shardOperation(Request request, ShardId shardId) throws IOException { + final ScriptContext scriptContext = request.executeScriptContext.scriptContext; + if (scriptContext == PainlessTestScript.CONTEXT) { + PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT); + PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams()); + String result = Objects.toString(painlessTestScript.execute()); + return new Response(result); + } else if (scriptContext == FilterScript.CONTEXT) { + return indexAndOpenIndexReader(request, (context, leafReaderContext) -> { + FilterScript.Factory factory = scriptService.compile(request.script, FilterScript.CONTEXT); + FilterScript.LeafFactory leafFactory = + factory.newFactory(request.getScript().getParams(), context.lookup()); + FilterScript filterScript = leafFactory.newInstance(leafReaderContext); + boolean result = filterScript.execute(); + return new Response(result); + }); + } else if (scriptContext == SearchScript.SCRIPT_SCORE_CONTEXT) { + return indexAndOpenIndexReader(request, (context, leafReaderContext) -> { + SearchScript.Factory factory = scriptService.compile(request.script, SearchScript.CONTEXT); + SearchScript.LeafFactory leafFactory = + factory.newFactory(request.getScript().getParams(), context.lookup()); + SearchScript searchScript = leafFactory.newInstance(leafReaderContext); + + if (request.executeScriptContext.query != null) { + Query luceneQuery = request.executeScriptContext.query.rewrite(context).toQuery(context); + IndexSearcher indexSearcher = new IndexSearcher(leafReaderContext.reader()); + luceneQuery = indexSearcher.rewrite(luceneQuery); + Weight weight = indexSearcher.createWeight(luceneQuery, true, 1f); + Scorer scorer = weight.scorer(indexSearcher.getIndexReader().leaves().get(0)); + // Consume the first (and only) match. + int docID = scorer.iterator().nextDoc(); + assert docID == scorer.docID(); + searchScript.setScorer(scorer); } - } - }); + + double result = searchScript.runAsDouble(); + return new Response(result); + }); + } else { + throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]"); + } } - private void indexAndOpenIndexReader(Request request, CheckedBiConsumer handler) throws IOException { + private Response indexAndOpenIndexReader(Request request, CheckedBiFunction handler) throws IOException { ClusterState clusterState = clusterService.state(); IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed(); @@ -601,7 +620,7 @@ private void indexAndOpenIndexReader(Request request, CheckedBiConsumer absoluteStartMillis, null); - handler.accept(context, indexReader.leaves().get(0)); + return handler.apply(context, indexReader.leaves().get(0)); } } } From 539abde7b702ddc800c93fed3be2ba47080aa6d2 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 24 May 2018 10:08:51 +0200 Subject: [PATCH 04/12] rename --- docs/painless/painless-execute-script.asciidoc | 10 +++++----- .../elasticsearch/painless/PainlessExecuteAction.java | 6 +++--- .../test/painless/70_execute_painless_scripts.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index f99bb05029769..48322096cd26b 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -9,9 +9,9 @@ The Painless execute API allows an arbitrary script to be executed and a result .Parameters [options="header"] |====== -| Name | Required | Default | Description -| `script` | yes | - | The script to execute -| `context` | no | `painless_test` | The context the script should be executed in. +| Name | Required | Default | Description +| `script` | yes | - | The script to execute +| `execute_context` | no | `painless_test` | The context the script should be executed in. |====== ==== Contexts @@ -77,7 +77,7 @@ POST /_scripts/painless/_execute "params": { "max_length": 4 } - "context": { + "execute_context": { "index": "my-index", "document": { "field": "four" @@ -121,7 +121,7 @@ POST /_scripts/painless/_execute "params": { "max_rank": 4 } - "context": { + "execute_context": { "index": "my-index", "document": { "rank": 4 diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index 94e54179dc3ae..6b2f0abfdaa02 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -122,7 +122,7 @@ public Response newResponse() { public static class Request extends SingleShardRequest implements ToXContent { private static final ParseField SCRIPT_FIELD = new ParseField("script"); - private static final ParseField CONTEXT_FIELD = new ParseField("context"); + private static final ParseField EXECUTE_CONTEXT_FIELD = new ParseField("execute_context"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "painless_execute_request", args -> new Request((Script) args[0], (ExecuteScriptContext) args[1])); @@ -139,7 +139,7 @@ public static class Request extends SingleShardRequest implements ToXCo token = p.nextToken(); assert token == XContentParser.Token.END_OBJECT; return context; - }, CONTEXT_FIELD); + }, EXECUTE_CONTEXT_FIELD); } static final Map> SUPPORTED_CONTEXTS; @@ -386,7 +386,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.field(SCRIPT_FIELD.getPreferredName(), script); - builder.field(CONTEXT_FIELD.getPreferredName(), executeScriptContext); + builder.field(EXECUTE_CONTEXT_FIELD.getPreferredName(), executeScriptContext); return builder; } diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml index 3fcf0d18bea45..2e1bafea0f632 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml +++ b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml @@ -35,7 +35,7 @@ setup: params: var1: 10 var2: 100 - context: + execute_context: painless_test: {} - match: { result: "-90" } @@ -48,7 +48,7 @@ setup: source: "doc['field'].value.length() <= params.max_length" params: max_length: 4 - context: + execute_context: filter: document: field: "four" @@ -64,7 +64,7 @@ setup: source: "doc['rank'].value / params.max_rank" params: max_rank: 5.0 - context: + execute_context: score: document: rank: 4 @@ -78,7 +78,7 @@ setup: source: "Math.round((_score + (doc['rank'].value / params.max_rank)) * 100.0) / 100.0" params: max_rank: 5.0 - context: + execute_context: score: document: rank: 4 From 7bc755c16dc2af50211dc7d37cf68540852a812d Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 24 May 2018 10:11:42 +0200 Subject: [PATCH 05/12] fixed typo --- docs/painless/painless-execute-script.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index 48322096cd26b..e48013f8b2cd4 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -108,7 +108,7 @@ Available parameters inside a score context: document:: Contains the document that will be temporarily indexed in-memory and is accessible from the script. index:: The name of an index containing a mapping that is compatable with the document being indexed. -query:: If `_score` is used in the script then a query can specified that will be used the compute a score. +query:: If `_score` is used in the script then a query can specified that will be used to compute a score. ====== Example From 65bbaf844e613298230a4e54fc561f99a2886173 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 24 May 2018 15:49:11 +0200 Subject: [PATCH 06/12] added unit like tests and made the code easier to test --- .../painless/PainlessExecuteAction.java | 46 ++++--- .../painless/PainlessExecuteApiTests.java | 113 ++++++++++++++++++ .../painless/70_execute_painless_scripts.yml | 18 --- 3 files changed, 142 insertions(+), 35 deletions(-) create mode 100644 modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index 6b2f0abfdaa02..125c0bd3bcc25 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -550,6 +550,25 @@ protected ShardsIterator shards(ClusterState state, InternalRequest request) { @Override protected Response shardOperation(Request request, ShardId shardId) throws IOException { + IndexService indexService; + if (request.getIndex() != null) { + ClusterState clusterState = clusterService.state(); + IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed(); + String indexExpression = request.executeScriptContext.index; + Index[] concreteIndices = + indexNameExpressionResolver.concreteIndices(clusterState, indicesOptions, indexExpression); + if (concreteIndices.length != 1) { + throw new IllegalArgumentException("[" + indexExpression + "] does not resolve to a single index"); + } + Index concreteIndex = concreteIndices[0]; + indexService = indicesServices.indexServiceSafe(concreteIndex); + } else { + indexService = null; + } + return innerShardOperation(request, scriptService, indexService); + } + + static Response innerShardOperation(Request request, ScriptService scriptService, IndexService indexService) throws IOException { final ScriptContext scriptContext = request.executeScriptContext.scriptContext; if (scriptContext == PainlessTestScript.CONTEXT) { PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT); @@ -557,20 +576,22 @@ protected Response shardOperation(Request request, ShardId shardId) throws IOExc String result = Objects.toString(painlessTestScript.execute()); return new Response(result); } else if (scriptContext == FilterScript.CONTEXT) { - return indexAndOpenIndexReader(request, (context, leafReaderContext) -> { + return prepareRamIndex(request, (context, leafReaderContext) -> { FilterScript.Factory factory = scriptService.compile(request.script, FilterScript.CONTEXT); FilterScript.LeafFactory leafFactory = factory.newFactory(request.getScript().getParams(), context.lookup()); FilterScript filterScript = leafFactory.newInstance(leafReaderContext); + filterScript.setDocument(0); boolean result = filterScript.execute(); return new Response(result); - }); + }, indexService); } else if (scriptContext == SearchScript.SCRIPT_SCORE_CONTEXT) { - return indexAndOpenIndexReader(request, (context, leafReaderContext) -> { + return prepareRamIndex(request, (context, leafReaderContext) -> { SearchScript.Factory factory = scriptService.compile(request.script, SearchScript.CONTEXT); SearchScript.LeafFactory leafFactory = factory.newFactory(request.getScript().getParams(), context.lookup()); SearchScript searchScript = leafFactory.newInstance(leafReaderContext); + searchScript.setDocument(0); if (request.executeScriptContext.query != null) { Query luceneQuery = request.executeScriptContext.query.rewrite(context).toQuery(context); @@ -586,30 +607,21 @@ protected Response shardOperation(Request request, ShardId shardId) throws IOExc double result = searchScript.runAsDouble(); return new Response(result); - }); + }, indexService); } else { throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]"); } } - private Response indexAndOpenIndexReader(Request request, CheckedBiFunction handler) throws IOException { + private static Response prepareRamIndex(Request request, + CheckedBiFunction handler, + IndexService indexService) throws IOException { - ClusterState clusterState = clusterService.state(); - IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed(); - String indexExpression = request.executeScriptContext.index; - Index[] concreteIndices = - indexNameExpressionResolver.concreteIndices(clusterState, indicesOptions, indexExpression); - if (concreteIndices.length != 1) { - throw new IllegalArgumentException("[" + indexExpression + "] does not resolve to a single index"); - } - Index concreteIndex = concreteIndices[0]; - IndexService indexService = indicesServices.indexServiceSafe(concreteIndex); Analyzer defaultAnalyzer = indexService.getIndexAnalyzers().getDefaultIndexAnalyzer(); try (RAMDirectory ramDirectory = new RAMDirectory()) { try (IndexWriter indexWriter = new IndexWriter(ramDirectory, new IndexWriterConfig(defaultAnalyzer))) { - String index = concreteIndex.getName(); + String index = indexService.index().getName(); String type = indexService.mapperService().documentMapper().type(); BytesReference document = request.executeScriptContext.document; XContentType xContentType = request.executeScriptContext.xContentType; diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java new file mode 100644 index 0000000000000..ba4629ce62635 --- /dev/null +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java @@ -0,0 +1,113 @@ +/* + * 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.painless; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.IndexService; +import org.elasticsearch.index.query.MatchQueryBuilder; +import org.elasticsearch.painless.PainlessExecuteAction.Request; +import org.elasticsearch.painless.PainlessExecuteAction.Response; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptException; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.test.ESSingleNodeTestCase; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.singletonMap; +import static org.elasticsearch.painless.PainlessExecuteAction.TransportAction.innerShardOperation; +import static org.hamcrest.Matchers.equalTo; + +public class PainlessExecuteApiTests extends ESSingleNodeTestCase { + + @Override + protected Collection> getPlugins() { + return Collections.singleton(PainlessPlugin.class); + } + + public void testDefaults() throws IOException { + ScriptService scriptService = getInstanceFromNode(ScriptService.class); + Request request = new Request(new Script("100.0 / 1000.0"), (String) null); + Response response = innerShardOperation(request, scriptService, null); + assertThat(response.getResult(), equalTo("0.1")); + + Map params = new HashMap<>(); + params.put("count", 100.0D); + params.put("total", 1000.0D); + request = new Request(new Script(ScriptType.INLINE, "painless", "params.count / params.total", params), (String) null); + response = innerShardOperation(request, scriptService, null); + assertThat(response.getResult(), equalTo("0.1")); + + Exception e = expectThrows(ScriptException.class, + () -> { + Request r = new Request(new Script(ScriptType.INLINE, + "painless", "params.count / params.total + doc['constant']", params), (String) null); + innerShardOperation(r, scriptService, null); + }); + assertThat(e.getCause().getMessage(), equalTo("Variable [doc] is not defined.")); + } + + public void testFilterExecutionContext() throws IOException { + ScriptService scriptService = getInstanceFromNode(ScriptService.class); + IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "field", "type=long"); + + Request request = new Request(new Script("doc['field'].value >= 3"), "filter"); + request.setIndex("index"); + request.setDocument(new BytesArray("{\"field\": 3}")); + request.setXContentType(XContentType.JSON); + Response response = innerShardOperation(request, scriptService, indexService); + assertThat(response.getResult(), equalTo(true)); + + + request = new Request(new Script(ScriptType.INLINE, "painless", "doc['field'].value >= params.max", + singletonMap("max", 3)), "filter"); + request.setIndex("index"); + request.setDocument(new BytesArray("{\"field\": 3}")); + request.setXContentType(XContentType.JSON); + response = innerShardOperation(request, scriptService, indexService); + assertThat(response.getResult(), equalTo(true)); + + request.setDocument(new BytesArray("{\"field\": 2}")); + response = innerShardOperation(request, scriptService, indexService); + assertThat(response.getResult(), equalTo(false)); + } + + public void testScoreExecutionContext() throws IOException { + ScriptService scriptService = getInstanceFromNode(ScriptService.class); + IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "rank", "type=long", "text", "type=text"); + + Request request = new Request(new Script(ScriptType.INLINE, "painless", + "Math.round((_score + (doc['rank'].value / params.max_rank)) * 100.0) / 100.0", singletonMap("max_rank", 5.0)), "score"); + request.setIndex("index"); + request.setDocument(new BytesArray("{\"rank\": 4.0, \"text\": \"quick brown fox\"}")); + request.setXContentType(XContentType.JSON); + request.setQuery(new MatchQueryBuilder("text", "fox")); + Response response = innerShardOperation(request, scriptService, indexService); + assertThat(response.getResult(), equalTo(1.09D)); + } + +} diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml index 2e1bafea0f632..4eef24e6f7314 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml +++ b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml @@ -70,21 +70,3 @@ setup: rank: 4 index: "my-index" - match: { result: 0.8 } - - - do: - scripts_painless_execute: - body: - script: - source: "Math.round((_score + (doc['rank'].value / params.max_rank)) * 100.0) / 100.0" - params: - max_rank: 5.0 - execute_context: - score: - document: - rank: 4 - text: quick brown fox - index: "my-index" - query: - match: - text: fox - - match: { result: 1.09 } From fd3fb41814809798e4413fcd65a7cac3f82f3c6d Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 25 May 2018 07:41:37 +0200 Subject: [PATCH 07/12] Use ScoreScript instead of SearchScript (see #30816) --- .../painless/PainlessExecuteAction.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index 125c0bd3bcc25..bb75010028b51 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -79,11 +79,11 @@ import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.action.RestBuilderListener; import org.elasticsearch.script.FilterScript; +import org.elasticsearch.script.ScoreScript; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; -import org.elasticsearch.script.SearchScript; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -148,7 +148,7 @@ public static class Request extends SingleShardRequest implements ToXCo Map> supportedContexts = new HashMap<>(); supportedContexts.put("painless_test", PainlessTestScript.CONTEXT); supportedContexts.put("filter", FilterScript.CONTEXT); - supportedContexts.put("score", SearchScript.SCRIPT_SCORE_CONTEXT); + supportedContexts.put("score", ScoreScript.CONTEXT); SUPPORTED_CONTEXTS = Collections.unmodifiableMap(supportedContexts); } @@ -415,7 +415,7 @@ public String toString() { static boolean needDocumentAndIndex(ScriptContext scriptContext) { if (scriptContext == FilterScript.CONTEXT) { return true; - } else if (scriptContext == SearchScript.SCRIPT_SCORE_CONTEXT) { + } else if (scriptContext == ScoreScript.CONTEXT) { return true; } else { return false; @@ -585,13 +585,13 @@ static Response innerShardOperation(Request request, ScriptService scriptService boolean result = filterScript.execute(); return new Response(result); }, indexService); - } else if (scriptContext == SearchScript.SCRIPT_SCORE_CONTEXT) { + } else if (scriptContext == ScoreScript.CONTEXT) { return prepareRamIndex(request, (context, leafReaderContext) -> { - SearchScript.Factory factory = scriptService.compile(request.script, SearchScript.CONTEXT); - SearchScript.LeafFactory leafFactory = + ScoreScript.Factory factory = scriptService.compile(request.script, ScoreScript.CONTEXT); + ScoreScript.LeafFactory leafFactory = factory.newFactory(request.getScript().getParams(), context.lookup()); - SearchScript searchScript = leafFactory.newInstance(leafReaderContext); - searchScript.setDocument(0); + ScoreScript scoreScript = leafFactory.newInstance(leafReaderContext); + scoreScript.setDocument(0); if (request.executeScriptContext.query != null) { Query luceneQuery = request.executeScriptContext.query.rewrite(context).toQuery(context); @@ -602,10 +602,10 @@ static Response innerShardOperation(Request request, ScriptService scriptService // Consume the first (and only) match. int docID = scorer.iterator().nextDoc(); assert docID == scorer.docID(); - searchScript.setScorer(scorer); + scoreScript.setScorer(scorer); } - double result = searchScript.runAsDouble(); + double result = scoreScript.execute(); return new Response(result); }, indexService); } else { From caf6f260b0a7cdab4500a9709966937c501bac0a Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 25 May 2018 07:49:17 +0200 Subject: [PATCH 08/12] fixed docs --- docs/painless/painless-execute-script.asciidoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index e48013f8b2cd4..398c118209fb3 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -14,11 +14,11 @@ The Painless execute API allows an arbitrary script to be executed and a result | `execute_context` | no | `painless_test` | The context the script should be executed in. |====== -==== Contexts +==== Execute contexts -Contexts control how scripts are executed, what variables are available at runtime and what the return type is. +Execute contexts control how scripts are executed, what variables are available at runtime and what the return type is. -===== Painless test script context +===== Painless test execute context The `painless_test` context executes scripts as is and do not add any special parameters. The only variable that is available is `params`, which can be used to access user defined values. @@ -54,7 +54,7 @@ Response: -------------------------------------------------- // TESTRESPONSE -===== Filter script context +===== Filter execute context The `filter` context executes scripts as if they were executed inside a `script` query. For testing purposes a document must be provided that will be indexed temporarily in-memory and @@ -76,7 +76,7 @@ POST /_scripts/painless/_execute "source": "doc['field'].value.length() <= params.max_length", "params": { "max_length": 4 - } + }, "execute_context": { "index": "my-index", "document": { @@ -99,7 +99,7 @@ Response: // TESTRESPONSE -===== Score script context +===== Score execute context The `score` context executes scripts as if they were executed inside a `script_score` function in `function_score` query. @@ -120,7 +120,7 @@ POST /_scripts/painless/_execute "source": "doc['rank'].value / params.max_rank", "params": { "max_rank": 4 - } + }, "execute_context": { "index": "my-index", "document": { From 6da0dc70183a2a9aa81d7728881c807adc917678 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 25 May 2018 08:58:21 +0200 Subject: [PATCH 09/12] fixed docs for real --- .../painless/painless-execute-script.asciidoc | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index 398c118209fb3..5151272e2f35e 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -70,14 +70,29 @@ index:: The name of an index containing a mapping that is compatable with the do [source,js] ---------------------------------------------------------------- +PUT /my-index +{ + "mappings": { + "_doc": { + "properties": { + "field": { + "type": "keyword" + } + } + } + } +} + POST /_scripts/painless/_execute { "script": { "source": "doc['field'].value.length() <= params.max_length", "params": { "max_length": 4 - }, - "execute_context": { + } + }, + "execute_context": { + "filter": { "index": "my-index", "document": { "field": "four" @@ -114,14 +129,33 @@ query:: If `_score` is used in the script then a query can specified that will b [source,js] ---------------------------------------------------------------- +PUT /my-index +{ + "mappings": { + "_doc": { + "properties": { + "field": { + "type": "keyword" + }, + "rank": { + "type": "long" + } + } + } + } +} + + POST /_scripts/painless/_execute { "script": { "source": "doc['rank'].value / params.max_rank", "params": { - "max_rank": 4 - }, - "execute_context": { + "max_rank": 5.0 + } + }, + "execute_context": { + "score": { "index": "my-index", "document": { "rank": 4 From 95300fe483f0e0deadb0f339ff6f5825def501da Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 4 Jul 2018 10:33:29 +0200 Subject: [PATCH 10/12] iter --- docs/painless/painless-execute-script.asciidoc | 16 ++++++++-------- .../painless/PainlessExecuteAction.java | 15 ++++----------- .../painless/PainlessExecuteRequestTests.java | 2 -- .../painless/70_execute_painless_scripts.yml | 6 +++--- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index 5151272e2f35e..293338c117b49 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -11,14 +11,14 @@ The Painless execute API allows an arbitrary script to be executed and a result |====== | Name | Required | Default | Description | `script` | yes | - | The script to execute -| `execute_context` | no | `painless_test` | The context the script should be executed in. +| `context` | no | `painless_test` | The context the script should be executed in. |====== -==== Execute contexts +==== Contexts -Execute contexts control how scripts are executed, what variables are available at runtime and what the return type is. +Contexts control how scripts are executed, what variables are available at runtime and what the return type is. -===== Painless test execute context +===== Painless test context The `painless_test` context executes scripts as is and do not add any special parameters. The only variable that is available is `params`, which can be used to access user defined values. @@ -54,7 +54,7 @@ Response: -------------------------------------------------- // TESTRESPONSE -===== Filter execute context +===== Filter context The `filter` context executes scripts as if they were executed inside a `script` query. For testing purposes a document must be provided that will be indexed temporarily in-memory and @@ -91,7 +91,7 @@ POST /_scripts/painless/_execute "max_length": 4 } }, - "execute_context": { + "context": { "filter": { "index": "my-index", "document": { @@ -114,7 +114,7 @@ Response: // TESTRESPONSE -===== Score execute context +===== Score context The `score` context executes scripts as if they were executed inside a `script_score` function in `function_score` query. @@ -154,7 +154,7 @@ POST /_scripts/painless/_execute "max_rank": 5.0 } }, - "execute_context": { + "context": { "score": { "index": "my-index", "document": { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index f94030859d961..61b6cffbf3269 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -84,7 +84,6 @@ import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptService; import org.elasticsearch.script.ScriptType; -import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -117,7 +116,7 @@ public Response newResponse() { public static class Request extends SingleShardRequest implements ToXContent { private static final ParseField SCRIPT_FIELD = new ParseField("script"); - private static final ParseField EXECUTE_CONTEXT_FIELD = new ParseField("execute_context"); + private static final ParseField CONTEXT_FIELD = new ParseField("context"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "painless_execute_request", args -> new Request((Script) args[0], (ExecuteScriptContext) args[1])); @@ -134,7 +133,7 @@ public static class Request extends SingleShardRequest implements ToXCo token = p.nextToken(); assert token == XContentParser.Token.END_OBJECT; return context; - }, EXECUTE_CONTEXT_FIELD); + }, CONTEXT_FIELD); } static final Map> SUPPORTED_CONTEXTS; @@ -381,7 +380,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.field(SCRIPT_FIELD.getPreferredName(), script); - builder.field(EXECUTE_CONTEXT_FIELD.getPreferredName(), executeScriptContext); + builder.field(CONTEXT_FIELD.getPreferredName(), executeScriptContext); return builder; } @@ -408,13 +407,7 @@ public String toString() { } static boolean needDocumentAndIndex(ScriptContext scriptContext) { - if (scriptContext == FilterScript.CONTEXT) { - return true; - } else if (scriptContext == ScoreScript.CONTEXT) { - return true; - } else { - return false; - } + return scriptContext == FilterScript.CONTEXT || scriptContext == ScoreScript.CONTEXT; } } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java index 99926316279f4..9b8eb80caca90 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java @@ -18,12 +18,10 @@ */ package org.elasticsearch.painless; -import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml index 4eef24e6f7314..a4bc922e8444b 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml +++ b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml @@ -35,7 +35,7 @@ setup: params: var1: 10 var2: 100 - execute_context: + context: painless_test: {} - match: { result: "-90" } @@ -48,7 +48,7 @@ setup: source: "doc['field'].value.length() <= params.max_length" params: max_length: 4 - execute_context: + context: filter: document: field: "four" @@ -64,7 +64,7 @@ setup: source: "doc['rank'].value / params.max_rank" params: max_rank: 5.0 - execute_context: + context: score: document: rank: 4 From f3eab3fb24eb112fe17229e442f39e3249cc2c61 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 4 Jul 2018 11:21:57 +0200 Subject: [PATCH 11/12] introduced context_setup parameter --- .../painless/painless-execute-script.asciidoc | 27 ++- .../painless/PainlessExecuteAction.java | 155 ++++++++---------- .../painless/PainlessExecuteApiTests.java | 13 +- .../painless/PainlessExecuteRequestTests.java | 5 +- .../painless/70_execute_painless_scripts.yml | 23 ++- 5 files changed, 101 insertions(+), 122 deletions(-) diff --git a/docs/painless/painless-execute-script.asciidoc b/docs/painless/painless-execute-script.asciidoc index 293338c117b49..2aca959778699 100644 --- a/docs/painless/painless-execute-script.asciidoc +++ b/docs/painless/painless-execute-script.asciidoc @@ -12,6 +12,7 @@ The Painless execute API allows an arbitrary script to be executed and a result | Name | Required | Default | Description | `script` | yes | - | The script to execute | `context` | no | `painless_test` | The context the script should be executed in. +| `context_setup` | no | - | Additional parameters to the context. |====== ==== Contexts @@ -61,7 +62,7 @@ For testing purposes a document must be provided that will be indexed temporaril is accessible to the script being tested. Because of this the _source, stored fields and doc values are available in the script being tested. -The following parameters are required inside a filter context: +The following parameters may be specified in `context_setup` for a filter context: document:: Contains the document that will be temporarily indexed in-memory and is accessible from the script. index:: The name of an index containing a mapping that is compatable with the document being indexed. @@ -91,12 +92,11 @@ POST /_scripts/painless/_execute "max_length": 4 } }, - "context": { - "filter": { - "index": "my-index", - "document": { - "field": "four" - } + "context": "filter", + "context_setup": { + "index": "my-index", + "document": { + "field": "four" } } } @@ -119,7 +119,7 @@ Response: The `score` context executes scripts as if they were executed inside a `script_score` function in `function_score` query. -Available parameters inside a score context: +The following parameters may be specified in `context_setup` for a score context: document:: Contains the document that will be temporarily indexed in-memory and is accessible from the script. index:: The name of an index containing a mapping that is compatable with the document being indexed. @@ -154,12 +154,11 @@ POST /_scripts/painless/_execute "max_rank": 5.0 } }, - "context": { - "score": { - "index": "my-index", - "document": { - "rank": 4 - } + "context": "score", + "context_setup": { + "index": "my-index", + "document": { + "rank": 4 } } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index 61b6cffbf3269..e652bc03583b3 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -31,14 +31,12 @@ import org.apache.lucene.store.RAMDirectory; import org.elasticsearch.Version; import org.elasticsearch.action.Action; -import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.single.shard.SingleShardRequest; import org.elasticsearch.action.support.single.shard.TransportSingleShardAction; -import org.elasticsearch.client.ElasticsearchClient; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.block.ClusterBlockException; @@ -90,7 +88,6 @@ import java.io.IOException; import java.util.Collections; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -117,23 +114,18 @@ public static class Request extends SingleShardRequest implements ToXCo private static final ParseField SCRIPT_FIELD = new ParseField("script"); private static final ParseField CONTEXT_FIELD = new ParseField("context"); + private static final ParseField CONTEXT_SETUP_FIELD = new ParseField("context_setup"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( - "painless_execute_request", args -> new Request((Script) args[0], (ExecuteScriptContext) args[1])); + "painless_execute_request", args -> new Request((Script) args[0], (String) args[1], (ContextSetup) args[2])); static { PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> Script.parse(p), SCRIPT_FIELD); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), CONTEXT_FIELD); PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> { - XContentParser.Token token = p.nextToken(); - assert token == XContentParser.Token.FIELD_NAME; - String contextType = p.currentName(); - ScriptContext scriptContext = fromScriptContextName(contextType.toLowerCase(Locale.ROOT)); - ExecuteScriptContext context = ExecuteScriptContext.PARSER.parse(p, c); - context.scriptContext = scriptContext; + ContextSetup context = ContextSetup.PARSER.parse(p, c); context.xContentType = p.contentType().xContent().type(); - token = p.nextToken(); - assert token == XContentParser.Token.END_OBJECT; return context; - }, CONTEXT_FIELD); + }, CONTEXT_SETUP_FIELD); } static final Map> SUPPORTED_CONTEXTS; @@ -154,35 +146,33 @@ static ScriptContext fromScriptContextName(String name) { return scriptContext; } - static class ExecuteScriptContext implements Writeable, ToXContentObject { + static class ContextSetup implements Writeable, ToXContentObject { private static final ParseField INDEX_FIELD = new ParseField("index"); private static final ParseField DOCUMENT_FIELD = new ParseField("document"); private static final ParseField QUERY_FIELD = new ParseField("query"); - private static final ObjectParser PARSER = - new ObjectParser<>("execute_script_context", ExecuteScriptContext::new); + private static final ObjectParser PARSER = + new ObjectParser<>("execute_script_context", ContextSetup::new); static { - PARSER.declareString(ExecuteScriptContext::setIndex, INDEX_FIELD); - PARSER.declareObject(ExecuteScriptContext::setDocument, (p, c) -> { + PARSER.declareString(ContextSetup::setIndex, INDEX_FIELD); + PARSER.declareObject(ContextSetup::setDocument, (p, c) -> { try (XContentBuilder b = XContentBuilder.builder(p.contentType().xContent())) { b.copyCurrentStructure(p); return BytesReference.bytes(b); } }, DOCUMENT_FIELD); - PARSER.declareObject(ExecuteScriptContext::setQuery, (p, c) -> + PARSER.declareObject(ContextSetup::setQuery, (p, c) -> AbstractQueryBuilder.parseInnerQueryBuilder(p), QUERY_FIELD); } - private ScriptContext scriptContext = PainlessTestScript.CONTEXT; private String index; private BytesReference document; private QueryBuilder query; private XContentType xContentType; - ExecuteScriptContext(StreamInput in) throws IOException { - scriptContext = fromScriptContextName(in.readString()); + ContextSetup(StreamInput in) throws IOException { index = in.readOptionalString(); document = in.readOptionalBytesReference(); String xContentType = in.readOptionalString(); @@ -192,7 +182,7 @@ static class ExecuteScriptContext implements Writeable, ToXContentObject { query = in.readOptionalNamedWriteable(QueryBuilder.class); } - ExecuteScriptContext() { + ContextSetup() { } void setIndex(String index) { @@ -211,21 +201,19 @@ void setQuery(QueryBuilder query) { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - ExecuteScriptContext that = (ExecuteScriptContext) o; - return Objects.equals(scriptContext, that.scriptContext) && - Objects.equals(index, that.index) && + ContextSetup that = (ContextSetup) o; + return Objects.equals(index, that.index) && Objects.equals(document, that.document) && Objects.equals(query, that.query); } @Override public int hashCode() { - return Objects.hash(scriptContext, index, document, query); + return Objects.hash(index, document, query); } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeString(scriptContext.name); out.writeOptionalString(index); out.writeOptionalBytesReference(document); out.writeOptionalString(xContentType != null ? xContentType.mediaType(): null); @@ -234,8 +222,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public String toString() { - return "ExecuteScriptContext{" + - "scriptContext=" + scriptContext.name + + return "ContextSetup{" + ", index='" + index + '\'' + ", document=" + document + ", query=" + query + @@ -247,23 +234,19 @@ public String toString() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); { - builder.startObject(scriptContext.name); - { - if (index != null) { - builder.field(INDEX_FIELD.getPreferredName(), index); - } - if (document != null) { - builder.field(DOCUMENT_FIELD.getPreferredName()); - try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, - LoggingDeprecationHandler.INSTANCE, document, xContentType)) { - builder.generator().copyCurrentStructure(parser); - } - } - if (query != null) { - builder.field(QUERY_FIELD.getPreferredName(), query); + if (index != null) { + builder.field(INDEX_FIELD.getPreferredName(), index); + } + if (document != null) { + builder.field(DOCUMENT_FIELD.getPreferredName()); + try (XContentParser parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, document, xContentType)) { + builder.generator().copyCurrentStructure(parser); } } - builder.endObject(); + if (query != null) { + builder.field(QUERY_FIELD.getPreferredName(), query); + } } builder.endObject(); return builder; @@ -272,7 +255,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } private Script script; - private ExecuteScriptContext executeScriptContext = new ExecuteScriptContext(); + private ScriptContext context = PainlessTestScript.CONTEXT; + private ContextSetup contextSetup = new ContextSetup(); static Request parse(XContentParser parser) throws IOException { Request request = PARSER.parse(parser, null); @@ -280,19 +264,14 @@ static Request parse(XContentParser parser) throws IOException { return request; } - Request(Script script, ExecuteScriptContext context) { - this.script = Objects.requireNonNull(script); - if (context != null) { - this.executeScriptContext = context; - index(executeScriptContext.index); - } - } - - Request(Script script, String scriptContextName) { + Request(Script script, String scriptContextName, ContextSetup setup) { this.script = Objects.requireNonNull(script); if (scriptContextName != null) { - this.executeScriptContext = new ExecuteScriptContext(); - this.executeScriptContext.scriptContext = fromScriptContextName(scriptContextName); + this.context = fromScriptContextName(scriptContextName); + } + if (setup != null) { + this.contextSetup = setup; + index(contextSetup.index); } } @@ -304,36 +283,36 @@ public Script getScript() { } public String getIndex() { - return executeScriptContext.index; + return contextSetup.index; } public void setIndex(String index) { - this.executeScriptContext.index = index; - index(executeScriptContext.index); + this.contextSetup.index = index; + index(contextSetup.index); } public BytesReference getDocument() { - return executeScriptContext.document; + return contextSetup.document; } public void setDocument(BytesReference document) { - this.executeScriptContext.document = document; + this.contextSetup.document = document; } public XContentType getXContentType() { - return executeScriptContext.xContentType; + return contextSetup.xContentType; } public void setXContentType(XContentType xContentType) { - this.executeScriptContext.xContentType = xContentType; + this.contextSetup.xContentType = xContentType; } public QueryBuilder getQuery() { - return executeScriptContext.query; + return contextSetup.query; } public void setQuery(QueryBuilder query) { - this.executeScriptContext.query = query; + this.contextSetup.query = query; } @Override @@ -342,11 +321,11 @@ public ActionRequestValidationException validate() { if (script.getType() != ScriptType.INLINE) { validationException = addValidationError("only inline scripts are supported", validationException); } - if (needDocumentAndIndex(executeScriptContext.scriptContext)) { - if (executeScriptContext.index == null) { + if (needDocumentAndIndex(context)) { + if (contextSetup.index == null) { validationException = addValidationError("index is a required parameter for current context", validationException); } - if (executeScriptContext.document == null) { + if (contextSetup.document == null) { validationException = addValidationError("document is a required parameter for current context", validationException); } } @@ -361,7 +340,8 @@ public void readFrom(StreamInput in) throws IOException { byte scriptContextId = in.readByte(); assert scriptContextId == 0; } else { - executeScriptContext = new ExecuteScriptContext(in); + context = fromScriptContextName(in.readString()); + contextSetup = in.readOptionalWriteable(ContextSetup::new); } } @@ -372,7 +352,8 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getVersion().onOrBefore(Version.V_6_4_0)) { out.writeByte((byte) 0); } else { - executeScriptContext.writeTo(out); + out.writeString(context.name); + out.writeOptionalWriteable(contextSetup); } } @@ -380,7 +361,10 @@ public void writeTo(StreamOutput out) throws IOException { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.field(SCRIPT_FIELD.getPreferredName(), script); - builder.field(CONTEXT_FIELD.getPreferredName(), executeScriptContext); + builder.field(CONTEXT_FIELD.getPreferredName(), context.name); + if (contextSetup != null) { + builder.field(CONTEXT_SETUP_FIELD.getPreferredName(), contextSetup); + } return builder; } @@ -390,19 +374,21 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Request request = (Request) o; return Objects.equals(script, request.script) && - Objects.equals(executeScriptContext, request.executeScriptContext); + Objects.equals(context, request.context) && + Objects.equals(contextSetup, request.contextSetup); } @Override public int hashCode() { - return Objects.hash(script, executeScriptContext); + return Objects.hash(script, context, contextSetup); } @Override public String toString() { return "Request{" + "script=" + script + - ", executeScriptContext=" + executeScriptContext + + "context=" + context + + ", contextSetup=" + contextSetup + '}'; } @@ -412,13 +398,6 @@ static boolean needDocumentAndIndex(ScriptContext scriptContext) { } - public static class RequestBuilder extends ActionRequestBuilder { - - RequestBuilder(ElasticsearchClient client) { - super(client, INSTANCE, new Request()); - } - } - public static class Response extends ActionResponse implements ToXContentObject { private Object result; @@ -542,7 +521,7 @@ protected Response shardOperation(Request request, ShardId shardId) throws IOExc if (request.getIndex() != null) { ClusterState clusterState = clusterService.state(); IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed(); - String indexExpression = request.executeScriptContext.index; + String indexExpression = request.contextSetup.index; Index[] concreteIndices = indexNameExpressionResolver.concreteIndices(clusterState, indicesOptions, indexExpression); if (concreteIndices.length != 1) { @@ -557,7 +536,7 @@ protected Response shardOperation(Request request, ShardId shardId) throws IOExc } static Response innerShardOperation(Request request, ScriptService scriptService, IndexService indexService) throws IOException { - final ScriptContext scriptContext = request.executeScriptContext.scriptContext; + final ScriptContext scriptContext = request.context; if (scriptContext == PainlessTestScript.CONTEXT) { PainlessTestScript.Factory factory = scriptService.compile(request.script, PainlessTestScript.CONTEXT); PainlessTestScript painlessTestScript = factory.newInstance(request.script.getParams()); @@ -581,8 +560,8 @@ static Response innerShardOperation(Request request, ScriptService scriptService ScoreScript scoreScript = leafFactory.newInstance(leafReaderContext); scoreScript.setDocument(0); - if (request.executeScriptContext.query != null) { - Query luceneQuery = request.executeScriptContext.query.rewrite(context).toQuery(context); + if (request.contextSetup.query != null) { + Query luceneQuery = request.contextSetup.query.rewrite(context).toQuery(context); IndexSearcher indexSearcher = new IndexSearcher(leafReaderContext.reader()); luceneQuery = indexSearcher.rewrite(luceneQuery); Weight weight = indexSearcher.createWeight(luceneQuery, true, 1f); @@ -611,8 +590,8 @@ private static Response prepareRamIndex(Request request, try (IndexWriter indexWriter = new IndexWriter(ramDirectory, new IndexWriterConfig(defaultAnalyzer))) { String index = indexService.index().getName(); String type = indexService.mapperService().documentMapper().type(); - BytesReference document = request.executeScriptContext.document; - XContentType xContentType = request.executeScriptContext.xContentType; + BytesReference document = request.contextSetup.document; + XContentType xContentType = request.contextSetup.xContentType; SourceToParse sourceToParse = SourceToParse.source(index, type, "_id", document, xContentType); ParsedDocument parsedDocument = indexService.mapperService().documentMapper().parse(sourceToParse); indexWriter.addDocuments(parsedDocument.docs()); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java index ba4629ce62635..8172d696bc712 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java @@ -51,21 +51,21 @@ protected Collection> getPlugins() { public void testDefaults() throws IOException { ScriptService scriptService = getInstanceFromNode(ScriptService.class); - Request request = new Request(new Script("100.0 / 1000.0"), (String) null); + Request request = new Request(new Script("100.0 / 1000.0"), null, null); Response response = innerShardOperation(request, scriptService, null); assertThat(response.getResult(), equalTo("0.1")); Map params = new HashMap<>(); params.put("count", 100.0D); params.put("total", 1000.0D); - request = new Request(new Script(ScriptType.INLINE, "painless", "params.count / params.total", params), (String) null); + request = new Request(new Script(ScriptType.INLINE, "painless", "params.count / params.total", params), null, null); response = innerShardOperation(request, scriptService, null); assertThat(response.getResult(), equalTo("0.1")); Exception e = expectThrows(ScriptException.class, () -> { Request r = new Request(new Script(ScriptType.INLINE, - "painless", "params.count / params.total + doc['constant']", params), (String) null); + "painless", "params.count / params.total + doc['constant']", params), null, null); innerShardOperation(r, scriptService, null); }); assertThat(e.getCause().getMessage(), equalTo("Variable [doc] is not defined.")); @@ -75,7 +75,7 @@ public void testFilterExecutionContext() throws IOException { ScriptService scriptService = getInstanceFromNode(ScriptService.class); IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "field", "type=long"); - Request request = new Request(new Script("doc['field'].value >= 3"), "filter"); + Request request = new Request(new Script("doc['field'].value >= 3"), "filter", new Request.ContextSetup()); request.setIndex("index"); request.setDocument(new BytesArray("{\"field\": 3}")); request.setXContentType(XContentType.JSON); @@ -84,7 +84,7 @@ public void testFilterExecutionContext() throws IOException { request = new Request(new Script(ScriptType.INLINE, "painless", "doc['field'].value >= params.max", - singletonMap("max", 3)), "filter"); + singletonMap("max", 3)), "filter", new Request.ContextSetup()); request.setIndex("index"); request.setDocument(new BytesArray("{\"field\": 3}")); request.setXContentType(XContentType.JSON); @@ -101,7 +101,8 @@ public void testScoreExecutionContext() throws IOException { IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "rank", "type=long", "text", "type=text"); Request request = new Request(new Script(ScriptType.INLINE, "painless", - "Math.round((_score + (doc['rank'].value / params.max_rank)) * 100.0) / 100.0", singletonMap("max_rank", 5.0)), "score"); + "Math.round((_score + (doc['rank'].value / params.max_rank)) * 100.0) / 100.0", singletonMap("max_rank", 5.0)), "score", + new Request.ContextSetup()); request.setIndex("index"); request.setDocument(new BytesArray("{\"rank\": 4.0, \"text\": \"quick brown fox\"}")); request.setXContentType(XContentType.JSON); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java index 9b8eb80caca90..4676b804b6519 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java @@ -48,7 +48,8 @@ protected NamedXContentRegistry xContentRegistry() { protected PainlessExecuteAction.Request createTestInstance() { Script script = new Script(randomAlphaOfLength(10)); ScriptContext context = randomBoolean() ? randomFrom(PainlessExecuteAction.Request.SUPPORTED_CONTEXTS.values()) : null; - PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, context != null ? context.name : null); + PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, context != null ? context.name : null, + new PainlessExecuteAction.Request.ContextSetup()); if (randomBoolean()) { request.setIndex(randomAlphaOfLength(4)); } @@ -81,7 +82,7 @@ protected boolean supportsUnknownFields() { public void testValidate() { Script script = new Script(ScriptType.STORED, null, randomAlphaOfLength(10), Collections.emptyMap()); - PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, (String) null); + PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, null, null); Exception e = request.validate(); assertNotNull(e); assertEquals("Validation Failed: 1: only inline scripts are supported;", e.getMessage()); diff --git a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml index a4bc922e8444b..1e34a776189b8 100644 --- a/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml +++ b/modules/lang-painless/src/test/resources/rest-api-spec/test/painless/70_execute_painless_scripts.yml @@ -35,8 +35,7 @@ setup: params: var1: 10 var2: 100 - context: - painless_test: {} + context: "painless_test" - match: { result: "-90" } --- @@ -48,11 +47,11 @@ setup: source: "doc['field'].value.length() <= params.max_length" params: max_length: 4 - context: - filter: - document: - field: "four" - index: "my-index" + context: "filter" + context_setup: + document: + field: "four" + index: "my-index" - match: { result: true } --- @@ -64,9 +63,9 @@ setup: source: "doc['rank'].value / params.max_rank" params: max_rank: 5.0 - context: - score: - document: - rank: 4 - index: "my-index" + context: "score" + context_setup: + document: + rank: 4 + index: "my-index" - match: { result: 0.8 } From 3afa5d0c6f4ccb8ef48cb7ab9ffc50380065874d Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Wed, 4 Jul 2018 11:55:10 +0200 Subject: [PATCH 12/12] made context setup fields final --- .../painless/PainlessExecuteAction.java | 98 ++++++++----------- .../painless/PainlessExecuteApiTests.java | 29 +++--- .../painless/PainlessExecuteRequestTests.java | 37 ++++--- 3 files changed, 78 insertions(+), 86 deletions(-) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java index e652bc03583b3..229c919a2e65d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessExecuteAction.java @@ -54,7 +54,6 @@ import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; -import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -121,11 +120,7 @@ public static class Request extends SingleShardRequest implements ToXCo static { PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> Script.parse(p), SCRIPT_FIELD); PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), CONTEXT_FIELD); - PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> { - ContextSetup context = ContextSetup.PARSER.parse(p, c); - context.xContentType = p.contentType().xContent().type(); - return context; - }, CONTEXT_SETUP_FIELD); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), ContextSetup::parse, CONTEXT_SETUP_FIELD); } static final Map> SUPPORTED_CONTEXTS; @@ -151,27 +146,40 @@ static class ContextSetup implements Writeable, ToXContentObject { private static final ParseField INDEX_FIELD = new ParseField("index"); private static final ParseField DOCUMENT_FIELD = new ParseField("document"); private static final ParseField QUERY_FIELD = new ParseField("query"); - private static final ObjectParser PARSER = - new ObjectParser<>("execute_script_context", ContextSetup::new); + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("execute_script_context", + args -> new ContextSetup((String) args[0], (BytesReference) args[1], (QueryBuilder) args[2])); static { - PARSER.declareString(ContextSetup::setIndex, INDEX_FIELD); - PARSER.declareObject(ContextSetup::setDocument, (p, c) -> { + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), INDEX_FIELD); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> { try (XContentBuilder b = XContentBuilder.builder(p.contentType().xContent())) { b.copyCurrentStructure(p); return BytesReference.bytes(b); } }, DOCUMENT_FIELD); - PARSER.declareObject(ContextSetup::setQuery, (p, c) -> + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> AbstractQueryBuilder.parseInnerQueryBuilder(p), QUERY_FIELD); } - private String index; - private BytesReference document; - private QueryBuilder query; + private final String index; + private final BytesReference document; + private final QueryBuilder query; private XContentType xContentType; + static ContextSetup parse(XContentParser parser, Void context) throws IOException { + ContextSetup contextSetup = PARSER.parse(parser, null); + contextSetup.setXContentType(parser.contentType()); + return contextSetup; + } + + ContextSetup(String index, BytesReference document, QueryBuilder query) { + this.index = index; + this.document = document; + this.query = query; + } + ContextSetup(StreamInput in) throws IOException { index = in.readOptionalString(); document = in.readOptionalBytesReference(); @@ -182,19 +190,24 @@ static class ContextSetup implements Writeable, ToXContentObject { query = in.readOptionalNamedWriteable(QueryBuilder.class); } - ContextSetup() { + public String getIndex() { + return index; } - void setIndex(String index) { - this.index = index; + public BytesReference getDocument() { + return document; } - void setDocument(BytesReference document) { - this.document = document; + public QueryBuilder getQuery() { + return query; } - void setQuery(QueryBuilder query) { - this.query = query; + public XContentType getXContentType() { + return xContentType; + } + + public void setXContentType(XContentType xContentType) { + this.xContentType = xContentType; } @Override @@ -256,12 +269,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws private Script script; private ScriptContext context = PainlessTestScript.CONTEXT; - private ContextSetup contextSetup = new ContextSetup(); + private ContextSetup contextSetup; static Request parse(XContentParser parser) throws IOException { - Request request = PARSER.parse(parser, null); - request.setXContentType(parser.contentType()); - return request; + return PARSER.parse(parser, null); } Request(Script script, String scriptContextName, ContextSetup setup) { @@ -282,37 +293,12 @@ public Script getScript() { return script; } - public String getIndex() { - return contextSetup.index; - } - - public void setIndex(String index) { - this.contextSetup.index = index; - index(contextSetup.index); - } - - public BytesReference getDocument() { - return contextSetup.document; - } - - public void setDocument(BytesReference document) { - this.contextSetup.document = document; - } - - public XContentType getXContentType() { - return contextSetup.xContentType; - } - - public void setXContentType(XContentType xContentType) { - this.contextSetup.xContentType = xContentType; - } - - public QueryBuilder getQuery() { - return contextSetup.query; + public ScriptContext getContext() { + return context; } - public void setQuery(QueryBuilder query) { - this.contextSetup.query = query; + public ContextSetup getContextSetup() { + return contextSetup; } @Override @@ -504,7 +490,7 @@ protected ClusterBlockException checkRequestBlock(ClusterState state, InternalRe @Override protected boolean resolveIndex(Request request) { - return request.getIndex() != null; + return request.contextSetup != null && request.contextSetup.getIndex() != null; } @Override @@ -518,7 +504,7 @@ protected ShardsIterator shards(ClusterState state, InternalRequest request) { @Override protected Response shardOperation(Request request, ShardId shardId) throws IOException { IndexService indexService; - if (request.getIndex() != null) { + if (request.contextSetup != null && request.contextSetup.getIndex() != null) { ClusterState clusterState = clusterService.state(); IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed(); String indexExpression = request.contextSetup.index; diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java index 8172d696bc712..ce92a224f4e90 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteApiTests.java @@ -75,23 +75,23 @@ public void testFilterExecutionContext() throws IOException { ScriptService scriptService = getInstanceFromNode(ScriptService.class); IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "field", "type=long"); - Request request = new Request(new Script("doc['field'].value >= 3"), "filter", new Request.ContextSetup()); - request.setIndex("index"); - request.setDocument(new BytesArray("{\"field\": 3}")); - request.setXContentType(XContentType.JSON); + Request.ContextSetup contextSetup = new Request.ContextSetup("index", new BytesArray("{\"field\": 3}"), null); + contextSetup.setXContentType(XContentType.JSON); + Request request = new Request(new Script("doc['field'].value >= 3"), "filter", contextSetup); Response response = innerShardOperation(request, scriptService, indexService); assertThat(response.getResult(), equalTo(true)); - + contextSetup = new Request.ContextSetup("index", new BytesArray("{\"field\": 3}"), null); + contextSetup.setXContentType(XContentType.JSON); request = new Request(new Script(ScriptType.INLINE, "painless", "doc['field'].value >= params.max", - singletonMap("max", 3)), "filter", new Request.ContextSetup()); - request.setIndex("index"); - request.setDocument(new BytesArray("{\"field\": 3}")); - request.setXContentType(XContentType.JSON); + singletonMap("max", 3)), "filter", contextSetup); response = innerShardOperation(request, scriptService, indexService); assertThat(response.getResult(), equalTo(true)); - request.setDocument(new BytesArray("{\"field\": 2}")); + contextSetup = new Request.ContextSetup("index", new BytesArray("{\"field\": 2}"), null); + contextSetup.setXContentType(XContentType.JSON); + request = new Request(new Script(ScriptType.INLINE, "painless", "doc['field'].value >= params.max", + singletonMap("max", 3)), "filter", contextSetup); response = innerShardOperation(request, scriptService, indexService); assertThat(response.getResult(), equalTo(false)); } @@ -100,13 +100,12 @@ public void testScoreExecutionContext() throws IOException { ScriptService scriptService = getInstanceFromNode(ScriptService.class); IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "rank", "type=long", "text", "type=text"); + Request.ContextSetup contextSetup = new Request.ContextSetup("index", + new BytesArray("{\"rank\": 4.0, \"text\": \"quick brown fox\"}"), new MatchQueryBuilder("text", "fox")); + contextSetup.setXContentType(XContentType.JSON); Request request = new Request(new Script(ScriptType.INLINE, "painless", "Math.round((_score + (doc['rank'].value / params.max_rank)) * 100.0) / 100.0", singletonMap("max_rank", 5.0)), "score", - new Request.ContextSetup()); - request.setIndex("index"); - request.setDocument(new BytesArray("{\"rank\": 4.0, \"text\": \"quick brown fox\"}")); - request.setXContentType(XContentType.JSON); - request.setQuery(new MatchQueryBuilder("text", "fox")); + contextSetup); Response response = innerShardOperation(request, scriptService, indexService); assertThat(response.getResult(), equalTo(1.09D)); } diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java index 4676b804b6519..44cd6b5304dc4 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/PainlessExecuteRequestTests.java @@ -18,11 +18,14 @@ */ package org.elasticsearch.painless; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.painless.PainlessExecuteAction.Request.ContextSetup; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptType; @@ -48,21 +51,8 @@ protected NamedXContentRegistry xContentRegistry() { protected PainlessExecuteAction.Request createTestInstance() { Script script = new Script(randomAlphaOfLength(10)); ScriptContext context = randomBoolean() ? randomFrom(PainlessExecuteAction.Request.SUPPORTED_CONTEXTS.values()) : null; - PainlessExecuteAction.Request request = new PainlessExecuteAction.Request(script, context != null ? context.name : null, - new PainlessExecuteAction.Request.ContextSetup()); - if (randomBoolean()) { - request.setIndex(randomAlphaOfLength(4)); - } - if (randomBoolean()) { - // TODO: Pass down XContentType here. - // Otherwise the document's xcontent type is incompatible with the that of the XContentParser xcontent type -// request.setDocument(new BytesArray("{}")); -// request.setXContentType(XContentType.JSON); - } - if (randomBoolean()) { - request.setQuery(new MatchAllQueryBuilder()); - } - return request; + ContextSetup contextSetup = randomBoolean() ? randomContextSetup() : null; + return new PainlessExecuteAction.Request(script, context != null ? context.name : null, contextSetup); } @Override @@ -87,4 +77,21 @@ public void testValidate() { assertNotNull(e); assertEquals("Validation Failed: 1: only inline scripts are supported;", e.getMessage()); } + + private static ContextSetup randomContextSetup() { + String index = randomBoolean() ? randomAlphaOfLength(4) : null; + QueryBuilder query = randomBoolean() ? new MatchAllQueryBuilder() : null; + // TODO: pass down XContextType to createTestInstance() method. + // otherwise the document itself is different causing test failures. + // This should be done in a seperate change as the test instance is created before xcontent type is randomly picked and + // all the createTestInstance() methods need to be changed, which will make this a big chnage +// BytesReference doc = randomBoolean() ? new BytesArray("{}") : null; + BytesReference doc = null; + + ContextSetup contextSetup = new ContextSetup(index, doc, query); +// if (doc != null) { +// contextSetup.setXContentType(XContentType.JSON); +// } + return contextSetup; + } }