Skip to content

Commit d009b96

Browse files
committed
Add Runtime Fields Contexts to Painless Execute API (#71374)
This change adds support for the 7 different runtime fields contexts to the Painless Execute API. Each context can accept the standard script input (source and params) along with a user-defined document and an index name to pull mappings from. The results depend on the output of the runtime field type. Closes #70467
1 parent 9aea851 commit d009b96

File tree

3 files changed

+468
-0
lines changed

3 files changed

+468
-0
lines changed

modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.elasticsearch.common.xcontent.XContentType;
5252
import org.elasticsearch.index.Index;
5353
import org.elasticsearch.index.IndexService;
54+
import org.elasticsearch.index.mapper.DateFieldMapper;
5455
import org.elasticsearch.index.mapper.ParsedDocument;
5556
import org.elasticsearch.index.mapper.SourceToParse;
5657
import org.elasticsearch.index.query.AbstractQueryBuilder;
@@ -61,12 +62,19 @@
6162
import org.elasticsearch.rest.BaseRestHandler;
6263
import org.elasticsearch.rest.RestRequest;
6364
import org.elasticsearch.rest.action.RestToXContentListener;
65+
import org.elasticsearch.script.BooleanFieldScript;
66+
import org.elasticsearch.script.DateFieldScript;
67+
import org.elasticsearch.script.DoubleFieldScript;
6468
import org.elasticsearch.script.FilterScript;
69+
import org.elasticsearch.script.GeoPointFieldScript;
70+
import org.elasticsearch.script.IpFieldScript;
71+
import org.elasticsearch.script.LongFieldScript;
6572
import org.elasticsearch.script.ScoreScript;
6673
import org.elasticsearch.script.Script;
6774
import org.elasticsearch.script.ScriptContext;
6875
import org.elasticsearch.script.ScriptService;
6976
import org.elasticsearch.script.ScriptType;
77+
import org.elasticsearch.script.StringFieldScript;
7078
import org.elasticsearch.threadpool.ThreadPool;
7179
import org.elasticsearch.transport.TransportService;
7280

@@ -114,6 +122,13 @@ public static class Request extends SingleShardRequest<Request> implements ToXCo
114122
supportedContexts.put("painless_test", PainlessTestScript.CONTEXT);
115123
supportedContexts.put("filter", FilterScript.CONTEXT);
116124
supportedContexts.put("score", ScoreScript.CONTEXT);
125+
supportedContexts.put("boolean_field", BooleanFieldScript.CONTEXT);
126+
supportedContexts.put("date_field", DateFieldScript.CONTEXT);
127+
supportedContexts.put("double_field", DoubleFieldScript.CONTEXT);
128+
supportedContexts.put("geo_point_field", GeoPointFieldScript.CONTEXT);
129+
supportedContexts.put("ip_field", IpFieldScript.CONTEXT);
130+
supportedContexts.put("long_field", LongFieldScript.CONTEXT);
131+
supportedContexts.put("string_field", StringFieldScript.CONTEXT);
117132
SUPPORTED_CONTEXTS = Collections.unmodifiableMap(supportedContexts);
118133
}
119134

