Skip to content

Commit 62de8a5

Browse files
authored
Rehydrate vectors at the deepest available object in the JSON path (#131663)
This change ensures that when restoring vectors removed from the source, we locate the deepest existing object along the JSON path to rehydrate them accurately.
1 parent a345f56 commit 62de8a5

File tree

6 files changed

+441
-465
lines changed

6 files changed

+441
-465
lines changed

server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.elasticsearch.core.TimeValue;
2222

2323
import java.util.ArrayList;
24+
import java.util.Arrays;
2425
import java.util.Collections;
2526
import java.util.HashMap;
2627
import java.util.List;
@@ -574,4 +575,118 @@ public static String[] nodeStringArrayValue(Object node) {
574575
return Strings.splitStringByCommaToArray(node.toString());
575576
}
576577
}
578+
579+
public static void insertValue(String path, Map<?, ?> map, Object newValue) {
580+
insertValue(path, map, newValue, true);
581+
}
582+
583+
/**
584+
* <p>
585+
* Insert or replace the path's value in the map with the provided new value. The map will be modified in-place.
586+
* If the complete path does not exist in the map, it will be added to the deepest (sub-)map possible.
587+
* </p>
588+
* <p>
589+
* For example, given the map:
590+
* </p>
591+
* <pre>
592+
* {
593+
* "path1": {
594+
* "path2": {
595+
* "key1": "value1"
596+
* }
597+
* }
598+
* }
599+
* </pre>
600+
* <p>
601+
* And the caller wanted to insert {@code "path1.path2.path3.key2": "value2"}, the method would emit the modified map:
602+
* </p>
603+
* <pre>
604+
* {
605+
* "path1": {
606+
* "path2": {
607+
* "key1": "value1",
608+
* "path3.key2": "value2"
609+
* }
610+
* }
611+
* }
612+
* </pre>
613+
*
614+
* @param path the value's path in the map.
615+
* @param map the map to search and modify in-place.
616+
* @param newValue the new value to assign to the path.
617+
* @param failOnMultiMap whether the insertion should fail with an {@link IllegalArgumentException}
618+
* if the path can be resolved in multiple ways.
619+
*
620+
* @throws IllegalArgumentException If either the path cannot be fully traversed.
621+
*/
622+
public static void insertValue(String path, Map<?, ?> map, Object newValue, boolean failOnMultiMap) {
623+
String[] pathElements = path.split("\\.");
624+
if (pathElements.length == 0) {
625+
return;
626+
}
627+
628+
List<SuffixMap> suffixMaps = extractSuffixMaps(pathElements, 0, map);
629+
if (suffixMaps.isEmpty()) {
630+
// This should never happen. Throw in case it does for some reason.
631+
throw new IllegalStateException("extractSuffixMaps returned an empty suffix map list");
632+
} else if (suffixMaps.size() > 1 && failOnMultiMap) {
633+
throw new IllegalArgumentException(
634+
"Path [" + path + "] could be inserted in " + suffixMaps.size() + " distinct ways, it is ambiguous which one to use"
635+
);
636+
}
637+
SuffixMap suffixMap = suffixMaps.getFirst();
638+
suffixMap.map().put(suffixMap.suffix(), newValue);
639+
}
640+
641+
record SuffixMap(String suffix, Map<String, Object> map) {}
642+
643+
private static List<SuffixMap> extractSuffixMaps(String[] pathElements, int index, Object currentValue) {
644+
if (currentValue instanceof List<?> valueList) {
645+
List<SuffixMap> suffixMaps = new ArrayList<>(valueList.size());
646+
for (Object o : valueList) {
647+
suffixMaps.addAll(extractSuffixMaps(pathElements, index, o));
648+
}
649+
650+
return suffixMaps;
651+
} else if (currentValue instanceof Map<?, ?>) {
652+
@SuppressWarnings("unchecked")
653+
Map<String, Object> map = (Map<String, Object>) currentValue;
654+
List<SuffixMap> suffixMaps = new ArrayList<>(map.size());
655+
656+
String key = pathElements[index];
657+
while (index < pathElements.length) {
658+
if (map.containsKey(key)) {
659+
if (index + 1 == pathElements.length) {
660+
// We found the complete path
661+
suffixMaps.add(new SuffixMap(key, map));
662+
} else {
663+
// We've matched that path partially, keep traversing to try to match it fully
664+
suffixMaps.addAll(extractSuffixMaps(pathElements, index + 1, map.get(key)));
665+
}
666+
}
667+
668+
if (++index < pathElements.length) {
669+
key += "." + pathElements[index];
670+
}
671+
}
672+
673+
if (suffixMaps.isEmpty()) {
674+
// We checked for all remaining elements in the path, and they do not exist. This means we found a leaf map that we should
675+
// add the value to.
676+
suffixMaps.add(new SuffixMap(key, map));
677+
}
678+
679+
return suffixMaps;
680+
} else {
681+
throw new IllegalArgumentException(
682+
"Path ["
683+
+ String.join(".", Arrays.copyOfRange(pathElements, 0, index))
684+
+ "] has value ["
685+
+ currentValue
686+
+ "] of type ["
687+
+ currentValue.getClass().getSimpleName()
688+
+ "], which cannot be traversed into further"
689+
);
690+
}
691+
}
577692
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ private static void applyPatches(String rootPath, Map<String, Object> map, List<
476476
for (SyntheticVectorPatch patch : patches) {
477477
if (patch instanceof LeafSyntheticVectorPath leaf) {
478478
String key = extractRelativePath(rootPath, leaf.fullPath());
479-
map.put(key, leaf.value());
479+
XContentMapValues.insertValue(key, map, leaf.value(), false);
480480
} else if (patch instanceof NestedSyntheticVectorPath nested) {
481481
String nestedPath = extractRelativePath(rootPath, nested.fullPath());
482482
List<Map<?, ?>> nestedMaps = XContentMapValues.extractNestedSources(nestedPath, map);

0 commit comments

Comments
 (0)