Skip to content
Open
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
344 changes: 344 additions & 0 deletions server/src/main/java/org/opensearch/index/mapper/DocumentParser.java

Large diffs are not rendered by default.

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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,18 @@ public static XContentFieldType fromString(String value) {
public abstract String defaultMappingType();
}

/** Parses a dynamic template without plugin-type validation: any unknown match_mapping_type is kept as a plugin match type. */
public static DynamicTemplate parse(String name, Map<String, Object> conf) throws MapperParsingException {
return parse(name, conf, null);
}

/**
* Parses a dynamic template. If {@code knownPluginTypes} is non-null, a {@code match_mapping_type}
* that is neither an {@link XContentFieldType} nor a registered plugin type fails here, before any
* {@link DynamicTemplate} is constructed.
*/
static DynamicTemplate parse(String name, Map<String, Object> conf, Map<String, DynamicTemplateTypeHandler> knownPluginTypes)
throws MapperParsingException {
String match = null;
String pathMatch = null;
String unmatch = null;
Expand Down Expand Up @@ -235,8 +246,28 @@ public static DynamicTemplate parse(String name, Map<String, Object> conf) throw
}

XContentFieldType xcontentFieldType = null;
if (matchMappingType != null && matchMappingType.equals("*") == false) {
xcontentFieldType = XContentFieldType.fromString(matchMappingType);
String pluginMatchType = null;
if (matchMappingType != null && !matchMappingType.equals("*")) {
for (XContentFieldType t : XContentFieldType.values()) {
if (t.toString().equals(matchMappingType)) {
xcontentFieldType = t;
break;
}
}
if (xcontentFieldType == null) {
pluginMatchType = matchMappingType;
// Validate plugin type before constructing the template
if (knownPluginTypes != null && !knownPluginTypes.containsKey(pluginMatchType)) {
List<String> allTypes = new ArrayList<>();
for (XContentFieldType t : XContentFieldType.values()) {
allTypes.add(t.toString());
}
allTypes.addAll(knownPluginTypes.keySet());
throw new IllegalArgumentException(
"No field type matched on [" + pluginMatchType + "], possible values are " + allTypes
);
}
}
}

final MatchType matchType = MatchType.fromString(matchPattern);
Expand All @@ -256,7 +287,7 @@ public static DynamicTemplate parse(String name, Map<String, Object> conf) throw
}
}

return new DynamicTemplate(name, pathMatch, pathUnmatch, match, unmatch, xcontentFieldType, matchType, mapping);
return new DynamicTemplate(name, pathMatch, pathUnmatch, match, unmatch, xcontentFieldType, pluginMatchType, matchType, mapping);
}

private final String name;
Expand All @@ -273,6 +304,8 @@ public static DynamicTemplate parse(String name, Map<String, Object> conf) throw

private final XContentFieldType xcontentFieldType;

private final String pluginMatchType;

private final Map<String, Object> mapping;

private DynamicTemplate(
Expand All @@ -282,6 +315,7 @@ private DynamicTemplate(
String match,
String unmatch,
XContentFieldType xcontentFieldType,
String pluginMatchType,
MatchType matchType,
Map<String, Object> mapping
) {
Expand All @@ -292,6 +326,7 @@ private DynamicTemplate(
this.unmatch = unmatch;
this.matchType = matchType;
this.xcontentFieldType = xcontentFieldType;
this.pluginMatchType = pluginMatchType;
this.mapping = mapping;
}

Expand All @@ -303,25 +338,26 @@ public String pathMatch() {
return pathMatch;
}

private boolean matchesPathAndName(String path, String name) {
if (pathMatch != null && !matchType.matches(pathMatch, path)) return false;
if (match != null && !matchType.matches(match, name)) return false;
if (pathUnmatch != null && matchType.matches(pathUnmatch, path)) return false;
if (unmatch != null && matchType.matches(unmatch, name)) return false;
return true;
}

public boolean match(String path, String name, XContentFieldType xcontentFieldType) {
if (pathMatch != null && !matchType.matches(pathMatch, path)) {
return false;
}
if (match != null && !matchType.matches(match, name)) {
return false;
}
if (pathUnmatch != null && matchType.matches(pathUnmatch, path)) {
return false;
}
if (unmatch != null && matchType.matches(unmatch, name)) {
return false;
}
if (this.xcontentFieldType != null && this.xcontentFieldType != xcontentFieldType) {
return false;
}
if (!matchesPathAndName(path, name)) return false;
if (this.xcontentFieldType != null && this.xcontentFieldType != xcontentFieldType) return false;
return true;
}

/** Returns true if this template's {@code match_mapping_type} equals {@code pluginType} and the path/name patterns match. */
public boolean matchesPluginType(String path, String name, String pluginType) {
if (this.pluginMatchType == null || !this.pluginMatchType.equals(pluginType)) return false;
return matchesPathAndName(path, name);
}

public String mappingType(String dynamicType) {
String type;
if (mapping.containsKey("type")) {
Expand Down Expand Up @@ -401,6 +437,11 @@ XContentFieldType getXContentFieldType() {
return xcontentFieldType;
}

/** Returns the plugin-registered match_mapping_type string, or null if this is a standard template. */
public String getPluginMatchType() {
return pluginMatchType;
}

Map<String, Object> getMapping() {
return mapping;
}
Expand All @@ -422,6 +463,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
}
if (xcontentFieldType != null) {
builder.field("match_mapping_type", xcontentFieldType);
} else if (pluginMatchType != null) {
builder.field("match_mapping_type", pluginMatchType);
} else if (match == null && pathMatch == null) {
builder.field("match_mapping_type", "*");
}
Expand Down
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;
}
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,20 @@ public Set<String> getMetadataFields() {
return Collections.unmodifiableSet(mapperRegistry.getMetadataMapperParsers().keySet());
}

/**
* Returns the list of registered dynamic field type inferencers.
*/
public List<DynamicFieldTypeInferencer> getDynamicFieldTypeInferencers() {
return mapperRegistry.getDynamicFieldTypeInferencers();
}

/**
* Returns the map of registered dynamic template type handlers.
*/
public Map<String, DynamicTemplateTypeHandler> getDynamicTemplateTypes() {
return mapperRegistry.getDynamicTemplateTypes();
}

/**
* An analyzer wrapper that can lookup fields within the index mappings
*/
Expand Down
Loading
Loading