@@ -542,6 +557,69 @@ static Response innerShardOperation(Request request, ScriptService scriptService
542557
double result = scoreScript.execute(null);
543558
return new Response(result);
544559
}, indexService);
560+
} else if (scriptContext == BooleanFieldScript.CONTEXT) {
561+
return prepareRamIndex(request, (context, leafReaderContext) -> {
562+
BooleanFieldScript.Factory factory = scriptService.compile(request.script, BooleanFieldScript.CONTEXT);
563+
BooleanFieldScript.LeafFactory leafFactory =
564+
factory.newFactory("boolean_field", request.getScript().getParams(), context.lookup());
565+
BooleanFieldScript booleanFieldScript = leafFactory.newInstance(leafReaderContext);
566+
booleanFieldScript.runForDoc(0);
567+
return new Response(booleanFieldScript.asDocValues());
568+
}, indexService);
569+
} else if (scriptContext == DateFieldScript.CONTEXT) {
570+
return prepareRamIndex(request, (context, leafReaderContext) -> {
571+
DateFieldScript.Factory factory = scriptService.compile(request.script, DateFieldScript.CONTEXT);
572+
DateFieldScript.LeafFactory leafFactory = factory.newFactory("date_field",
573+
request.getScript().getParams(), context.lookup(), DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER);
574+
DateFieldScript dateFieldScript = leafFactory.newInstance(leafReaderContext);
575+
dateFieldScript.runForDoc(0);
576+
return new Response(dateFieldScript.asDocValues());
577+
}, indexService);
578+
} else if (scriptContext == DoubleFieldScript.CONTEXT) {
579+
return prepareRamIndex(request, (context, leafReaderContext) -> {
580+
DoubleFieldScript.Factory factory = scriptService.compile(request.script, DoubleFieldScript.CONTEXT);
581+
DoubleFieldScript.LeafFactory leafFactory =
582+
factory.newFactory("double_field", request.getScript().getParams(), context.lookup());
583+
DoubleFieldScript doubleFieldScript = leafFactory.newInstance(leafReaderContext);
584+
doubleFieldScript.runForDoc(0);
585+
return new Response(doubleFieldScript.asDocValues());
586+
}, indexService);
587+
} else if (scriptContext == GeoPointFieldScript.CONTEXT) {
588+
return prepareRamIndex(request, (context, leafReaderContext) -> {
589+
GeoPointFieldScript.Factory factory = scriptService.compile(request.script, GeoPointFieldScript.CONTEXT);
590+
GeoPointFieldScript.LeafFactory leafFactory =
591+
factory.newFactory("geo_point_field", request.getScript().getParams(), context.lookup());
592+
GeoPointFieldScript geoPointFieldScript = leafFactory.newInstance(leafReaderContext);
593+
geoPointFieldScript.runForDoc(0);
594+
return new Response(geoPointFieldScript.asDocValues());
595+
}, indexService);
596+
} else if (scriptContext == IpFieldScript.CONTEXT) {
597+
return prepareRamIndex(request, (context, leafReaderContext) -> {
598+
IpFieldScript.Factory factory = scriptService.compile(request.script, IpFieldScript.CONTEXT);
599+
IpFieldScript.LeafFactory leafFactory =
600+
factory.newFactory("ip_field", request.getScript().getParams(), context.lookup());
601+
IpFieldScript ipFieldScript = leafFactory.newInstance(leafReaderContext);
602+
ipFieldScript.runForDoc(0);
603+
return new Response(ipFieldScript.asDocValues());
604+
}, indexService);
605+
} else if (scriptContext == LongFieldScript.CONTEXT) {
606+
return prepareRamIndex(request, (context, leafReaderContext) -> {
607+
LongFieldScript.Factory factory = scriptService.compile(request.script, LongFieldScript.CONTEXT);
608+
LongFieldScript.LeafFactory leafFactory =
609+
factory.newFactory("long_field", request.getScript().getParams(), context.lookup());
610+
LongFieldScript longFieldScript = leafFactory.newInstance(leafReaderContext);
611+
longFieldScript.runForDoc(0);
612+
return new Response(longFieldScript.asDocValues());
613+
}, indexService);
614+
} else if (scriptContext == StringFieldScript.CONTEXT) {
615+
return prepareRamIndex(request, (context, leafReaderContext) -> {
616+
StringFieldScript.Factory factory = scriptService.compile(request.script, StringFieldScript.CONTEXT);
617+
StringFieldScript.LeafFactory leafFactory =
618+
factory.newFactory("string_field", request.getScript().getParams(), context.lookup());
619+
StringFieldScript stringFieldScript = leafFactory.newInstance(leafReaderContext);
620+
stringFieldScript.resultsForDoc(0);
621+
return new Response(stringFieldScript.asDocValues());
622+
}, indexService);
545623
} else {
546624
throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]");
547625
}

