-
Notifications
You must be signed in to change notification settings - Fork 25.7k
Reserve square bracket syntax for ingest document flexible field access pattern #134172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
d5c2ef4
5a8478e
d3dc2a9
4f4e7a4
c405ce2
646405a
129024d
5d9a346
3f2a386
7de9e84
7a454dd
fa626e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,7 @@ | |
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.stream.Collectors; | ||
|
|
@@ -201,7 +202,7 @@ 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) { | ||
| final FieldPath fieldPath = FieldPath.of(path); | ||
| final FieldPath fieldPath = FieldPath.of(path, getCurrentAccessPatternSafe()); | ||
| Object context = fieldPath.initialContext(this); | ||
| ResolveResult result = resolve(fieldPath.pathElements, fieldPath.pathElements.length, path, context, getCurrentAccessPatternSafe()); | ||
| if (result.wasSuccessful) { | ||
|
|
@@ -270,7 +271,7 @@ public boolean hasField(String path) { | |
| * @throws IllegalArgumentException if the path is null, empty or invalid. | ||
| */ | ||
| public boolean hasField(String path, boolean failOutOfRange) { | ||
| final FieldPath fieldPath = FieldPath.of(path); | ||
| final FieldPath fieldPath = FieldPath.of(path, getCurrentAccessPatternSafe()); | ||
| Object context = fieldPath.initialContext(this); | ||
| int leafKeyIndex = fieldPath.pathElements.length - 1; | ||
| int lastContainerIndex = fieldPath.pathElements.length - 2; | ||
|
|
@@ -424,7 +425,7 @@ public void removeField(String path) { | |
| * @throws IllegalArgumentException if the path is null, empty, or invalid; or if the field doesn't exist (and ignoreMissing is false). | ||
| */ | ||
| public void removeField(String path, boolean ignoreMissing) { | ||
| final FieldPath fieldPath = FieldPath.of(path); | ||
| final FieldPath fieldPath = FieldPath.of(path, getCurrentAccessPatternSafe()); | ||
| Object context = fieldPath.initialContext(this); | ||
| String leafKey = fieldPath.pathElements[fieldPath.pathElements.length - 1]; | ||
| ResolveResult result = resolve( | ||
|
|
@@ -734,7 +735,7 @@ public void setFieldValue(String path, Object value, boolean ignoreEmptyValue) { | |
| } | ||
|
|
||
| private void setFieldValue(String path, Object value, boolean append, boolean allowDuplicates) { | ||
| final FieldPath fieldPath = FieldPath.of(path); | ||
| final FieldPath fieldPath = FieldPath.of(path, getCurrentAccessPatternSafe()); | ||
| Object context = fieldPath.initialContext(this); | ||
| int leafKeyIndex = fieldPath.pathElements.length - 1; | ||
| int lastContainerIndex = fieldPath.pathElements.length - 2; | ||
|
|
@@ -1155,18 +1156,18 @@ List<String> getPipelineStack() { | |
| } | ||
|
|
||
| /** | ||
| * @return The access pattern for any currently executing pipelines, or null if no pipelines are in progress for this doc | ||
| * @return The access pattern for any currently executing pipelines, or empty if no pipelines are in progress for this doc | ||
| */ | ||
| public IngestPipelineFieldAccessPattern getCurrentAccessPattern() { | ||
| return accessPatternStack.peek(); | ||
| public Optional<IngestPipelineFieldAccessPattern> getCurrentAccessPattern() { | ||
| return Optional.ofNullable(accessPatternStack.peek()); | ||
| } | ||
|
|
||
| /** | ||
| * @return The access pattern for any currently executing pipelines, or {@link IngestPipelineFieldAccessPattern#CLASSIC} if no | ||
| * pipelines are in progress for this doc for the sake of backwards compatibility | ||
| */ | ||
| private IngestPipelineFieldAccessPattern getCurrentAccessPatternSafe() { | ||
| return Objects.requireNonNullElse(getCurrentAccessPattern(), IngestPipelineFieldAccessPattern.CLASSIC); | ||
| return getCurrentAccessPattern().orElse(IngestPipelineFieldAccessPattern.CLASSIC); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1290,8 +1291,15 @@ public String getFieldName() { | |
|
|
||
| private static final class FieldPath { | ||
|
|
||
| /** | ||
| * A compound cache key for tracking previously parsed field paths | ||
| * @param path The field path as given by the caller | ||
| * @param accessPattern The access pattern used to parse the field path | ||
| */ | ||
| private record CacheKey(String path, IngestPipelineFieldAccessPattern accessPattern) {} | ||
|
|
||
| private static final int MAX_SIZE = 512; | ||
| private static final Map<String, FieldPath> CACHE = ConcurrentCollections.newConcurrentMapWithAggressiveConcurrency(); | ||
| private static final Map<CacheKey, FieldPath> CACHE = ConcurrentCollections.newConcurrentMapWithAggressiveConcurrency(); | ||
|
|
||
| // 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 | ||
|
|
@@ -1300,27 +1308,28 @@ private static final class FieldPath { | |
| // 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) { | ||
| static FieldPath of(String path, IngestPipelineFieldAccessPattern accessPattern) { | ||
| if (Strings.isEmpty(path)) { | ||
| throw new IllegalArgumentException("path cannot be null nor empty"); | ||
| } | ||
| FieldPath res = CACHE.get(path); | ||
| CacheKey cacheKey = new CacheKey(path, accessPattern); | ||
| FieldPath res = CACHE.get(cacheKey); | ||
| if (res != null) { | ||
| return res; | ||
| } | ||
| res = new FieldPath(path); | ||
| res = new FieldPath(path, accessPattern); | ||
| if (CACHE.size() > MAX_SIZE) { | ||
| CACHE.clear(); | ||
| } | ||
| CACHE.put(path, res); | ||
| CACHE.put(cacheKey, 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) { | ||
| private FieldPath(String path, IngestPipelineFieldAccessPattern accessPattern) { | ||
| String newPath; | ||
| if (path.startsWith(INGEST_KEY_PREFIX)) { | ||
| useIngestContext = true; | ||
|
|
@@ -1333,10 +1342,50 @@ private FieldPath(String path) { | |
| newPath = path; | ||
| } | ||
| } | ||
| this.pathElements = newPath.split("\\."); | ||
| if (pathElements.length == 1 && pathElements[0].isEmpty()) { | ||
| throw new IllegalArgumentException("path [" + path + "] is not valid"); | ||
| String[] pathParts = newPath.split("\\."); | ||
| this.pathElements = processPathParts(path, pathParts, accessPattern); | ||
| } | ||
|
|
||
| private static String[] processPathParts(String fullPath, String[] pathParts, IngestPipelineFieldAccessPattern accessPattern) { | ||
| if (pathParts.length == 1 && pathParts[0].isEmpty()) { | ||
| throw new IllegalArgumentException("path [" + fullPath + "] is not valid"); | ||
| } | ||
| return switch (accessPattern) { | ||
| case CLASSIC -> validateClassicFields(fullPath, pathParts); | ||
| case FLEXIBLE -> parseFlexibleFields(fullPath, pathParts); | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Parses path syntax that is specific to the {@link IngestPipelineFieldAccessPattern#CLASSIC} ingest doc access pattern. Supports | ||
| * syntax like context aware array access. | ||
| * @param fullPath The un-split path to use for error messages | ||
| * @param pathParts The tokenized field path to parse | ||
| * @return An array of Strings | ||
| */ | ||
| private static String[] validateClassicFields(String fullPath, String[] pathParts) { | ||
| for (String pathPart : pathParts) { | ||
| if (pathPart.isEmpty()) { | ||
| throw new IllegalArgumentException("path [" + fullPath + "] is not valid"); | ||
| } | ||
| } | ||
| return pathParts; | ||
| } | ||
|
|
||
| /** | ||
| * Parses path syntax that is specific to the {@link IngestPipelineFieldAccessPattern#FLEXIBLE} ingest doc access pattern. Supports | ||
| * syntax like square bracket array access, which is the only way to index arrays in flexible mode. | ||
| * @param fullPath The un-split path to use for error messages | ||
| * @param pathParts The tokenized field path to parse | ||
| * @return An array of Strings | ||
| */ | ||
| private static String[] parseFlexibleFields(String fullPath, String[] pathParts) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it meaningful that this is named
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I heavily adapted this code from #133790 with the hope that I can rebase it on top of these changes with minimal heartburn. I think this is just the method name from that PR. |
||
| for (String pathPart : pathParts) { | ||
| if (pathPart.isEmpty() || pathPart.indexOf('[') >= 0 || pathPart.indexOf(']') >= 0) { | ||
jbaiera marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| throw new IllegalArgumentException("path [" + fullPath + "] is not valid"); | ||
| } | ||
| } | ||
| return pathParts; | ||
| } | ||
|
|
||
| public Object initialContext(IngestDocument document) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.