Skip to content

Commit a4c159e

Browse files
original-brownbearjpountz
authored andcommitted
prevent duplicate fields when mixing parent and root nested includes (#27072)
Closes #26990
1 parent 3812d3c commit a4c159e

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed

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

+32
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,38 @@ public Builder dynamicTemplates(Collection<DynamicTemplate> templates) {
7474
return this;
7575
}
7676

77+
@Override
78+
public RootObjectMapper build(BuilderContext context) {
79+
fixRedundantIncludes(this, true);
80+
return super.build(context);
81+
}
82+
83+
/**
84+
* Removes redundant root includes in {@link ObjectMapper.Nested} trees to avoid duplicate
85+
* fields on the root mapper when {@code isIncludeInRoot} is {@code true} for a node that is
86+
* itself included into a parent node, for which either {@code isIncludeInRoot} is
87+
* {@code true} or which is transitively included in root by a chain of nodes with
88+
* {@code isIncludeInParent} returning {@code true}.
89+
* @param omb Builder whose children to check.
90+
* @param parentIncluded True iff node is a child of root or a node that is included in
91+
* root
92+
*/
93+
private static void fixRedundantIncludes(ObjectMapper.Builder omb, boolean parentIncluded) {
94+
for (Object mapper : omb.mappersBuilders) {
95+
if (mapper instanceof ObjectMapper.Builder) {
96+
ObjectMapper.Builder child = (ObjectMapper.Builder) mapper;
97+
Nested nested = child.nested;
98+
boolean isNested = nested.isNested();
99+
boolean includeInRootViaParent = parentIncluded && isNested && nested.isIncludeInParent();
100+
boolean includedInRoot = isNested && nested.isIncludeInRoot();
101+
if (includeInRootViaParent && includedInRoot) {
102+
child.nested = Nested.newNested(true, false);
103+
}
104+
fixRedundantIncludes(child, includeInRootViaParent || includedInRoot);
105+
}
106+
}
107+
}
108+
77109
@Override
78110
protected ObjectMapper createMapper(String name, String fullPath, boolean enabled, Nested nested, Dynamic dynamic,
79111
Map<String, Mapper> mappers, @Nullable Settings settings) {

core/src/test/java/org/elasticsearch/index/mapper/NestedObjectMapperTests.java

+64
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919

2020
package org.elasticsearch.index.mapper;
2121

22+
import java.util.HashMap;
23+
import java.util.HashSet;
24+
import org.apache.lucene.index.IndexableField;
2225
import org.elasticsearch.Version;
2326
import org.elasticsearch.common.compress.CompressedXContent;
2427
import org.elasticsearch.common.settings.Settings;
@@ -333,6 +336,67 @@ public void testMultiRootAndNested1() throws Exception {
333336
assertThat(doc.docs().get(6).getFields("nested1.nested2.field2").length, equalTo(4));
334337
}
335338

339+
/**
340+
* Checks that multiple levels of nested includes where a node is both directly and transitively
341+
* included in root by {@code include_in_root} and a chain of {@code include_in_parent} does not
342+
* lead to duplicate fields on the root document.
343+
*/
344+
public void testMultipleLevelsIncludeRoot1() throws Exception {
345+
String mapping = XContentFactory.jsonBuilder()
346+
.startObject().startObject("type").startObject("properties")
347+
.startObject("nested1").field("type", "nested").field("include_in_root", true).field("include_in_parent", true).startObject("properties")
348+
.startObject("nested2").field("type", "nested").field("include_in_root", true).field("include_in_parent", true)
349+
.endObject().endObject().endObject()
350+
.endObject().endObject().endObject().string();
351+
352+
DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse("type", new CompressedXContent(mapping));
353+
354+
ParsedDocument doc = docMapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
355+
.startObject().startArray("nested1")
356+
.startObject().startArray("nested2").startObject().field("foo", "bar")
357+
.endObject().endArray().endObject().endArray()
358+
.endObject()
359+
.bytes(),
360+
XContentType.JSON));
361+
362+
final Collection<IndexableField> fields = doc.rootDoc().getFields();
363+
assertThat(fields.size(), equalTo(new HashSet<>(fields).size()));
364+
}
365+
366+
/**
367+
* Same as {@link NestedObjectMapperTests#testMultipleLevelsIncludeRoot1()} but tests for the
368+
* case where the transitive {@code include_in_parent} and redundant {@code include_in_root}
369+
* happen on a chain of nodes that starts from a parent node that is not directly connected to
370+
* root by a chain of {@code include_in_parent}, i.e. that has {@code include_in_parent} set to
371+
* {@code false} and {@code include_in_root} set to {@code true}.
372+
*/
373+
public void testMultipleLevelsIncludeRoot2() throws Exception {
374+
String mapping = XContentFactory.jsonBuilder()
375+
.startObject().startObject("type").startObject("properties")
376+
.startObject("nested1").field("type", "nested")
377+
.field("include_in_root", true).field("include_in_parent", true).startObject("properties")
378+
.startObject("nested2").field("type", "nested")
379+
.field("include_in_root", true).field("include_in_parent", false).startObject("properties")
380+
.startObject("nested3").field("type", "nested")
381+
.field("include_in_root", true).field("include_in_parent", true)
382+
.endObject().endObject().endObject().endObject().endObject()
383+
.endObject().endObject().endObject().string();
384+
385+
DocumentMapper docMapper = createIndex("test").mapperService().documentMapperParser().parse("type", new CompressedXContent(mapping));
386+
387+
ParsedDocument doc = docMapper.parse(SourceToParse.source("test", "type", "1", XContentFactory.jsonBuilder()
388+
.startObject().startArray("nested1")
389+
.startObject().startArray("nested2")
390+
.startObject().startArray("nested3").startObject().field("foo", "bar")
391+
.endObject().endArray().endObject().endArray().endObject().endArray()
392+
.endObject()
393+
.bytes(),
394+
XContentType.JSON));
395+
396+
final Collection<IndexableField> fields = doc.rootDoc().getFields();
397+
assertThat(fields.size(), equalTo(new HashSet<>(fields).size()));
398+
}
399+
336400
public void testNestedArrayStrict() throws Exception {
337401
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type").startObject("properties")
338402
.startObject("nested1").field("type", "nested").field("dynamic", "strict").startObject("properties")

0 commit comments

Comments
 (0)