Add more contexts to painless execute api (#30511)

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

Also 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).
This commit is contained in:
Martijn van Groningen 2018-07-18 12:42:07 +02:00 committed by GitHub
parent 8235b254ab
commit 1924f5d07c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 694 additions and 87 deletions

View File

@ -9,23 +9,24 @@ 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
| `context` | no | `painless_test` | The context the script should be executed in.
| `context_setup` | no | - | Additional parameters to the context.
|======
==== Contexts
Contexts control how scripts are executed, what variables are available at runtime and what the return type is.
===== Painless test script 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.
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 +53,124 @@ Response:
"result": "0.1"
}
--------------------------------------------------
// TESTRESPONSE
// TESTRESPONSE
===== 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
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 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.
====== Example
[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
}
},
"context": "filter",
"context_setup": {
"index": "my-index",
"document": {
"field": "four"
}
}
}
----------------------------------------------------------------
// CONSOLE
Response:
[source,js]
--------------------------------------------------
{
"result": true
}
--------------------------------------------------
// TESTRESPONSE
===== Score context
The `score` context executes scripts as if they were executed inside a `script_score` function in
`function_score` query.
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.
query:: If `_score` is used in the script then a query can specified that will be used to compute a score.
====== Example
[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": 5.0
}
},
"context": "score",
"context_setup": {
"index": "my-index",
"document": {
"rank": 4
}
}
}
----------------------------------------------------------------
// CONSOLE
Response:
[source,js]
--------------------------------------------------
{
"result": 0.8
}
--------------------------------------------------
// TESTRESPONSE

View File

