Skip to content

Commit efe19c2

Browse files
authored
Make document parsing aware of runtime fields (#65210)
Runtime fields are defined in a separate runtime section in the mappings. Since the runtime section was introduced, runtime fields are not taken into account when parsing documents. That means that if a document gets indexed that holds a field that's already defined as a runtime field, the field gets dynamically mapped as a concrete field although it will always be shadowed by the runtime field defined with the same name. A more sensible default would be to instead consider runtime fields like ordinary mapped fields, so a dynamic update is not necessary whenever a field is defined as part of the runtime section. As a consequence, the field does not get indexed. If users prefer to keep indexing the field although it is shadowed, we consider this an exception, and they can do so by mapping the field under properties explicitly. Relates to #62906
1 parent 299f24b commit efe19c2

File tree

5 files changed

+211
-29
lines changed

5 files changed

+211
-29
lines changed

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

+101-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.apache.lucene.document.Field;
2323
import org.apache.lucene.index.IndexableField;
24+
import org.apache.lucene.search.Query;
2425
import org.elasticsearch.ElasticsearchParseException;
2526
import org.elasticsearch.Version;
2627
import org.elasticsearch.common.Strings;
@@ -29,15 +30,19 @@
2930
import org.elasticsearch.common.time.DateFormatter;
3031
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
3132
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
33+
import org.elasticsearch.common.xcontent.XContentBuilder;
3234
import org.elasticsearch.common.xcontent.XContentHelper;
3335
import org.elasticsearch.common.xcontent.XContentParser;
3436
import org.elasticsearch.common.xcontent.XContentType;
3537
import org.elasticsearch.index.mapper.DynamicTemplate.XContentFieldType;
38+
import org.elasticsearch.index.query.QueryShardContext;
39+
import org.elasticsearch.search.lookup.SearchLookup;
3640

3741
import java.io.IOException;
3842
import java.time.format.DateTimeParseException;
3943
import java.util.ArrayList;
4044
import java.util.Collections;
45+
import java.util.Comparator;
4146
import java.util.Iterator;
4247
import java.util.List;
4348
import java.util.Objects;
@@ -224,7 +229,7 @@ static Mapping createDynamicUpdate(Mapping mapping, DocumentMapper docMapper, Li
224229
// We build a mapping by first sorting the mappers, so that all mappers containing a common prefix
225230
// will be processed in a contiguous block. When the prefix is no longer seen, we pop the extra elements
226231
// off the stack, merging them upwards into the existing mappers.
227-
Collections.sort(dynamicMappers, (Mapper o1, Mapper o2) -> o1.name().compareTo(o2.name()));
232+
dynamicMappers.sort(Comparator.comparing(Mapper::name));
228233
Iterator<Mapper> dynamicMapperItr = dynamicMappers.iterator();
229234
List<ObjectMapper> parentMappers = new ArrayList<>();
230235
Mapper firstUpdate = dynamicMapperItr.next();
@@ -829,14 +834,14 @@ private static Tuple<Integer, ObjectMapper> getDynamicParentMapper(ParseContext
829834
int pathsAdded = 0;
830835
ObjectMapper parent = mapper;
831836
for (int i = 0; i < paths.length-1; i++) {
832-
String currentPath = context.path().pathAsText(paths[i]);
833-
Mapper existingFieldMapper = context.docMapper().mappers().getMapper(currentPath);
834-
if (existingFieldMapper != null) {
835-
throw new MapperParsingException(
837+
String currentPath = context.path().pathAsText(paths[i]);
838+
Mapper existingFieldMapper = context.docMapper().mappers().getMapper(currentPath);
839+
if (existingFieldMapper != null) {
840+
throw new MapperParsingException(
836841
"Could not dynamically add mapping for field [{}]. Existing mapping for [{}] must be of type object but found [{}].",
837842
null, String.join(".", paths), currentPath, existingFieldMapper.typeName());
838-
}
839-
mapper = context.docMapper().mappers().objectMappers().get(currentPath);
843+
}
844+
mapper = context.docMapper().mappers().objectMappers().get(currentPath);
840845
if (mapper == null) {
841846
// One mapping is missing, check if we are allowed to create a dynamic one.
842847
ObjectMapper.Dynamic dynamic = dynamicOrDefault(parent, context);
@@ -905,7 +910,7 @@ private static Mapper getMapper(final ParseContext context, ObjectMapper objectM
905910

906911
for (int i = 0; i < subfields.length - 1; ++i) {
907912
mapper = objectMapper.getMapper(subfields[i]);
908-
if (mapper == null || (mapper instanceof ObjectMapper) == false) {
913+
if (mapper instanceof ObjectMapper == false) {
909914
return null;
910915
}
911916
objectMapper = (ObjectMapper)mapper;
@@ -915,6 +920,93 @@ private static Mapper getMapper(final ParseContext context, ObjectMapper objectM
915920
+ mapper.name() + "]");
916921
}
917922
}
918-
return objectMapper.getMapper(subfields[subfields.length - 1]);
923+
String leafName = subfields[subfields.length - 1];
924+
mapper = objectMapper.getMapper(leafName);
925+
if (mapper != null) {
926+
return mapper;
927+
}
928+
//concrete fields take the precedence over runtime fields when parsing documents, though when a field is defined as runtime field
929+
//only, and not under properties, it is ignored when it is sent as part of _source
930+
RuntimeFieldType runtimeFieldType = context.docMapper().mapping().root.getRuntimeFieldType(fieldPath);
931+
if (runtimeFieldType != null) {
932+
return new NoOpFieldMapper(leafName, runtimeFieldType);
933+
}
934+
return null;
935+
}
936+
937+
private static class NoOpFieldMapper extends FieldMapper {
938+
NoOpFieldMapper(String simpleName, RuntimeFieldType runtimeField) {
939+
super(simpleName, new MappedFieldType(runtimeField.name(), false, false, false, TextSearchInfo.NONE, Collections.emptyMap()) {
940+
@Override
941+
public ValueFetcher valueFetcher(QueryShardContext context, SearchLookup searchLookup, String format) {
942+
throw new UnsupportedOperationException();
943+
}
944+
945+
@Override
946+
public String typeName() {
947+
throw new UnsupportedOperationException();
948+
}
949+
950+
@Override
951+
public Query termQuery(Object value, QueryShardContext context) {
952+
throw new UnsupportedOperationException();
953+
}
954+
}, MultiFields.empty(), CopyTo.empty());
955+
}
956+
957+
@Override
958+
protected void parseCreateField(ParseContext context) throws IOException {
959+
//field defined as runtime field, don't index anything
960+
}
961+
962+
@Override
963+
public String name() {
964+
throw new UnsupportedOperationException();
965+
}
966+
967+
@Override
968+
public String typeName() {
969+
throw new UnsupportedOperationException();
970+
}
971+
972+
@Override
973+
public MappedFieldType fieldType() {
974+
throw new UnsupportedOperationException();
975+
}
976+
977+
@Override
978+
public MultiFields multiFields() {
979+
throw new UnsupportedOperationException();
980+
}
981+
982+
@Override
983+
public Iterator<Mapper> iterator() {
984+
throw new UnsupportedOperationException();
985+
}
986+
987+
@Override
988+
protected void doValidate(MappingLookup mappers) {
989+
throw new UnsupportedOperationException();
990+
}
991+
992+
@Override
993+
protected void checkIncomingMergeType(FieldMapper mergeWith) {
994+
throw new UnsupportedOperationException();
995+
}
996+
997+
@Override
998+
public Builder getMergeBuilder() {
999+
throw new UnsupportedOperationException();
1000+
}
1001+
1002+
@Override
1003+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
1004+
throw new UnsupportedOperationException();
1005+
}
1006+
1007+
@Override
1008+
protected String contentType() {
1009+
throw new UnsupportedOperationException();
1010+
}
9191011
}
9201012
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ Collection<RuntimeFieldType> runtimeFieldTypes() {
274274
return runtimeFieldTypes.values();
275275
}
276276

277+
RuntimeFieldType getRuntimeFieldType(String name) {
278+
return runtimeFieldTypes.get(name);
279+
}
280+
277281
public Mapper.Builder findTemplateBuilder(ParseContext context, String name, XContentFieldType matchType) {
278282
return findTemplateBuilder(context, name, matchType, null);
279283
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ private void fetch(TermsLookup termsLookup, Client client, ActionListener<List<O
444444
client.get(getRequest, ActionListener.delegateFailure(actionListener, (delegatedListener, getResponse) -> {
445445
List<Object> terms = new ArrayList<>();
446446
if (getResponse.isSourceEmpty() == false) { // extract terms only if the doc source exists
447-
List<Object> extractedValues = XContentMapValues.extractRawValues(termsLookup.path(), getResponse.getSourceAsMap());
447+
List<Object> extractedValues = XContentMapValues. extractRawValues(termsLookup.path(), getResponse.getSourceAsMap());
448448
terms.addAll(extractedValues);
449449
}
450450
delegatedListener.onResponse(terms);

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

+73-10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.elasticsearch.common.Strings;
2828
import org.elasticsearch.common.bytes.BytesArray;
2929
import org.elasticsearch.common.bytes.BytesReference;
30+
import org.elasticsearch.common.xcontent.XContentBuilder;
3031
import org.elasticsearch.common.xcontent.XContentFactory;
3132
import org.elasticsearch.common.xcontent.XContentParser;
3233
import org.elasticsearch.common.xcontent.XContentType;
@@ -59,20 +60,82 @@ protected Collection<? extends Plugin> getPlugins() {
5960
return org.elasticsearch.common.collect.List.of(new DocumentParserTestsPlugin(), new TestRuntimeField.Plugin());
6061
}
6162

62-
public void testDynamicUpdateWithRuntimeField() throws Exception {
63+
public void testParseWithRuntimeField() throws Exception {
6364
DocumentMapper mapper = createDocumentMapper(runtimeFieldMapping(b -> b.field("type", "test")));
64-
ParsedDocument doc = mapper.parse(source(b -> b.field("test", "value")));
65-
RootObjectMapper root = doc.dynamicMappingsUpdate().root;
66-
assertEquals(0, root.runtimeFieldTypes().size());
67-
assertNotNull(root.getMapper("test"));
65+
ParsedDocument doc = mapper.parse(source(b -> b.field("field", "value")));
66+
//field defined as runtime field but not under properties: no dynamic updates, the field does not get indexed
67+
assertNull(doc.dynamicMappingsUpdate());
68+
assertNull(doc.rootDoc().getField("field"));
6869
}
6970

70-
public void testDynamicUpdateWithRuntimeFieldSameName() throws Exception {
71-
DocumentMapper mapper = createDocumentMapper(runtimeFieldMapping(b -> b.field("type", "test")));
71+
public void testParseWithShadowedField() throws Exception {
72+
XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject("_doc");
73+
builder.startObject("runtime");
74+
builder.startObject("field").field("type", "test").endObject();
75+
builder.endObject();
76+
builder.startObject("properties");
77+
builder.startObject("field").field("type", "keyword").endObject();
78+
builder.endObject().endObject().endObject();
79+
80+
DocumentMapper mapper = createDocumentMapper(builder);
7281
ParsedDocument doc = mapper.parse(source(b -> b.field("field", "value")));
73-
RootObjectMapper root = doc.dynamicMappingsUpdate().root;
74-
assertEquals(0, root.runtimeFieldTypes().size());
75-
assertNotNull(root.getMapper("field"));
82+
//field defined as runtime field as well as under properties: no dynamic updates, the field gets indexed
83+
assertNull(doc.dynamicMappingsUpdate());
84+
assertNotNull(doc.rootDoc().getField("field"));
85+
}
86+
87+
public void testParseWithRuntimeFieldDottedNameDisabledObject() throws Exception {
88+
XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject("_doc");
89+
builder.startObject("runtime");
90+
builder.startObject("path1.path2.path3.field").field("type", "test").endObject();
91+
builder.endObject();
92+
builder.startObject("properties");
93+
builder.startObject("path1").field("type", "object").field("enabled", false).endObject();
94+
builder.endObject().endObject().endObject();
95+
MapperService mapperService = createMapperService(builder);
96+
ParsedDocument doc = mapperService.documentMapper().parse(source(b -> {
97+
b.startObject("path1").startObject("path2").startObject("path3");
98+
b.field("field", "value");
99+
b.endObject().endObject().endObject();
100+
}));
101+
assertNull(doc.dynamicMappingsUpdate());
102+
assertNull(doc.rootDoc().getField("path1.path2.path3.field"));
103+
}
104+
105+
public void testParseWithShadowedSubField() throws Exception {
106+
XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject("_doc");
107+
builder.startObject("runtime");
108+
builder.startObject("field.keyword").field("type", "test").endObject();
109+
builder.endObject();
110+
builder.startObject("properties");
111+
builder.startObject("field").field("type", "text");
112+
builder.startObject("fields").startObject("keyword").field("type", "keyword").endObject().endObject();
113+
builder.endObject().endObject().endObject().endObject();
114+
115+
DocumentMapper mapper = createDocumentMapper(builder);
116+
ParsedDocument doc = mapper.parse(source(b -> b.field("field", "value")));
117+
//field defined as runtime field as well as under properties: no dynamic updates, the field gets indexed
118+
assertNull(doc.dynamicMappingsUpdate());
119+
assertNotNull(doc.rootDoc().getField("field"));
120+
assertNotNull(doc.rootDoc().getField("field.keyword"));
121+
}
122+
123+
public void testParseWithShadowedMultiField() throws Exception {
124+
XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject("_doc");
125+
builder.startObject("runtime");
126+
builder.startObject("field").field("type", "test").endObject();
127+
builder.endObject();
128+
builder.startObject("properties");
129+
builder.startObject("field").field("type", "text");
130+
builder.startObject("fields").startObject("keyword").field("type", "keyword").endObject().endObject();
131+
builder.endObject().endObject().endObject().endObject();
132+
133+
DocumentMapper mapper = createDocumentMapper(builder);
134+
ParsedDocument doc = mapper.parse(source(b -> b.field("field", "value")));
135+
//field defined as runtime field as well as under properties: no dynamic updates, the field gets indexed
136+
assertNull(doc.dynamicMappingsUpdate());
137+
assertNotNull(doc.rootDoc().getField("field"));
138+
assertNotNull(doc.rootDoc().getField("field.keyword"));
76139
}
77140

78141
public void testFieldDisabled() throws Exception {

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

+32-9
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,43 @@ public void testDynamicUpdateWithRuntimeField() throws Exception {
192192
Mapping merged = mapperService.documentMapper().mapping();
193193
assertNotNull(merged.root.getMapper("test"));
194194
assertEquals(1, merged.root.runtimeFieldTypes().size());
195-
assertEquals("field", merged.root.runtimeFieldTypes().iterator().next().name());
195+
assertNotNull(merged.root.getRuntimeFieldType("field"));
196196
}
197197

198-
public void testDynamicUpdateWithRuntimeFieldSameName() throws Exception {
199-
MapperService mapperService = createMapperService(runtimeFieldMapping(b -> b.field("type", "test")));
200-
ParsedDocument doc = mapperService.documentMapper().parse(source(b -> b.field("field", "value")));
201-
assertEquals("{\"_doc\":{\"properties\":{" +
202-
"\"field\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\",\"ignore_above\":256}}}}}}",
203-
Strings.toString(doc.dynamicMappingsUpdate().root));
198+
public void testDynamicUpdateWithRuntimeFieldDottedName() throws Exception {
199+
MapperService mapperService = createMapperService(runtimeMapping(
200+
b -> b.startObject("path1.path2.path3.field").field("type", "test").endObject()));
201+
ParsedDocument doc = mapperService.documentMapper().parse(source(b -> {
202+
b.startObject("path1").startObject("path2").startObject("path3");
203+
b.field("field", "value");
204+
b.endObject().endObject().endObject();
205+
}));
206+
RootObjectMapper root = doc.dynamicMappingsUpdate().root;
207+
assertEquals(0, root.runtimeFieldTypes().size());
208+
{
209+
//the runtime field is defined but the object structure is not, hence it is defined under properties
210+
Mapper path1 = root.getMapper("path1");
211+
assertThat(path1, instanceOf(ObjectMapper.class));
212+
Mapper path2 = ((ObjectMapper) path1).getMapper("path2");
213+
assertThat(path2, instanceOf(ObjectMapper.class));
214+
Mapper path3 = ((ObjectMapper) path2).getMapper("path3");
215+
assertThat(path3, instanceOf(ObjectMapper.class));
216+
assertFalse(path3.iterator().hasNext());
217+
}
218+
assertNull(doc.rootDoc().getField("path1.path2.path3.field"));
204219
merge(mapperService, dynamicMapping(doc.dynamicMappingsUpdate()));
205220
Mapping merged = mapperService.documentMapper().mapping();
206-
assertNotNull(merged.root.getMapper("field"));
221+
{
222+
Mapper path1 = merged.root.getMapper("path1");
223+
assertThat(path1, instanceOf(ObjectMapper.class));
224+
Mapper path2 = ((ObjectMapper) path1).getMapper("path2");
225+
assertThat(path2, instanceOf(ObjectMapper.class));
226+
Mapper path3 = ((ObjectMapper) path2).getMapper("path3");
227+
assertThat(path3, instanceOf(ObjectMapper.class));
228+
assertFalse(path3.iterator().hasNext());
229+
}
207230
assertEquals(1, merged.root.runtimeFieldTypes().size());
208-
assertEquals("field", merged.root.runtimeFieldTypes().iterator().next().name());
231+
assertNotNull(merged.root.getRuntimeFieldType("path1.path2.path3.field"));
209232
}
210233

211234
public void testIncremental() throws Exception {

0 commit comments

Comments
 (0)