Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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