Repository: opensearch-project/OpenSearch
Pull request #17612: Add dfs transformation function in XContentMapValues
Author: login: jmazanec15
Labels: name: bug
description: Something isn't working
name: cicd
Created: 2025-03-17T19:02:13Z | Merged: 2025-03-20T16:04:09Z

### Description
In KNN, we are developing feature to remove vectors from source from documents before serialization. Originally, we were using the XContentMapValues.filter function to remove the vector fields from the source objects ([ref](https://github.com/opensearch-project/k-NN/blob/main/src/main/java/org/opensearch/knn/index/codec/KNN10010Codec/KNN10010DerivedSourceStoredFieldsWriter.java#L87-L88)). However, the problem with filter is that if a filter produces an empty object, then it will be removed from the array if its in an array. This alters the structure of the source map. Thus, putting the vectors back becomes very complex for nested cases (https://github.com/opensearch-project/k-NN/pull/2583).

After some discussion, we are pivoting to an approach that instead of filtering the fields, we will mask the field with a smaller representation on write, and vice versa on read. I added PoC here: https://github.com/jmazanec15/k-NN-1/tree/mask-derived-poc. This change adds a transform method to XContentMapValues that performs depth first traversal on a map, potentially applying transformations to different values along the way. I could add in k-NN plugin, but thought it might be useful in future in core. The DFS nature works well with the architecture of nested documents.

### Related Issues
Resolves https://github.com/opensearch-project/k-NN/issues/2377

### Check List
- [X] Functionality includes testing.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check [here](https://github.com/opensearch-project/OpenSearch/blob/main/CONTRIBUTING.md#developer-certificate-of-origin).



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 37eef849fe59b..ace34a4e4dc30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 - Change priority for scheduling reroute during timeout([#16445](https://github.com/opensearch-project/OpenSearch/pull/16445))
 - Renaming the node role search to warm ([#17573](https://github.com/opensearch-project/OpenSearch/pull/17573))
 - Introduce a new search node role to hold search only shards ([#17620](https://github.com/opensearch-project/OpenSearch/pull/17620))
+- Add dfs transformation function in XContentMapValues ([#17612](https://github.com/opensearch-project/OpenSearch/pull/17612))
 
 ### Dependencies
 - Bump `ch.qos.logback:logback-core` from 1.5.16 to 1.5.17 ([#17609](https://github.com/opensearch-project/OpenSearch/pull/17609))
diff --git a/server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java b/server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
index 7240252b51d83..8175c001cdcd5 100644
--- a/server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
+++ b/server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
@@ -43,9 +43,11 @@
 import org.opensearch.common.unit.TimeValue;
 import org.opensearch.core.common.Strings;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Deque;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -60,6 +62,8 @@
  */
 public class XContentMapValues {
 
+    private static final String TRANSFORMER_TRIE_LEAF_KEY = "$transformer";
+
     /**
      * Extracts raw values (string, int, and so on) based on the path provided returning all of them
      * as a single list.
@@ -621,4 +625,149 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @param inPlace If true, modify the source map directly; if false, create a copy
+     * @return Map with transformations applied
+     */
+    public static Map<String, Object> transform(
+        Map<String, Object> source,
+        Map<String, Function<Object, Object>> transformers,
+        boolean inPlace
+    ) {
+        return transform(transformers, inPlace).apply(source);
+    }
+
+    /**
+     * Returns function that performs a depth first traversal of a map and applies a transformation for each field
+     * matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
+     * the transformer for the shorter path is applied.
+     *
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                     the current value and returns a transformed value
+     * @param inPlace If true, modify the source map directly; if false, create a copy
+     * @return Function that takes a map and returns a transformed version of the map
+     */
+    public static Function<Map<String, Object>, Map<String, Object>> transform(
+        Map<String, Function<Object, Object>> transformers,
+        boolean inPlace
+    ) {
+        Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
+        return source -> {
+            Deque<TransformContext> stack = new ArrayDeque<>();
+            Map<String, Object> result = inPlace ? source : new HashMap<>(source);
+            stack.push(new TransformContext(result, transformerTrie));
+
+            processStack(stack, inPlace);
+            return result;
+        };
+    }
+
+    @SuppressWarnings("unchecked")
+    private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
+        Map<String, Object> trie = new HashMap<>();
+        for (Map.Entry<String, Function<Object, Object>> entry : transformers.entrySet()) {
+            String[] pathElements = entry.getKey().split("\\.");
+            Map<String, Object> subTrie = trie;
+            for (String pathElement : pathElements) {
+                subTrie = (Map<String, Object>) subTrie.computeIfAbsent(pathElement, k -> new HashMap<>());
+            }
+            subTrie.put(TRANSFORMER_TRIE_LEAF_KEY, entry.getValue());
+        }
+        return trie;
+    }
+
+    private static void processStack(Deque<TransformContext> stack, boolean inPlace) {
+        while (!stack.isEmpty()) {
+            TransformContext ctx = stack.pop();
+            processMap(ctx.map, ctx.trie, stack, inPlace);
+        }
+    }
+
+    private static void processMap(
+        Map<String, Object> currentMap,
+        Map<String, Object> currentTrie,
+        Deque<TransformContext> stack,
+        boolean inPlace
+    ) {
+        for (Map.Entry<String, Object> entry : currentMap.entrySet()) {
+            processEntry(entry, currentTrie, stack, inPlace);
+        }
+    }
+
+    private static void processEntry(
+        Map.Entry<String, Object> entry,
+        Map<String, Object> currentTrie,
+        Deque<TransformContext> stack,
+        boolean inPlace
+    ) {
+        String key = entry.getKey();
+        Object value = entry.getValue();
+
+        Object subTrieObj = currentTrie.get(key);
+        if (subTrieObj instanceof Map == false) {
+            return;
+        }
+        Map<String, Object> subTrie = nodeMapValue(subTrieObj, "transform");
+
+        // Apply transformation if available
+        Function<Object, Object> transformer = (Function<Object, Object>) subTrie.get(TRANSFORMER_TRIE_LEAF_KEY);
+        if (transformer != null) {
+            entry.setValue(transformer.apply(value));
+            return;
+        }
+
+        // Process nested structures
+        if (value instanceof Map) {
+            Map<String, Object> subMap = nodeMapValue(value, "transform");
+            if (inPlace == false) {
+                subMap = new HashMap<>(subMap);
+                entry.setValue(subMap);
+            }
+            stack.push(new TransformContext(subMap, subTrie));
+        } else if (value instanceof List<?> list) {
+            List<Object> subList = (List<Object>) list;
+            if (inPlace == false) {
+                subList = new ArrayList<>(list);
+                entry.setValue(subList);
+            }
+            processList(subList, subTrie, stack, inPlace);
+        }
+    }
+
+    private static void processList(
+        List<Object> list,
+        Map<String, Object> transformerTrie,
+        Deque<TransformContext> stack,
+        boolean inPlace
+    ) {
+        for (int i = list.size() - 1; i >= 0; i--) {
+            Object value = list.get(i);
+            if (value instanceof Map) {
+                Map<String, Object> subMap = nodeMapValue(value, "transform");
+                if (inPlace == false) {
+                    subMap = new HashMap<>(subMap);
+                    list.set(i, subMap);
+                }
+                stack.push(new TransformContext(subMap, transformerTrie));
+            }
+        }
+    }
+
+    private static class TransformContext {
+        Map<String, Object> map;
+        Map<String, Object> trie;
+
+        TransformContext(Map<String, Object> map, Map<String, Object> trie) {
+            this.map = map;
+            this.trie = trie;
+        }
+    }
 }
diff --git a/server/src/test/java/org/opensearch/common/xcontent/support/XContentMapValuesTests.java b/server/src/test/java/org/opensearch/common/xcontent/support/XContentMapValuesTests.java
index be194c070135a..08ee5d4e8d3a9 100644
--- a/server/src/test/java/org/opensearch/common/xcontent/support/XContentMapValuesTests.java
+++ b/server/src/test/java/org/opensearch/common/xcontent/support/XContentMapValuesTests.java
@@ -32,6 +32,7 @@
 
 package org.opensearch.common.xcontent.support;
 
+import org.opensearch.common.collect.MapBuilder;
 import org.opensearch.common.collect.Tuple;
 import org.opensearch.common.xcontent.XContentFactory;
 import org.opensearch.common.xcontent.XContentType;
@@ -48,9 +49,12 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.IntStream;
 
 import static java.util.Collections.emptySet;
 import static java.util.Collections.singleton;
@@ -629,6 +633,127 @@ public void testPrefix() {
         assertEquals(expected, filtered);
     }
 
+    public void testTransformFlat() {
+        Map<String, Object> mapToTransform = Map.of(
+            "test1",
+            "value_before",
+            "test2",
+            List.of("value_before", "value_before", "value_before")
+        );
+
+        Map<String, Function<Object, Object>> transformers = Map.of("test1", v -> "value_after", "test2", v -> "value_after");
+
+        Map<String, Object> expected = Map.of("test1", "value_after", "test2", "value_after");
+
+        Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, false);
+        assertEquals(expected, transformedMapped);
+    }
+
+    public void testTransformNested() {
+        Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder()
+            .put("test1", "value_before")
+            .put("test2", Map.of("nest2", "value_before"))
+            .put("test3", List.of(Map.of("nest3", "value_before"), Map.of("nest3", "value_before"), Map.of("nest3", "value_before")))
+            .put(
+                "test4",
+                List.of(
+                    Map.of(
+                        "nest4",
+                        List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before"), Map.of("nest5", "value_before")),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of(
+                        "nest4",
+                        List.of(
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before")
+                        ),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of("nest4", List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before")), "test5", "no_change")
+                )
+            )
+            .put("test6", null)
+            .immutableMap();
+
+        Iterator<String> test3Stream = IntStream.rangeClosed(1, 3).mapToObj(i -> "value_after" + i).toList().iterator();
+        Iterator<String> test4Stream = IntStream.rangeClosed(1, 9).mapToObj(i -> "value_after" + i).toList().iterator();
+        Map<String, Function<Object, Object>> transformers = Map.of(
+            "test1",
+            v -> "value_after",
+            "test2.nest2",
+            v -> "value_after",
+            "test3.nest3",
+            v -> test3Stream.next(),
+            "test4.nest4.nest5",
+            v -> test4Stream.next(),
+            "test6",
+            v -> v == null ? v : "value_after"
+        );
+
+        Map<String, Object> expected = MapBuilder.<String, Object>newMapBuilder()
+            .put("test1", "value_after")
+            .put("test2", Map.of("nest2", "value_after"))
+            .put("test3", List.of(Map.of("nest3", "value_after1"), Map.of("nest3", "value_after2"), Map.of("nest3", "value_after3")))
+            .put(
+                "test4",
+                List.of(
+                    Map.of(
+                        "nest4",
+                        List.of(Map.of("nest5", "value_after1"), Map.of("nest5", "value_after2"), Map.of("nest5", "value_after3")),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of(
+                        "nest4",
+                        List.of(
+                            Map.of("nest5", "value_after4"),
+                            Map.of("nest5", "value_after5"),
+                            Map.of("nest5", "value_after6"),
+                            Map.of("nest5", "value_after7")
+                        ),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of("nest4", List.of(Map.of("nest5", "value_after8"), Map.of("nest5", "value_after9")), "test5", "no_change")
+                )
+            )
+            .put("test6", null)
+            .immutableMap();
+
+        Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, false);
+        assertEquals(expected, transformedMapped);
+    }
+
+    public void testTransformInPlace() {
+        Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder().put("test1", "value_before").map();
+        Map<String, Function<Object, Object>> transformers = Map.of("test1", v -> "value_after");
+        Map<String, Object> expected = MapBuilder.<String, Object>newMapBuilder().put("test1", "value_after").immutableMap();
+
+        Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, true);
+        assertEquals(expected, transformedMapped);
+    }
+
+    public void testTransformSharedPaths() {
+        Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder()
+            .put("test", "value_before")
+            .put("test.nested", "nested_value_before")
+            .map();
+        Map<String, Function<Object, Object>> transformers = Map.of("test", v -> "value_after", "test.nested", v -> "nested_value_after");
+
+        Map<String, Object> expected = MapBuilder.<String, Object>newMapBuilder()
+            .put("test", "value_after")
+            .put("test.nested", "nested_value_before")
+            .immutableMap();
+
+        Map<String, Object> transformedMap = XContentMapValues.transform(mapToTransform, transformers, true);
+        assertEquals(expected, transformedMap);
+    }
+
     private static Map<String, Object> toMap(Builder test, XContentType xContentType, boolean humanReadable) throws IOException {
         ToXContentObject toXContent = (builder, params) -> test.apply(builder);
         return convertToMap(toXContent(toXContent, xContentType, humanReadable), true, xContentType).v2();


Reviews:
user: login: Vikasht34
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T20:11:29Z
user: login: 0ctopus13prime
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-17T20:50:21Z
user: login: jmazanec15
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T22:21:14Z
user: login: jmazanec15
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T22:21:32Z
user: login: Vikasht34
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T22:28:06Z
user: login: Vikasht34
body: LGTM ...Thanks 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T22:28:30Z
user: login: msfroh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T23:56:59Z
user: login: msfroh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T23:59:15Z
user: login: Vikasht34
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T02:01:27Z
user: login: jmazanec15
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:08:33Z
user: login: jmazanec15
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:37:04Z
user: login: jed326
body: Thanks @jmazanec15 , had one suggestion for an additional test case but otherwise LGTM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:52:30Z
user: login: jmazanec15
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T18:28:01Z
user: login: jed326
body: LGTM, thanks! @msfroh do you want to take another pass at this?
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T18:31:24Z
user: login: jmazanec15
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T14:12:40Z

Review comments:
diff_hunk: @@ -621,4 +621,95 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: Vikasht34
body: I got gist of transformers , At high level it looks good. There are couple of things we can improve on transformation !! 
1. Deep Recursion is really tough to debug and control , though we can control using explicit stack.
2. We can simplify the transform function to a. BuildTransformationTrie and ApplyTransformationOnTrie though we are doing in this function , but checks like can be offloaded when we are building the trie.
```
if (entry.getKey() == null || entry.getKey().isEmpty()) {
                continue;
          }

```
3. The other problem I can see with deep recursion , it copy the objects before the applying the transformation. It can be controlled when we use explicit stack like here 
```
if (value instanceof Map) {
    value = new HashMap<>(nodeMapValue(value, "transform")); // Unnecessary copy
    transformRecursive(value, subTrie);
    entry.setValue(value);
} 
``` 
and here 

```
else if (value instanceof Iterable<?> iterable) {
    List<Object> copy = new ArrayList<>();
    iterable.forEach(copy::add);
    transformRecursive(copy, subTrie);
    entry.setValue(copy);
}

```


Do u think below version is more debugable and maintanable. 


```
public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
    Map<String, Object> transformerTrie = buildTransformerTrie(transformers);

    Deque<TransformContext> stack = new ArrayDeque<>();
    Map<String, Object> result = new HashMap<>(source);
    stack.push(new TransformContext(result, transformerTrie));

    processStack(stack); // 

    return result;
}

private static void processStack(Deque<TransformContext> stack) {
    while (!stack.isEmpty()) {
        TransformContext ctx = stack.pop();
        processMap(ctx.map, ctx.trie, stack);
    }
}

private static void processMap(Map<String, Object> currentMap, Map<String, Object> currentTrie, Deque<TransformContext> stack) {
    for (Map.Entry<String, Object> entry : currentMap.entrySet()) {
        processEntry(entry, currentTrie, stack);
    }
}

private static void processEntry(Map.Entry<String, Object> entry, Map<String, Object> currentTrie, Deque<TransformContext> stack) {
    String key = entry.getKey();
    Object value = entry.getValue();

    Object subTrieObj = currentTrie.get(key);
    if (!(subTrieObj instanceof Map)) {
        return;
    }
    Map<String, Object> subTrie = (Map<String, Object>) subTrieObj;

    // Apply transformation if available
    Function<Object, Object> transformer = (Function<Object, Object>) subTrie.get("$transformer");
    if (transformer != null) {
        entry.setValue(transformer.apply(value));
        return;
    }

    // Process nested structures
    if (value instanceof Map) {
        stack.push(new TransformContext((Map<String, Object>) value, subTrie));
    } else if (value instanceof List) {
        processList((List<Object>) value, subTrie, stack);
    }
}

private static void processList(List<Object> list, Map<String, Object> transformerTrie, Deque<TransformContext> stack) {
    for (Object value : list) {
        if (value instanceof Map) {
            stack.push(new TransformContext((Map<String, Object>) value, transformerTrie));
        }
    }
}

private static class TransformContext {
    Map<String, Object> map;
    Map<String, Object> trie;

    TransformContext(Map<String, Object> map, Map<String, Object> trie) {
        this.map = map;
        this.trie = trie;
    }
}

```


created_at: 2025-03-17T20:11:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -621,4 +621,95 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        // Create a trie structure for the transformers, where the keys in the structure are components of the path and
+        // the leaf value is the transformer. Non-leaves point to sub tries
+        Map<String, Object> transformerTrie = new HashMap<>();
+        for (Map.Entry<String, Function<Object, Object>> entry : transformers.entrySet()) {
+            if (entry.getKey() == null || entry.getKey().isEmpty()) {
+                continue;
+            }
+            String[] pathElements = entry.getKey().split("\\.");
+            addToTransformerTrie(transformerTrie, pathElements, 0, entry.getValue());
+        }
+        Map<String, Object> copy = new HashMap<>(source);
+        transformRecursive(copy, transformerTrie);
+        return copy;
+    }
+
+    private static void addToTransformerTrie(
+        Map<String, Object> trie,
+        String[] pathElements,
+        int index,
+        Function<Object, Object> transformer
+    ) {
+        if (index == pathElements.length) {
+            trie.put("$transformer", transformer);
+            return;
+        }
+
+        String key = pathElements[index];
+        @SuppressWarnings("unchecked")
+        Map<String, Object> subTrie = (Map<String, Object>) trie.computeIfAbsent(key, k -> new HashMap<>());
+        addToTransformerTrie(subTrie, pathElements, index + 1, transformer);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void transformRecursive(Object current, Map<String, Object> transformerTrie) {
+        if (current instanceof Map) {
+            // For maps, we loop through the keys and check if any transformers apply. Additionally, we recurse down
+            // if necessary
+            Map<String, Object> map = nodeMapValue(current, "transform");
+            for (Map.Entry<String, Object> entry : map.entrySet()) {
+                String key = entry.getKey();
+                Object value = entry.getValue();
+                Map<String, Object> subTrie = (Map<String, Object>) transformerTrie.get(key);
+                if (subTrie == null) {
+                    continue;
+                }
+
+                // If the subTrie is present, we apply. Further, we do not recurse on top of this value.
+                Function<Object, Object> transformer = (Function<Object, Object>) subTrie.get("$transformer");
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: 0ctopus13prime
body: NIT : Could you have this as a static constant?
created_at: 2025-03-17T20:50:21Z
author_association: NONE
diff_hunk: @@ -621,4 +621,95 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: jmazanec15
body: makes sense with stack based. I pushed update. I dont think we'll be able to avoid copy unfortunately given that some might provide immutable maps.
created_at: 2025-03-17T22:21:14Z
author_association: MEMBER
diff_hunk: @@ -621,4 +621,95 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        // Create a trie structure for the transformers, where the keys in the structure are components of the path and
+        // the leaf value is the transformer. Non-leaves point to sub tries
+        Map<String, Object> transformerTrie = new HashMap<>();
+        for (Map.Entry<String, Function<Object, Object>> entry : transformers.entrySet()) {
+            if (entry.getKey() == null || entry.getKey().isEmpty()) {
+                continue;
+            }
+            String[] pathElements = entry.getKey().split("\\.");
+            addToTransformerTrie(transformerTrie, pathElements, 0, entry.getValue());
+        }
+        Map<String, Object> copy = new HashMap<>(source);
+        transformRecursive(copy, transformerTrie);
+        return copy;
+    }
+
+    private static void addToTransformerTrie(
+        Map<String, Object> trie,
+        String[] pathElements,
+        int index,
+        Function<Object, Object> transformer
+    ) {
+        if (index == pathElements.length) {
+            trie.put("$transformer", transformer);
+            return;
+        }
+
+        String key = pathElements[index];
+        @SuppressWarnings("unchecked")
+        Map<String, Object> subTrie = (Map<String, Object>) trie.computeIfAbsent(key, k -> new HashMap<>());
+        addToTransformerTrie(subTrie, pathElements, index + 1, transformer);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static void transformRecursive(Object current, Map<String, Object> transformerTrie) {
+        if (current instanceof Map) {
+            // For maps, we loop through the keys and check if any transformers apply. Additionally, we recurse down
+            // if necessary
+            Map<String, Object> map = nodeMapValue(current, "transform");
+            for (Map.Entry<String, Object> entry : map.entrySet()) {
+                String key = entry.getKey();
+                Object value = entry.getValue();
+                Map<String, Object> subTrie = (Map<String, Object>) transformerTrie.get(key);
+                if (subTrie == null) {
+                    continue;
+                }
+
+                // If the subTrie is present, we apply. Further, we do not recurse on top of this value.
+                Function<Object, Object> transformer = (Function<Object, Object>) subTrie.get("$transformer");
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: jmazanec15
body: makes sense - added
created_at: 2025-03-17T22:21:32Z
author_association: MEMBER
diff_hunk: @@ -621,4 +621,95 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: Vikasht34
body: Thanks !! 
created_at: 2025-03-17T22:28:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -621,4 +625,128 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        return transform(transformers).apply(source);
+    }
+
+    /**
+     * Returns function that performs a depth first traversal of a map and applies a transformation for each field
+     * matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
+     * the transformer for the shorter path is applied.
+     *
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                     the current value and returns a transformed value
+     * @return Function that takes a map and returns a transformed copy of the map
+     */
+    public static Function<Map<String, Object>, Map<String, Object>> transform(Map<String, Function<Object, Object>> transformers) {
+        Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
+        return source -> {
+            Deque<TransformContext> stack = new ArrayDeque<>();
+            Map<String, Object> result = new HashMap<>(source);
+            stack.push(new TransformContext(result, transformerTrie));
+
+            processStack(stack);
+            return result;
+        };
+    }
+
+    private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: msfroh
body: So, there's a fun trick that I learned from a Lucene issue that I've been working on. If you sort the keys of the map, you can construct the trie in linear time with no recursion.

Let's say you have transformers keyed to `a.a.a.b`, `a.a.c.b`, and `a.b.c.b`. Since that list is already sorted, you can process the first one, and guarantee that subsequent elements will be sorted by descending length of common prefixes. So, after you build the chain for `a.a.a.b`, you can step through `a.a`, add a child for `c`, then add the child for `b`. On the third element, you can step to `a`, then guarantee that you're adding a chain for `b.c.b`. So, I think you could probably `buildTransformerTrie` with a pair of `for`-loops.

Check out Lucene's `StringsToAutomaton` class for an example of this process in action (though that does end up using recursion, because it registers states after popping from the stack).
created_at: 2025-03-17T23:56:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -621,4 +625,128 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        return transform(transformers).apply(source);
+    }
+
+    /**
+     * Returns function that performs a depth first traversal of a map and applies a transformation for each field
+     * matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
+     * the transformer for the shorter path is applied.
+     *
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                     the current value and returns a transformed value
+     * @return Function that takes a map and returns a transformed copy of the map
+     */
+    public static Function<Map<String, Object>, Map<String, Object>> transform(Map<String, Function<Object, Object>> transformers) {
+        Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
+        return source -> {
+            Deque<TransformContext> stack = new ArrayDeque<>();
+            Map<String, Object> result = new HashMap<>(source);
+            stack.push(new TransformContext(result, transformerTrie));
+
+            processStack(stack);
+            return result;
+        };
+    }
+
+    private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: msfroh
body: Actually, you might not even need them to be sorted for your use-case, because you have a random-access `Map`. The Lucene one works by comparing the previous path to the current path and guaranteeing that they'll have the longest possible prefix. That may not be necessary here.
created_at: 2025-03-17T23:59:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -621,4 +625,128 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        return transform(transformers).apply(source);
+    }
+
+    /**
+     * Returns function that performs a depth first traversal of a map and applies a transformation for each field
+     * matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
+     * the transformer for the shorter path is applied.
+     *
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                     the current value and returns a transformed value
+     * @return Function that takes a map and returns a transformed copy of the map
+     */
+    public static Function<Map<String, Object>, Map<String, Object>> transform(Map<String, Function<Object, Object>> transformers) {
+        Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
+        return source -> {
+            Deque<TransformContext> stack = new ArrayDeque<>();
+            Map<String, Object> result = new HashMap<>(source);
+            stack.push(new TransformContext(result, transformerTrie));
+
+            processStack(stack);
+            return result;
+        };
+    }
+
+    private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: Vikasht34
body: Here is my 2 cent on StringsToAutomaton , I looked the code from lucene, From the code it's looks like , Lucene’s StringsToAutomaton  constructs a deterministic finite state automaton (DFA) and is optimized for DFAs, but for a simple hierarchical trie. 

Here is best fit for our use case 

1. Our Problem is About Key Hierarchies, NOT Finite State Machines
2. Lucene sorts input strings to guarantee longest common prefix (LCP) reuse, that's not valid in this case.
3. Lucene’s automaton is built once and then read-only.
4. Lucene’s Automaton is Designed for Searching(Lucene uses FSAs for fast string matching (e.g., autocomplete, search terms)), Not Path Mapping.

I feel that's optimised for whole different use case,   @jmazanec15  what’s your thought on this.?

created_at: 2025-03-18T02:01:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -621,4 +625,128 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        return transform(transformers).apply(source);
+    }
+
+    /**
+     * Returns function that performs a depth first traversal of a map and applies a transformation for each field
+     * matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
+     * the transformer for the shorter path is applied.
+     *
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                     the current value and returns a transformed value
+     * @return Function that takes a map and returns a transformed copy of the map
+     */
+    public static Function<Map<String, Object>, Map<String, Object>> transform(Map<String, Function<Object, Object>> transformers) {
+        Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
+        return source -> {
+            Deque<TransformContext> stack = new ArrayDeque<>();
+            Map<String, Object> result = new HashMap<>(source);
+            stack.push(new TransformContext(result, transformerTrie));
+
+            processStack(stack);
+            return result;
+        };
+    }
+
+    private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: jmazanec15
body: Thanks @msfroh and @Vikasht34. Thats interesting @msfroh, let me see if I can add that. And yes @Vikasht34 I agree, I dont think there will be too many transformers that will be passed in for our use case, so I dont think it will impact performance. I think if implementation gets too complex, I might hold off on adding it. For my use case, I added another transform function that returns a function to apply the transformation so that if its the same type of transformation applied for many docs, it will only compute the trie once.
created_at: 2025-03-18T15:08:32Z
author_association: MEMBER
diff_hunk: @@ -621,4 +625,128 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        return transform(transformers).apply(source);
+    }
+
+    /**
+     * Returns function that performs a depth first traversal of a map and applies a transformation for each field
+     * matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
+     * the transformer for the shorter path is applied.
+     *
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                     the current value and returns a transformed value
+     * @return Function that takes a map and returns a transformed copy of the map
+     */
+    public static Function<Map<String, Object>, Map<String, Object>> transform(Map<String, Function<Object, Object>> transformers) {
+        Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
+        return source -> {
+            Deque<TransformContext> stack = new ArrayDeque<>();
+            Map<String, Object> result = new HashMap<>(source);
+            stack.push(new TransformContext(result, transformerTrie));
+
+            processStack(stack);
+            return result;
+        };
+    }
+
+    private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: jmazanec15
body: I implemented in a non recursive fashion. In terms of the stepping back, I guess the idea would be to store the information about the path to a given subTrie via a stack and pop until a common prefix is matched and then proceed from there. Is that correct @msfroh ?
created_at: 2025-03-18T15:37:04Z
author_association: MEMBER
diff_hunk: @@ -629,6 +633,111 @@ public void testPrefix() {
         assertEquals(expected, filtered);
     }
 
+    public void testTransformFlat() {
+        Map<String, Object> mapToTransform = Map.of(
+            "test1",
+            "value_before",
+            "test2",
+            List.of("value_before", "value_before", "value_before")
+        );
+
+        Map<String, Function<Object, Object>> transformers = Map.of("test1", v -> "value_after", "test2", v -> "value_after");
+
+        Map<String, Object> expected = Map.of("test1", "value_after", "test2", "value_after");
+
+        Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, false);
+        assertEquals(expected, transformedMapped);
+    }
+
+    public void testTransformNested() {
+        Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder()
+            .put("test1", "value_before")
+            .put("test2", Map.of("nest2", "value_before"))
+            .put("test3", List.of(Map.of("nest3", "value_before"), Map.of("nest3", "value_before"), Map.of("nest3", "value_before")))
+            .put(
+                "test4",
+                List.of(
+                    Map.of(
+                        "nest4",
+                        List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before"), Map.of("nest5", "value_before")),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of(
+                        "nest4",
+                        List.of(
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before")
+                        ),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of("nest4", List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before")), "test5", "no_change")
+                )
+            )
+            .put("test6", null)
+            .immutableMap();
+
+        Iterator<String> test3Stream = IntStream.rangeClosed(1, 3).mapToObj(i -> "value_after" + i).toList().iterator();
+        Iterator<String> test4Stream = IntStream.rangeClosed(1, 9).mapToObj(i -> "value_after" + i).toList().iterator();
+        Map<String, Function<Object, Object>> transformers = Map.of(
+            "test1",
+            v -> "value_after",
+            "test2.nest2",
+            v -> "value_after",
+            "test3.nest3",
+            v -> test3Stream.next(),
+            "test4.nest4.nest5",
+            v -> test4Stream.next(),
+            "test6",
+            v -> v == null ? v : "value_after"
+        );
path: server/src/test/java/org/opensearch/common/xcontent/support/XContentMapValuesTests.java
user: login: jed326
body: Can you also add a test case to cover the case where only the transformer for the shorter path is applied?
created_at: 2025-03-19T17:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -629,6 +633,111 @@ public void testPrefix() {
         assertEquals(expected, filtered);
     }
 
+    public void testTransformFlat() {
+        Map<String, Object> mapToTransform = Map.of(
+            "test1",
+            "value_before",
+            "test2",
+            List.of("value_before", "value_before", "value_before")
+        );
+
+        Map<String, Function<Object, Object>> transformers = Map.of("test1", v -> "value_after", "test2", v -> "value_after");
+
+        Map<String, Object> expected = Map.of("test1", "value_after", "test2", "value_after");
+
+        Map<String, Object> transformedMapped = XContentMapValues.transform(mapToTransform, transformers, false);
+        assertEquals(expected, transformedMapped);
+    }
+
+    public void testTransformNested() {
+        Map<String, Object> mapToTransform = MapBuilder.<String, Object>newMapBuilder()
+            .put("test1", "value_before")
+            .put("test2", Map.of("nest2", "value_before"))
+            .put("test3", List.of(Map.of("nest3", "value_before"), Map.of("nest3", "value_before"), Map.of("nest3", "value_before")))
+            .put(
+                "test4",
+                List.of(
+                    Map.of(
+                        "nest4",
+                        List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before"), Map.of("nest5", "value_before")),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of(
+                        "nest4",
+                        List.of(
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before"),
+                            Map.of("nest5", "value_before")
+                        ),
+                        "test5",
+                        "no_change"
+                    ),
+                    Map.of("nest4", List.of(Map.of("nest5", "value_before"), Map.of("nest5", "value_before")), "test5", "no_change")
+                )
+            )
+            .put("test6", null)
+            .immutableMap();
+
+        Iterator<String> test3Stream = IntStream.rangeClosed(1, 3).mapToObj(i -> "value_after" + i).toList().iterator();
+        Iterator<String> test4Stream = IntStream.rangeClosed(1, 9).mapToObj(i -> "value_after" + i).toList().iterator();
+        Map<String, Function<Object, Object>> transformers = Map.of(
+            "test1",
+            v -> "value_after",
+            "test2.nest2",
+            v -> "value_after",
+            "test3.nest3",
+            v -> test3Stream.next(),
+            "test4.nest4.nest5",
+            v -> test4Stream.next(),
+            "test6",
+            v -> v == null ? v : "value_after"
+        );
path: server/src/test/java/org/opensearch/common/xcontent/support/XContentMapValuesTests.java
user: login: jmazanec15
body: sure - added 
created_at: 2025-03-19T18:28:01Z
author_association: MEMBER
diff_hunk: @@ -621,4 +625,128 @@ public static String[] nodeStringArrayValue(Object node) {
             return Strings.splitStringByCommaToArray(node.toString());
         }
     }
+
+    /**
+     * Performs a depth first traversal of a map and applies a transformation for each field matched along the way. For
+     * duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only the transformer for
+     * the shorter path is applied.
+     *
+     * @param source Source map to perform transformation on
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                    the current value and returns a transformed value
+     * @return Copy of the source map with the transformations applied
+     */
+    public static Map<String, Object> transform(Map<String, Object> source, Map<String, Function<Object, Object>> transformers) {
+        return transform(transformers).apply(source);
+    }
+
+    /**
+     * Returns function that performs a depth first traversal of a map and applies a transformation for each field
+     * matched along the way. For duplicated paths with transformers (i.e. "test.nested" and "test.nested.field"), only
+     * the transformer for the shorter path is applied.
+     *
+     * @param transformers Map from path to transformer to apply to each path. Each transformer is a function that takes
+     *                     the current value and returns a transformed value
+     * @return Function that takes a map and returns a transformed copy of the map
+     */
+    public static Function<Map<String, Object>, Map<String, Object>> transform(Map<String, Function<Object, Object>> transformers) {
+        Map<String, Object> transformerTrie = buildTransformerTrie(transformers);
+        return source -> {
+            Deque<TransformContext> stack = new ArrayDeque<>();
+            Map<String, Object> result = new HashMap<>(source);
+            stack.push(new TransformContext(result, transformerTrie));
+
+            processStack(stack);
+            return result;
+        };
+    }
+
+    private static Map<String, Object> buildTransformerTrie(Map<String, Function<Object, Object>> transformers) {
path: server/src/main/java/org/opensearch/common/xcontent/support/XContentMapValues.java
user: login: jmazanec15
body: Will keep as is - resolving for now.
created_at: 2025-03-20T14:12:40Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-17T20:01:25Z
body: :white_check_mark: Gradle check result for 461789554d8b4b0def55aec88b2fff027d977be9: [SUCCESS](https://build.ci.opensearch.org/job/gradle-check/54629/)
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-17T20:04:15Z
body: ## [Codecov](https://app.codecov.io/gh/opensearch-project/OpenSearch/pull/17612?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) Report
Attention: Patch coverage is `91.93548%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 72.41%. Comparing base [(`af5835f`)](https://app.codecov.io/gh/opensearch-project/OpenSearch/commit/af5835f561ddf383bd6869fa4c6703a3cc2d2433?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) to head [(`f43e1d6`)](https://app.codecov.io/gh/opensearch-project/OpenSearch/commit/f43e1d68e1170fba9bd7240e72e2b535fb9edfbb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/opensearch-project/OpenSearch/pull/17612?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) | Patch % | Lines |
|---|---|---|
| [...rch/common/xcontent/support/XContentMapValues.java](https://app.codecov.io/gh/opensearch-project/OpenSearch/pull/17612?src=pr&el=tree&filepath=server%2Fsrc%2Fmain%2Fjava%2Forg%2Fopensearch%2Fcommon%2Fxcontent%2Fsupport%2FXContentMapValues.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project#diff-c2VydmVyL3NyYy9tYWluL2phdmEvb3JnL29wZW5zZWFyY2gvY29tbW9uL3hjb250ZW50L3N1cHBvcnQvWENvbnRlbnRNYXBWYWx1ZXMuamF2YQ==) | 91.93% | [0 Missing and 5 partials :warning: ](https://app.codecov.io/gh/opensearch-project/OpenSearch/pull/17612?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##               main   #17612      +/-   ##
============================================
- Coverage     72.44%   72.41%   -0.04%     
- Complexity    65732    65773      +41     
============================================
  Files          5311     5311              
  Lines        305011   305073      +62     
  Branches      44232    44243      +11     
============================================
- Hits         220969   220921      -48     
- Misses        65967    66116     +149     
+ Partials      18075    18036      -39     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/opensearch-project/OpenSearch/pull/17612?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=opensearch-project).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-17T23:14:49Z
body: :white_check_mark: Gradle check result for c59f8dd31864d657cccee0d68ace76da92168176: [SUCCESS](https://build.ci.opensearch.org/job/gradle-check/54645/)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-18T16:39:59Z
body: :grey_exclamation: Gradle check result for bcd303512a131c39218dd9c5e321153ca1b17a67: [UNSTABLE](https://build.ci.opensearch.org/job/gradle-check/54676/) 
* **TEST FAILURES:**
```
      2 org.opensearch.cluster.MinimumClusterManagerNodesIT.testThreeNodesNoClusterManagerBlock
      1 org.opensearch.index.ShardIndexingPressureIT.testShardIndexingPressureTrackingDuringBulkWrites
```

Please review all [flaky tests](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) that succeeded after retry and create an issue if one does not already exist to track the flaky failure.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T03:58:26Z
body: :white_check_mark: Gradle check result for 6e6b7aef8533fd916f91ae5478664047164d0ca7: [SUCCESS](https://build.ci.opensearch.org/job/gradle-check/54719/)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T15:19:35Z
body: :grey_exclamation: Gradle check result for 4e06dc509118da4923c330811d2bd28d37de0e7d: [UNSTABLE](https://build.ci.opensearch.org/job/gradle-check/54741/) 
* **TEST FAILURES:**
```
      1 org.opensearch.remotestore.RemoteStoreStatsIT.testZeroLagOnCreateIndex
```

Please review all [flaky tests](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) that succeeded after retry and create an issue if one does not already exist to track the flaky failure.
author_association: CONTRIBUTOR
user: login: jmazanec15
created_at: 2025-03-19T18:28:13Z
body: Thanks @jed326  - added the test
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-19T18:59:36Z
body: :x: Gradle check result for f9b40ec217c8340e241c3a715ea3effd4b490df5: [FAILURE](https://build.ci.opensearch.org/job/gradle-check/54752/)

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure [a flaky test](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) unrelated to your change?
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T19:56:27Z
body: :x: Gradle check result for f9b40ec217c8340e241c3a715ea3effd4b490df5: [FAILURE](https://build.ci.opensearch.org/job/gradle-check/54755/)

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure [a flaky test](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) unrelated to your change?
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-19T20:37:02Z
body: :x: Gradle check result for f9b40ec217c8340e241c3a715ea3effd4b490df5: [FAILURE](https://build.ci.opensearch.org/job/gradle-check/54759/)

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure [a flaky test](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) unrelated to your change?
author_association: CONTRIBUTOR
user: login: jed326
created_at: 2025-03-20T01:41:31Z
body: > ❌ Gradle check result for [f9b40ec](https://github.com/opensearch-project/OpenSearch/commit/f9b40ec217c8340e241c3a715ea3effd4b490df5): [FAILURE](https://build.ci.opensearch.org/job/gradle-check/54759/)
> 
> Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure [a flaky test](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) unrelated to your change?

I'm not sure what is actually failing here, @jmazanec15 maybe try rebasing your branch if the next retry fails again
author_association: CONTRIBUTOR
user: login: jmazanec15
created_at: 2025-03-20T02:19:28Z
body: Sure @jed326 , just rebased
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-03-20T03:15:57Z
body: :x: Gradle check result for f43e1d68e1170fba9bd7240e72e2b535fb9edfbb: [FAILURE](https://build.ci.opensearch.org/job/gradle-check/54779/)

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure [a flaky test](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) unrelated to your change?
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T04:14:06Z
body: :x: Gradle check result for f43e1d68e1170fba9bd7240e72e2b535fb9edfbb: [FAILURE](https://build.ci.opensearch.org/job/gradle-check/54780/)

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure [a flaky test](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) unrelated to your change?
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T16:02:06Z
body: :grey_exclamation: Gradle check result for f43e1d68e1170fba9bd7240e72e2b535fb9edfbb: [UNSTABLE](https://build.ci.opensearch.org/job/gradle-check/54797/) 
* **TEST FAILURES:**
```
      1 org.opensearch.snapshots.DedicatedClusterSnapshotRestoreIT.testSnapshotWithStuckNode
```

Please review all [flaky tests](https://github.com/opensearch-project/OpenSearch/blob/main/DEVELOPER_GUIDE.md#flaky-tests) that succeeded after retry and create an issue if one does not already exist to track the flaky failure.
author_association: CONTRIBUTOR