Skip to content

Commit 23bfb86

Browse files
authored
RuntimeField to expose more than one MappedFieldType (#73900)
Up until now, there was a 1:1 relationship between a RuntimeField and a MappedFieldType. Actually, the two were only recently split to support emitting multiple fields from a single runtime script. The next step is to change the signature of asMappedFieldType to make it return multiple sub-fields. The leaf fields that are supported to-date will return a collection with a single item, but the upcoming runtime "object" field will return as many subfields as are listed in its definition. Together with this, we are introducing two consistency checks: - sub-fields exposed by a RuntimeField either have its same name, or belong to its namespace (e.g. object.subfield) - there can't be two mapped field types with the same name as part of the same runtime section, overrides happen defining the same field in two different sections (index mappings and search request)
1 parent 9ec8d4c commit 23bfb86

File tree

9 files changed

+188
-48
lines changed

9 files changed

+188
-48
lines changed

server/src/main/java/org/elasticsearch/index/mapper/AbstractScriptFieldType.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.io.IOException;
2929
import java.time.ZoneId;
3030
import java.util.ArrayList;
31+
import java.util.Collection;
3132
import java.util.Collections;
3233
import java.util.List;
3334
import java.util.Locale;
@@ -58,8 +59,8 @@ abstract class AbstractScriptFieldType<LeafFactory> extends MappedFieldType impl
5859
}
5960

6061
@Override
61-
public final MappedFieldType asMappedFieldType() {
62-
return this;
62+
public final Collection<MappedFieldType> asMappedFieldTypes() {
63+
return Collections.singleton(this);
6364
}
6465

6566
@Override

server/src/main/java/org/elasticsearch/index/mapper/DocumentParser.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -885,9 +885,11 @@ private static Mapper getLeafMapper(final ParseContext context,
885885
// if a leaf field is not mapped, and is defined as a runtime field, then we
886886
// don't create a dynamic mapping for it and don't index it.
887887
String fieldPath = context.path().pathAsText(fieldName);
888-
RuntimeField runtimeField = context.root().getRuntimeField(fieldPath);
889-
if (runtimeField != null) {
890-
return new NoOpFieldMapper(subfields[subfields.length - 1], runtimeField.asMappedFieldType().name());
888+
MappedFieldType fieldType = context.mappingLookup().getFieldType(fieldPath);
889+
if (fieldType != null) {
890+
//we haven't found a mapper with this name above, which means if a field type is found it is for sure a runtime field.
891+
assert fieldType.hasDocValues() == false && fieldType.isAggregatable() && fieldType.isSearchable();
892+
return new NoOpFieldMapper(subfields[subfields.length - 1], fieldType.name());
891893
}
892894
return null;
893895
}

server/src/main/java/org/elasticsearch/index/mapper/FieldTypeLookup.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,9 @@ final class FieldTypeLookup {
8080
}
8181
}
8282

83-
for (RuntimeField runtimeField : runtimeFields) {
84-
MappedFieldType runtimeFieldType = runtimeField.asMappedFieldType();
83+
for (MappedFieldType fieldType : RuntimeField.collectFieldTypes(runtimeFields).values()) {
8584
//this will override concrete fields with runtime fields that have the same name
86-
fullNameToFieldType.put(runtimeFieldType.name(), runtimeFieldType);
85+
fullNameToFieldType.put(fieldType.name(), fieldType);
8786
}
8887
}
8988

server/src/main/java/org/elasticsearch/index/mapper/RuntimeField.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
import org.elasticsearch.common.xcontent.XContentBuilder;
1313

1414
import java.io.IOException;
15+
import java.util.Collection;
1516
import java.util.Collections;
1617
import java.util.HashMap;
1718
import java.util.Iterator;
1819
import java.util.List;
1920
import java.util.Map;
2021
import java.util.function.Function;
22+
import java.util.stream.Collectors;
2123

2224
/**
2325
* Definition of a runtime field that can be defined as part of the runtime section of the index mappings
@@ -51,10 +53,10 @@ default XContentBuilder toXContent(XContentBuilder builder, Params params) throw
5153
String typeName();
5254

5355
/**
54-
* Exposes the {@link MappedFieldType} backing this runtime field, used to execute queries, run aggs etc.
55-
* @return the {@link MappedFieldType} backing this runtime field
56+
* Exposes the {@link MappedFieldType}s backing this runtime field, used to execute queries, run aggs etc.
57+
* @return the {@link MappedFieldType}s backing this runtime field
5658
*/
57-
MappedFieldType asMappedFieldType();
59+
Collection<MappedFieldType> asMappedFieldTypes();
5860

5961
/**
6062
* For runtime fields the {@link RuntimeField.Parser} returns directly the {@link MappedFieldType}.
@@ -175,4 +177,31 @@ static Map<String, RuntimeField> parseRuntimeFields(Map<String, Object> node,
175177
}
176178
return Collections.unmodifiableMap(runtimeFields);
177179
}
180+
181+
/**
182+
* Collect and return all {@link MappedFieldType} exposed by the provided {@link RuntimeField}s.
183+
* Note that validation is performed to make sure that there are no name clashes among the collected runtime fields.
184+
* This is because runtime fields with the same name are not accepted as part of the same section.
185+
* @param runtimeFields the runtime to extract the mapped field types from
186+
* @return the collected mapped field types
187+
*/
188+
static Map<String, MappedFieldType> collectFieldTypes(Collection<RuntimeField> runtimeFields) {
189+
return runtimeFields.stream()
190+
.flatMap(runtimeField -> {
191+
List<String> names = runtimeField.asMappedFieldTypes().stream().map(MappedFieldType::name)
192+
.filter(name -> name.equals(runtimeField.name()) == false
193+
&& (name.startsWith(runtimeField.name() + ".") == false
194+
|| name.length() > runtimeField.name().length() + 1 == false))
195+
.collect(Collectors.toList());
196+
if (names.isEmpty() == false) {
197+
throw new IllegalStateException("Found sub-fields with name not belonging to the parent field they are part of "
198+
+ names);
199+
}
200+
return runtimeField.asMappedFieldTypes().stream();
201+
})
202+
.collect(Collectors.toMap(MappedFieldType::name, mappedFieldType -> mappedFieldType,
203+
(t, t2) -> {
204+
throw new IllegalArgumentException("Found two runtime fields with same name [" + t.name() + "]");
205+
}));
206+
}
178207
}