@ -18,41 +18,75 @@
*/
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.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;
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.client.ElasticsearchClient;
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.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.CheckedBiFunction;
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.io.stream.Writeable;
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.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.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;
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.ScoreScript;
import org.elasticsearch.script.Script;
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;
import java.io.IOException;
import java.util.Locale;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@ -75,40 +109,181 @@ public class PainlessExecuteAction extends Action<PainlessExecuteAction.Response
return new Response();
}
public static class Request extends ActionRequest implements ToXContent {
public static class Request extends SingleShardRequest<Request> implements ToXContent {
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<Request, Void> PARSER = new ConstructingObjectParser<>(
"painless_execute_request", args -> new Request((Script) args[0], (SupportedContext) 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.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();
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));
}, CONTEXT_FIELD);
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), CONTEXT_FIELD);
PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), ContextSetup::parse, CONTEXT_SETUP_FIELD);
}
static final Map<String, ScriptContext<?>> SUPPORTED_CONTEXTS;
static {
Map<String, ScriptContext<?>> supportedContexts = new HashMap<>();
supportedContexts.put("painless_test", PainlessTestScript.CONTEXT);
supportedContexts.put("filter", FilterScript.CONTEXT);
supportedContexts.put("score", ScoreScript.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 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 ConstructingObjectParser<ContextSetup, Void> PARSER =
new ConstructingObjectParser<>("execute_script_context",
args -> new ContextSetup((String) args[0], (BytesReference) args[1], (QueryBuilder) args[2]));
static {
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(ConstructingObjectParser.optionalConstructorArg(), (p, c) ->
AbstractQueryBuilder.parseInnerQueryBuilder(p), QUERY_FIELD);
}
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();
String xContentType = in.readOptionalString();
if (xContentType != null) {
this.xContentType = XContentType.fromMediaType(xContentType);
}
query = in.readOptionalNamedWriteable(QueryBuilder.class);
}
public String getIndex() {
return index;
}
public BytesReference getDocument() {
return document;
}
public QueryBuilder getQuery() {
return query;
}
public XContentType getXContentType() {
return xContentType;
}
public void setXContentType(XContentType xContentType) {
this.xContentType = xContentType;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
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(index, document, query);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeOptionalString(index);
out.writeOptionalBytesReference(document);
out.writeOptionalString(xContentType != null ? xContentType.mediaType(): null);
out.writeOptionalNamedWriteable(query);
}
@Override
public String toString() {
return "ContextSetup{" +
", index='" + index + '\'' +
", document=" + document +
", query=" + query +
", xContentType=" + xContentType +
'}';
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
{
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);
}
}
builder.endObject();
return builder;
}
}
private Script script;
private SupportedContext context;
private ScriptContext<?> context = PainlessTestScript.CONTEXT;
private ContextSetup contextSetup;
static Request parse(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
Request(Script script, SupportedContext context) {
Request(Script script, String scriptContextName, ContextSetup setup) {
this.script = Objects.requireNonNull(script);
this.context = context != null ? context : SupportedContext.PAINLESS_TEST;
if (scriptContextName != null) {
this.context = fromScriptContextName(scriptContextName);
}
if (setup != null) {
this.contextSetup = setup;
index(contextSetup.index);
}
}
Request() {
@ -118,16 +293,28 @@ public class PainlessExecuteAction extends Action<PainlessExecuteAction.Response
return script;
}
public SupportedContext getContext() {
public ScriptContext<?> getContext() {
return context;
}
public ContextSetup getContextSetup() {
return contextSetup;
}
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (script.getType() != ScriptType.INLINE) {
validationException = addValidationError("only inline scripts are supported", validationException);
}
if (needDocumentAndIndex(context)) {
if (contextSetup.index == null) {
validationException = addValidationError("index is a required parameter for current context", validationException);
}
if (contextSetup.document == null) {
validationException = addValidationError("document is a required parameter for current context", validationException);
}
}
return validationException;
}
@ -135,26 +322,35 @@ public class PainlessExecuteAction extends Action<PainlessExecuteAction.Response
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
script = new Script(in);
context = SupportedContext.fromId(in.readByte());
if (in.getVersion().onOrBefore(Version.V_6_4_0)) {
byte scriptContextId = in.readByte();
assert scriptContextId == 0;
} else {
context = fromScriptContextName(in.readString());
contextSetup = in.readOptionalWriteable(ContextSetup::new);
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
script.writeTo(out);
out.writeByte(context.id);
if (out.getVersion().onOrBefore(Version.V_6_4_0)) {
out.writeByte((byte) 0);
} else {
out.writeString(context.name);
out.writeOptionalWriteable(contextSetup);
}
}
// For testing only:
@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());
builder.endObject();
builder.field(CONTEXT_FIELD.getPreferredName(), context.name);
if (contextSetup != null) {
builder.field(CONTEXT_SETUP_FIELD.getPreferredName(), contextSetup);
}
builder.endObject();
return builder;
}
@ -164,41 +360,28 @@ public class PainlessExecuteAction extends Action<PainlessExecuteAction.Response
if (o == null || getClass() != o.getClass()) return false;
Request request = (Request) o;
return Objects.equals(script, request.script) &&
context == request.context;
Objects.equals(context, request.context) &&
Objects.equals(contextSetup, request.contextSetup);
}
@Override
public int hashCode() {
return Objects.hash(script, context);
return Objects.hash(script, context, contextSetup);
}
public enum SupportedContext {
PAINLESS_TEST((byte) 0);
private final byte id;
SupportedContext(byte id) {
this.id = id;
}
public static SupportedContext fromId(byte id) {
switch (id) {
case 0:
return PAINLESS_TEST;
default:
throw new IllegalArgumentException("unknown context [" + id + "]");
}
}
@Override
public String toString() {
return "Request{" +
"script=" + script +
"context=" + context +
", contextSetup=" + contextSetup +
'}';
}
}
public static class RequestBuilder extends ActionRequestBuilder<Request, Response> {
RequestBuilder(ElasticsearchClient client) {
super(client, INSTANCE, new Request());
static boolean needDocumentAndIndex(ScriptContext<?> scriptContext) {
return scriptContext == FilterScript.CONTEXT || scriptContext == ScoreScript.CONTEXT;
}
}
public static class Response extends ActionResponse implements ToXContentObject {
@ -274,31 +457,139 @@ public class PainlessExecuteAction extends Action<PainlessExecuteAction.Response
}
public static class TransportAction extends HandledTransportAction<Request, Response> {
public static class TransportAction extends TransportSingleShardAction<Request, Response> {
private final ScriptService scriptService;
private final IndicesService indicesServices;
@Inject
public TransportAction(Settings settings, TransportService transportService,
ActionFilters actionFilters, ScriptService scriptService) {
super(settings, NAME, transportService, actionFilters, Request::new);
public TransportAction(Settings settings, ThreadPool threadPool, TransportService transportService,
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
ScriptService scriptService, ClusterService clusterService, IndicesService indicesServices) {
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.indicesServices = indicesServices;
}
@Override
protected void doExecute(Task task, Request request, ActionListener<Response> 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));
break;
default:
throw new UnsupportedOperationException("unsupported context [" + request.context + "]");
protected Response newResponse() {
return new Response();
}
@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.contextSetup != null && request.contextSetup.getIndex() != null;
}
@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 {
IndexService indexService;
if (request.contextSetup != null && request.contextSetup.getIndex() != null) {
ClusterState clusterState = clusterService.state();
IndicesOptions indicesOptions = IndicesOptions.strictSingleIndexNoExpandForbidClosed();
String indexExpression = request.contextSetup.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.context;
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 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 == ScoreScript.CONTEXT) {
return prepareRamIndex(request, (context, leafReaderContext) -> {
ScoreScript.Factory factory = scriptService.compile(request.script, ScoreScript.CONTEXT);
ScoreScript.LeafFactory leafFactory =
factory.newFactory(request.getScript().getParams(), context.lookup());
ScoreScript scoreScript = leafFactory.newInstance(leafReaderContext);
scoreScript.setDocument(0);
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);
Scorer scorer = weight.scorer(indexSearcher.getIndexReader().leaves().get(0));
// Consume the first (and only) match.
int docID = scorer.iterator().nextDoc();
assert docID == scorer.docID();
scoreScript.setScorer(scorer);
}
double result = scoreScript.execute();
return new Response(result);
}, indexService);
} else {
throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]");
}
}
private static Response prepareRamIndex(Request request,
CheckedBiFunction<QueryShardContext, LeafReaderContext, Response, IOException> handler,
IndexService indexService) throws IOException {
Analyzer defaultAnalyzer = indexService.getIndexAnalyzers().getDefaultIndexAnalyzer();
try (RAMDirectory ramDirectory = new RAMDirectory()) {
try (IndexWriter indexWriter = new IndexWriter(ramDirectory, new IndexWriterConfig(defaultAnalyzer))) {
String index = indexService.index().getName();
String type = indexService.mapperService().documentMapper().type();
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());
try (IndexReader indexReader = DirectoryReader.open(indexWriter)) {
final long absoluteStartMillis = System.currentTimeMillis();
QueryShardContext context =
indexService.newQueryShardContext(0, indexReader, () -> absoluteStartMillis, null);
return handler.apply(context, indexReader.leaves().get(0));
}
}
}
}
}
static class RestAction extends BaseRestHandler {

View File

@ -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<Class<? extends Plugin>> 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"), null, null);
Response response = innerShardOperation(request, scriptService, null);
assertThat(response.getResult(), equalTo("0.1"));
Map<String, Object> 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), 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), null, 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.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", contextSetup);
response = innerShardOperation(request, scriptService, indexService);
assertThat(response.getResult(), equalTo(true));
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));
}
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",
contextSetup);
Response response = innerShardOperation(request, scriptService, indexService);
assertThat(response.getResult(), equalTo(1.09D));
}
}

View File

@ -18,9 +18,18 @@
*/
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;
import org.elasticsearch.search.SearchModule;
import org.elasticsearch.test.AbstractStreamableXContentTestCase;
import java.io.IOException;
@ -28,12 +37,22 @@ import java.util.Collections;
public class PainlessExecuteRequestTests extends AbstractStreamableXContentTestCase<PainlessExecuteAction.Request> {
@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;
ContextSetup contextSetup = randomBoolean() ? randomContextSetup() : null;
return new PainlessExecuteAction.Request(script, context != null ? context.name : null, contextSetup);
}
@Override
@ -53,9 +72,26 @@ public class PainlessExecuteRequestTests extends AbstractStreamableXContentTestC
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, null, null);
Exception e = request.validate();
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;
}
}

View File

@ -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:
@ -20,6 +35,37 @@
params:
var1: 10
var2: 100
context:
painless_test: {}
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"
context_setup:
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"
context_setup:
document:
rank: 4
index: "my-index"
- match: { result: 0.8 }