Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix missing intermediate object error when applying dynamic template #87622

Merged
merged 8 commits into from
Jun 17, 2022

Conversation

javanna
Copy link
Member

@javanna javanna commented Jun 13, 2022

When we apply dynamic mappings, we iterate over all the dynamic mappers retrieved from the DocumentParserContext, which are registered while parsing the document, and for each field look up their parent (going back multiple levels if necessary), and add it to the dynamic mapping update with the added/modified sub-field.

Dynamic mappers that are iterated through consist of a flat set of mappers, which can be both object mappers or field mappers. Most times, the object mappers from such set have no sub-fields as they are mapped as a result of parsing a document where the object appears for the first time, which also has sub-fields that are going to be added to the set of dynamic mappers themselves once they are parsed.

There are scenarios though where a dynamic template matches an object, and defines its structure including its subobjects as well as sub-fields. In that case the dynamically mapped object may hold sub-fields as well as define non-default values for dynamic, enabled or subobjects. The described situation was not well covered in tests so far, and is currently affected by a bug introduced with #81449. When an object mapper is dynamically mapped, it is added to the map of dynamic object mappers, which makes it discoverable for sub-fields that will need to be added to it later, as well as to the set of dynamic mappers so that it becomes part of the mappings in case the document being parsed defines no sub-fields for it. What is missing is to recursively add its sub-fields to the dynamic object mappers. As a result of missing this step, intermediate objects that were dynamically mapped are not made discoverable which causes a cryptic "Missing intermediate object" error.

This commit fixed the problem by recursively registering inner sub-objects to the dynamic mappers whenever an object mapper is added to the dynamic mappers. It also changes the "missing intermediate object" error to make it more evident that it's an internal error and not a user error: it is now an IllegalStateException instead of an IllegalArgumentException.

Closes #87513

@javanna javanna force-pushed the fix/dynamic_template_objects branch 2 times, most recently from a3a8981 to f73fa0d Compare June 14, 2022 10:24
@javanna javanna changed the title Clean up dynamic mappings Fix missing intermediate object error whil applying dynamic template Jun 14, 2022
@javanna javanna changed the title Fix missing intermediate object error whil applying dynamic template Fix missing intermediate object error when applying dynamic template Jun 14, 2022
@javanna javanna force-pushed the fix/dynamic_template_objects branch from f73fa0d to eb82193 Compare June 14, 2022 10:25
@javanna javanna added v8.3.1 :Search Foundations/Mapping Index mappings, including merging and defining field types >bug labels Jun 14, 2022
@javanna javanna marked this pull request as ready for review June 14, 2022 10:26
@elasticmachine elasticmachine added the Team:Search Meta label for search team label Jun 14, 2022
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-search (Team:Search)

@elasticsearchmachine
Copy link
Collaborator

Hi @javanna, I've created a changelog YAML for you.

When we apply dynamic mappings, we iterate over all the dynamic mappers retrieved from the DocumentParserContext, which are registered while parsing the document, and for each field look up their parent (going back multiple levels if necessary), and add it to the dynamic mapping update with the added/modified sub-field.

Dynamic mappers that are iterated through consist of a flat set of mappers, which can be both object mappers or field mappers. Most times, the object mappers from such set have no sub-fields as they are mapped as a result of parsing a document where the object appears for the first time, which also has sub-fields that are going to be added to the set of dynamic mappers themselves once they are parsed.

There are scenarios though where a dynamic template matches an object, and defines its structure including its subobjects as well as sub-fields. In that case the dynamically mapped object may hold sub-fields as well as define non-default values for dynamic, enabled or subobjects. The described situation was not well covered in tests so far, and is currently affected by a bug introduced with elastic#81449. When an object mapper is dynamically mapped, it is added to the map of dynamic object mappers, which makes it discoverable for sub-fields that will need to be added to it later, as well as to the set of dynamic mappers so that it becomes part of the mappings in case the document being parsed defines no sub-fields for it. What is missing is to recursively add its sub-fields to the dynamic object mappers. As a result of missing this step, intermediate objects that were dynamically mapped are not made discoverable which causes a cryptic "Missing intermediate object" error.

This commit fixed the problem by recursively registering inner sub-objects to the dynamic mappers whenever an object mapper is added to the dynamic mappers. It also changes the "missing intermediate object" error to make it more evident that it's an internal error and not a user error: it is now an IllegalStateException instead of an IllegalArgumentException.

Closes elastic#87513
@javanna javanna force-pushed the fix/dynamic_template_objects branch from 3a55470 to 76f0af5 Compare June 14, 2022 10:28
@@ -447,9 +447,10 @@ private static void throwOnCopyToOnObject(Mapper mapper, List<String> copyToFiel
);
}

private static void parseObject(final DocumentParserContext context, ObjectMapper mapper, String currentFieldName) throws IOException {
private static void parseObject(final DocumentParserContext context, ObjectMapper parentObjectMapper, String currentFieldName)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the changes in this file from now on are just renaming mapper -> parentObjectMapper

}
throw new IllegalArgumentException("Missing intermediate object " + fullChildName);
throw new IllegalStateException("Missing intermediate object " + fullName);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the only relevant change in this file: IllegalArgumentException -> IllegalStateException . All the rest is renaming child to parent.

@javanna javanna requested a review from romseygeek June 15, 2022 13:39
Copy link
Contributor

@romseygeek romseygeek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @javanna! I left a couple of comments.

String fullParentName = prefix == null ? parentName : prefix + "." + parentName;
ObjectMapper.Builder parentBuilder = findParentBuilder(fullParentName, context);
parentBuilder.addDynamic(name.substring(firstDotIndex + 1), fullParentName, mapper, context);
add(parentBuilder);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate that child maybe isn't the best name here, but I don't think that parent is very good either. Maybe immediateChild? It's a child of the object mapper that addDynamic is being called on, so I think calling it parent will just confuse things.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++ I see what you mean.

@@ -1383,4 +1383,376 @@ public void testSubobjectsFalseWithInnerNestedFromDynamicTemplate() {
exception.getRootCause().getMessage()
);
}

public void testDynamicSubobject() throws IOException {
MapperService mapperService = createMapperService(topMapping(b -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth building these mappings as a text block as well? I think it reads much more easily than xcontentbuilders

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the tricky bit is that we define only part of the mappings, and part is defined in topMapping. I would leave this for another time to be honest :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topMapping is just {"_doc":{ ... } } though. Up to you :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to spend time on fixing dynamic object mappers lookup in the context etc. over rewriting tests that work fine :)

@javanna javanna requested a review from romseygeek June 17, 2022 09:45
if (child != null) {
return child.newBuilder(context.indexSettings().getIndexVersionCreated());
private static ObjectMapper.Builder findObjectBuilder(String fullName, DocumentParserContext context) {
// does the parent mapper already exist? if so, use that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/parent/object

child = context.getDynamicObjectMapper(fullChildName);
if (child != null) {
return child.newBuilder(context.indexSettings().getIndexVersionCreated());
// has the parent mapper been added as a dynamic update already?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/parent/object/

@@ -1383,4 +1383,376 @@ public void testSubobjectsFalseWithInnerNestedFromDynamicTemplate() {
exception.getRootCause().getMessage()
);
}

public void testDynamicSubobject() throws IOException {
MapperService mapperService = createMapperService(topMapping(b -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topMapping is just {"_doc":{ ... } } though. Up to you :)

@javanna javanna requested a review from romseygeek June 17, 2022 10:37
Copy link
Contributor

@romseygeek romseygeek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks @javanna

@javanna javanna merged commit 28085cf into elastic:master Jun 17, 2022
javanna added a commit to javanna/elasticsearch that referenced this pull request Jun 17, 2022
…lastic#87622)

When we apply dynamic mappings, we iterate over all the dynamic mappers retrieved from the DocumentParserContext, which are registered while parsing the document, and for each field look up their parent (going back multiple levels if necessary), and add it to the dynamic mapping update with the added/modified sub-field.

Dynamic mappers that are iterated through consist of a flat set of mappers, which can be both object mappers or field mappers. Most times, the object mappers from such set have no sub-fields as they are mapped as a result of parsing a document where the object appears for the first time, which also has sub-fields that are going to be added to the set of dynamic mappers themselves once they are parsed.

There are scenarios though where a dynamic template matches an object, and defines its structure including its subobjects as well as sub-fields. In that case the dynamically mapped object may hold sub-fields as well as define non-default values for dynamic, enabled or subobjects. The described situation was not well covered in tests so far, and is currently affected by a bug introduced with elastic#81449. When an object mapper is dynamically mapped, it is added to the map of dynamic object mappers, which makes it discoverable for sub-fields that will need to be added to it later, as well as to the set of dynamic mappers so that it becomes part of the mappings in case the document being parsed defines no sub-fields for it. What is missing is to recursively add its sub-fields to the dynamic object mappers. As a result of missing this step, intermediate objects that were dynamically mapped are not made discoverable which causes a cryptic "Missing intermediate object" error.

This commit fixed the problem by recursively registering inner sub-objects to the dynamic mappers whenever an object mapper is added to the dynamic mappers. It also changes the "missing intermediate object" error to make it more evident that it's an internal error and not a user error: it is now an IllegalStateException instead of an IllegalArgumentException.

Closes elastic#87513
javanna added a commit that referenced this pull request Jun 17, 2022
…87622)

When we apply dynamic mappings, we iterate over all the dynamic mappers retrieved from the DocumentParserContext, which are registered while parsing the document, and for each field look up their parent (going back multiple levels if necessary), and add it to the dynamic mapping update with the added/modified sub-field.

Dynamic mappers that are iterated through consist of a flat set of mappers, which can be both object mappers or field mappers. Most times, the object mappers from such set have no sub-fields as they are mapped as a result of parsing a document where the object appears for the first time, which also has sub-fields that are going to be added to the set of dynamic mappers themselves once they are parsed.

There are scenarios though where a dynamic template matches an object, and defines its structure including its subobjects as well as sub-fields. In that case the dynamically mapped object may hold sub-fields as well as define non-default values for dynamic, enabled or subobjects. The described situation was not well covered in tests so far, and is currently affected by a bug introduced with #81449. When an object mapper is dynamically mapped, it is added to the map of dynamic object mappers, which makes it discoverable for sub-fields that will need to be added to it later, as well as to the set of dynamic mappers so that it becomes part of the mappings in case the document being parsed defines no sub-fields for it. What is missing is to recursively add its sub-fields to the dynamic object mappers. As a result of missing this step, intermediate objects that were dynamically mapped are not made discoverable which causes a cryptic "Missing intermediate object" error.

This commit fixed the problem by recursively registering inner sub-objects to the dynamic mappers whenever an object mapper is added to the dynamic mappers. It also changes the "missing intermediate object" error to make it more evident that it's an internal error and not a user error: it is now an IllegalStateException instead of an IllegalArgumentException.

Closes #87513
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>bug :Search Foundations/Mapping Index mappings, including merging and defining field types Team:Search Meta label for search team v8.3.1 v8.4.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Missing intermediate objects when applying dynamic template that targets an object
4 participants