modules/lang-painless/src/test/java/org/elasticsearch/painless/action/PainlessExecuteApiTests.java

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
*/
88
package org.elasticsearch.painless.action;
99

10+
import org.apache.lucene.util.BytesRef;
1011
import org.elasticsearch.common.bytes.BytesArray;
1112
import org.elasticsearch.common.settings.Settings;
1213
import org.elasticsearch.common.xcontent.XContentType;
1314
import org.elasticsearch.index.IndexService;
15+
import org.elasticsearch.index.query.MatchAllQueryBuilder;
1416
import org.elasticsearch.index.query.MatchQueryBuilder;
1517
import org.elasticsearch.painless.PainlessPlugin;
1618
import org.elasticsearch.painless.action.PainlessExecuteAction.Request;
@@ -28,6 +30,7 @@
2830
import java.util.HashMap;
2931
import java.util.Map;
3032

33+
import static java.util.Collections.emptyMap;
3134
import static java.util.Collections.singletonMap;
3235
import static org.elasticsearch.painless.action.PainlessExecuteAction.TransportAction.innerShardOperation;
3336
import static org.hamcrest.Matchers.equalTo;
@@ -100,6 +103,168 @@ public void testScoreExecutionContext() throws IOException {
100103
assertThat(response.getResult(), equalTo(0.93D));
101104
}
102105

