Skip to content

Commit d763805

Browse files
authored
Optimize IngestDocument FieldPath allocation (#120573)
1 parent c4bb9b3 commit d763805

File tree

2 files changed

+47
-14
lines changed

2 files changed

+47
-14
lines changed

docs/changelog/120573.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 120573
2+
summary: Optimize `IngestDocument` `FieldPath` allocation
3+
area: Ingest Node
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/ingest/IngestDocument.java

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.common.Strings;
1313
import org.elasticsearch.common.util.CollectionUtils;
1414
import org.elasticsearch.common.util.Maps;
15+
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
1516
import org.elasticsearch.common.util.set.Sets;
1617
import org.elasticsearch.core.UpdateForV10;
1718
import org.elasticsearch.index.VersionType;
@@ -190,8 +191,8 @@ public <T> T getFieldValue(String path, Class<T> clazz) {
190191
* or if the field that is found at the provided path is not of the expected type.
191192
*/
192193
public <T> T getFieldValue(String path, Class<T> clazz, boolean ignoreMissing) {
193-
FieldPath fieldPath = new FieldPath(path);
194-
Object context = fieldPath.initialContext;
194+
final FieldPath fieldPath = FieldPath.of(path);
195+
Object context = fieldPath.initialContext(this);
195196
for (String pathElement : fieldPath.pathElements) {
196197
ResolveResult result = resolve(pathElement, path, context);
197198
if (result.wasSuccessful) {
@@ -261,8 +262,8 @@ public boolean hasField(String path) {
261262
* @throws IllegalArgumentException if the path is null, empty or invalid.
262263
*/
263264
public boolean hasField(String path, boolean failOutOfRange) {
264-
FieldPath fieldPath = new FieldPath(path);
265-
Object context = fieldPath.initialContext;
265+
final FieldPath fieldPath = FieldPath.of(path);
266+
Object context = fieldPath.initialContext(this);
266267
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
267268
String pathElement = fieldPath.pathElements[i];
268269
if (context == null) {
@@ -329,8 +330,8 @@ public boolean hasField(String path, boolean failOutOfRange) {
329330
* @throws IllegalArgumentException if the path is null, empty, invalid or if the field doesn't exist.
330331
*/
331332
public void removeField(String path) {
332-
FieldPath fieldPath = new FieldPath(path);
333-
Object context = fieldPath.initialContext;
333+
final FieldPath fieldPath = FieldPath.of(path);
334+
Object context = fieldPath.initialContext(this);
334335
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
335336
ResolveResult result = resolve(fieldPath.pathElements[i], path, context);
336337
if (result.wasSuccessful) {
@@ -544,8 +545,8 @@ public void setFieldValue(String path, Object value, boolean ignoreEmptyValue) {
544545
}
545546

546547
private void setFieldValue(String path, Object value, boolean append, boolean allowDuplicates) {
547-
FieldPath fieldPath = new FieldPath(path);
548-
Object context = fieldPath.initialContext;
548+
final FieldPath fieldPath = FieldPath.of(path);
549+
Object context = fieldPath.initialContext(this);
549550
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
550551
String pathElement = fieldPath.pathElements[i];
551552
if (context == null) {
@@ -998,21 +999,45 @@ public String getFieldName() {
998999
}
9991000
}
10001001

1001-
private class FieldPath {
1002+
private static final class FieldPath {
10021003

1003-
private final String[] pathElements;
1004-
private final Object initialContext;
1004+
private static final int MAX_SIZE = 512;
1005+
private static final Map<String, FieldPath> CACHE = ConcurrentCollections.newConcurrentMapWithAggressiveConcurrency();
10051006

1006-
private FieldPath(String path) {
1007+
// constructing a new FieldPath requires that we parse a String (e.g. "foo.bar.baz") into an array
1008+
// of path elements (e.g. ["foo", "bar", "baz"]). Calling String#split results in the allocation
1009+
// of an ArrayList to hold the results, then a new String is created for each path element, and
1010+
// then finally a String[] is allocated to hold the actual result -- in addition to all that, we
1011+
// do some processing ourselves on the path and path elements to validate and prepare them.
1012+
// the above CACHE and the below 'FieldPath.of' method allow us to almost always avoid this work.
1013+
1014+
static FieldPath of(String path) {
10071015
if (Strings.isEmpty(path)) {
10081016
throw new IllegalArgumentException("path cannot be null nor empty");
10091017
}
1018+
FieldPath res = CACHE.get(path);
1019+
if (res != null) {
1020+
return res;
1021+
}
1022+
res = new FieldPath(path);
1023+
if (CACHE.size() > MAX_SIZE) {
1024+
CACHE.clear();
1025+
}
1026+
CACHE.put(path, res);
1027+
return res;
1028+
}
1029+
1030+
private final String[] pathElements;
1031+
private final boolean useIngestContext;
1032+
1033+
// you shouldn't call this directly, use the FieldPath.of method above instead!
1034+
private FieldPath(String path) {
10101035
String newPath;
10111036
if (path.startsWith(INGEST_KEY_PREFIX)) {
1012-
initialContext = ingestMetadata;
1037+
useIngestContext = true;
10131038
newPath = path.substring(INGEST_KEY_PREFIX.length());
10141039
} else {
1015-
initialContext = ctxMap;
1040+
useIngestContext = false;
10161041
if (path.startsWith(SOURCE_PREFIX)) {
10171042
newPath = path.substring(SOURCE_PREFIX.length());
10181043
} else {
@@ -1025,6 +1050,9 @@ private FieldPath(String path) {
10251050
}
10261051
}
10271052

1053+
public Object initialContext(IngestDocument document) {
1054+
return useIngestContext ? document.getIngestMetadata() : document.getCtxMap();
1055+
}
10281056
}
10291057

10301058
private static class ResolveResult {

0 commit comments

Comments
 (0)