Emit multiple fields from a runtime field script (#75108)

We have recently introduced support for grok and dissect to the runtime fields 
Painless context that allows to split a field into multiple fields. However, each runtime 
field can only emit values for a single field. This commit introduces support for emitting 
multiple fields from the same script.

The API call to define a runtime field that emits multiple fields is the following:

```
PUT localhost:9200/logs/_mappings
{
    "runtime" : {
      "log" : {
        "type" : "composite",
        "script" : "emit(grok(\"%{COMMONAPACHELOG}\").extract(doc[\"message.keyword\"].value))",
        "fields" : {
            "clientip" : {
                "type" : "ip"
            },
            "response" : {
                "type" : "long"
            }
        }
      }
    }
}
```

The script context for this new field type accepts two emit signatures:

* `emit(String, Object)`
* `emit(Map)`

Sub-fields need to be declared under fields in order to be discoverable through 
the field_caps API and accessible through the search API. 

The way that it emits multiple fields is by returning multiple MappedFieldTypes 
from RuntimeField#asMappedFieldTypes. The sub-fields are instances of the 
runtime fields that are already supported, with a little tweak to adapt the script 
defined by their parent to an artificial script factory for each of the sub-fields 
that makes its corresponding sub-field accessible. This approach allows to reuse 
all of the existing runtime fields code for the sub-fields.

The runtime section has been flat so far as it has not supported objects until now. 
That stays the same, meaning that runtime fields can have dots in their names. 
Because there are though two ways to create the same field with the introduction 
of the ability to emit multiple fields, we have to make sure that a runtime field with 
a certain name cannot be defined twice, which is why the following mappings are 
rejected with the error `Found two runtime fields with same name [log.response]`:

```
PUT localhost:9200/logs/_mappings
{
    "runtime" : {
        "log.response" : {
            "type" : "keyword"
        },
        "log" : {
            "type" : "composite",
            "script" : "emit(\"response\", grok(\"%{COMMONAPACHELOG}\").extract(doc[\"message.keyword\"].value)?.response)",
            "fields" : {
                "response" : {
                    "type" : "long"
                }
            }
        }
    }
}
```

Closes #68203
This commit is contained in:
Luca Cavanna 2021-08-10 14:07:53 +02:00 committed by GitHub
parent ad6e0dc6cb
commit 32d2f60f8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 1237 additions and 66 deletions

View file

@ -68,6 +68,7 @@ import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.rest.action.RestToXContentListener;
import org.elasticsearch.script.BooleanFieldScript; import org.elasticsearch.script.BooleanFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.DateFieldScript; import org.elasticsearch.script.DateFieldScript;
import org.elasticsearch.script.DocValuesDocReader; import org.elasticsearch.script.DocValuesDocReader;
import org.elasticsearch.script.DoubleFieldScript; import org.elasticsearch.script.DoubleFieldScript;
@ -624,12 +625,20 @@ public class PainlessExecuteAction extends ActionType<PainlessExecuteAction.Resp
return prepareRamIndex(request, (context, leafReaderContext) -> { return prepareRamIndex(request, (context, leafReaderContext) -> {
StringFieldScript.Factory factory = scriptService.compile(request.script, StringFieldScript.CONTEXT); StringFieldScript.Factory factory = scriptService.compile(request.script, StringFieldScript.CONTEXT);
StringFieldScript.LeafFactory leafFactory = StringFieldScript.LeafFactory leafFactory =
factory.newFactory(StringFieldScript.CONTEXT.name, request.getScript().getParams(), context.lookup()); factory.newFactory(StringFieldScript.CONTEXT.name, request.getScript().getParams(), context.lookup());
StringFieldScript stringFieldScript = leafFactory.newInstance(leafReaderContext); StringFieldScript stringFieldScript = leafFactory.newInstance(leafReaderContext);
List<String> keywords = new ArrayList<>(); List<String> keywords = new ArrayList<>();
stringFieldScript.runForDoc(0, keywords::add); stringFieldScript.runForDoc(0, keywords::add);
return new Response(keywords); return new Response(keywords);
}, indexService); }, indexService);
} else if (scriptContext == CompositeFieldScript.CONTEXT) {
return prepareRamIndex(request, (context, leafReaderContext) -> {
CompositeFieldScript.Factory factory = scriptService.compile(request.script, CompositeFieldScript.CONTEXT);
CompositeFieldScript.LeafFactory leafFactory =
factory.newFactory(CompositeFieldScript.CONTEXT.name, request.getScript().getParams(), context.lookup());
CompositeFieldScript compositeFieldScript = leafFactory.newInstance(leafReaderContext);
return new Response(compositeFieldScript.runForDoc(0));
}, indexService);
} else { } else {
throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]"); throw new UnsupportedOperationException("unsupported context [" + scriptContext.name + "]");
} }

View file

@ -0,0 +1,21 @@
#
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License
# 2.0 and the Server Side Public License, v 1; you may not use this file except
# in compliance with, at your election, the Elastic License 2.0 or the Server
# Side Public License, v 1.
#
# The whitelist for composite runtime fields
# These two whitelists are required for painless to find the classes
class org.elasticsearch.script.CompositeFieldScript @no_import {
}
class org.elasticsearch.script.CompositeFieldScript$Factory @no_import {
}
static_import {
# The `emit` callback to collect values for the fields
void emit(org.elasticsearch.script.CompositeFieldScript, String, Object) bound_to org.elasticsearch.script.CompositeFieldScript$EmitField
void emit(org.elasticsearch.script.CompositeFieldScript, Map) bound_to org.elasticsearch.script.CompositeFieldScript$EmitMap
}

View file

@ -288,6 +288,20 @@ public class PainlessExecuteApiTests extends ESSingleNodeTestCase {
assertEquals(Arrays.asList("test", "baz was not here", "Data", "-10", "20", "9"), response.getResult()); assertEquals(Arrays.asList("test", "baz was not here", "Data", "-10", "20", "9"), response.getResult());
} }
public void testCompositeExecutionContext() throws IOException {
ScriptService scriptService = getInstanceFromNode(ScriptService.class);
IndexService indexService = createIndex("index", Settings.EMPTY, "doc", "rank", "type=long", "text", "type=keyword");
Request.ContextSetup contextSetup = new Request.ContextSetup("index", new BytesArray("{}"), new MatchAllQueryBuilder());
contextSetup.setXContentType(XContentType.JSON);
Request request = new Request(new Script(ScriptType.INLINE, "painless",
"emit(\"foo\", \"bar\"); emit(\"foo2\", 2);", emptyMap()), "composite_field", contextSetup);
Response response = innerShardOperation(request, scriptService, indexService);
assertEquals(Map.of(
"composite_field.foo", List.of("bar"),
"composite_field.foo2", List.of(2)), response.getResult());
}
public void testContextWhitelists() throws IOException { public void testContextWhitelists() throws IOException {
ScriptService scriptService = getInstanceFromNode(ScriptService.class); ScriptService scriptService = getInstanceFromNode(ScriptService.class);
// score // score

View file

@ -0,0 +1,104 @@
---
setup:
- do:
indices.create:
index: http_logs
body:
settings:
number_of_shards: 1
number_of_replicas: 0
mappings:
runtime:
http:
type: composite
script:
source: |
emit(grok('%{COMMONAPACHELOG}').extract(doc["message"].value));
fields:
clientip:
type: ip
verb:
type: keyword
response:
type: long
properties:
timestamp:
type: date
message:
type: keyword
- do:
bulk:
index: http_logs
refresh: true
body: |
{"index":{}}
{"timestamp": "1998-04-30T14:30:17-05:00", "message" : "40.135.0.0 - - [30/Apr/1998:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp": "1998-04-30T14:30:53-05:00", "message" : "232.0.0.0 - - [30/Apr/1998:14:30:53 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp": "1998-04-30T14:31:12-05:00", "message" : "26.1.0.0 - - [30/Apr/1998:14:31:12 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp": "1998-04-30T14:31:19-05:00", "message" : "247.37.0.0 - - [30/Apr/1998:14:31:19 -0500] \"GET /french/splash_inet.html HTTP/1.0\" 200 3781"}
{"index":{}}
{"timestamp": "1998-04-30T14:31:22-05:00", "message" : "247.37.0.0 - - [30/Apr/1998:14:31:22 -0500] \"GET /images/hm_nbg.jpg HTTP/1.0\" 304 0"}
{"index":{}}
{"timestamp": "1998-04-30T14:31:27-05:00", "message" : "252.0.0.0 - - [30/Apr/1998:14:31:27 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp": "1998-04-30T14:31:28-05:00", "message" : "not a valid apache log"}
---
fetch:
- do:
search:
index: http_logs
body:
sort: timestamp
fields:
- http.clientip
- http.verb
- http.response
- match: {hits.total.value: 7}
- match: {hits.hits.0.fields.http\.clientip: [40.135.0.0] }
- match: {hits.hits.0.fields.http\.verb: [GET] }
- match: {hits.hits.0.fields.http\.response: [200] }
- is_false: hits.hits.6.fields.http\.clientip
- is_false: hits.hits.6.fields.http\.verb
- is_false: hits.hits.6.fields.http\.response
---
query:
- do:
search:
index: http_logs
body:
query:
term:
http.verb: GET
- match: { hits.total.value: 6 }
- do:
search:
index: http_logs
body:
query:
range:
http.clientip:
from: 232.0.0.0
to: 253.0.0.0
- match: { hits.total.value: 4 }
---
"terms agg":
- do:
search:
index: http_logs
body:
aggs:
response:
terms:
field: http.response
- match: {hits.total.value: 7}
- match: {aggregations.response.buckets.0.key: 200 }
- match: {aggregations.response.buckets.0.doc_count: 5 }
- match: {aggregations.response.buckets.1.key: 304 }
- match: {aggregations.response.buckets.1.doc_count: 1 }

View file

@ -18,9 +18,9 @@ import org.elasticsearch.common.geo.ShapeRelation;
import org.elasticsearch.common.time.DateMathParser; import org.elasticsearch.common.time.DateMathParser;
import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext; import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
import java.time.ZoneId; import java.time.ZoneId;
@ -194,31 +194,49 @@ abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldType {
abstract static class Builder<Factory> extends RuntimeField.Builder { abstract static class Builder<Factory> extends RuntimeField.Builder {
private final ScriptContext<Factory> scriptContext; private final ScriptContext<Factory> scriptContext;
private final Factory parseFromSourceFactory;
final FieldMapper.Parameter<Script> script = new FieldMapper.Parameter<>( final FieldMapper.Parameter<Script> script = new FieldMapper.Parameter<>(
"script", "script",
true, true,
() -> null, () -> null,
Builder::parseScript, RuntimeField::parseScript,
initializerNotSupported() RuntimeField.initializerNotSupported()
).setSerializerCheck((id, ic, v) -> ic); ).setSerializerCheck((id, ic, v) -> ic);
Builder(String name, ScriptContext<Factory> scriptContext, Factory parseFromSourceFactory) { Builder(String name, ScriptContext<Factory> scriptContext) {
super(name); super(name);
this.scriptContext = scriptContext; this.scriptContext = scriptContext;
this.parseFromSourceFactory = parseFromSourceFactory;
} }
abstract Factory getParseFromSourceFactory();
abstract Factory getCompositeLeafFactory(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory);
@Override @Override
protected final RuntimeField createRuntimeField(MappingParserContext parserContext) { protected final RuntimeField createRuntimeField(MappingParserContext parserContext) {
if (script.get() == null) { if (script.get() == null) {
return createRuntimeField(parseFromSourceFactory); return createRuntimeField(getParseFromSourceFactory());
} }
Factory factory = parserContext.scriptCompiler().compile(script.getValue(), scriptContext); Factory factory = parserContext.scriptCompiler().compile(script.getValue(), scriptContext);
return createRuntimeField(factory); return createRuntimeField(factory);
} }
@Override
protected final RuntimeField createChildRuntimeField(MappingParserContext parserContext,
String parent,
Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
if (script.isConfigured()) {
throw new IllegalArgumentException("Cannot use [script] parameter on sub-field [" + name +
"] of composite field [" + parent + "]");
}
String fullName = parent + "." + name;
return new LeafRuntimeField(
name,
createFieldType(fullName, getCompositeLeafFactory(parentScriptFactory), getScript(), meta()),
getParameters()
);
}
final RuntimeField createRuntimeField(Factory scriptFactory) { final RuntimeField createRuntimeField(Factory scriptFactory) {
AbstractScriptFieldType<?> fieldType = createFieldType(name, scriptFactory, getScript(), meta()); AbstractScriptFieldType<?> fieldType = createFieldType(name, scriptFactory, getScript(), meta());
return new LeafRuntimeField(name, fieldType, getParameters()); return new LeafRuntimeField(name, fieldType, getParameters());
@ -239,17 +257,5 @@ abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldType {
} }
return script.get(); return script.get();
} }
private static Script parseScript(String name, MappingParserContext parserContext, Object scriptObject) {
Script script = Script.parse(scriptObject);
if (script.getType() == ScriptType.STORED) {
throw new IllegalArgumentException("stored scripts are not supported for runtime field [" + name + "]");
}
return script;
}
}
static <T> Function<FieldMapper, T> initializerNotSupported() {
return mapper -> { throw new UnsupportedOperationException(); };
} }
} }

