mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-28 09:28:55 -04:00
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:
parent
ad6e0dc6cb
commit
32d2f60f8a
35 changed files with 1237 additions and 66 deletions
|
@ -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 + "]");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
|
@ -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(); };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
||||||
|
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue