-
Notifications
You must be signed in to change notification settings - Fork 24.9k
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
Add 'flatten' parameter to object mappers #78997
Changes from all commits
f54f87a
578179d
28233e3
2b8b94f
75ca57c
4ee36b4
81a3613
4a47404
36c6701
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
pr: 78997 | ||
summary: Add 'flatten' parameter to object mappers | ||
area: Mapping | ||
type: enhancement | ||
issues: [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,23 +17,22 @@ | |
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.common.time.DateFormatter; | ||
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; | ||
import org.elasticsearch.xcontent.NamedXContentRegistry; | ||
import org.elasticsearch.xcontent.XContentBuilder; | ||
import org.elasticsearch.common.xcontent.XContentHelper; | ||
import org.elasticsearch.xcontent.XContentParser; | ||
import org.elasticsearch.xcontent.XContentType; | ||
import org.elasticsearch.core.Tuple; | ||
import org.elasticsearch.index.IndexSettings; | ||
import org.elasticsearch.index.analysis.IndexAnalyzers; | ||
import org.elasticsearch.index.fielddata.IndexFieldDataCache; | ||
import org.elasticsearch.index.query.SearchExecutionContext; | ||
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; | ||
import org.elasticsearch.search.lookup.SearchLookup; | ||
import org.elasticsearch.xcontent.NamedXContentRegistry; | ||
import org.elasticsearch.xcontent.XContentBuilder; | ||
import org.elasticsearch.xcontent.XContentParser; | ||
import org.elasticsearch.xcontent.XContentType; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.HashMap; | ||
import java.util.Iterator; | ||
import java.util.LinkedList; | ||
|
@@ -102,8 +101,7 @@ public ParsedDocument parseDocument(SourceToParse source, MappingLookup mappingL | |
context.reorderParentAndGetDocs(), | ||
context.sourceToParse().source(), | ||
context.sourceToParse().getXContentType(), | ||
createDynamicUpdate(mappingLookup, | ||
context.getDynamicMappers(), context.getDynamicRuntimeFields()) | ||
createDynamicUpdate(context) | ||
); | ||
} | ||
|
||
|
@@ -250,171 +248,21 @@ private static String[] splitAndValidatePath(String fullFieldPath) { | |
} | ||
} | ||
|
||
/** | ||
* Creates a Mapping containing any dynamically added fields, or returns null if there were no dynamic mappings. | ||
*/ | ||
static Mapping createDynamicUpdate(MappingLookup mappingLookup, | ||
List<Mapper> dynamicMappers, | ||
List<RuntimeField> dynamicRuntimeFields) { | ||
if (dynamicMappers.isEmpty() && dynamicRuntimeFields.isEmpty()) { | ||
static Mapping createDynamicUpdate(DocumentParserContext context) { | ||
if (context.getDynamicMappers().isEmpty() && context.getDynamicRuntimeFields().isEmpty()) { | ||
return null; | ||
} | ||
RootObjectMapper root; | ||
if (dynamicMappers.isEmpty() == false) { | ||
root = createDynamicUpdate(mappingLookup, dynamicMappers); | ||
root.fixRedundantIncludes(); | ||
} else { | ||
root = mappingLookup.getMapping().getRoot().copyAndReset(); | ||
} | ||
root.addRuntimeFields(dynamicRuntimeFields); | ||
return mappingLookup.getMapping().mappingUpdate(root); | ||
} | ||
|
||
private static RootObjectMapper createDynamicUpdate(MappingLookup mappingLookup, | ||
List<Mapper> dynamicMappers) { | ||
|
||
// We build a mapping by first sorting the mappers, so that all mappers containing a common prefix | ||
// will be processed in a contiguous block. When the prefix is no longer seen, we pop the extra elements | ||
// off the stack, merging them upwards into the existing mappers. | ||
dynamicMappers.sort(Comparator.comparing(Mapper::name)); | ||
Iterator<Mapper> dynamicMapperItr = dynamicMappers.iterator(); | ||
List<ObjectMapper> parentMappers = new ArrayList<>(); | ||
Mapper firstUpdate = dynamicMapperItr.next(); | ||
parentMappers.add(createUpdate(mappingLookup.getMapping().getRoot(), splitAndValidatePath(firstUpdate.name()), 0, firstUpdate)); | ||
Mapper previousMapper = null; | ||
while (dynamicMapperItr.hasNext()) { | ||
Mapper newMapper = dynamicMapperItr.next(); | ||
if (previousMapper != null && newMapper.name().equals(previousMapper.name())) { | ||
// We can see the same mapper more than once, for example, if we had foo.bar and foo.baz, where | ||
// foo did not yet exist. This will create 2 copies in dynamic mappings, which should be identical. | ||
// Here we just skip over the duplicates, but we merge them to ensure there are no conflicts. | ||
newMapper.merge(previousMapper); | ||
continue; | ||
} | ||
previousMapper = newMapper; | ||
String[] nameParts = splitAndValidatePath(newMapper.name()); | ||
|
||
// We first need the stack to only contain mappers in common with the previously processed mapper | ||
// For example, if the first mapper processed was a.b.c, and we now have a.d, the stack will contain | ||
// a.b, and we want to merge b back into the stack so it just contains a | ||
int i = removeUncommonMappers(parentMappers, nameParts); | ||
|
||
// Then we need to add back mappers that may already exist within the stack, but are not on it. | ||
// For example, if we processed a.b, followed by an object mapper a.c.d, and now are adding a.c.d.e | ||
// then the stack will only have a on it because we will have already merged a.c.d into the stack. | ||
// So we need to pull a.c, followed by a.c.d, onto the stack so e can be added to the end. | ||
i = expandCommonMappers(parentMappers, nameParts, i); | ||
|
||
// If there are still parents of the new mapper which are not on the stack, we need to pull them | ||
// from the existing mappings. In order to maintain the invariant that the stack only contains | ||
// fields which are updated, we cannot simply add the existing mappers to the stack, since they | ||
// may have other subfields which will not be updated. Instead, we pull the mapper from the existing | ||
// mappings, and build an update with only the new mapper and its parents. This then becomes our | ||
// "new mapper", and can be added to the stack. | ||
if (i < nameParts.length - 1) { | ||
newMapper = createExistingMapperUpdate(parentMappers, nameParts, i, mappingLookup, newMapper); | ||
} | ||
|
||
if (newMapper instanceof ObjectMapper) { | ||
parentMappers.add((ObjectMapper) newMapper); | ||
} else { | ||
addToLastMapper(parentMappers, newMapper, true); | ||
} | ||
RootObjectMapper.Builder rootBuilder = context.updateRoot(); | ||
for (Mapper mapper : context.getDynamicMappers()) { | ||
splitAndValidatePath(mapper.name()); | ||
rootBuilder.addDynamic(mapper.name(), null, mapper, context); | ||
} | ||
popMappers(parentMappers, 1, true); | ||
assert parentMappers.size() == 1; | ||
return (RootObjectMapper) parentMappers.get(0); | ||
} | ||
|
||
private static void popMappers(List<ObjectMapper> parentMappers, int keepBefore, boolean merge) { | ||
assert keepBefore >= 1; // never remove the root mapper | ||
// pop off parent mappers not needed by the current mapper, | ||
// merging them backwards since they are immutable | ||
for (int i = parentMappers.size() - 1; i >= keepBefore; --i) { | ||
addToLastMapper(parentMappers, parentMappers.remove(i), merge); | ||
for (RuntimeField runtimeField : context.getDynamicRuntimeFields()) { | ||
rootBuilder.addRuntimeField(runtimeField); | ||
} | ||
} | ||
|
||
/** | ||
* Adds a mapper as an update into the last mapper. If merge is true, the new mapper | ||
* will be merged in with other child mappers of the last parent, otherwise it will be a new update. | ||
*/ | ||
private static void addToLastMapper(List<ObjectMapper> parentMappers, Mapper mapper, boolean merge) { | ||
assert parentMappers.size() >= 1; | ||
int lastIndex = parentMappers.size() - 1; | ||
ObjectMapper withNewMapper = parentMappers.get(lastIndex).mappingUpdate(mapper); | ||
if (merge) { | ||
withNewMapper = parentMappers.get(lastIndex).merge(withNewMapper); | ||
} | ||
parentMappers.set(lastIndex, withNewMapper); | ||
} | ||
|
||
/** | ||
* Removes mappers that exist on the stack, but are not part of the path of the current nameParts, | ||
* Returns the next unprocessed index from nameParts. | ||
*/ | ||
private static int removeUncommonMappers(List<ObjectMapper> parentMappers, String[] nameParts) { | ||
int keepBefore = 1; | ||
while (keepBefore < parentMappers.size() && | ||
parentMappers.get(keepBefore).simpleName().equals(nameParts[keepBefore - 1])) { | ||
++keepBefore; | ||
} | ||
popMappers(parentMappers, keepBefore, true); | ||
return keepBefore - 1; | ||
} | ||
|
||
/** | ||
* Adds mappers from the end of the stack that exist as updates within those mappers. | ||
* Returns the next unprocessed index from nameParts. | ||
*/ | ||
private static int expandCommonMappers(List<ObjectMapper> parentMappers, String[] nameParts, int i) { | ||
ObjectMapper last = parentMappers.get(parentMappers.size() - 1); | ||
while (i < nameParts.length - 1 && last.getMapper(nameParts[i]) != null) { | ||
Mapper newLast = last.getMapper(nameParts[i]); | ||
assert newLast instanceof ObjectMapper; | ||
last = (ObjectMapper) newLast; | ||
parentMappers.add(last); | ||
++i; | ||
} | ||
return i; | ||
} | ||
|
||
/** | ||
* Creates an update for intermediate object mappers that are not on the stack, but parents of newMapper. | ||
*/ | ||
private static ObjectMapper createExistingMapperUpdate(List<ObjectMapper> parentMappers, String[] nameParts, int i, | ||
MappingLookup mappingLookup, Mapper newMapper) { | ||
String updateParentName = nameParts[i]; | ||
final ObjectMapper lastParent = parentMappers.get(parentMappers.size() - 1); | ||
if (parentMappers.size() > 1) { | ||
// only prefix with parent mapper if the parent mapper isn't the root (which has a fake name) | ||
updateParentName = lastParent.name() + '.' + nameParts[i]; | ||
} | ||
ObjectMapper updateParent = mappingLookup.objectMappers().get(updateParentName); | ||
assert updateParent != null : updateParentName + " doesn't exist"; | ||
return createUpdate(updateParent, nameParts, i + 1, newMapper); | ||
} | ||
|
||
/** | ||
* Build an update for the parent which will contain the given mapper and any intermediate fields. | ||
*/ | ||
private static ObjectMapper createUpdate(ObjectMapper parent, String[] nameParts, int i, Mapper mapper) { | ||
List<ObjectMapper> parentMappers = new ArrayList<>(); | ||
ObjectMapper previousIntermediate = parent; | ||
for (; i < nameParts.length - 1; ++i) { | ||
Mapper intermediate = previousIntermediate.getMapper(nameParts[i]); | ||
assert intermediate != null : "Field " + previousIntermediate.name() + " does not have a subfield " + nameParts[i]; | ||
assert intermediate instanceof ObjectMapper; | ||
parentMappers.add((ObjectMapper) intermediate); | ||
previousIntermediate = (ObjectMapper) intermediate; | ||
} | ||
if (parentMappers.isEmpty() == false) { | ||
// add the new mapper to the stack, and pop down to the original parent level | ||
addToLastMapper(parentMappers, mapper, false); | ||
popMappers(parentMappers, 1, false); | ||
mapper = parentMappers.get(0); | ||
} | ||
return parent.mappingUpdate(mapper); | ||
RootObjectMapper root = rootBuilder.build(MapperBuilderContext.ROOT); | ||
root.fixRedundantIncludes(); | ||
return context.mappingLookup().getMapping().mappingUpdate(root); | ||
} | ||
|
||
static void parseObjectOrNested(DocumentParserContext context, ObjectMapper mapper) throws IOException { | ||
|
@@ -679,12 +527,24 @@ private static void parseValue(final DocumentParserContext context, ObjectMapper | |
if (mapper != null) { | ||
parseObjectOrField(context, mapper); | ||
} else { | ||
currentFieldName = paths[paths.length - 1]; | ||
Tuple<Integer, ObjectMapper> parentMapperTuple = getDynamicParentMapper(context, paths, parentMapper); | ||
parentMapper = parentMapperTuple.v2(); | ||
parseDynamicValue(context, parentMapper, currentFieldName, token); | ||
for (int i = 0; i < parentMapperTuple.v1(); i++) { | ||
context.path().remove(); | ||
if (parentMapper.flatten) { | ||
parseDynamicValue(context, parentMapper, currentFieldName, token); | ||
} else { | ||
Tuple<Integer, ObjectMapper> parentMapperTuple = getDynamicParentMapper(context, paths, parentMapper); | ||
parentMapper = parentMapperTuple.v2(); | ||
int pathLength = parentMapperTuple.v1(); | ||
// If our dynamic parent mapper is flattened, then we can't just assume that our name | ||
// is the last path part. We instead need to construct it by concatenating all path | ||
// parts from the parent mapper onwards. | ||
StringBuilder compositeFieldName = new StringBuilder(paths[pathLength]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part allows us to handle deeply nested object paths that become flattened at some arbitrary point. |
||
while (pathLength < paths.length - 1) { | ||
pathLength++; | ||
compositeFieldName.append(".").append(paths[pathLength]); | ||
} | ||
parseDynamicValue(context, parentMapper, compositeFieldName.toString(), token); | ||
for (int i = 0; i < parentMapperTuple.v1(); i++) { | ||
context.path().remove(); | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -813,6 +673,9 @@ private static Tuple<Integer, ObjectMapper> getDynamicParentMapper(DocumentParse | |
context.path().add(paths[i]); | ||
pathsAdded++; | ||
parent = mapper; | ||
if (parent.flatten) { | ||
break; | ||
} | ||
} | ||
return new Tuple<>(pathsAdded, mapper); | ||
} | ||
|
@@ -859,6 +722,12 @@ private static Mapper getMapper(final DocumentParserContext context, | |
return mapper; | ||
} | ||
|
||
// Is the full name of the mapper a direct child of this object? | ||
mapper = objectMapper.getMapper(fieldPath); | ||
if (mapper != null) { | ||
return mapper; | ||
} | ||
|
||
for (int i = 0; i < subfields.length - 1; ++i) { | ||
mapper = objectMapper.getMapper(subfields[i]); | ||
if (mapper instanceof ObjectMapper == false) { | ||
|
@@ -976,7 +845,14 @@ protected String contentType() { | |
|
||
private static class NoOpObjectMapper extends ObjectMapper { | ||
NoOpObjectMapper(String name, String fullPath) { | ||
super(name, fullPath, new Explicit<>(true, false), Dynamic.RUNTIME, Collections.emptyMap()); | ||
super( | ||
name, | ||
fullPath, | ||
new Explicit<>(true, false), | ||
false, | ||
Dynamic.RUNTIME, | ||
Collections.emptyMap() | ||
); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
|
||
package org.elasticsearch.index.mapper; | ||
|
||
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.xcontent.ToXContentFragment; | ||
|
||
import java.util.Map; | ||
|
@@ -66,4 +67,8 @@ public final String simpleName() { | |
*/ | ||
public abstract void validate(MappingLookup mappers); | ||
|
||
@Override | ||
public String toString() { | ||
return name() + ":" + Strings.toString(this); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not strictly related, but it made debugging this so much easier that I thought it worth leaving in. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The vast bulk of this logic is now moved into the new
ObjectMapper.Builder#addDynamic()
method. Instead of creating concrete object mappers and recursively merging them, instead we pass each mapper to a root object builder. The builder checks to see if there are any dots in the mapper's field name, and if there are and the object is not flattened then we strip off the first object name, create the relevant object builder if it doesn't already exist, and pass the shortened name along with the mapper down to the next level. If the object is flattened, then we just add the mapper directly underneath it.