View file

@ -18,6 +18,7 @@ import org.elasticsearch.core.Booleans;
import org.elasticsearch.index.fielddata.BooleanScriptFieldData; import org.elasticsearch.index.fielddata.BooleanScriptFieldData;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.BooleanFieldScript; import org.elasticsearch.script.BooleanFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
@ -27,6 +28,7 @@ import org.elasticsearch.search.runtime.BooleanScriptFieldTermQuery;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class BooleanScriptFieldType extends AbstractScriptFieldType<BooleanFieldScript.LeafFactory> { public final class BooleanScriptFieldType extends AbstractScriptFieldType<BooleanFieldScript.LeafFactory> {
@ -35,7 +37,7 @@ public final class BooleanScriptFieldType extends AbstractScriptFieldType<Boolea
private static class Builder extends AbstractScriptFieldType.Builder<BooleanFieldScript.Factory> { private static class Builder extends AbstractScriptFieldType.Builder<BooleanFieldScript.Factory> {
Builder(String name) { Builder(String name) {
super(name, BooleanFieldScript.CONTEXT, BooleanFieldScript.PARSE_FROM_SOURCE); super(name, BooleanFieldScript.CONTEXT);
} }
@Override @Override
@ -45,6 +47,17 @@ public final class BooleanScriptFieldType extends AbstractScriptFieldType<Boolea
Map<String, String> meta) { Map<String, String> meta) {
return new BooleanScriptFieldType(name, factory, script, meta); return new BooleanScriptFieldType(name, factory, script, meta);
} }
@Override
BooleanFieldScript.Factory getParseFromSourceFactory() {
return BooleanFieldScript.PARSE_FROM_SOURCE;
}
@Override
BooleanFieldScript.Factory getCompositeLeafFactory(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
return BooleanFieldScript.leafAdapter(parentScriptFactory);
}
} }
public static RuntimeField sourceOnly(String name) { public static RuntimeField sourceOnly(String name) {

View file

@ -0,0 +1,134 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.index.mapper;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* A runtime field of type object. Defines a script at the top level, which emits multiple sub-fields.
* The sub-fields are declared within the object in order to be made available to the field_caps and search API.
*/
public class CompositeRuntimeField implements RuntimeField {
public static final String CONTENT_TYPE = "composite";
public static final Parser PARSER = new Parser(name ->
new RuntimeField.Builder(name) {
private final FieldMapper.Parameter<Script> script = new FieldMapper.Parameter<>(
"script",
false,
() -> null,
RuntimeField::parseScript,
RuntimeField.initializerNotSupported()
).setValidator(s -> {
if (s == null) {
throw new IllegalArgumentException("composite runtime field [" + name + "] must declare a [script]");
}
});
private final FieldMapper.Parameter<Map<String, Object>> fields = new FieldMapper.Parameter<Map<String, Object>>(
"fields",
false,
Collections::emptyMap,
(f, p, o) -> parseFields(f, o),
RuntimeField.initializerNotSupported()
).setValidator(objectMap -> {
if (objectMap == null || objectMap.isEmpty()) {
throw new IllegalArgumentException("composite runtime field [" + name + "] must declare its [fields]");
}
});
@Override
protected List<FieldMapper.Parameter<?>> getParameters() {
List<FieldMapper.Parameter<?>> parameters = new ArrayList<>(super.getParameters());
parameters.add(script);
parameters.add(fields);
return Collections.unmodifiableList(parameters);
}
@Override
protected RuntimeField createChildRuntimeField(MappingParserContext parserContext,
String parent,
Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
throw new IllegalArgumentException("Composite field [" + name + "] cannot be a child of composite field [" + parent + "]");
}
@Override
protected RuntimeField createRuntimeField(MappingParserContext parserContext) {
CompositeFieldScript.Factory factory = parserContext.scriptCompiler().compile(script.get(), CompositeFieldScript.CONTEXT);
Function<RuntimeField.Builder, RuntimeField> builder = b -> b.createChildRuntimeField(
parserContext,
name,
lookup -> factory.newFactory(name, script.get().getParams(), lookup)
);
Map<String, RuntimeField> runtimeFields
= RuntimeField.parseRuntimeFields(fields.getValue(), parserContext, builder, false);
return new CompositeRuntimeField(name, getParameters(), runtimeFields.values());
}
});
private final String name;
private final List<FieldMapper.Parameter<?>> parameters;
private final Collection<RuntimeField> subfields;
CompositeRuntimeField(String name, List<FieldMapper.Parameter<?>> parameters, Collection<RuntimeField> subfields) {
this.name = name;
this.parameters = parameters;
this.subfields = subfields;
}
@Override
public String name() {
return name;
}
@Override
public Stream<MappedFieldType> asMappedFieldTypes() {
return subfields.stream().flatMap(RuntimeField::asMappedFieldTypes);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(name);
builder.field("type", "composite");
boolean includeDefaults = params.paramAsBoolean("include_defaults", false);
for (FieldMapper.Parameter<?> parameter : parameters) {
parameter.toXContent(builder, includeDefaults);
}
builder.startObject("fields");
for (RuntimeField subfield : subfields) {
subfield.toXContent(builder, params);
}
builder.endObject();
builder.endObject();
return builder;
}
private static Map<String, Object> parseFields(String name, Object fieldsObject) {
if (fieldsObject instanceof Map == false) {
throw new MapperParsingException("[fields] must be an object, got " + fieldsObject.getClass().getSimpleName() +
"[" + fieldsObject + "] for field [" + name +"]");
}
@SuppressWarnings("unchecked")
Map<String, Object> fields = (Map<String, Object>) fieldsObject;
return fields;
}
}

View file

@ -23,6 +23,7 @@ import org.elasticsearch.index.mapper.DateFieldMapper.DateFieldType;
import org.elasticsearch.index.mapper.DateFieldMapper.Resolution; import org.elasticsearch.index.mapper.DateFieldMapper.Resolution;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.DateFieldScript; import org.elasticsearch.script.DateFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
@ -40,6 +41,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript.LeafFactory> { public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript.LeafFactory> {
@ -50,7 +52,7 @@ public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript
private final FieldMapper.Parameter<String> format = FieldMapper.Parameter.stringParam( private final FieldMapper.Parameter<String> format = FieldMapper.Parameter.stringParam(
"format", "format",
true, true,
initializerNotSupported(), RuntimeField.initializerNotSupported(),
null null
).setSerializer((b, n, v) -> { ).setSerializer((b, n, v) -> {
if (v != null && false == v.equals(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.pattern())) { if (v != null && false == v.equals(DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.pattern())) {
@ -63,7 +65,7 @@ public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript
true, true,
() -> null, () -> null,
(n, c, o) -> o == null ? null : LocaleUtils.parse(o.toString()), (n, c, o) -> o == null ? null : LocaleUtils.parse(o.toString()),
initializerNotSupported() RuntimeField.initializerNotSupported()
).setSerializer((b, n, v) -> { ).setSerializer((b, n, v) -> {
if (v != null && false == v.equals(Locale.ROOT)) { if (v != null && false == v.equals(Locale.ROOT)) {
b.field(n, v.toString()); b.field(n, v.toString());
@ -71,7 +73,7 @@ public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript
}, Object::toString).acceptsNull(); }, Object::toString).acceptsNull();
Builder(String name) { Builder(String name) {
super(name, DateFieldScript.CONTEXT, DateFieldScript.PARSE_FROM_SOURCE); super(name, DateFieldScript.CONTEXT);
} }
@Override @Override
@ -89,6 +91,16 @@ public class DateScriptFieldType extends AbstractScriptFieldType<DateFieldScript
DateFormatter dateTimeFormatter = DateFormatter.forPattern(pattern).withLocale(locale); DateFormatter dateTimeFormatter = DateFormatter.forPattern(pattern).withLocale(locale);
return new DateScriptFieldType(name, factory, dateTimeFormatter, script, meta); return new DateScriptFieldType(name, factory, dateTimeFormatter, script, meta);
} }
@Override
DateFieldScript.Factory getParseFromSourceFactory() {
return DateFieldScript.PARSE_FROM_SOURCE;
}
@Override
DateFieldScript.Factory getCompositeLeafFactory(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
return DateFieldScript.leafAdapter(parentScriptFactory);
}
} }
public static RuntimeField sourceOnly(String name, DateFormatter dateTimeFormatter) { public static RuntimeField sourceOnly(String name, DateFormatter dateTimeFormatter) {

View file

@ -18,6 +18,7 @@ import org.elasticsearch.index.fielddata.DoubleScriptFieldData;
import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType; import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.DoubleFieldScript; import org.elasticsearch.script.DoubleFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
@ -29,6 +30,7 @@ import org.elasticsearch.search.runtime.DoubleScriptFieldTermsQuery;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class DoubleScriptFieldType extends AbstractScriptFieldType<DoubleFieldScript.LeafFactory> { public final class DoubleScriptFieldType extends AbstractScriptFieldType<DoubleFieldScript.LeafFactory> {
@ -37,7 +39,7 @@ public final class DoubleScriptFieldType extends AbstractScriptFieldType<DoubleF
private static class Builder extends AbstractScriptFieldType.Builder<DoubleFieldScript.Factory> { private static class Builder extends AbstractScriptFieldType.Builder<DoubleFieldScript.Factory> {
Builder(String name) { Builder(String name) {
super(name, DoubleFieldScript.CONTEXT, DoubleFieldScript.PARSE_FROM_SOURCE); super(name, DoubleFieldScript.CONTEXT);
} }
@Override @Override
@ -47,6 +49,16 @@ public final class DoubleScriptFieldType extends AbstractScriptFieldType<DoubleF
Map<String, String> meta) { Map<String, String> meta) {
return new DoubleScriptFieldType(name, factory, script, meta); return new DoubleScriptFieldType(name, factory, script, meta);
} }
@Override
DoubleFieldScript.Factory getParseFromSourceFactory() {
return DoubleFieldScript.PARSE_FROM_SOURCE;
}
@Override
DoubleFieldScript.Factory getCompositeLeafFactory(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
return DoubleFieldScript.leafAdapter(parentScriptFactory);
}
} }
public static RuntimeField sourceOnly(String name) { public static RuntimeField sourceOnly(String name) {

View file

@ -196,8 +196,8 @@ final class DynamicFieldsBuilder {
if (parser == null) { if (parser == null) {
throw new MapperParsingException("failed to find type parsed [" + mappingType + "] for [" + fullName + "]"); throw new MapperParsingException("failed to find type parsed [" + mappingType + "] for [" + fullName + "]");
} }
RuntimeField runtimeField = parser.parse(fullName, mapping, parserContext); RuntimeField.Builder builder = parser.parse(fullName, mapping, parserContext);
Runtime.createDynamicField(runtimeField, context); Runtime.createDynamicField(builder.createRuntimeField(parserContext), context);
} else { } else {
Mapper.Builder builder = parseDynamicTemplateMapping(name, mappingType, mapping, dateFormatter, context); Mapper.Builder builder = parseDynamicTemplateMapping(name, mappingType, mapping, dateFormatter, context);
CONCRETE.createDynamicField(builder, context); CONCRETE.createDynamicField(builder, context);

View file

@ -740,7 +740,7 @@ public abstract class FieldMapper extends Mapper implements Cloneable {
return this; return this;
} }
private void validate() { void validate() {
if (validator != null) { if (validator != null) {
validator.accept(getValue()); validator.accept(getValue());
} }

View file

@ -22,6 +22,7 @@ import org.elasticsearch.geometry.Geometry;
import org.elasticsearch.index.fielddata.GeoPointScriptFieldData; import org.elasticsearch.index.fielddata.GeoPointScriptFieldData;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.GeoPointFieldScript; import org.elasticsearch.script.GeoPointFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.search.runtime.GeoPointScriptFieldDistanceFeatureQuery; import org.elasticsearch.search.runtime.GeoPointScriptFieldDistanceFeatureQuery;
@ -31,12 +32,13 @@ import org.elasticsearch.search.runtime.GeoPointScriptFieldGeoShapeQuery;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class GeoPointScriptFieldType extends AbstractScriptFieldType<GeoPointFieldScript.LeafFactory> implements GeoShapeQueryable { public final class GeoPointScriptFieldType extends AbstractScriptFieldType<GeoPointFieldScript.LeafFactory> implements GeoShapeQueryable {
public static final RuntimeField.Parser PARSER = new RuntimeField.Parser(name -> public static final RuntimeField.Parser PARSER = new RuntimeField.Parser(name ->
new Builder<>(name, GeoPointFieldScript.CONTEXT, GeoPointFieldScript.PARSE_FROM_SOURCE) { new Builder<>(name, GeoPointFieldScript.CONTEXT) {
@Override @Override
AbstractScriptFieldType<?> createFieldType(String name, AbstractScriptFieldType<?> createFieldType(String name,
GeoPointFieldScript.Factory factory, GeoPointFieldScript.Factory factory,
@ -44,6 +46,17 @@ public final class GeoPointScriptFieldType extends AbstractScriptFieldType<GeoPo
Map<String, String> meta) { Map<String, String> meta) {
return new GeoPointScriptFieldType(name, factory, getScript(), meta()); return new GeoPointScriptFieldType(name, factory, getScript(), meta());
} }
@Override
GeoPointFieldScript.Factory getParseFromSourceFactory() {
return GeoPointFieldScript.PARSE_FROM_SOURCE;
}
@Override
GeoPointFieldScript.Factory getCompositeLeafFactory(
Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
return GeoPointFieldScript.leafAdapter(parentScriptFactory);
}
}); });
GeoPointScriptFieldType( GeoPointScriptFieldType(

View file

@ -22,6 +22,7 @@ import org.elasticsearch.core.Tuple;
import org.elasticsearch.index.fielddata.IpScriptFieldData; import org.elasticsearch.index.fielddata.IpScriptFieldData;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.IpFieldScript; import org.elasticsearch.script.IpFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
@ -37,12 +38,13 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class IpScriptFieldType extends AbstractScriptFieldType<IpFieldScript.LeafFactory> { public final class IpScriptFieldType extends AbstractScriptFieldType<IpFieldScript.LeafFactory> {
public static final RuntimeField.Parser PARSER = new RuntimeField.Parser(name -> public static final RuntimeField.Parser PARSER = new RuntimeField.Parser(name ->
new Builder<>(name, IpFieldScript.CONTEXT, IpFieldScript.PARSE_FROM_SOURCE) { new Builder<>(name, IpFieldScript.CONTEXT) {
@Override @Override
AbstractScriptFieldType<?> createFieldType(String name, AbstractScriptFieldType<?> createFieldType(String name,
IpFieldScript.Factory factory, IpFieldScript.Factory factory,
@ -50,6 +52,16 @@ public final class IpScriptFieldType extends AbstractScriptFieldType<IpFieldScri
Map<String, String> meta) { Map<String, String> meta) {
return new IpScriptFieldType(name, factory, getScript(), meta()); return new IpScriptFieldType(name, factory, getScript(), meta());
} }
@Override
IpFieldScript.Factory getParseFromSourceFactory() {
return IpFieldScript.PARSE_FROM_SOURCE;
}
@Override
IpFieldScript.Factory getCompositeLeafFactory(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
return IpFieldScript.leafAdapter(parentScriptFactory);
}
}); });
IpScriptFieldType( IpScriptFieldType(

View file

@ -16,6 +16,7 @@ import org.elasticsearch.common.time.DateMathParser;
import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.fielddata.StringScriptFieldData; import org.elasticsearch.index.fielddata.StringScriptFieldData;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.script.StringFieldScript; import org.elasticsearch.script.StringFieldScript;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
@ -33,6 +34,7 @@ import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toSet;
@ -43,7 +45,7 @@ public final class KeywordScriptFieldType extends AbstractScriptFieldType<String
private static class Builder extends AbstractScriptFieldType.Builder<StringFieldScript.Factory> { private static class Builder extends AbstractScriptFieldType.Builder<StringFieldScript.Factory> {
Builder(String name) { Builder(String name) {
super(name, StringFieldScript.CONTEXT, StringFieldScript.PARSE_FROM_SOURCE); super(name, StringFieldScript.CONTEXT);
} }
@Override @Override
@ -53,6 +55,16 @@ public final class KeywordScriptFieldType extends AbstractScriptFieldType<String
Map<String, String> meta) { Map<String, String> meta) {
return new KeywordScriptFieldType(name, factory, script, meta); return new KeywordScriptFieldType(name, factory, script, meta);
} }
@Override
StringFieldScript.Factory getParseFromSourceFactory() {
return StringFieldScript.PARSE_FROM_SOURCE;
}
@Override
StringFieldScript.Factory getCompositeLeafFactory(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
return StringFieldScript.leafAdapter(parentScriptFactory);
}
} }
public static RuntimeField sourceOnly(String name) { public static RuntimeField sourceOnly(String name) {

View file

@ -11,13 +11,12 @@ package org.elasticsearch.index.mapper;
import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
/** /**
* RuntimeField base class for leaf fields that will only ever return * RuntimeField base class for leaf fields that will only ever return a single {@link MappedFieldType}
* a single MappedFieldType from {@link RuntimeField#asMappedFieldTypes()} * from {@link RuntimeField#asMappedFieldTypes()}. Can be a standalone runtime field, or part of a composite.
*/ */
public final class LeafRuntimeField implements RuntimeField { public final class LeafRuntimeField implements RuntimeField {
private final String name; private final String name;
@ -28,17 +27,17 @@ public final class LeafRuntimeField implements RuntimeField {
this.name = name; this.name = name;
this.mappedFieldType = mappedFieldType; this.mappedFieldType = mappedFieldType;
this.parameters = parameters; this.parameters = parameters;
assert name.equals(mappedFieldType.name()); assert mappedFieldType.name().endsWith(name) : "full name: " + mappedFieldType.name() + " - leaf name: " + name;
} }
@Override @Override
public String name() { public String name() {
return name; return mappedFieldType.name();
} }
@Override @Override
public Collection<MappedFieldType> asMappedFieldTypes() { public Stream<MappedFieldType> asMappedFieldTypes() {
return Collections.singleton(mappedFieldType); return Stream.of(mappedFieldType);
} }
@Override @Override

View file

@ -18,6 +18,7 @@ import org.elasticsearch.index.fielddata.LongScriptFieldData;
import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType; import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.script.LongFieldScript; import org.elasticsearch.script.LongFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script; import org.elasticsearch.script.Script;
import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
@ -29,6 +30,7 @@ import org.elasticsearch.search.runtime.LongScriptFieldTermsQuery;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class LongScriptFieldType extends AbstractScriptFieldType<LongFieldScript.LeafFactory> { public final class LongScriptFieldType extends AbstractScriptFieldType<LongFieldScript.LeafFactory> {
@ -37,13 +39,23 @@ public final class LongScriptFieldType extends AbstractScriptFieldType<LongField
private static class Builder extends AbstractScriptFieldType.Builder<LongFieldScript.Factory> { private static class Builder extends AbstractScriptFieldType.Builder<LongFieldScript.Factory> {
Builder(String name) { Builder(String name) {
super(name, LongFieldScript.CONTEXT, LongFieldScript.PARSE_FROM_SOURCE); super(name, LongFieldScript.CONTEXT);
} }
@Override @Override
AbstractScriptFieldType<?> createFieldType(String name, LongFieldScript.Factory factory, Script script, Map<String, String> meta) { AbstractScriptFieldType<?> createFieldType(String name, LongFieldScript.Factory factory, Script script, Map<String, String> meta) {
return new LongScriptFieldType(name, factory, script, meta); return new LongScriptFieldType(name, factory, script, meta);
} }
@Override
LongFieldScript.Factory getParseFromSourceFactory() {
return LongFieldScript.PARSE_FROM_SOURCE;
}
@Override
LongFieldScript.Factory getCompositeLeafFactory(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory) {
return LongFieldScript.leafAdapter(parentScriptFactory);
}
} }
public static RuntimeField sourceOnly(String name) { public static RuntimeField sourceOnly(String name) {

View file

@ -161,9 +161,7 @@ public final class MappingLookup {
this.shadowedFields = new HashSet<>(); this.shadowedFields = new HashSet<>();
for (RuntimeField runtimeField : mapping.getRoot().runtimeFields()) { for (RuntimeField runtimeField : mapping.getRoot().runtimeFields()) {
for (MappedFieldType mft : runtimeField.asMappedFieldTypes()) { runtimeField.asMappedFieldTypes().forEach(mft -> shadowedFields.add(mft.name()));
shadowedFields.add(mft.name());
}
} }
this.fieldTypeLookup = new FieldTypeLookup(mappers, aliasMappers, mapping.getRoot().runtimeFields()); this.fieldTypeLookup = new FieldTypeLookup(mappers, aliasMappers, mapping.getRoot().runtimeFields());

View file

@ -214,7 +214,12 @@ public class RootObjectMapper extends ObjectMapper {
return true; return true;
} else if (fieldName.equals("runtime")) { } else if (fieldName.equals("runtime")) {
if (fieldNode instanceof Map) { if (fieldNode instanceof Map) {
builder.setRuntime(RuntimeField.parseRuntimeFields((Map<String, Object>) fieldNode, parserContext, true)); Map<String, RuntimeField> fields = RuntimeField.parseRuntimeFields(
(Map<String, Object>) fieldNode,
parserContext,
true
);
builder.setRuntime(fields);
return true; return true;
} else { } else {
throw new ElasticsearchParseException("runtime must be a map type"); throw new ElasticsearchParseException("runtime must be a map type");

View file

@ -10,6 +10,10 @@ package org.elasticsearch.index.mapper;
import org.elasticsearch.common.xcontent.ToXContentFragment; import org.elasticsearch.common.xcontent.ToXContentFragment;
import org.elasticsearch.index.mapper.FieldMapper.Parameter; import org.elasticsearch.index.mapper.FieldMapper.Parameter;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.lookup.SearchLookup;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -19,6 +23,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* Definition of a runtime field that can be defined as part of the runtime section of the index mappings * Definition of a runtime field that can be defined as part of the runtime section of the index mappings
@ -35,7 +40,7 @@ public interface RuntimeField extends ToXContentFragment {
* Exposes the {@link MappedFieldType}s backing this runtime field, used to execute queries, run aggs etc. * Exposes the {@link MappedFieldType}s backing this runtime field, used to execute queries, run aggs etc.
* @return the {@link MappedFieldType}s backing this runtime field * @return the {@link MappedFieldType}s backing this runtime field
*/ */
Collection<MappedFieldType> asMappedFieldTypes(); Stream<MappedFieldType> asMappedFieldTypes();
abstract class Builder { abstract class Builder {
final String name; final String name;
@ -55,6 +60,12 @@ public interface RuntimeField extends ToXContentFragment {
protected abstract RuntimeField createRuntimeField(MappingParserContext parserContext); protected abstract RuntimeField createRuntimeField(MappingParserContext parserContext);
protected abstract RuntimeField createChildRuntimeField(
MappingParserContext parserContext,
String parentName,
Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory
);
public final void parse(String name, MappingParserContext parserContext, Map<String, Object> fieldNode) { public final void parse(String name, MappingParserContext parserContext, Map<String, Object> fieldNode) {
Map<String, Parameter<?>> paramsMap = new HashMap<>(); Map<String, Parameter<?>> paramsMap = new HashMap<>();
for (Parameter<?> param : getParameters()) { for (Parameter<?> param : getParameters()) {
@ -78,6 +89,9 @@ public interface RuntimeField extends ToXContentFragment {
parameter.parse(name, parserContext, propNode); parameter.parse(name, parserContext, propNode);
iterator.remove(); iterator.remove();
} }
for (Parameter<?> parameter : getParameters()) {
parameter.validate();
}
} }
} }
@ -92,12 +106,14 @@ public interface RuntimeField extends ToXContentFragment {
this.builderFunction = builderFunction; this.builderFunction = builderFunction;
} }
RuntimeField parse(String name, Map<String, Object> node, MappingParserContext parserContext) RuntimeField.Builder parse(String name,
Map<String, Object> node,
MappingParserContext parserContext)
throws MapperParsingException { throws MapperParsingException {
RuntimeField.Builder builder = builderFunction.apply(name); RuntimeField.Builder builder = builderFunction.apply(name);
builder.parse(name, parserContext, node); builder.parse(name, parserContext, node);
return builder.createRuntimeField(parserContext); return builder;
} }
} }
@ -112,6 +128,27 @@ public interface RuntimeField extends ToXContentFragment {
static Map<String, RuntimeField> parseRuntimeFields(Map<String, Object> node, static Map<String, RuntimeField> parseRuntimeFields(Map<String, Object> node,
MappingParserContext parserContext, MappingParserContext parserContext,
boolean supportsRemoval) { boolean supportsRemoval) {
return parseRuntimeFields(node, parserContext, b -> b.createRuntimeField(parserContext), supportsRemoval);
}
/**
* Parse runtime fields from the provided map, using the provided parser context.
*
* This method also allows you to define how the runtime field will be created from its
* builder, so that it can be used by composite fields to build child fields using
* parent factory parameters.
*
* @param node the map that holds the runtime fields configuration
* @param parserContext the parser context that holds info needed when parsing mappings
* @param builder a function to convert a RuntimeField.Builder into a RuntimeField
* @param supportsRemoval whether a null value for a runtime field should be properly parsed and
* translated to the removal of such runtime field
* @return the parsed runtime fields
*/
static Map<String, RuntimeField> parseRuntimeFields(Map<String, Object> node,
MappingParserContext parserContext,
Function<RuntimeField.Builder, RuntimeField> builder,
boolean supportsRemoval) {
Map<String, RuntimeField> runtimeFields = new HashMap<>(); Map<String, RuntimeField> runtimeFields = new HashMap<>();
Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
@ -139,7 +176,7 @@ public interface RuntimeField extends ToXContentFragment {
throw new MapperParsingException("No handler for type [" + type + throw new MapperParsingException("No handler for type [" + type +
"] declared on runtime field [" + fieldName + "]"); "] declared on runtime field [" + fieldName + "]");
} }
runtimeFields.put(fieldName, typeParser.parse(fieldName, propNode, parserContext)); runtimeFields.put(fieldName, builder.apply(typeParser.parse(fieldName, propNode, parserContext)));
propNode.remove("type"); propNode.remove("type");
MappingParser.checkNoRemainingFields(fieldName, propNode); MappingParser.checkNoRemainingFields(fieldName, propNode);
iterator.remove(); iterator.remove();
@ -161,7 +198,7 @@ public interface RuntimeField extends ToXContentFragment {
static Map<String, MappedFieldType> collectFieldTypes(Collection<RuntimeField> runtimeFields) { static Map<String, MappedFieldType> collectFieldTypes(Collection<RuntimeField> runtimeFields) {
return runtimeFields.stream() return runtimeFields.stream()
.flatMap(runtimeField -> { .flatMap(runtimeField -> {
List<String> names = runtimeField.asMappedFieldTypes().stream().map(MappedFieldType::name) List<String> names = runtimeField.asMappedFieldTypes().map(MappedFieldType::name)
.filter(name -> name.equals(runtimeField.name()) == false .filter(name -> name.equals(runtimeField.name()) == false
&& (name.startsWith(runtimeField.name() + ".") == false && (name.startsWith(runtimeField.name() + ".") == false
|| name.length() > runtimeField.name().length() + 1 == false)) || name.length() > runtimeField.name().length() + 1 == false))
@ -170,11 +207,23 @@ public interface RuntimeField extends ToXContentFragment {
throw new IllegalStateException("Found sub-fields with name not belonging to the parent field they are part of " throw new IllegalStateException("Found sub-fields with name not belonging to the parent field they are part of "
+ names); + names);
} }
return runtimeField.asMappedFieldTypes().stream(); return runtimeField.asMappedFieldTypes();
}) })
.collect(Collectors.toUnmodifiableMap(MappedFieldType::name, mappedFieldType -> mappedFieldType, .collect(Collectors.toUnmodifiableMap(MappedFieldType::name, mappedFieldType -> mappedFieldType,
(t, t2) -> { (t, t2) -> {
throw new IllegalArgumentException("Found two runtime fields with same name [" + t.name() + "]"); throw new IllegalArgumentException("Found two runtime fields with same name [" + t.name() + "]");
})); }));
} }
static <T> Function<FieldMapper, T> initializerNotSupported() {
return mapper -> { throw new UnsupportedOperationException(); };
}
static Script parseScript(String name, MappingParserContext parserContext, Object scriptObject) {
Script script = Script.parse(scriptObject);
if (script.getType() == ScriptType.STORED) {
throw new IllegalArgumentException("stored scripts are not supported for runtime field [" + name + "]");
}
return script;
}
} }

View file

@ -635,8 +635,12 @@ public class SearchExecutionContext extends QueryRewriteContext {
if (runtimeMappings.isEmpty()) { if (runtimeMappings.isEmpty()) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
Map<String, RuntimeField> runtimeFields = RuntimeField.parseRuntimeFields(new HashMap<>(runtimeMappings), //TODO add specific tests to SearchExecutionTests similar to the ones in FieldTypeLookupTests
mapperService.parserContext(), false); MappingParserContext parserContext = mapperService.parserContext();
Map<String, RuntimeField> runtimeFields = RuntimeField.parseRuntimeFields(
new HashMap<>(runtimeMappings),
parserContext,
false);
return RuntimeField.collectFieldTypes(runtimeFields.values()); return RuntimeField.collectFieldTypes(runtimeFields.values());
} }

View file

@ -20,17 +20,24 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.index.mapper.BinaryFieldMapper; import org.elasticsearch.index.mapper.BinaryFieldMapper;
import org.elasticsearch.index.mapper.BooleanFieldMapper; import org.elasticsearch.index.mapper.BooleanFieldMapper;
import org.elasticsearch.index.mapper.BooleanScriptFieldType;
import org.elasticsearch.index.mapper.CompletionFieldMapper; import org.elasticsearch.index.mapper.CompletionFieldMapper;
import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.index.mapper.DateFieldMapper;
import org.elasticsearch.index.mapper.DateScriptFieldType;
import org.elasticsearch.index.mapper.DocCountFieldMapper; import org.elasticsearch.index.mapper.DocCountFieldMapper;
import org.elasticsearch.index.mapper.DoubleScriptFieldType;
import org.elasticsearch.index.mapper.FieldAliasMapper; import org.elasticsearch.index.mapper.FieldAliasMapper;
import org.elasticsearch.index.mapper.FieldNamesFieldMapper; import org.elasticsearch.index.mapper.FieldNamesFieldMapper;
import org.elasticsearch.index.mapper.GeoPointFieldMapper; import org.elasticsearch.index.mapper.GeoPointFieldMapper;
import org.elasticsearch.index.mapper.GeoPointScriptFieldType;
import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.IdFieldMapper;
import org.elasticsearch.index.mapper.IgnoredFieldMapper; import org.elasticsearch.index.mapper.IgnoredFieldMapper;
import org.elasticsearch.index.mapper.IndexFieldMapper; import org.elasticsearch.index.mapper.IndexFieldMapper;
import org.elasticsearch.index.mapper.IpFieldMapper; import org.elasticsearch.index.mapper.IpFieldMapper;
import org.elasticsearch.index.mapper.IpScriptFieldType;
import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper;
import org.elasticsearch.index.mapper.KeywordScriptFieldType;
import org.elasticsearch.index.mapper.LongScriptFieldType;
import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.Mapper;
import org.elasticsearch.index.mapper.MapperRegistry; import org.elasticsearch.index.mapper.MapperRegistry;
import org.elasticsearch.index.mapper.MetadataFieldMapper; import org.elasticsearch.index.mapper.MetadataFieldMapper;
@ -38,6 +45,7 @@ import org.elasticsearch.index.mapper.NestedObjectMapper;
import org.elasticsearch.index.mapper.NestedPathFieldMapper; import org.elasticsearch.index.mapper.NestedPathFieldMapper;
import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.index.mapper.NumberFieldMapper;
import org.elasticsearch.index.mapper.ObjectMapper; import org.elasticsearch.index.mapper.ObjectMapper;
import org.elasticsearch.index.mapper.CompositeRuntimeField;
import org.elasticsearch.index.mapper.RangeType; import org.elasticsearch.index.mapper.RangeType;
import org.elasticsearch.index.mapper.RoutingFieldMapper; import org.elasticsearch.index.mapper.RoutingFieldMapper;
import org.elasticsearch.index.mapper.RuntimeField; import org.elasticsearch.index.mapper.RuntimeField;
@ -53,13 +61,6 @@ import org.elasticsearch.index.shard.PrimaryReplicaSyncer;
import org.elasticsearch.indices.cluster.IndicesClusterStateService; import org.elasticsearch.indices.cluster.IndicesClusterStateService;
import org.elasticsearch.indices.store.IndicesStore; import org.elasticsearch.indices.store.IndicesStore;
import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.MapperPlugin;
import org.elasticsearch.index.mapper.BooleanScriptFieldType;
import org.elasticsearch.index.mapper.DateScriptFieldType;
import org.elasticsearch.index.mapper.DoubleScriptFieldType;
import org.elasticsearch.index.mapper.GeoPointScriptFieldType;
import org.elasticsearch.index.mapper.IpScriptFieldType;
import org.elasticsearch.index.mapper.KeywordScriptFieldType;
import org.elasticsearch.index.mapper.LongScriptFieldType;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -150,6 +151,7 @@ public class IndicesModule extends AbstractModule {
runtimeParsers.put(DateFieldMapper.CONTENT_TYPE, DateScriptFieldType.PARSER); runtimeParsers.put(DateFieldMapper.CONTENT_TYPE, DateScriptFieldType.PARSER);
runtimeParsers.put(KeywordFieldMapper.CONTENT_TYPE, KeywordScriptFieldType.PARSER); runtimeParsers.put(KeywordFieldMapper.CONTENT_TYPE, KeywordScriptFieldType.PARSER);
runtimeParsers.put(GeoPointFieldMapper.CONTENT_TYPE, GeoPointScriptFieldType.PARSER); runtimeParsers.put(GeoPointFieldMapper.CONTENT_TYPE, GeoPointScriptFieldType.PARSER);
runtimeParsers.put(CompositeRuntimeField.CONTENT_TYPE, CompositeRuntimeField.PARSER);
for (MapperPlugin mapperPlugin : mapperPlugins) { for (MapperPlugin mapperPlugin : mapperPlugins) {
for (Map.Entry<String, RuntimeField.Parser> entry : mapperPlugin.getRuntimeFields().entrySet()) { for (Map.Entry<String, RuntimeField.Parser> entry : mapperPlugin.getRuntimeFields().entrySet()) {

View file

@ -86,7 +86,7 @@ public abstract class AbstractFieldScript extends DocBasedScript {
/** /**
* Set the document to run the script against. * Set the document to run the script against.
*/ */
public final void setDocument(int docId) { public void setDocument(int docId) {
this.leafSearchLookup.setDocument(docId); this.leafSearchLookup.setDocument(docId);
} }
@ -108,6 +108,16 @@ public abstract class AbstractFieldScript extends DocBasedScript {
return XContentMapValues.extractRawValues(path, leafSearchLookup.source().source()); return XContentMapValues.extractRawValues(path, leafSearchLookup.source().source());
} }
protected final void emitFromCompositeScript(CompositeFieldScript compositeFieldScript) {
List<Object> values = compositeFieldScript.getValues(fieldName);
if (values == null) {
return;
}
for (Object value : values) {
emitFromObject(value);
}
}
protected abstract void emitFromObject(Object v); protected abstract void emitFromObject(Object v);
protected final void emitFromSource() { protected final void emitFromSource() {

View file

@ -14,6 +14,7 @@ import org.elasticsearch.search.lookup.SearchLookup;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
public abstract class BooleanFieldScript extends AbstractFieldScript { public abstract class BooleanFieldScript extends AbstractFieldScript {
@ -33,6 +34,26 @@ public abstract class BooleanFieldScript extends AbstractFieldScript {
} }
}; };
public static Factory leafAdapter(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentFactory) {
return (leafFieldName, params, searchLookup) -> {
CompositeFieldScript.LeafFactory parentLeafFactory = parentFactory.apply(searchLookup);
return (LeafFactory) ctx -> {
CompositeFieldScript compositeFieldScript = parentLeafFactory.newInstance(ctx);
return new BooleanFieldScript(leafFieldName, params, searchLookup, ctx) {
@Override
public void setDocument(int docId) {
compositeFieldScript.setDocument(docId);
}
@Override
public void execute() {
emitFromCompositeScript(compositeFieldScript);
}
};
};
};
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final String[] PARAMETERS = {}; public static final String[] PARAMETERS = {};

View file

@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.script;
import org.apache.lucene.index.LeafReaderContext;
import org.elasticsearch.search.lookup.SearchLookup;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A script that emits a map of multiple values, that can then be accessed
* by child runtime fields.
*/
public abstract class CompositeFieldScript extends AbstractFieldScript {
public static final ScriptContext<CompositeFieldScript.Factory> CONTEXT = newContext("composite_field", Factory.class);
@SuppressWarnings("unused")
public static final String[] PARAMETERS = {};
public interface Factory extends ScriptFactory {
CompositeFieldScript.LeafFactory newFactory(String fieldName, Map<String, Object> params, SearchLookup searchLookup);
}
public interface LeafFactory {
CompositeFieldScript newInstance(LeafReaderContext ctx);
}
private final Map<String, List<Object>> fieldValues = new HashMap<>();
public CompositeFieldScript(String fieldName, Map<String, Object> params, SearchLookup searchLookup, LeafReaderContext ctx) {
super(fieldName, params, searchLookup, ctx);
}
/**
* Runs the object script and returns the values that were emitted for the provided field name
* @param field the field name to extract values from
* @return the values that were emitted for the provided field
*/
public final List<Object> getValues(String field) {
//TODO for now we re-run the script every time a leaf field is accessed, but we could cache the values?
fieldValues.clear();
execute();
List<Object> values = fieldValues.get(field);
fieldValues.clear(); // don't hold on to values unnecessarily
return values;
}
public final Map<String, List<Object>> runForDoc(int doc) {
setDocument(doc);
fieldValues.clear();
execute();
return fieldValues;
}
protected final void emit(String field, Object value) {
//fields will be emitted without the prefix, yet they will be looked up using their full name, hence we store the full name
List<Object> values = this.fieldValues.computeIfAbsent(fieldName + "." + field, s -> new ArrayList<>());
values.add(value);
}
@Override
protected void emitFromObject(Object v) {
throw new UnsupportedOperationException();
}
public static class EmitField {
private final CompositeFieldScript script;
public EmitField(CompositeFieldScript script) {
this.script = script;
}
/**
* Emits a value for the provided field. Note that ideally we would have typed the value, and have
* one emit per supported data type, but the arity in Painless does not take arguments type into account, only method name and
* number of arguments. That means that we would have needed a different method name per type, and given that we need the Object
* variant anyways to be able to emit an entire map, we went for taking an object also for the keyed emit variant.
*
* @param field the field name
* @param value the value
*/
public void emit(String field, Object value) {
script.emit(field, value);
}
}
public static class EmitMap {
private final CompositeFieldScript script;
public EmitMap(CompositeFieldScript script) {
this.script = script;
}
/**
* Emits all the subfields in one go. The key in the provided map is the field name, and the value their value(s)
* @param subfields the map that holds the key-value pairs
*/
public void emit(Map<String, Object> subfields) {
if (subfields == null) {
return;
}
for (Map.Entry<String, Object> entry : subfields.entrySet()) {
script.emit(entry.getKey(), entry.getValue());
}
}
}
}

View file

@ -14,6 +14,7 @@ import org.elasticsearch.search.lookup.SearchLookup;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
public abstract class DateFieldScript extends AbstractLongFieldScript { public abstract class DateFieldScript extends AbstractLongFieldScript {
public static final ScriptContext<Factory> CONTEXT = newContext("date_field", Factory.class); public static final ScriptContext<Factory> CONTEXT = newContext("date_field", Factory.class);
@ -33,6 +34,26 @@ public abstract class DateFieldScript extends AbstractLongFieldScript {
} }
}; };
public static Factory leafAdapter(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentFactory) {
return (leafFieldName, params, searchLookup, formatter) -> {
CompositeFieldScript.LeafFactory parentLeafFactory = parentFactory.apply(searchLookup);
return (LeafFactory) ctx -> {
CompositeFieldScript compositeFieldScript = parentLeafFactory.newInstance(ctx);
return new DateFieldScript(leafFieldName, params, searchLookup, formatter, ctx) {
@Override
public void setDocument(int docId) {
compositeFieldScript.setDocument(docId);
}
@Override
public void execute() {
emitFromCompositeScript(compositeFieldScript);
}
};
};
};
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final String[] PARAMETERS = {}; public static final String[] PARAMETERS = {};

View file

@ -14,6 +14,7 @@ import org.elasticsearch.search.lookup.SearchLookup;
import java.util.Map; import java.util.Map;
import java.util.function.DoubleConsumer; import java.util.function.DoubleConsumer;
import java.util.function.Function;
public abstract class DoubleFieldScript extends AbstractFieldScript { public abstract class DoubleFieldScript extends AbstractFieldScript {
public static final ScriptContext<Factory> CONTEXT = newContext("double_field", Factory.class); public static final ScriptContext<Factory> CONTEXT = newContext("double_field", Factory.class);
@ -32,6 +33,26 @@ public abstract class DoubleFieldScript extends AbstractFieldScript {
} }
}; };
public static Factory leafAdapter(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentFactory) {
return (leafFieldName, params, searchLookup) -> {
CompositeFieldScript.LeafFactory parentLeafFactory = parentFactory.apply(searchLookup);
return (LeafFactory) ctx -> {
CompositeFieldScript compositeFieldScript = parentLeafFactory.newInstance(ctx);
return new DoubleFieldScript(leafFieldName, params, searchLookup, ctx) {
@Override
public void setDocument(int docId) {
compositeFieldScript.setDocument(docId);
}
@Override
public void execute() {
emitFromCompositeScript(compositeFieldScript);
}
};
};
};
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final String[] PARAMETERS = {}; public static final String[] PARAMETERS = {};

View file

@ -20,6 +20,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude; import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
@ -46,6 +47,26 @@ public abstract class GeoPointFieldScript extends AbstractLongFieldScript {
} }
}; };
public static Factory leafAdapter(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentFactory) {
return (leafFieldName, params, searchLookup) -> {
CompositeFieldScript.LeafFactory parentLeafFactory = parentFactory.apply(searchLookup);
return (LeafFactory) ctx -> {
CompositeFieldScript compositeFieldScript = parentLeafFactory.newInstance(ctx);
return new GeoPointFieldScript(leafFieldName, params, searchLookup, ctx) {
@Override
public void setDocument(int docId) {
compositeFieldScript.setDocument(docId);
}
@Override
public void execute() {
emitFromCompositeScript(compositeFieldScript);
}
};
};
};
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final String[] PARAMETERS = {}; public static final String[] PARAMETERS = {};

View file

@ -21,6 +21,7 @@ import java.net.Inet6Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
/** /**
* Script producing IP addresses. Unlike the other {@linkplain AbstractFieldScript}s * Script producing IP addresses. Unlike the other {@linkplain AbstractFieldScript}s
@ -53,6 +54,26 @@ public abstract class IpFieldScript extends AbstractFieldScript {
} }
}; };
public static Factory leafAdapter(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentFactory) {
return (leafFieldName, params, searchLookup) -> {
CompositeFieldScript.LeafFactory parentLeafFactory = parentFactory.apply(searchLookup);
return (LeafFactory) ctx -> {
CompositeFieldScript compositeFieldScript = parentLeafFactory.newInstance(ctx);
return new IpFieldScript(leafFieldName, params, searchLookup, ctx) {
@Override
public void setDocument(int docId) {
compositeFieldScript.setDocument(docId);
}
@Override
public void execute() {
emitFromCompositeScript(compositeFieldScript);
}
};
};
};
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final String[] PARAMETERS = {}; public static final String[] PARAMETERS = {};

View file

@ -13,6 +13,7 @@ import org.elasticsearch.index.mapper.NumberFieldMapper;
import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.SearchLookup;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
public abstract class LongFieldScript extends AbstractLongFieldScript { public abstract class LongFieldScript extends AbstractLongFieldScript {
public static final ScriptContext<Factory> CONTEXT = newContext("long_field", Factory.class); public static final ScriptContext<Factory> CONTEXT = newContext("long_field", Factory.class);
@ -31,6 +32,26 @@ public abstract class LongFieldScript extends AbstractLongFieldScript {
} }
}; };
public static Factory leafAdapter(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentFactory) {
return (leafFieldName, params, searchLookup) -> {
CompositeFieldScript.LeafFactory parentLeafFactory = parentFactory.apply(searchLookup);
return (LeafFactory) ctx -> {
CompositeFieldScript compositeFieldScript = parentLeafFactory.newInstance(ctx);
return new LongFieldScript(leafFieldName, params, searchLookup, ctx) {
@Override
public void setDocument(int docId) {
compositeFieldScript.setDocument(docId);
}
@Override
public void execute() {
emitFromCompositeScript(compositeFieldScript);
}
};
};
};
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final String[] PARAMETERS = {}; public static final String[] PARAMETERS = {};

View file

@ -28,8 +28,15 @@ import java.util.stream.Stream;
*/ */
public class ScriptModule { public class ScriptModule {
public static final Set<ScriptContext<?>> RUNTIME_FIELDS_CONTEXTS = Set.of(BooleanFieldScript.CONTEXT, DateFieldScript.CONTEXT, public static final Set<ScriptContext<?>> RUNTIME_FIELDS_CONTEXTS = Set.of(
DoubleFieldScript.CONTEXT, LongFieldScript.CONTEXT, StringFieldScript.CONTEXT, GeoPointFieldScript.CONTEXT, IpFieldScript.CONTEXT); BooleanFieldScript.CONTEXT,
DateFieldScript.CONTEXT,
DoubleFieldScript.CONTEXT,
LongFieldScript.CONTEXT,
StringFieldScript.CONTEXT,
GeoPointFieldScript.CONTEXT,
IpFieldScript.CONTEXT,
CompositeFieldScript.CONTEXT);
public static final Map<String, ScriptContext<?>> CORE_CONTEXTS; public static final Map<String, ScriptContext<?>> CORE_CONTEXTS;
static { static {

View file

@ -16,6 +16,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
public abstract class StringFieldScript extends AbstractFieldScript { public abstract class StringFieldScript extends AbstractFieldScript {
/** /**
@ -39,6 +40,26 @@ public abstract class StringFieldScript extends AbstractFieldScript {
} }
}; };
public static Factory leafAdapter(Function<SearchLookup, CompositeFieldScript.LeafFactory> parentFactory) {
return (leafFieldName, params, searchLookup) -> {
CompositeFieldScript.LeafFactory parentLeafFactory = parentFactory.apply(searchLookup);
return (LeafFactory) ctx -> {
CompositeFieldScript compositeFieldScript = parentLeafFactory.newInstance(ctx);
return new StringFieldScript(leafFieldName, params, searchLookup, ctx) {
@Override
public void setDocument(int docId) {
compositeFieldScript.setDocument(docId);
}
@Override
public void execute() {
emitFromCompositeScript(compositeFieldScript);
}
};
};
};
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final String[] PARAMETERS = {}; public static final String[] PARAMETERS = {};

View file

@ -0,0 +1,438 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.index.mapper;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.script.LongFieldScript;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.search.lookup.LeafSearchLookup;
import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
public class CompositeRuntimeFieldTests extends MapperServiceTestCase {
@Override
@SuppressWarnings("unchecked")
protected <T> T compileScript(Script script, ScriptContext<T> context) {
if (context == CompositeFieldScript.CONTEXT) {
return (T) (CompositeFieldScript.Factory) (fieldName, params, searchLookup) -> ctx -> new CompositeFieldScript(
fieldName,
params,
searchLookup,
ctx
){
@Override
public void execute() {
if (script.getIdOrCode().equals("split-str-long")) {
List<Object> values = extractFromSource("field");
String input = values.get(0).toString();
String[] parts = input.split(" ");
emit("str", parts[0]);
emit("long", parts[1]);
}
}
};
}
if (context == LongFieldScript.CONTEXT) {
return (T) (LongFieldScript.Factory) (field, params, lookup) -> ctx -> new LongFieldScript(field, params, lookup, ctx) {
@Override
public void execute() {
}
};
}
throw new UnsupportedOperationException("Unknown context " + context.name);
}
public void testObjectDefinition() throws IOException {
MapperService mapperService = createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.startObject("script").field("source", "dummy").endObject();
b.startObject("fields");
b.startObject("long-subfield").field("type", "long").endObject();
b.startObject("str-subfield").field("type", "keyword").endObject();
b.startObject("double-subfield").field("type", "double").endObject();
b.startObject("boolean-subfield").field("type", "boolean").endObject();
b.startObject("ip-subfield").field("type", "ip").endObject();
b.startObject("geopoint-subfield").field("type", "geo_point").endObject();
b.endObject();
b.endObject();
b.endObject();
}));
assertNull(mapperService.mappingLookup().getFieldType("obj"));
assertNull(mapperService.mappingLookup().getFieldType("long-subfield"));
assertNull(mapperService.mappingLookup().getFieldType("str-subfield"));
assertNull(mapperService.mappingLookup().getFieldType("double-subfield"));
assertNull(mapperService.mappingLookup().getFieldType("boolean-subfield"));
assertNull(mapperService.mappingLookup().getFieldType("ip-subfield"));
assertNull(mapperService.mappingLookup().getFieldType("geopoint-subfield"));
assertNull(mapperService.mappingLookup().getFieldType("obj.any-subfield"));
MappedFieldType longSubfield = mapperService.mappingLookup().getFieldType("obj.long-subfield");
assertEquals("obj.long-subfield", longSubfield.name());
assertEquals("long", longSubfield.typeName());
MappedFieldType strSubfield = mapperService.mappingLookup().getFieldType("obj.str-subfield");
assertEquals("obj.str-subfield", strSubfield.name());
assertEquals("keyword", strSubfield.typeName());
MappedFieldType doubleSubfield = mapperService.mappingLookup().getFieldType("obj.double-subfield");
assertEquals("obj.double-subfield", doubleSubfield.name());
assertEquals("double", doubleSubfield.typeName());
MappedFieldType booleanSubfield = mapperService.mappingLookup().getFieldType("obj.boolean-subfield");
assertEquals("obj.boolean-subfield", booleanSubfield.name());
assertEquals("boolean", booleanSubfield.typeName());
MappedFieldType ipSubfield = mapperService.mappingLookup().getFieldType("obj.ip-subfield");
assertEquals("obj.ip-subfield", ipSubfield.name());
assertEquals("ip", ipSubfield.typeName());
MappedFieldType geoPointSubfield = mapperService.mappingLookup().getFieldType("obj.geopoint-subfield");
assertEquals("obj.geopoint-subfield", geoPointSubfield.name());
assertEquals("geo_point", geoPointSubfield.typeName());
RuntimeField rf = mapperService.mappingLookup().getMapping().getRoot().getRuntimeField("obj");
assertEquals("obj", rf.name());
Collection<MappedFieldType> mappedFieldTypes = rf.asMappedFieldTypes().collect(Collectors.toList());
for (MappedFieldType mappedFieldType : mappedFieldTypes) {
if (mappedFieldType.name().equals("obj.long-subfield")) {
assertSame(longSubfield, mappedFieldType);
} else if (mappedFieldType.name().equals("obj.str-subfield")) {
assertSame(strSubfield, mappedFieldType);
} else if (mappedFieldType.name().equals("obj.double-subfield")) {
assertSame(doubleSubfield, mappedFieldType);
} else if (mappedFieldType.name().equals("obj.boolean-subfield")) {
assertSame(booleanSubfield, mappedFieldType);
} else if (mappedFieldType.name().equals("obj.ip-subfield")) {
assertSame(ipSubfield, mappedFieldType);
} else if (mappedFieldType.name().equals("obj.geopoint-subfield")) {
assertSame(geoPointSubfield, mappedFieldType);
} else {
fail("unexpected subfield [" + mappedFieldType.name() + "]");
}
}
}
public void testUnsupportedLeafType() {
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.startObject("script").field("source", "dummy").endObject();
b.startObject("fields");
b.startObject("long-subfield").field("type", "unsupported").endObject();
b.endObject();
b.endObject();
b.endObject();
})));
assertThat(e.getMessage(), containsString(""));
}
public void testToXContent() throws IOException {
MapperService mapperService = createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("message");
b.field("type", "composite");
b.field("script", "dummy");
b.startObject("meta").field("test-meta", "value").endObject();
b.startObject("fields").startObject("response").field("type", "long").endObject().endObject();
b.endObject();
b.endObject();
}));
assertEquals("{\"_doc\":{\"runtime\":{" +
"\"message\":{\"type\":\"composite\"," +
"\"meta\":{\"test-meta\":\"value\"}," +
"\"script\":{\"source\":\"dummy\",\"lang\":\"painless\"}," +
"\"fields\":{\"response\":{\"type\":\"long\"}}}}}}",
Strings.toString(mapperService.mappingLookup().getMapping()));
}
public void testScriptOnSubFieldThrowsError() {
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(runtimeMapping(b -> {
b.startObject("obj");
b.field("type", "composite");
b.field("script", "dummy");
b.startObject("fields");
b.startObject("long").field("type", "long").field("script", "dummy").endObject();
b.endObject();
b.endObject();
})));
assertThat(e.getMessage(), containsString("Cannot use [script] parameter on sub-field [long] of composite field [obj]"));
}
public void testObjectWithoutScript() {
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(runtimeMapping(b -> {
b.startObject("obj");
b.field("type", "composite");
b.startObject("fields");
b.startObject("long").field("type", "long").endObject();
b.endObject();
b.endObject();
})));
assertThat(e.getMessage(), containsString("composite runtime field [obj] must declare a [script]"));
}
public void testObjectNullScript() {
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(runtimeMapping(b -> {
b.startObject("obj");
b.field("type", "composite");
b.nullField("script");
b.startObject("fields");
b.startObject("long").field("type", "long").endObject();
b.endObject();
b.endObject();
})));
assertThat(e.getMessage(), containsString(" [script] on runtime field [obj] of type [composite] must not have a [null] value"));
}
public void testObjectWithoutFields() {
{
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(runtimeMapping(b -> {
b.startObject("obj");
b.field("type", "composite");
b.field("script", "dummy");
b.endObject();
})));
assertThat(e.getMessage(), containsString("composite runtime field [obj] must declare its [fields]"));
}
{
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(runtimeMapping(b -> {
b.startObject("obj");
b.field("type", "composite");
b.field("script", "dummy");
b.startObject("fields").endObject();
b.endObject();
})));
assertThat(e.getMessage(), containsString("composite runtime field [obj] must declare its [fields]"));
}
}
public void testMappingUpdate() throws IOException {
MapperService mapperService = createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.startObject("script").field("source", "dummy").endObject();
b.startObject("fields");
b.startObject("long-subfield").field("type", "long").endObject();
b.startObject("str-subfield").field("type", "keyword").endObject();
b.endObject();
b.endObject();
b.endObject();
}));
XContentBuilder b = XContentBuilder.builder(XContentType.JSON.xContent());
b.startObject();
b.startObject("_doc");
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.startObject("script").field("source", "dummy2").endObject();
b.startObject("fields");
b.startObject("double-subfield").field("type", "double").endObject();
b.endObject();
b.endObject();
b.endObject();
b.endObject();
b.endObject();
merge(mapperService, b);
assertNull(mapperService.mappingLookup().getFieldType("obj.long-subfield"));
assertNull(mapperService.mappingLookup().getFieldType("obj.str-subfield"));
MappedFieldType doubleSubField = mapperService.mappingLookup().getFieldType("obj.double-subfield");
assertEquals("obj.double-subfield", doubleSubField.name());
assertEquals("double", doubleSubField.typeName());
RuntimeField rf = mapperService.mappingLookup().getMapping().getRoot().getRuntimeField("obj");
assertEquals("obj", rf.name());
Collection<MappedFieldType> mappedFieldTypes = rf.asMappedFieldTypes().collect(Collectors.toList());
assertEquals(1, mappedFieldTypes.size());
assertSame(doubleSubField, mappedFieldTypes.iterator().next());
assertEquals("{\"obj\":{\"type\":\"composite\"," +
"\"script\":{\"source\":\"dummy2\",\"lang\":\"painless\"}," +
"\"fields\":{\"double-subfield\":{\"type\":\"double\"}}}}", Strings.toString(rf));
}
public void testFieldDefinedTwiceWithSameName() throws IOException {
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("obj.long-subfield").field("type", "long").endObject();
b.startObject("obj");
b.field("type", "composite");
b.startObject("script").field("source", "dummy").endObject();
b.startObject("fields");
b.startObject("long-subfield").field("type", "long").endObject();
b.endObject();
b.endObject();
b.endObject();
})));
assertThat(e.getMessage(), containsString("Found two runtime fields with same name [obj.long-subfield]"));
MapperService mapperService = createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("obj.str-subfield").field("type", "long").endObject();
b.startObject("obj");
b.field("type", "composite");
b.startObject("script").field("source", "dummy").endObject();
b.startObject("fields");
b.startObject("long-subfield").field("type", "long").endObject();
b.endObject();
b.endObject();
b.endObject();
}));
XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
builder.startObject();
builder.startObject("_doc");
builder.startObject("runtime");
builder.startObject("obj.long-subfield").field("type", "long").endObject();
builder.endObject();
builder.endObject();
builder.endObject();
IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> merge(mapperService, builder));
assertThat(iae.getMessage(), containsString("Found two runtime fields with same name [obj.long-subfield]"));
}
public void testParseDocumentSubFieldAccess() throws IOException {
MapperService mapperService = createMapperService(topMapping(b -> {
b.field("dynamic", false);
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.field("script", "split-str-long");
b.startObject("fields");
b.startObject("str").field("type", "keyword").endObject();
b.startObject("long").field("type", "long").endObject();
b.endObject();
b.endObject();
b.endObject();
}));
ParsedDocument doc1 = mapperService.documentMapper().parse(source(b -> b.field("field", "foo 1")));
ParsedDocument doc2 = mapperService.documentMapper().parse(source(b -> b.field("field", "bar 2")));
withLuceneIndex(mapperService, iw -> iw.addDocuments(Arrays.asList(doc1.rootDoc(), doc2.rootDoc())), reader -> {
SearchLookup searchLookup = new SearchLookup(
mapperService::fieldType,
(mft, lookupSupplier) -> mft.fielddataBuilder("test", lookupSupplier).build(null, null)
);
LeafSearchLookup leafSearchLookup = searchLookup.getLeafSearchLookup(reader.leaves().get(0));
leafSearchLookup.setDocument(0);
assertEquals("foo", leafSearchLookup.doc().get("obj.str").get(0));
assertEquals(1L, leafSearchLookup.doc().get("obj.long").get(0));
leafSearchLookup.setDocument(1);
assertEquals("bar", leafSearchLookup.doc().get("obj.str").get(0));
assertEquals(2L, leafSearchLookup.doc().get("obj.long").get(0));
});
}
public void testParseDocumentDynamicMapping() throws IOException {
MapperService mapperService = createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.field("script", "dummy");
b.startObject("fields");
b.startObject("str").field("type", "keyword").endObject();
b.startObject("long").field("type", "long").endObject();
b.endObject();
b.endObject();
b.endObject();
}));
ParsedDocument doc1 = mapperService.documentMapper().parse(source(b -> b.field("obj.long", 1L).field("obj.str", "value")));
assertNull(doc1.rootDoc().get("obj.long"));
assertNull(doc1.rootDoc().get("obj.str"));
assertNull(mapperService.mappingLookup().getMapper("obj.long"));
assertNull(mapperService.mappingLookup().getMapper("obj.str"));
assertNotNull(mapperService.mappingLookup().getFieldType("obj.long"));
assertNotNull(mapperService.mappingLookup().getFieldType("obj.str"));
XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
builder.startObject();
builder.startObject("_doc");
builder.startObject("properties");
builder.startObject("obj");
builder.startObject("properties");
builder.startObject("long").field("type", "long").endObject();
builder.endObject();
builder.endObject();
builder.endObject();
builder.endObject();
builder.endObject();
merge(mapperService, builder);
ParsedDocument doc2 = mapperService.documentMapper().parse(source(b -> b.field("obj.long", 2L)));
assertNotNull(doc2.rootDoc().get("obj.long"));
assertNull(doc2.rootDoc().get("obj.str"));
assertNotNull(mapperService.mappingLookup().getMapper("obj.long"));
assertNull(mapperService.mappingLookup().getMapper("obj.str"));
assertNotNull(mapperService.mappingLookup().getFieldType("obj.long"));
assertNotNull(mapperService.mappingLookup().getFieldType("obj.str"));
}
public void testParseDocumentSubfieldsOutsideRuntimeObject() throws IOException{
MapperService mapperService = createMapperService(topMapping(b -> {
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.field("script", "dummy");
b.startObject("fields");
b.startObject("long").field("type", "long").endObject();
b.endObject();
b.endObject();
b.endObject();
}));
ParsedDocument doc1 = mapperService.documentMapper().parse(source(b -> b.field("obj.long", 1L).field("obj.bool", true)));
assertNull(doc1.rootDoc().get("obj.long"));
assertNotNull(doc1.rootDoc().get("obj.bool"));
assertEquals("{\"_doc\":{\"properties\":{\"obj\":{\"properties\":{\"bool\":{\"type\":\"boolean\"}}}}}}",
Strings.toString(doc1.dynamicMappingsUpdate()));
MapperService mapperService2 = createMapperService(topMapping(b -> {
b.field("dynamic", "runtime");
b.startObject("runtime");
b.startObject("obj");
b.field("type", "composite");
b.field("script", "dummy");
b.startObject("fields");
b.startObject("long").field("type", "long").endObject();
b.endObject();
b.endObject();
b.endObject();
}));
ParsedDocument doc2 = mapperService2.documentMapper().parse(source(b -> b.field("obj.long", 1L).field("obj.bool", true)));
assertNull(doc2.rootDoc().get("obj.long"));
assertNull(doc2.rootDoc().get("obj.bool"));
assertEquals("{\"_doc\":{\"dynamic\":\"runtime\",\"runtime\":{\"obj.bool\":{\"type\":\"boolean\"}}}}",
Strings.toString(doc2.dynamicMappingsUpdate()));
}
}

View file

@ -23,6 +23,8 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.MapperPlugin;
import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.script.CompositeFieldScript;
import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -33,6 +35,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import static org.elasticsearch.test.StreamsUtils.copyToBytesFromClasspath; import static org.elasticsearch.test.StreamsUtils.copyToBytesFromClasspath;
import static org.elasticsearch.test.StreamsUtils.copyToStringFromClasspath; import static org.elasticsearch.test.StreamsUtils.copyToStringFromClasspath;
@ -1959,7 +1962,6 @@ public class DocumentParserTests extends MapperServiceTestCase {
/** /**
* Mapper plugin providing a mock metadata field mapper implementation that supports setting its value * Mapper plugin providing a mock metadata field mapper implementation that supports setting its value
* as well as a mock runtime field parser.
*/ */
private static final class DocumentParserTestsPlugin extends Plugin implements MapperPlugin { private static final class DocumentParserTestsPlugin extends Plugin implements MapperPlugin {
/** /**
@ -2001,12 +2003,22 @@ public class DocumentParserTests extends MapperServiceTestCase {
"test-composite", "test-composite",
new RuntimeField.Parser(n -> new RuntimeField.Builder(n) { new RuntimeField.Parser(n -> new RuntimeField.Builder(n) {
@Override @Override
protected RuntimeField createRuntimeField(MappingParserContext parserContext) { protected RuntimeField createRuntimeField(MappingParserContext parserContext)
{
return new TestRuntimeField(n, List.of( return new TestRuntimeField(n, List.of(
new KeywordFieldMapper.KeywordFieldType(n + ".foo"), new KeywordFieldMapper.KeywordFieldType(n + ".foo"),
new KeywordFieldMapper.KeywordFieldType(n + ".bar") new KeywordFieldMapper.KeywordFieldType(n + ".bar")
)); ));
} }
@Override
protected RuntimeField createChildRuntimeField(
MappingParserContext parserContext,
String parentName,
Function<SearchLookup, CompositeFieldScript.LeafFactory> parentScriptFactory
) {
throw new UnsupportedOperationException();
}
}) })
); );
} }

View file

@ -15,6 +15,7 @@ import org.elasticsearch.index.query.SearchExecutionContext;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.stream.Stream;
public final class TestRuntimeField implements RuntimeField { public final class TestRuntimeField implements RuntimeField {
@ -38,8 +39,8 @@ public final class TestRuntimeField implements RuntimeField {
} }
@Override @Override
public Collection<MappedFieldType> asMappedFieldTypes() { public Stream<MappedFieldType> asMappedFieldTypes() {
return subfields; return subfields.stream();
} }
@Override @Override

View file

@ -286,6 +286,15 @@ public class MockScriptEngine implements ScriptEngine {
} }
}; };
return context.factoryClazz.cast(geoPointFieldScript); return context.factoryClazz.cast(geoPointFieldScript);
} else if (context.instanceClazz.equals(CompositeFieldScript.class)) {
CompositeFieldScript.Factory objectFieldScript = (f, p, s) -> ctx -> new CompositeFieldScript(f, p, s, ctx) {
@Override
public void execute() {
emit("field1", "value1");
emit("field2", "value2");
}
};
return context.factoryClazz.cast(objectFieldScript);
} }
ContextCompiler compiler = contexts.get(context); ContextCompiler compiler = contexts.get(context);
if (compiler != null) { if (compiler != null) {