-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Add plugin SPI for dynamic field-type inference and dynamic-template types #22607
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
Open
naykudev
wants to merge
19
commits into
opensearch-project:main
Choose a base branch
from
naykudev:dynamic-knn-vector-mapping
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
df1ac5b
Add plugin SPI for dynamic field-type inference and templates
naykudev 7863ebc
Fail on ambiguous dynamic type inference
naykudev 6dae7af
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev 000d534
Add tests for inferencer error/edge branches
naykudev 840945c
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev b31f7a0
Address review feedback on dynamic-mapping SPI
naykudev 497ce79
Keep dynamic-mapping SPI generic and validate plugin type early
naykudev a42ac75
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev 8301dbf
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev 1e88a41
Name both plugins in duplicate template type error
naykudev dbd28f0
Release ContentPath slots on all inference exit paths
naykudev f0210af
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev d8dd803
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev ecb227f
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev f4da253
Restrict inferred dynamic field types to plugin types
naykudev 6710bd9
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev 5ac01a0
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev f719d9e
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev 86c83b0
Merge branch 'main' into dynamic-knn-vector-mapping
naykudev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
344 changes: 344 additions & 0 deletions
344
server/src/main/java/org/opensearch/index/mapper/DocumentParser.java
Large diffs are not rendered by default.
Oops, something went wrong.
51 changes: 51 additions & 0 deletions
51
server/src/main/java/org/opensearch/index/mapper/DynamicFieldTypeInferencer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /* | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
|
|
||
| package org.opensearch.index.mapper; | ||
|
|
||
| import org.opensearch.common.annotation.ExperimentalApi; | ||
| import org.opensearch.core.xcontent.XContentParser; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * SPI for plugins to register dynamic field type inference logic. | ||
| * | ||
| * <p>When DocumentParser encounters an unmapped field and no dynamic template matches, it buffers | ||
| * the field content and calls {@link #inferFieldType} on each registered inferencer in order. The | ||
| * first non-null config map wins; the type is read from the {@code "type"} key, the mapper is built, | ||
| * and the field content is replayed through it. If no inferencer claims the field, existing fallback | ||
| * behavior applies. | ||
| * | ||
| * <p>Rather than deserializing the field value into a fixed Java representation, core hands the | ||
| * inferencer a {@link FieldValueParserSupplier} that produces a fresh {@link XContentParser} over the | ||
| * buffered bytes. Each call to {@code fieldValueParser.get()} returns an independent parser positioned before | ||
| * the field value, so the plugin can inspect the content however it needs — for example streaming | ||
| * through tokens to count array elements. This keeps core free of any representation contract: each | ||
| * plugin decides how to interpret the value. | ||
| * | ||
| * @opensearch.experimental | ||
| */ | ||
| @ExperimentalApi | ||
| public interface DynamicFieldTypeInferencer { | ||
|
|
||
| /** | ||
| * Inspect the buffered field value and decide whether to claim it. | ||
| * | ||
| * @param fieldValueParser produces a fresh {@link XContentParser} over the buffered field bytes; | ||
| * call {@code get()} and advance the parser to read the value. The returned | ||
| * parser should be closed by the caller (e.g. via try-with-resources). | ||
| * @return a mutable mapping config map with at minimum a {@code "type"} key (e.g. | ||
| * {@code {"type": "knn_vector", "dimension": 384}}), or {@code null} to pass to the | ||
| * next inferencer. The map MUST be mutable — TypeParser implementations call | ||
| * {@code node.remove()} on it during parsing. | ||
| * @throws IOException if reading from the parser fails | ||
| */ | ||
| Map<String, Object> inferFieldType(FieldValueParserSupplier fieldValueParser) throws IOException; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
server/src/main/java/org/opensearch/index/mapper/DynamicTemplateTypeHandler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /* | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
|
|
||
| package org.opensearch.index.mapper; | ||
|
|
||
| import org.opensearch.common.annotation.ExperimentalApi; | ||
| import org.opensearch.core.xcontent.XContentParser; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Handler for plugin-registered dynamic template types. | ||
| * Called when a dynamic template matches on a plugin-registered type string | ||
| * (e.g. "knn_vector") to allow the plugin to adjust the mapping configuration | ||
| * before the mapper is built. | ||
| * | ||
| * <p>Rather than deserializing the field value for the handler, core hands it a | ||
| * {@link FieldValueParserSupplier} that produces a fresh {@link XContentParser} over the buffered | ||
| * bytes. A handler whose template config is already complete never calls {@code get()}, | ||
| * so no parsing happens for fully-specified templates. A handler that needs a | ||
| * data-derived parameter (e.g. a vector's dimension) creates a parser and reads what it | ||
| * needs. | ||
| * | ||
| * @opensearch.experimental | ||
| */ | ||
| @ExperimentalApi | ||
| public interface DynamicTemplateTypeHandler { | ||
|
|
||
| /** | ||
| * Adjust the mapping configuration before the TypeParser builds the mapper. | ||
| * Called when a dynamic template matches but before the mapper is constructed. | ||
| * | ||
| * @param mappingConfig the mutable mapping config from the template (modified in place) | ||
| * @param fieldValueParser produces a fresh {@link XContentParser} over the buffered field bytes; | ||
| * only call {@code get()} if the config is missing a parameter that must | ||
| * be derived from the data. Close the returned parser (e.g. via | ||
| * try-with-resources). | ||
| * @throws IOException if reading from the parser fails | ||
| */ | ||
| void adjustMappingConfig(Map<String, Object> mappingConfig, FieldValueParserSupplier fieldValueParser) throws IOException; | ||
|
|
||
| /** | ||
| * Returns {@code true} if the given template mapping config is fully specified — i.e. building a | ||
| * mapper from it requires no parameter derived from a document ({@link #adjustMappingConfig} would | ||
| * not need to read the field value). When this returns {@code true}, core validates the template | ||
| * eagerly at index-creation time by handing the config to the {@link Mapper.TypeParser}, which | ||
| * reports any invalid content. When it returns {@code false}, validation is deferred to | ||
| * document-parse time, where the data-derived parameters become available. | ||
| * | ||
| * @param mappingConfig the mapping config from the matched template | ||
| * @return whether the config can be validated without inspecting a document | ||
| */ | ||
| default boolean isConfigComplete(Map<String, Object> mappingConfig) { | ||
| return false; | ||
| } | ||
| } |
81 changes: 81 additions & 0 deletions
81
server/src/main/java/org/opensearch/index/mapper/FieldValueParserSupplier.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /* | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| * | ||
| * The OpenSearch Contributors require contributions made to | ||
| * this file be licensed under the Apache-2.0 license or a | ||
| * compatible open source license. | ||
| */ | ||
|
|
||
| package org.opensearch.index.mapper; | ||
|
|
||
| import org.opensearch.common.annotation.ExperimentalApi; | ||
| import org.opensearch.core.xcontent.DeprecationHandler; | ||
| import org.opensearch.core.xcontent.MediaType; | ||
| import org.opensearch.core.xcontent.NamedXContentRegistry; | ||
| import org.opensearch.core.xcontent.XContentParser; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| /** | ||
| * Supplies a fresh {@link XContentParser} positioned at the start of a buffered field value. | ||
| * | ||
| * <p>Handed to dynamic-mapping plugin extension points ({@link DynamicFieldTypeInferencer} and | ||
| * {@link DynamicTemplateTypeHandler}) so they can inspect an unmapped field's value without core | ||
| * committing to any deserialized representation. Each call to {@link #get()} creates an independent | ||
| * parser over the same buffered bytes, so a plugin may read the value more than once. The caller | ||
| * closes the returned parser (e.g. via try-with-resources). | ||
| * | ||
| * <p>Plugins whose configuration is already complete need not call {@link #get()} at all — index-creation | ||
| * validation constructs the supplier with {@link #withoutValue()}, whose {@link #get()} throws, so a | ||
| * complete config that never reads the value is validated without any field data being available. | ||
| * | ||
| * @opensearch.experimental | ||
| */ | ||
| @ExperimentalApi | ||
| public class FieldValueParserSupplier { | ||
|
|
||
| private final MediaType contentType; | ||
| private final NamedXContentRegistry registry; | ||
| private final DeprecationHandler deprecationHandler; | ||
| private final byte[] rawContent; | ||
|
|
||
| /** | ||
| * Creates a supplier over the buffered field value. {@link #get()} produces a fresh parser | ||
| * positioned at the value's first token on each call. | ||
| */ | ||
| public FieldValueParserSupplier( | ||
| MediaType contentType, | ||
| NamedXContentRegistry registry, | ||
| DeprecationHandler deprecationHandler, | ||
| byte[] rawContent | ||
| ) { | ||
| this.contentType = contentType; | ||
| this.registry = registry; | ||
| this.deprecationHandler = deprecationHandler; | ||
| this.rawContent = rawContent; | ||
| } | ||
|
|
||
| /** | ||
| * A supplier with no field value behind it — {@link #get()} throws. Used at index-creation time, | ||
| * where a fully specified template config must be validated without reading any document. | ||
| */ | ||
| public static FieldValueParserSupplier withoutValue() { | ||
| return new FieldValueParserSupplier(null, null, null, null); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a fresh {@link XContentParser} positioned at the first token of the field value. The | ||
| * caller is responsible for closing it (e.g. via try-with-resources). | ||
| * | ||
| * @throws IOException if the parser cannot be created | ||
| * @throws IllegalStateException if this supplier has no field value ({@link #withoutValue()}) | ||
| */ | ||
| public XContentParser get() throws IOException { | ||
| if (rawContent == null) { | ||
| throw new IllegalStateException("No field value is available to parse"); | ||
| } | ||
| XContentParser parser = contentType.xContent().createParser(registry, deprecationHandler, rawContent); | ||
| parser.nextToken(); // position at the start of the value | ||
| return parser; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.