org.elasticsearch.index.mapper.MapperService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of elasticsearch Show documentation
Show all versions of elasticsearch Show documentation
Elasticsearch subproject :server
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.index.mapper;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.cluster.metadata.MappingMetadata;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.compress.CompressorFactory;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.index.AbstractIndexComponent;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.analysis.AnalysisRegistry;
import org.elasticsearch.index.analysis.IndexAnalyzers;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.index.similarity.SimilarityService;
import org.elasticsearch.indices.IndicesModule;
import org.elasticsearch.script.ScriptCompiler;
import org.elasticsearch.xcontent.NamedXContentRegistry;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.XContentType;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
public class MapperService extends AbstractIndexComponent implements Closeable {
/**
* The reason why a mapping is being merged.
*/
public enum MergeReason {
/**
* Pre-flight check before sending a dynamic mapping update to the master
*/
MAPPING_AUTO_UPDATE_PREFLIGHT {
@Override
public boolean isAutoUpdate() {
return true;
}
},
/**
* Dynamic mapping updates
*/
MAPPING_AUTO_UPDATE {
@Override
public boolean isAutoUpdate() {
return true;
}
},
/**
* Create or update a mapping.
*/
MAPPING_UPDATE,
/**
* Merge mappings from a composable index template.
*/
INDEX_TEMPLATE,
/**
* Recovery of an existing mapping, for instance because of a restart,
* if a shard was moved to a different node or for administrative
* purposes.
*/
MAPPING_RECOVERY;
public boolean isAutoUpdate() {
return false;
}
}
public static final String SINGLE_MAPPING_NAME = "_doc";
public static final String TYPE_FIELD_NAME = "_type";
public static final Setting INDEX_MAPPING_NESTED_FIELDS_LIMIT_SETTING = Setting.longSetting(
"index.mapping.nested_fields.limit",
50L,
0,
Property.Dynamic,
Property.IndexScope
);
// maximum allowed number of nested json objects across all fields in a single document
public static final Setting INDEX_MAPPING_NESTED_DOCS_LIMIT_SETTING = Setting.longSetting(
"index.mapping.nested_objects.limit",
10000L,
0,
Property.Dynamic,
Property.IndexScope
);
public static final Setting INDEX_MAPPING_TOTAL_FIELDS_LIMIT_SETTING = Setting.longSetting(
"index.mapping.total_fields.limit",
1000L,
0,
Property.Dynamic,
Property.IndexScope,
Property.ServerlessPublic
);
public static final Setting INDEX_MAPPING_IGNORE_DYNAMIC_BEYOND_LIMIT_SETTING = Setting.boolSetting(
"index.mapping.total_fields.ignore_dynamic_beyond_limit",
false,
Property.Dynamic,
Property.IndexScope
);
public static final Setting INDEX_MAPPING_DEPTH_LIMIT_SETTING = Setting.longSetting(
"index.mapping.depth.limit",
20L,
1,
Property.Dynamic,
Property.IndexScope
);
public static final Setting INDEX_MAPPING_FIELD_NAME_LENGTH_LIMIT_SETTING = Setting.longSetting(
"index.mapping.field_name_length.limit",
Long.MAX_VALUE,
1L,
Property.Dynamic,
Property.IndexScope
);
public static final Setting INDEX_MAPPING_DIMENSION_FIELDS_LIMIT_SETTING = Setting.longSetting(
"index.mapping.dimension_fields.limit",
32_768,
0,
Property.Dynamic,
Property.IndexScope
);
private final IndexAnalyzers indexAnalyzers;
private final MappingParser mappingParser;
private final DocumentParser documentParser;
private final IndexVersion indexVersionCreated;
private final MapperRegistry mapperRegistry;
private final Supplier mappingParserContextSupplier;
private volatile DocumentMapper mapper;
private volatile long mappingVersion;
public MapperService(
ClusterService clusterService,
IndexSettings indexSettings,
IndexAnalyzers indexAnalyzers,
XContentParserConfiguration parserConfiguration,
SimilarityService similarityService,
MapperRegistry mapperRegistry,
Supplier searchExecutionContextSupplier,
IdFieldMapper idFieldMapper,
ScriptCompiler scriptCompiler
) {
this(
() -> clusterService.state().getMinTransportVersion(),
indexSettings,
indexAnalyzers,
parserConfiguration,
similarityService,
mapperRegistry,
searchExecutionContextSupplier,
idFieldMapper,
scriptCompiler
);
}
@SuppressWarnings("this-escape")
public MapperService(
Supplier clusterTransportVersion,
IndexSettings indexSettings,
IndexAnalyzers indexAnalyzers,
XContentParserConfiguration parserConfiguration,
SimilarityService similarityService,
MapperRegistry mapperRegistry,
Supplier searchExecutionContextSupplier,
IdFieldMapper idFieldMapper,
ScriptCompiler scriptCompiler
) {
super(indexSettings);
this.indexVersionCreated = indexSettings.getIndexVersionCreated();
this.indexAnalyzers = indexAnalyzers;
this.mapperRegistry = mapperRegistry;
this.mappingParserContextSupplier = () -> new MappingParserContext(
similarityService::getSimilarity,
type -> mapperRegistry.getMapperParser(type, indexVersionCreated),
mapperRegistry.getRuntimeFieldParsers()::get,
indexVersionCreated,
clusterTransportVersion,
searchExecutionContextSupplier,
scriptCompiler,
indexAnalyzers,
indexSettings,
idFieldMapper
);
this.documentParser = new DocumentParser(parserConfiguration, this.mappingParserContextSupplier.get());
Map metadataMapperParsers = mapperRegistry.getMetadataMapperParsers(
indexSettings.getIndexVersionCreated()
);
this.mappingParser = new MappingParser(
mappingParserContextSupplier,
metadataMapperParsers,
this::getMetadataMappers,
this::resolveDocumentType
);
}
public boolean hasNested() {
return mappingLookup().nestedLookup() != NestedLookup.EMPTY;
}
public IndexAnalyzers getIndexAnalyzers() {
return this.indexAnalyzers;
}
public MappingParserContext parserContext() {
return mappingParserContextSupplier.get();
}
/**
* Exposes a {@link DocumentParser}
* @return a document parser to be used to parse incoming documents
*/
public DocumentParser documentParser() {
return this.documentParser;
}
Map, MetadataFieldMapper> getMetadataMappers() {
final MappingParserContext mappingParserContext = parserContext();
final DocumentMapper existingMapper = mapper;
final Map metadataMapperParsers = mapperRegistry.getMetadataMapperParsers(
indexSettings.getIndexVersionCreated()
);
Map, MetadataFieldMapper> metadataMappers = new LinkedHashMap<>();
if (existingMapper == null) {
for (MetadataFieldMapper.TypeParser parser : metadataMapperParsers.values()) {
MetadataFieldMapper metadataFieldMapper = parser.getDefault(mappingParserContext);
// A MetadataFieldMapper may choose to not be added to the metadata mappers
// of an index (eg TimeSeriesIdFieldMapper is only added to time series indices)
// In this case its TypeParser will return null instead of the MetadataFieldMapper
// instance.
if (metadataFieldMapper != null) {
metadataMappers.put(metadataFieldMapper.getClass(), metadataFieldMapper);
}
}
} else {
metadataMappers.putAll(existingMapper.mapping().getMetadataMappersMap());
}
return metadataMappers;
}
/**
* Parses the mappings (formatted as JSON) into a map
*/
public static Map parseMapping(NamedXContentRegistry xContentRegistry, String mappingSource) throws IOException {
if ("{}".equals(mappingSource)) {
// empty JSON is a common default value so it makes sense to optimize for it a little
return Map.of();
}
try (XContentParser parser = XContentType.JSON.xContent().createParser(parserConfig(xContentRegistry), mappingSource)) {
return parser.map();
}
}
/**
* Parses the mappings (formatted as JSON) into a map
*/
public static Map parseMapping(NamedXContentRegistry xContentRegistry, CompressedXContent mappingSource)
throws IOException {
try (
InputStream in = CompressorFactory.COMPRESSOR.threadLocalInputStream(mappingSource.compressedReference().streamInput());
XContentParser parser = XContentType.JSON.xContent().createParser(parserConfig(xContentRegistry), in)
) {
return parser.map();
}
}
private static XContentParserConfiguration parserConfig(NamedXContentRegistry xContentRegistry) {
return XContentParserConfiguration.EMPTY.withRegistry(xContentRegistry).withDeprecationHandler(LoggingDeprecationHandler.INSTANCE);
}
/**
* Update local mapping by applying the incoming mapping that have already been merged with the current one on the master
*/
public void updateMapping(final IndexMetadata currentIndexMetadata, final IndexMetadata newIndexMetadata) {
assert newIndexMetadata.getIndex().equals(index())
: "index mismatch: expected " + index() + " but was " + newIndexMetadata.getIndex();
if (currentIndexMetadata != null && currentIndexMetadata.getMappingVersion() == newIndexMetadata.getMappingVersion()) {
assert assertNoUpdateRequired(newIndexMetadata);
return;
}
MappingMetadata newMappingMetadata = newIndexMetadata.mapping();
if (newMappingMetadata != null) {
String type = newMappingMetadata.type();
CompressedXContent incomingMappingSource = newMappingMetadata.source();
Mapping incomingMapping = parseMapping(type, incomingMappingSource);
DocumentMapper previousMapper;
synchronized (this) {
previousMapper = this.mapper;
assert assertRefreshIsNotNeeded(previousMapper, type, incomingMapping);
this.mapper = newDocumentMapper(incomingMapping, MergeReason.MAPPING_RECOVERY, incomingMappingSource);
this.mappingVersion = newIndexMetadata.getMappingVersion();
}
String op = previousMapper != null ? "updated" : "added";
if (logger.isDebugEnabled() && incomingMappingSource.compressed().length < 512) {
logger.debug("[{}] {} mapping, source [{}]", index(), op, incomingMappingSource.string());
} else if (logger.isTraceEnabled()) {
logger.trace("[{}] {} mapping, source [{}]", index(), op, incomingMappingSource.string());
} else {
logger.debug("[{}] {} mapping (source suppressed due to length, use TRACE level if needed)", index(), op);
}
}
}
private boolean assertRefreshIsNotNeeded(DocumentMapper currentMapper, String type, Mapping incomingMapping) {
Mapping mergedMapping = mergeMappings(currentMapper, incomingMapping, MergeReason.MAPPING_RECOVERY, indexSettings);
// skip the runtime section or removed runtime fields will make the assertion fail
ToXContent.MapParams params = new ToXContent.MapParams(Collections.singletonMap(RootObjectMapper.TOXCONTENT_SKIP_RUNTIME, "true"));
CompressedXContent mergedMappingSource;
try {
mergedMappingSource = new CompressedXContent(mergedMapping, params);
} catch (Exception e) {
throw new AssertionError("failed to serialize source for type [" + type + "]", e);
}
CompressedXContent incomingMappingSource;
try {
incomingMappingSource = new CompressedXContent(incomingMapping, params);
} catch (Exception e) {
throw new AssertionError("failed to serialize source for type [" + type + "]", e);
}
// we used to ask the master to refresh its mappings whenever the result of merging the incoming mappings with the
// current mappings differs from the incoming mappings. We now rather assert that this situation never happens.
assert mergedMappingSource.equals(incomingMappingSource)
: "["
+ index()
+ "] parsed mapping, and got different sources\n"
+ "incoming:\n"
+ incomingMappingSource
+ "\nmerged:\n"
+ mergedMappingSource;
return true;
}
boolean assertNoUpdateRequired(final IndexMetadata newIndexMetadata) {
MappingMetadata mapping = newIndexMetadata.mapping();
if (mapping != null) {
// mapping representations may change between versions (eg text field mappers
// used to always explicitly serialize analyzers), so we cannot simply check
// that the incoming mappings are the same as the current ones: we need to
// parse the incoming mappings into a DocumentMapper and check that its
// serialization is the same as the existing mapper
Mapping newMapping = parseMapping(mapping.type(), mapping.source());
final CompressedXContent currentSource = this.mapper.mappingSource();
final CompressedXContent newSource = newMapping.toCompressedXContent();
if (Objects.equals(currentSource, newSource) == false
&& mapper.isSyntheticSourceMalformed(currentSource, indexVersionCreated) == false) {
throw new IllegalStateException(
"expected current mapping [" + currentSource + "] to be the same as new mapping [" + newSource + "]"
);
}
}
return true;
}
public void merge(IndexMetadata indexMetadata, MergeReason reason) {
assert reason != MergeReason.MAPPING_AUTO_UPDATE_PREFLIGHT;
MappingMetadata mappingMetadata = indexMetadata.mapping();
if (mappingMetadata != null) {
merge(mappingMetadata.type(), mappingMetadata.source(), reason);
}
}
/**
* Merging the provided mappings. Actual merging is done in the raw, non-parsed, form of the mappings. This allows to do a proper bulk
* merge, where parsing is done only when all raw mapping settings are already merged.
*/
public DocumentMapper merge(String type, List mappingSources, MergeReason reason) {
final DocumentMapper currentMapper = this.mapper;
if (currentMapper != null && mappingSources.size() == 1 && currentMapper.mappingSource().equals(mappingSources.get(0))) {
return currentMapper;
}
Map mergedRawMapping = null;
for (CompressedXContent mappingSource : mappingSources) {
Map rawMapping = MappingParser.convertToMap(mappingSource);
// normalize mappings, making sure that all have the provided type as a single root
if (rawMapping.containsKey(type)) {
if (rawMapping.size() > 1) {
throw new MapperParsingException("cannot merge a map with multiple roots, one of which is [" + type + "]");
}
} else {
rawMapping = Map.of(type, rawMapping);
}
if (mergedRawMapping == null) {
mergedRawMapping = rawMapping;
} else {
XContentHelper.merge(type, mergedRawMapping, rawMapping, RawFieldMappingMerge.INSTANCE);
}
}
if (mergedRawMapping != null && mergedRawMapping.size() > 1) {
throw new MapperParsingException("cannot merge mapping sources with different roots");
}
return (mergedRawMapping != null) ? doMerge(type, reason, mergedRawMapping) : null;
}
/**
* A {@link org.elasticsearch.common.xcontent.XContentHelper.CustomMerge} for raw map merges that are suitable for index/field mappings.
* The default raw map merge algorithm doesn't override values - if there are multiple values for a key, then:
*
* - if the values are of map type, the old and new values will be merged recursively
*
- otherwise, the original value will be maintained
*
* When merging field mappings, we want something else. Specifically:
*
* - within field mappings node (which is nested within a {@code properties} node):
*
* - if both the base mapping and the mapping to merge into it are of mergeable types (e.g {@code object -> object},
* {@code object -> nested}), then we only want to merge specific mapping entries:
*
* - {@code properties} node - merging fields from both mappings
*
- {@code subobjects} entry - since this setting affects an entire subtree, we need to keep it when merging
*
* - otherwise, for any couple of non-mergeable types ((e.g {@code object -> long}, {@code long -> long}) - we just want
* to replace the entire mappings subtree, let the last one win
*
* - any other map values that are not encountered within a {@code properties} node (e.g. "_doc", "_meta" or "properties"
* itself) - apply recursive merge as the default algorithm would apply
*
- any non-map values - override the value of the base map with the value of the merged map
*
*/
private static class RawFieldMappingMerge implements XContentHelper.CustomMerge {
private static final XContentHelper.CustomMerge INSTANCE = new RawFieldMappingMerge();
private static final Set MERGEABLE_OBJECT_TYPES = Set.of(ObjectMapper.CONTENT_TYPE, NestedObjectMapper.CONTENT_TYPE);
private RawFieldMappingMerge() {}
@SuppressWarnings("unchecked")
@Override
public Object merge(String parent, String key, Object oldValue, Object newValue) {
if (oldValue instanceof Map && newValue instanceof Map) {
if ("properties".equals(parent)) {
// merging two mappings of the same field, where "key" is the field name
Map baseMap = (Map) oldValue;
Map mapToMerge = (Map) newValue;
if (shouldMergeFieldMappings(baseMap, mapToMerge)) {
// if two field mappings are to be merged, we only want to keep some specific entries from the base mapping and
// let all others be overridden by the second mapping
Map mergedMappings = new HashMap<>();
// we must keep the "properties" node, otherwise our merge has no point
if (baseMap.containsKey("properties")) {
mergedMappings.put("properties", new HashMap<>((Map) baseMap.get("properties")));
}
// the "subobjects" setting affects an entire subtree and not only locally where it is configured
if (baseMap.containsKey("subobjects")) {
mergedMappings.put("subobjects", baseMap.get("subobjects"));
}
// recursively merge these two field mappings
XContentHelper.merge(key, mergedMappings, mapToMerge, INSTANCE);
return mergedMappings;
} else {
// non-mergeable types - replace the entire mapping subtree for this field
return mapToMerge;
}
}
// anything else (e.g. "_doc", "_meta", "properties") - no custom merge, rely on caller merge logic
// field mapping entries of Map type (like "fields" and "meta") are handled above and should never reach here
return null;
} else {
if (key.equals("required")) {
// we look for explicit `_routing.required` settings because we use them to detect contradictions of this setting
// that comes from mappings with such that comes from the optional `data_stream` configuration of composable index
// templates
if ("_routing".equals(parent) && oldValue != newValue) {
throw new MapperParsingException("contradicting `_routing.required` settings");
}
}
return newValue;
}
}
/**
* Normally, we don't want to merge raw field mappings, however there are cases where we do, for example - two
* "object" (or "nested") mappings.
*
* @param mappings1 first mapping of a field
* @param mappings2 second mapping of a field
* @return {@code true} if the second mapping should be merged into the first mapping
*/
private boolean shouldMergeFieldMappings(Map mappings1, Map mappings2) {
String type1 = (String) mappings1.get("type");
if (type1 == null && mappings1.get("properties") != null) {
type1 = ObjectMapper.CONTENT_TYPE;
}
String type2 = (String) mappings2.get("type");
if (type2 == null && mappings2.get("properties") != null) {
type2 = ObjectMapper.CONTENT_TYPE;
}
if (type1 == null || type2 == null) {
return false;
}
return MERGEABLE_OBJECT_TYPES.contains(type1) && MERGEABLE_OBJECT_TYPES.contains(type2);
}
}
public DocumentMapper merge(String type, CompressedXContent mappingSource, MergeReason reason) {
final DocumentMapper currentMapper = this.mapper;
if (currentMapper != null && currentMapper.mappingSource().equals(mappingSource)) {
return currentMapper;
}
Map mappingSourceAsMap = MappingParser.convertToMap(mappingSource);
return doMerge(type, reason, mappingSourceAsMap);
}
private synchronized DocumentMapper doMerge(String type, MergeReason reason, Map mappingSourceAsMap) {
Mapping incomingMapping = parseMapping(type, mappingSourceAsMap);
Mapping mapping = mergeMappings(this.mapper, incomingMapping, reason, this.indexSettings);
// TODO: In many cases the source here is equal to mappingSource so we need not serialize again.
// We should identify these cases reliably and save expensive serialization here
DocumentMapper newMapper = newDocumentMapper(mapping, reason, mapping.toCompressedXContent());
if (reason == MergeReason.MAPPING_AUTO_UPDATE_PREFLIGHT) {
return newMapper;
}
this.mapper = newMapper;
assert assertSerialization(newMapper);
return newMapper;
}
private DocumentMapper newDocumentMapper(Mapping mapping, MergeReason reason, CompressedXContent mappingSource) {
DocumentMapper newMapper = new DocumentMapper(documentParser, mapping, mappingSource, indexVersionCreated);
newMapper.validate(indexSettings, reason != MergeReason.MAPPING_RECOVERY);
return newMapper;
}
public Mapping parseMapping(String mappingType, CompressedXContent mappingSource) {
try {
return mappingParser.parse(mappingType, mappingSource);
} catch (Exception e) {
throw new MapperParsingException("Failed to parse mapping: {}", e, e.getMessage());
}
}
/**
* A method to parse mapping from a source in a map form.
*
* @param mappingType the mapping type
* @param mappingSource mapping source already converted to a map form, but not yet processed otherwise
* @return a parsed mapping
*/
public Mapping parseMapping(String mappingType, Map mappingSource) {
try {
return mappingParser.parse(mappingType, mappingSource);
} catch (Exception e) {
throw new MapperParsingException("Failed to parse mapping: {}", e, e.getMessage());
}
}
public static Mapping mergeMappings(
DocumentMapper currentMapper,
Mapping incomingMapping,
MergeReason reason,
IndexSettings indexSettings
) {
return mergeMappings(currentMapper, incomingMapping, reason, getMaxFieldsToAddDuringMerge(currentMapper, indexSettings, reason));
}
private static long getMaxFieldsToAddDuringMerge(DocumentMapper currentMapper, IndexSettings indexSettings, MergeReason reason) {
if (reason.isAutoUpdate() && indexSettings.isIgnoreDynamicFieldsBeyondLimit()) {
// If the index setting ignore_dynamic_beyond_limit is enabled,
// data nodes only add new dynamic fields until the limit is reached while parsing documents to be ingested.
// However, if there are concurrent mapping updates,
// data nodes may add dynamic fields under an outdated assumption that enough capacity is still available.
// When data nodes send the dynamic mapping update request to the master node,
// it will only add as many fields as there's actually capacity for when merging mappings.
long totalFieldsLimit = indexSettings.getMappingTotalFieldsLimit();
return Optional.ofNullable(currentMapper)
.map(DocumentMapper::mappers)
.map(ml -> ml.remainingFieldsUntilLimit(totalFieldsLimit))
.orElse(totalFieldsLimit);
} else {
// Else, we're not limiting the number of fields so that the merged mapping fails validation if it exceeds total_fields.limit.
// This is the desired behavior when making an explicit mapping update, even if ignore_dynamic_beyond_limit is enabled.
// When ignore_dynamic_beyond_limit is disabled and a dynamic mapping update would exceed the field limit,
// the document will get rejected.
// Normally, this happens on the data node in DocumentParserContext.addDynamicMapper but if there's a race condition,
// data nodes may add dynamic fields under an outdated assumption that enough capacity is still available.
// In this case, the master node will reject mapping updates that would exceed the limit when handling the mapping update.
return Long.MAX_VALUE;
}
}
static Mapping mergeMappings(DocumentMapper currentMapper, Mapping incomingMapping, MergeReason reason, long newFieldsBudget) {
Mapping newMapping;
if (currentMapper == null) {
newMapping = incomingMapping.withFieldsBudget(newFieldsBudget);
} else {
newMapping = currentMapper.mapping().merge(incomingMapping, reason, newFieldsBudget);
}
return newMapping;
}
private boolean assertSerialization(DocumentMapper mapper) {
// capture the source now, it may change due to concurrent parsing
final CompressedXContent mappingSource = mapper.mappingSource();
Mapping newMapping = parseMapping(mapper.type(), mappingSource);
if (newMapping.toCompressedXContent().equals(mappingSource) == false) {
throw new AssertionError(
"Mapping serialization result is different from source. \n--> Source ["
+ mappingSource
+ "]\n--> Result ["
+ newMapping.toCompressedXContent()
+ "]"
);
}
return true;
}
/**
* Return the document mapper, or {@code null} if no mapping has been put yet
* or no documents have been indexed in the current index yet (which triggers a dynamic mapping update)
*/
public DocumentMapper documentMapper() {
return mapper;
}
public long mappingVersion() {
return mappingVersion;
}
/**
* Returns {@code true} if the given {@code mappingSource} includes a type
* as a top-level object.
*/
public static boolean isMappingSourceTyped(String type, Map mapping) {
return mapping.size() == 1 && mapping.keySet().iterator().next().equals(type);
}
/**
* Resolves a type from a mapping-related request into the type that should be used when
* merging and updating mappings.
*
* If the special `_doc` type is provided, then we replace it with the actual type that is
* being used in the mappings. This allows typeless APIs such as 'index' or 'put mappings'
* to work against indices with a custom type name.
*/
private String resolveDocumentType(String type) {
if (MapperService.SINGLE_MAPPING_NAME.equals(type)) {
if (mapper != null) {
return mapper.type();
}
}
return type;
}
/**
* Given the full name of a field, returns its {@link MappedFieldType}.
*/
public MappedFieldType fieldType(String fullName) {
return mappingLookup().fieldTypesLookup().get(fullName);
}
/**
* Exposes a snapshot of the mappings for the current index.
* If no mappings have been registered for the current index, an empty {@link MappingLookup} instance is returned.
* An index does not have mappings only if it was created without providing mappings explicitly,
* and no documents have yet been indexed in it.
*/
public MappingLookup mappingLookup() {
DocumentMapper mapper = this.mapper;
return mapper == null ? MappingLookup.EMPTY : mapper.mappers();
}
/**
* Returns field types that have eager global ordinals.
*/
public Iterable getEagerGlobalOrdinalsFields() {
DocumentMapper mapper = this.mapper;
if (mapper == null) {
return Collections.emptySet();
}
MappingLookup mappingLookup = mapper.mappers();
return mappingLookup.getMatchingFieldNames("*")
.stream()
.map(mappingLookup::getFieldType)
.filter(MappedFieldType::eagerGlobalOrdinals)
.toList();
}
/**
* Return the index-time analyzer associated with a particular field
* @param field the field name
* @param unindexedFieldAnalyzer a function to return an Analyzer for a field with no
* directly associated index-time analyzer
*/
public NamedAnalyzer indexAnalyzer(String field, Function unindexedFieldAnalyzer) {
return mappingLookup().indexAnalyzer(field, unindexedFieldAnalyzer);
}
@Override
public void close() throws IOException {
indexAnalyzers.close();
}
/**
* @return Whether a field is a metadata field
* Deserialization of SearchHit objects sent from pre 7.8 nodes and GetResults objects sent from pre 7.3 nodes,
* uses this method to divide fields into meta and document fields.
* TODO: remove in v 9.0
* @deprecated Use an instance method isMetadataField instead
*/
@Deprecated
public static boolean isMetadataFieldStatic(String fieldName) {
if (IndicesModule.getBuiltInMetadataFields().contains(fieldName)) {
return true;
}
// if a node had Size Plugin installed, _size field should also be considered a meta-field
return fieldName.equals("_size");
}
/**
* @return Whether a field is a metadata field.
* this method considers all mapper plugins
*/
public boolean isMetadataField(String field) {
return mapperRegistry.getMetadataMapperParsers(indexVersionCreated).containsKey(field);
}
/**
* @return If this field is defined as a multifield of another field
*/
public boolean isMultiField(String field) {
return mappingLookup().isMultiField(field);
}
/**
* Reload any search analyzers that have reloadable components if resource is {@code null},
* otherwise only the provided resource is reloaded.
* @param registry the analysis registry
* @param resource the name of the reloadable resource or {@code null} if all resources should be reloaded.
* @param preview {@code false} applies analyzer reloading. {@code true} previews the reloading operation, so analyzers are not reloaded
* but the results retrieved. This is useful for understanding analyzers usage in the different indices.
* @return The names of reloaded resources (or resources that would be reloaded if {@code preview} is true).
* @throws IOException
*/
public synchronized List reloadSearchAnalyzers(AnalysisRegistry registry, @Nullable String resource, boolean preview)
throws IOException {
logger.debug("reloading search analyzers for index [{}]", indexSettings.getIndex().getName());
// TODO this should bust the cache somehow. Tracked in https://github.com/elastic/elasticsearch/issues/66722
return indexAnalyzers.reload(registry, indexSettings, resource, preview);
}
/**
* @return Returns all dynamic templates defined in this mapping.
*/
public DynamicTemplate[] getAllDynamicTemplates() {
return documentMapper().mapping().getRoot().dynamicTemplates();
}
public MapperRegistry getMapperRegistry() {
return mapperRegistry;
}
}