106+
public void testBooleanFieldExecutionContext() throws IOException {
107+
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
108+
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "rank", "type=long", "text", "type=text");
109+
110+
Request.ContextSetup contextSetup = new Request.ContextSetup("index",
111+
new BytesArray("{\"rank\": 4.0, \"text\": \"quick brown fox\"}"), new MatchQueryBuilder("text", "fox"));
112+
contextSetup.setXContentType(XContentType.JSON);
113+
Request request = new Request(new Script(ScriptType.INLINE, "painless",
114+
"emit(doc['rank'].value < params.max_rank)", singletonMap("max_rank", 5.0)), "boolean_field",
115+
contextSetup);
116+
Response response = innerShardOperation(request, scriptService, indexService);
117+
assertArrayEquals((boolean[])response.getResult(), new boolean[] {true});
118+
119+
contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
120+
contextSetup.setXContentType(XContentType.JSON);
121+
request = new Request(new Script(ScriptType.INLINE, "painless",
122+
"emit(false); emit(true); emit (false);", emptyMap()), "boolean_field",
123+
contextSetup);
124+
response = innerShardOperation(request, scriptService, indexService);
125+
assertArrayEquals((boolean[])response.getResult(), new boolean[] {false, false, true});
126+
}
127+
128+
public void testDateFieldExecutionContext() throws IOException {
129+
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
130+
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "test_date", "type=date");
131+
132+
Request.ContextSetup contextSetup = new Request.ContextSetup("index",
133+
new BytesArray("{\"test_date\":\"2015-01-01T12:10:30Z\"}"), new MatchAllQueryBuilder());
134+
contextSetup.setXContentType(XContentType.JSON);
135+
Request request = new Request(new Script(ScriptType.INLINE, "painless",
136+
"emit(doc['test_date'].value.toInstant().toEpochMilli())", emptyMap()), "date_field",
137+
contextSetup);
138+
Response response = innerShardOperation(request, scriptService, indexService);
139+
assertArrayEquals((long[])response.getResult(), new long[] {1420114230000L});
140+
141+
contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
142+
contextSetup.setXContentType(XContentType.JSON);
143+
request = new Request(new Script(ScriptType.INLINE, "painless",
144+
"emit(ZonedDateTime.parse(\"2021-01-01T00:00:00Z\").toInstant().toEpochMilli());\n" +
145+
"emit(ZonedDateTime.parse(\"1942-05-31T15:16:17Z\").toInstant().toEpochMilli());\n" +
146+
"emit(ZonedDateTime.parse(\"2035-10-13T10:54:19Z\").toInstant().toEpochMilli());",
147+
emptyMap()), "date_field", contextSetup);
148+
response = innerShardOperation(request, scriptService, indexService);
149+
assertArrayEquals((long[])response.getResult(), new long[] {-870597823000L, 1609459200000L, 2075885659000L});
150+
}
151+
152+
public void testDoubleFieldExecutionContext() throws IOException {
153+
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
154+
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "rank", "type=long", "text", "type=text");
155+
156+
Request.ContextSetup contextSetup = new Request.ContextSetup("index",
157+
new BytesArray("{\"rank\": 4.0, \"text\": \"quick brown fox\"}"), new MatchQueryBuilder("text", "fox"));
158+
contextSetup.setXContentType(XContentType.JSON);
159+
Request request = new Request(new Script(ScriptType.INLINE, "painless",
160+
"emit(doc['rank'].value); emit(Math.log(doc['rank'].value))", emptyMap()), "double_field",
161+
contextSetup);
162+
Response response = innerShardOperation(request, scriptService, indexService);
163+
assertArrayEquals((double[])response.getResult(), new double[] {Math.log(4.0), 4.0}, 0.00001);
164+
165+
contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
166+
contextSetup.setXContentType(XContentType.JSON);
167+
request = new Request(new Script(ScriptType.INLINE, "painless",
168+
"emit(3.1); emit(2.29); emit(-12.47); emit(-12.46); emit(Double.MAX_VALUE); emit(0.0);",
169+
emptyMap()), "double_field", contextSetup);
170+
response = innerShardOperation(request, scriptService, indexService);
171+
assertArrayEquals((double[])response.getResult(), new double[] {-12.47, -12.46, 0.0, 2.29, 3.1, Double.MAX_VALUE}, 0.00001);
172+
}
173+
174+
public void testGeoPointFieldExecutionContext() throws IOException {
175+
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
176+
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "test_point", "type=geo_point");
177+
178+
Request.ContextSetup contextSetup = new Request.ContextSetup("index",
179+
new BytesArray("{\"test_point\":\"30.0,40.0\"}"), new MatchAllQueryBuilder());
180+
contextSetup.setXContentType(XContentType.JSON);
181+
Request request = new Request(new Script(ScriptType.INLINE, "painless",
182+
"emit(doc['test_point'].value.lat + 1.0, doc['test_point'].value.lon - 1.0)", emptyMap()),
183+
"geo_point_field", contextSetup);
184+
Response response = innerShardOperation(request, scriptService, indexService);
185+
assertArrayEquals((long[])response.getResult(), new long[] {3176939252927413179L});
186+
187+
contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
188+
contextSetup.setXContentType(XContentType.JSON);
189+
request = new Request(new Script(ScriptType.INLINE, "painless",
190+
"emit(78.96, 12.12); emit(13.45, 56.78);",
191+
emptyMap()), "geo_point_field", contextSetup);
192+
response = innerShardOperation(request, scriptService, indexService);
193+
assertArrayEquals((long[])response.getResult(), new long[] {1378381707499043786L, 8091971733044486384L});
194+
}
195+
196+
public void testIpFieldExecutionContext() throws IOException {
197+
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
198+
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "test_ip", "type=ip");
199+
200+
Request.ContextSetup contextSetup = new Request.ContextSetup("index",
201+
new BytesArray("{\"test_ip\":\"192.168.1.254\"}"), new MatchAllQueryBuilder());
202+
contextSetup.setXContentType(XContentType.JSON);
203+
Request request = new Request(new Script(ScriptType.INLINE, "painless",
204+
"emit(doc['test_ip'].value);", emptyMap()),
205+
"ip_field", contextSetup);
206+
Response response = innerShardOperation(request, scriptService, indexService);
207+
assertArrayEquals((BytesRef[])response.getResult(),
208+
new BytesRef[] {new BytesRef(new byte[] {0,0,0,0,0,0,0,0,0,0,(byte)255,(byte)255,(byte)192,(byte)168,1,(byte)254})});
209+
210+
contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
211+
contextSetup.setXContentType(XContentType.JSON);
212+
request = new Request(new Script(ScriptType.INLINE, "painless",
213+
"emit(\"192.168.0.1\"); emit(\"127.0.0.1\"); emit(\"255.255.255.255\"); emit(\"0.0.0.0\");",
214+
emptyMap()), "ip_field", contextSetup);
215+
response = innerShardOperation(request, scriptService, indexService);
216+
assertArrayEquals((BytesRef[])response.getResult(), new BytesRef[] {
217+
new BytesRef(new byte[] {0,0,0,0,0,0,0,0,0,0,-1,-1,0,0,0,0}),
218+
new BytesRef(new byte[] {0,0,0,0,0,0,0,0,0,0,-1,-1,127,0,0,1}),
219+
new BytesRef(new byte[] {0,0,0,0,0,0,0,0,0,0,-1,-1,-64,-88,0,1}),
220+
new BytesRef(new byte[] {0,0,0,0,0,0,0,0,0,0,-1,-1,-1,-1,-1,-1})});
221+
}
222+
223+
public void testLongFieldExecutionContext() throws IOException {
224+
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
225+
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "test_point", "type=geo_point");
226+
227+
Request.ContextSetup contextSetup = new Request.ContextSetup("index",
228+
new BytesArray("{\"test_point\":\"30.2,40.2\"}"), new MatchAllQueryBuilder());
229+
contextSetup.setXContentType(XContentType.JSON);
230+
Request request = new Request(new Script(ScriptType.INLINE, "painless",
231+
"emit((long)doc['test_point'].value.lat); emit((long)doc['test_point'].value.lon);", emptyMap()),
232+
"long_field", contextSetup);
233+
Response response = innerShardOperation(request, scriptService, indexService);
234+
assertArrayEquals((long[])response.getResult(), new long[] {30, 40});
235+
236+
contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
237+
contextSetup.setXContentType(XContentType.JSON);
238+
request = new Request(new Script(ScriptType.INLINE, "painless",
239+
"emit(3L); emit(1L); emit(20000000000L); emit(10L); emit(-1000L); emit(0L);",
240+
emptyMap()), "long_field", contextSetup);
241+
response = innerShardOperation(request, scriptService, indexService);
242+
assertArrayEquals((long[])response.getResult(), new long[] {-1000L, 0L, 1L, 3L, 10L, 20000000000L});
243+
}
244+
245+
public void testStringFieldExecutionContext() throws IOException {
246+
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
247+
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "test_point", "type=geo_point");
248+
249+
Request.ContextSetup contextSetup = new Request.ContextSetup("index",
250+
new BytesArray("{\"test_point\":\"30.2,40.2\"}"), new MatchAllQueryBuilder());
251+
contextSetup.setXContentType(XContentType.JSON);
252+
Request request = new Request(new Script(ScriptType.INLINE, "painless",
253+
"emit(doc['test_point'].value.lat.toString().substring(0, 5)); " +
254+
"emit(doc['test_point'].value.lon.toString().substring(0, 5));", emptyMap()),
255+
"string_field", contextSetup);
256+
Response response = innerShardOperation(request, scriptService, indexService);
257+
assertArrayEquals((String[])response.getResult(), new String[] {"30.19", "40.19"});
258+
259+
contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
260+
contextSetup.setXContentType(XContentType.JSON);
261+
request = new Request(new Script(ScriptType.INLINE, "painless",
262+
"emit(\"test\"); emit(\"baz was not here\"); emit(\"Data\"); emit(\"-10\"); emit(\"20\"); emit(\"9\");",
263+
emptyMap()), "string_field", contextSetup);
264+
response = innerShardOperation(request, scriptService, indexService);
265+
assertArrayEquals((String[])response.getResult(), new String[] {"-10", "20", "9", "Data", "baz was not here", "test"});
266+
}
267+
103268
public void testContextWhitelists() throws IOException {
104269
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
105270
// score

0 commit comments

Comments
 (0)