Skip to content

Commit c21b9f5

Browse files
committed
Merge remote-tracking branch 'upstream/main' into 03132025/ReshardRoutingNumShards
merge with main
2 parents 2946f37 + 26254e3 commit c21b9f5

File tree

4 files changed

+406
-9
lines changed

4 files changed

+406
-9
lines changed

muted-tests.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,6 @@ tests:
411411
- class: org.elasticsearch.packaging.test.DockerTests
412412
method: test024InstallPluginFromArchiveUsingConfigFile
413413
issue: https://github.com/elastic/elasticsearch/issues/126936
414-
- class: org.elasticsearch.repositories.blobstore.testkit.analyze.RepositoryAnalysisFailureIT
415-
method: testFailsOnReadError
416-
issue: https://github.com/elastic/elasticsearch/issues/127029
417414
- class: org.elasticsearch.xpack.test.rest.XPackRestIT
418415
method: test {p0=esql/10_basic/basic with documents_found}
419416
issue: https://github.com/elastic/elasticsearch/issues/127039
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.inference.common;
9+
10+
import org.elasticsearch.common.Strings;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.regex.Pattern;
16+
17+
/**
18+
* Extracts fields from a {@link Map}.
19+
*
20+
* Uses a subset of the JSONPath schema to extract fields from a map.
21+
* For more information <a href="https://en.wikipedia.org/wiki/JSONPath">see here</a>.
22+
*
23+
* This implementation differs in how it handles lists in that JSONPath will flatten inner lists. This implementation
24+
* preserves inner lists.
25+
*
26+
* Examples of the schema:
27+
*
28+
* <pre>
29+
* {@code
30+
* $.field1.array[*].field2
31+
* $.field1.field2
32+
* }
33+
* </pre>
34+
*
35+
* Given the map
36+
* <pre>
37+
* {@code
38+
* {
39+
* "request_id": "B4AB89C8-B135-xxxx-A6F8-2BAB801A2CE4",
40+
* "latency": 38,
41+
* "usage": {
42+
* "token_count": 3072
43+
* },
44+
* "result": {
45+
* "embeddings": [
46+
* {
47+
* "index": 0,
48+
* "embedding": [
49+
* 2,
50+
* 4
51+
* ]
52+
* },
53+
* {
54+
* "index": 1,
55+
* "embedding": [
56+
* 1,
57+
* 2
58+
* ]
59+
* }
60+
* ]
61+
* }
62+
* }
63+
* }
64+
* </pre>
65+
*
66+
* <pre>
67+
* {@code
68+
* var embeddings = MapPathExtractor.extract(map, "$.result.embeddings[*].embedding");
69+
* }
70+
* </pre>
71+
*
72+
* Will result in:
73+
*
74+
* <pre>
75+
* {@code
76+
* [
77+
* [2, 4],
78+
* [1, 2]
79+
* ]
80+
* }
81+
* </pre>
82+
*
83+
* This implementation differs from JSONPath when handling a list of maps. JSONPath will flatten the result and return a single array.
84+
* this implementation will preserve each nested list while gathering the results.
85+
*
86+
* For example
87+
*
88+
* <pre>
89+
* {@code
90+
* {
91+
* "result": [
92+
* {
93+
* "key": [
94+
* {
95+
* "a": 1.1
96+
* },
97+
* {
98+
* "a": 2.2
99+
* }
100+
* ]
101+
* },
102+
* {
103+
* "key": [
104+
* {
105+
* "a": 3.3
106+
* },
107+
* {
108+
* "a": 4.4
109+
* }
110+
* ]
111+
* }
112+
* ]
113+
* }
114+
* }
115+
* {@code var embeddings = MapPathExtractor.extract(map, "$.result[*].key[*].a");}
116+
*
117+
* JSONPath: {@code [1.1, 2.2, 3.3, 4.4]}
118+
* This implementation: {@code [[1.1, 2.2], [3.3, 4.4]]}
119+
* </pre>
120+
*/
121+
public class MapPathExtractor {
122+
123+
private static final String DOLLAR = "$";
124+
125+
// default for testing
126+
static final Pattern dotFieldPattern = Pattern.compile("^\\.([^.\\[]+)(.*)");
127+
static final Pattern arrayWildcardPattern = Pattern.compile("^\\[\\*\\](.*)");
128+
129+
public static Object extract(Map<String, Object> data, String path) {
130+
if (data == null || data.isEmpty() || path == null || path.trim().isEmpty()) {
131+
return null;
132+
}
133+
134+
var cleanedPath = path.trim();
135+
136+
if (cleanedPath.startsWith(DOLLAR)) {
137+
cleanedPath = cleanedPath.substring(DOLLAR.length());
138+
} else {
139+
throw new IllegalArgumentException(Strings.format("Path [%s] must start with a dollar sign ($)", cleanedPath));
140+
}
141+
142+
return navigate(data, cleanedPath);
143+
}
144+
145+
private static Object navigate(Object current, String remainingPath) {
146+
if (current == null || remainingPath == null || remainingPath.isEmpty()) {
147+
return current;
148+
}
149+
150+
var dotFieldMatcher = dotFieldPattern.matcher(remainingPath);
151+
var arrayWildcardMatcher = arrayWildcardPattern.matcher(remainingPath);
152+
153+
if (dotFieldMatcher.matches()) {
154+
String field = dotFieldMatcher.group(1);
155+
if (field == null || field.isEmpty()) {
156+
throw new IllegalArgumentException(
157+
Strings.format(
158+
"Unable to extract field from remaining path [%s]. Fields must be delimited by a dot character.",
159+
remainingPath
160+
)
161+
);
162+
}
163+
164+
String nextPath = dotFieldMatcher.group(2);
165+
if (current instanceof Map<?, ?> currentMap) {
166+
var fieldFromMap = currentMap.get(field);
167+
if (fieldFromMap == null) {
168+
throw new IllegalArgumentException(Strings.format("Unable to find field [%s] in map", field));
169+
}
170+
171+
return navigate(currentMap.get(field), nextPath);
172+
} else {
173+
throw new IllegalArgumentException(
174+
Strings.format(
175+
"Current path [%s] matched the dot field pattern but the current object is not a map, "
176+
+ "found invalid type [%s] instead.",
177+
remainingPath,
178+
current.getClass().getSimpleName()
179+
)
180+
);
181+
}
182+
} else if (arrayWildcardMatcher.matches()) {
183+
String nextPath = arrayWildcardMatcher.group(1);
184+
if (current instanceof List<?> list) {
185+
List<Object> results = new ArrayList<>();
186+
187+
for (Object item : list) {
188+
Object result = navigate(item, nextPath);
189+
if (result != null) {
190+
results.add(result);
191+
}
192+
}
193+
194+
return results;
195+
} else {
196+
throw new IllegalArgumentException(
197+
Strings.format(
198+
"Current path [%s] matched the array field pattern but the current object is not a list, "
199+
+ "found invalid type [%s] instead.",
200+
remainingPath,
201+
current.getClass().getSimpleName()
202+
)
203+
);
204+
}
205+
}
206+
207+
throw new IllegalArgumentException(Strings.format("Invalid path received [%s], unable to extract a field name.", remainingPath));
208+
}
209+
}

0 commit comments

Comments
 (0)