Skip to content
5 changes: 5 additions & 0 deletions docs/changelog/120573.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 120573
summary: Optimize `IngestDocument` `FieldPath` allocation
area: Ingest Node
type: enhancement
issues: []
56 changes: 42 additions & 14 deletions server/src/main/java/org/elasticsearch/ingest/IngestDocument.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.common.util.Maps;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.core.UpdateForV10;
import org.elasticsearch.index.VersionType;
Expand Down Expand Up @@ -190,8 +191,8 @@ public <T> T getFieldValue(String path, Class<T> clazz) {
* or if the field that is found at the provided path is not of the expected type.
*/
public <T> T getFieldValue(String path, Class<T> clazz, boolean ignoreMissing) {
FieldPath fieldPath = new FieldPath(path);
Object context = fieldPath.initialContext;
final FieldPath fieldPath = FieldPath.of(path);
Object context = fieldPath.initialContext(this);
for (String pathElement : fieldPath.pathElements) {
ResolveResult result = resolve(pathElement, path, context);
if (result.wasSuccessful) {
Expand Down Expand Up @@ -261,8 +262,8 @@ public boolean hasField(String path) {
* @throws IllegalArgumentException if the path is null, empty or invalid.
*/
public boolean hasField(String path, boolean failOutOfRange) {
FieldPath fieldPath = new FieldPath(path);
Object context = fieldPath.initialContext;
final FieldPath fieldPath = FieldPath.of(path);
Object context = fieldPath.initialContext(this);
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
String pathElement = fieldPath.pathElements[i];
if (context == null) {
Expand Down Expand Up @@ -329,8 +330,8 @@ public boolean hasField(String path, boolean failOutOfRange) {
* @throws IllegalArgumentException if the path is null, empty, invalid or if the field doesn't exist.
*/
public void removeField(String path) {
FieldPath fieldPath = new FieldPath(path);
Object context = fieldPath.initialContext;
final FieldPath fieldPath = FieldPath.of(path);
Object context = fieldPath.initialContext(this);
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
ResolveResult result = resolve(fieldPath.pathElements[i], path, context);
if (result.wasSuccessful) {
Expand Down Expand Up @@ -544,8 +545,8 @@ public void setFieldValue(String path, Object value, boolean ignoreEmptyValue) {
}

private void setFieldValue(String path, Object value, boolean append, boolean allowDuplicates) {
FieldPath fieldPath = new FieldPath(path);
Object context = fieldPath.initialContext;
final FieldPath fieldPath = FieldPath.of(path);
Object context = fieldPath.initialContext(this);
for (int i = 0; i < fieldPath.pathElements.length - 1; i++) {
String pathElement = fieldPath.pathElements[i];
if (context == null) {
Expand Down Expand Up @@ -998,21 +999,45 @@ public String getFieldName() {
}
}

private class FieldPath {
private static final class FieldPath {

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

private FieldPath(String path) {
// constructing a new FieldPath requires that we parse a String (e.g. "foo.bar.baz") into an array
// of path elements (e.g. ["foo", "bar", "baz"]). Calling String#split results in the allocation
// of an ArrayList to hold the results, then a new String is created for each path element, and
// then finally a String[] is allocated to hold the actual result -- in addition to all that, we
// do some processing ourselves on the path and path elements to validate and prepare them.
// the above CACHE and the below 'FieldPath.of' method allow us to almost always avoid this work.

static FieldPath of(String path) {
if (Strings.isEmpty(path)) {
throw new IllegalArgumentException("path cannot be null nor empty");
}
FieldPath res = CACHE.get(path);
if (res != null) {
return res;
}
res = new FieldPath(path);
if (CACHE.size() > MAX_SIZE) {
CACHE.clear();
}
CACHE.put(path, res);
return res;
}

private final String[] pathElements;
private final boolean useIngestContext;

// you shouldn't call this directly, use the FieldPath.of method above instead!
private FieldPath(String path) {
String newPath;
if (path.startsWith(INGEST_KEY_PREFIX)) {
initialContext = ingestMetadata;
useIngestContext = true;
newPath = path.substring(INGEST_KEY_PREFIX.length());
} else {
initialContext = ctxMap;
useIngestContext = false;
if (path.startsWith(SOURCE_PREFIX)) {
newPath = path.substring(SOURCE_PREFIX.length());
} else {
Expand All @@ -1025,6 +1050,9 @@ private FieldPath(String path) {
}
}

public Object initialContext(IngestDocument document) {
return useIngestContext ? document.getIngestMetadata() : document.getCtxMap();
}
}

private static class ResolveResult {
Expand Down