server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -663,12 +663,7 @@ private static Map<String, MappedFieldType> parseRuntimeMappings(Map<String, Obj
663663
}
664664
Map<String, RuntimeField> runtimeFields = RuntimeField.parseRuntimeFields(new HashMap<>(runtimeMappings),
665665
mapperService.parserContext(), false);
666-
Map<String, MappedFieldType> runtimeFieldTypes = new HashMap<>();
667-
for (RuntimeField runtimeField : runtimeFields.values()) {
668-
MappedFieldType fieldType = runtimeField.asMappedFieldType();
669-
runtimeFieldTypes.put(fieldType.name(), fieldType);
670-
}
671-
return Collections.unmodifiableMap(runtimeFieldTypes);
666+
return RuntimeField.collectFieldTypes(runtimeFields.values());
672667
}
673668

674669
/**

server/src/test/java/org/elasticsearch/index/mapper/FieldTypeLookupTests.java

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static java.util.Collections.singletonList;
2323
import static org.hamcrest.CoreMatchers.equalTo;
2424
import static org.hamcrest.CoreMatchers.instanceOf;
25+
import static org.hamcrest.CoreMatchers.nullValue;
2526
import static org.hamcrest.Matchers.contains;
2627
import static org.hamcrest.Matchers.containsInAnyOrder;
2728
import static org.hamcrest.Matchers.hasSize;
@@ -55,6 +56,7 @@ public void testAddFieldAlias() {
5556
}
5657

5758
public void testGetMatchingFieldNames() {
59+
FlattenedFieldMapper flattened = createFlattenedMapper("flattened");
5860
MockFieldMapper field1 = new MockFieldMapper("foo");
5961
MockFieldMapper field2 = new MockFieldMapper("bar");
6062
MockFieldMapper field3 = new MockFieldMapper("baz");
@@ -63,18 +65,24 @@ public void testGetMatchingFieldNames() {
6365
FieldAliasMapper alias2 = new FieldAliasMapper("barometer", "barometer", "bar");
6466

6567
TestRuntimeField runtimeField = new TestRuntimeField("baz", "type");
68+
TestRuntimeField multi = new TestRuntimeField("flat", "multi",
69+
List.of(new TestRuntimeField.TestRuntimeFieldType("flat.first", "first"),
70+
new TestRuntimeField.TestRuntimeFieldType("flat.second", "second")));
6671

67-
FieldTypeLookup lookup = new FieldTypeLookup("_doc", List.of(field1, field2, field3), List.of(alias1, alias2),
68-
List.of(runtimeField));
69-
72+
FieldTypeLookup lookup = new FieldTypeLookup("_doc", List.of(field1, field2, field3, flattened), List.of(alias1, alias2),
73+
List.of(runtimeField, multi));
7074
{
7175
Collection<String> names = lookup.getMatchingFieldNames("*");
72-
assertThat(names, containsInAnyOrder("foo", "food", "bar", "baz", "barometer"));
76+
assertThat(names, containsInAnyOrder("foo", "food", "bar", "baz", "barometer", "flattened", "flat.first", "flat.second"));
7377
}
7478
{
7579
Collection<String> names = lookup.getMatchingFieldNames("b*");
7680
assertThat(names, containsInAnyOrder("bar", "baz", "barometer"));
7781
}
82+
{
83+
Collection<String> names = lookup.getMatchingFieldNames("fl*");
84+
assertThat(names, containsInAnyOrder("flattened", "flat.first", "flat.second"));
85+
}
7886
{
7987
Collection<String> names = lookup.getMatchingFieldNames("baro.any*");
8088
assertThat(names, hasSize(0));
@@ -83,6 +91,18 @@ public void testGetMatchingFieldNames() {
8391
Collection<String> names = lookup.getMatchingFieldNames("foo*");
8492
assertThat(names, containsInAnyOrder("foo", "food"));
8593
}
94+
{
95+
Collection<String> names = lookup.getMatchingFieldNames("flattened.anything");
96+
assertThat(names, containsInAnyOrder("flattened.anything"));
97+
}
98+
{
99+
Collection<String> names = lookup.getMatchingFieldNames("flat.first");
100+
assertThat(names, containsInAnyOrder("flat.first"));
101+
}
102+
{
103+
Collection<String> names = lookup.getMatchingFieldNames("flat.second");
104+
assertThat(names, containsInAnyOrder("flat.second"));
105+
}
86106
}
87107

88108
public void testSourcePathWithMultiFields() {
@@ -123,28 +143,48 @@ public void testTypeLookup() {
123143

124144
public void testRuntimeFieldsLookup() {
125145
MockFieldMapper concrete = new MockFieldMapper("concrete");
126-
TestRuntimeField runtime = new TestRuntimeField("runtime", "type");
146+
TestRuntimeField runtimeLong = new TestRuntimeField("multi.outside", "date");
147+
TestRuntimeField runtime = new TestRuntimeField("string", "type");
148+
TestRuntimeField multi = new TestRuntimeField("multi", "multi", List.of(
149+
new TestRuntimeField.TestRuntimeFieldType("multi.string", "string"),
150+
new TestRuntimeField.TestRuntimeFieldType("multi.long", "long")));
127151

128-
FieldTypeLookup fieldTypeLookup = new FieldTypeLookup("_doc", List.of(concrete), emptyList(), List.of(runtime));
152+
FieldTypeLookup fieldTypeLookup = new FieldTypeLookup("_doc", List.of(concrete), emptyList(), List.of(runtime, runtimeLong, multi));
129153
assertThat(fieldTypeLookup.get("concrete"), instanceOf(MockFieldMapper.FakeFieldType.class));
130-
assertThat(fieldTypeLookup.get("runtime"), instanceOf(TestRuntimeField.class));
154+
assertThat(fieldTypeLookup.get("string"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
155+
assertThat(fieldTypeLookup.get("string").typeName(), equalTo("type"));
156+
assertThat(fieldTypeLookup.get("multi"), nullValue());
157+
assertThat(fieldTypeLookup.get("multi.string"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
158+
assertThat(fieldTypeLookup.get("multi.string").typeName(), equalTo("string"));
159+
assertThat(fieldTypeLookup.get("multi.long"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
160+
assertThat(fieldTypeLookup.get("multi.long").typeName(), equalTo("long"));
161+
assertThat(fieldTypeLookup.get("multi.outside"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
162+
assertThat(fieldTypeLookup.get("multi.outside").typeName(), equalTo("date"));
163+
assertThat(fieldTypeLookup.get("multi.anything"), nullValue());
131164
}
132165

133166
public void testRuntimeFieldsOverrideConcreteFields() {
134167
FlattenedFieldMapper flattened = createFlattenedMapper("flattened");
135168
MockFieldMapper field = new MockFieldMapper("field");
136169
MockFieldMapper subfield = new MockFieldMapper("object.subfield");
137170
MockFieldMapper concrete = new MockFieldMapper("concrete");
138-
TestRuntimeField fieldOverride = new TestRuntimeField("field", "type");
139-
TestRuntimeField subfieldOverride = new TestRuntimeField("object.subfield", "type");
171+
TestRuntimeField fieldOverride = new TestRuntimeField("field", "string");
172+
TestRuntimeField subfieldOverride = new TestRuntimeField("object", "multi",
173+
Collections.singleton(new TestRuntimeField.TestRuntimeFieldType("object.subfield", "leaf")));
140174
TestRuntimeField runtime = new TestRuntimeField("runtime", "type");
175+
TestRuntimeField flattenedRuntime = new TestRuntimeField("flattened.runtime", "type");
141176

142177
FieldTypeLookup fieldTypeLookup = new FieldTypeLookup("_doc", List.of(field, concrete, subfield, flattened), emptyList(),
143-
List.of(fieldOverride, runtime, subfieldOverride));
144-
assertThat(fieldTypeLookup.get("field"), instanceOf(TestRuntimeField.class));
145-
assertThat(fieldTypeLookup.get("object.subfield"), instanceOf(TestRuntimeField.class));
178+
List.of(fieldOverride, runtime, subfieldOverride, flattenedRuntime));
179+
assertThat(fieldTypeLookup.get("field"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
180+
assertThat(fieldTypeLookup.get("field").typeName(), equalTo("string"));
181+
assertThat(fieldTypeLookup.get("object.subfield"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
182+
assertThat(fieldTypeLookup.get("object.subfield").typeName(), equalTo("leaf"));
146183
assertThat(fieldTypeLookup.get("concrete"), instanceOf(MockFieldMapper.FakeFieldType.class));
147-
assertThat(fieldTypeLookup.get("runtime"), instanceOf(TestRuntimeField.class));
184+
assertThat(fieldTypeLookup.get("runtime"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
185+
assertThat(fieldTypeLookup.get("runtime").typeName(), equalTo("type"));
186+
assertThat(fieldTypeLookup.get("flattened.anything"), instanceOf(FlattenedFieldMapper.KeyedFlattenedFieldType.class));
187+
assertThat(fieldTypeLookup.get("flattened.runtime"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
148188
}
149189

150190
public void testRuntimeFieldsSourcePaths() {
@@ -283,6 +323,54 @@ public void testMaxDynamicKeyDepth() {
283323
}
284324
}
285325

326+
public void testRuntimeFieldNameClashes() {
327+
{
328+
IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> new FieldTypeLookup(
329+
"_doc", Collections.emptySet(), Collections.emptySet(),
330+
List.of(new TestRuntimeField("field", "type"), new TestRuntimeField("field", "long"))));
331+
assertEquals(iae.getMessage(), "Found two runtime fields with same name [field]");
332+
}
333+
{
334+
TestRuntimeField multi = new TestRuntimeField("multi", "multi",
335+
Collections.singleton(new TestRuntimeField.TestRuntimeFieldType("multi.first", "leaf")));
336+
TestRuntimeField runtime = new TestRuntimeField("multi.first", "runtime");
337+
IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> new FieldTypeLookup(
338+
"_doc", Collections.emptySet(), Collections.emptySet(), List.of(multi, runtime)));
339+
assertEquals(iae.getMessage(), "Found two runtime fields with same name [multi.first]");
340+
}
341+
{
342+
TestRuntimeField multi = new TestRuntimeField("multi", "multi",
343+
List.of(new TestRuntimeField.TestRuntimeFieldType("multi", "leaf"),
344+
new TestRuntimeField.TestRuntimeFieldType("multi", "leaf")));
345+
346+
IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> new FieldTypeLookup(
347+
"_doc", Collections.emptySet(), Collections.emptySet(), List.of(multi)));
348+
assertEquals(iae.getMessage(), "Found two runtime fields with same name [multi]");
349+
}
350+
}
351+
352+
public void testRuntimeFieldNameOutsideContext() {
353+
{
354+
TestRuntimeField multi = new TestRuntimeField("multi", "multi",
355+
List.of(new TestRuntimeField.TestRuntimeFieldType("first", "leaf"),
356+
new TestRuntimeField.TestRuntimeFieldType("second", "leaf"),
357+
new TestRuntimeField.TestRuntimeFieldType("multi.third", "leaf")));
358+
IllegalStateException ise = expectThrows(IllegalStateException.class, () -> new FieldTypeLookup(
359+
"_doc", Collections.emptySet(), Collections.emptySet(), Collections.singletonList(multi)));
360+
assertEquals("Found sub-fields with name not belonging to the parent field they are part of [first, second]",
361+
ise.getMessage());
362+
}
363+
{
364+
TestRuntimeField multi = new TestRuntimeField("multi", "multi",
365+
List.of(new TestRuntimeField.TestRuntimeFieldType("multi.", "leaf"),
366+
new TestRuntimeField.TestRuntimeFieldType("multi.f", "leaf")));
367+
IllegalStateException ise = expectThrows(IllegalStateException.class, () -> new FieldTypeLookup(
368+
"_doc", Collections.emptySet(), Collections.emptySet(), Collections.singletonList(multi)));
369+
assertEquals("Found sub-fields with name not belonging to the parent field they are part of [multi.]",
370+
ise.getMessage());
371+
}
372+
}
373+
286374
private static FlattenedFieldMapper createFlattenedMapper(String fieldName) {
287375
return new FlattenedFieldMapper.Builder(fieldName).build(new ContentPath());
288376
}

server/src/test/java/org/elasticsearch/index/mapper/MappingLookupTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void testOnlyRuntimeField() {
4848
assertEquals(0, size(mappingLookup.fieldMappers()));
4949
assertEquals(0, mappingLookup.objectMappers().size());
5050
assertNull(mappingLookup.getMapper("test"));
51-
assertThat(mappingLookup.fieldTypesLookup().get("test"), instanceOf(TestRuntimeField.class));
51+
assertThat(mappingLookup.fieldTypesLookup().get("test"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
5252
}
5353

5454
public void testRuntimeFieldLeafOverride() {
@@ -58,7 +58,7 @@ public void testRuntimeFieldLeafOverride() {
5858
assertThat(mappingLookup.getMapper("test"), instanceOf(MockFieldMapper.class));
5959
assertEquals(1, size(mappingLookup.fieldMappers()));
6060
assertEquals(0, mappingLookup.objectMappers().size());
61-
assertThat(mappingLookup.fieldTypesLookup().get("test"), instanceOf(TestRuntimeField.class));
61+
assertThat(mappingLookup.fieldTypesLookup().get("test"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
6262
}
6363

6464
public void testSubfieldOverride() {
@@ -73,7 +73,7 @@ public void testSubfieldOverride() {
7373
assertThat(mappingLookup.getMapper("object.subfield"), instanceOf(MockFieldMapper.class));
7474
assertEquals(1, size(mappingLookup.fieldMappers()));
7575
assertEquals(1, mappingLookup.objectMappers().size());
76-
assertThat(mappingLookup.fieldTypesLookup().get("object.subfield"), instanceOf(TestRuntimeField.class));
76+
assertThat(mappingLookup.fieldTypesLookup().get("object.subfield"), instanceOf(TestRuntimeField.TestRuntimeFieldType.class));
7777
}
7878

7979
public void testAnalyzers() throws IOException {

0 commit comments

Comments
 (0)