/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.elasticsearch.schema.impl; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.hibernate.search.elasticsearch.client.impl.URLEncodedString; import org.hibernate.search.elasticsearch.logging.impl.Log; import org.hibernate.search.elasticsearch.schema.impl.json.AnalysisJsonElementEquivalence; import org.hibernate.search.elasticsearch.schema.impl.json.AnalysisParameterEquivalenceRegistry; import org.hibernate.search.elasticsearch.schema.impl.model.DataType; import org.hibernate.search.elasticsearch.schema.impl.model.DynamicType; import org.hibernate.search.elasticsearch.schema.impl.model.IndexMetadata; import org.hibernate.search.elasticsearch.schema.impl.model.IndexType; import org.hibernate.search.elasticsearch.schema.impl.model.PropertyMapping; import org.hibernate.search.elasticsearch.schema.impl.model.TypeMapping; import org.hibernate.search.elasticsearch.settings.impl.model.AnalysisDefinition; import org.hibernate.search.elasticsearch.settings.impl.model.AnalyzerDefinition; import org.hibernate.search.elasticsearch.settings.impl.model.CharFilterDefinition; import org.hibernate.search.elasticsearch.settings.impl.model.IndexSettings; import org.hibernate.search.elasticsearch.settings.impl.model.TokenFilterDefinition; import org.hibernate.search.elasticsearch.settings.impl.model.TokenizerDefinition; import org.hibernate.search.exception.AssertionFailure; import org.hibernate.search.util.impl.CollectionHelper; import org.hibernate.search.util.logging.impl.LoggerFactory; import org.jboss.logging.Messages; import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; /** * An {@link ElasticsearchSchemaValidator} implementation for Elasticsearch 2. * <p> * <strong>Important implementation note:</strong> unexpected attributes (i.e. those not mapped to a field in TypeMapping) * are totally ignored. This allows users to leverage Elasticsearch features that are not supported in * Hibernate Search, by setting those attributes manually. * * @author Yoann Rodiere */ public class Elasticsearch2SchemaValidator implements ElasticsearchSchemaValidator { private static final Log LOG = LoggerFactory.make( Log.class ); private static final ElasticsearchValidationMessages MESSAGES = Messages.getBundle( ElasticsearchValidationMessages.class ); private static final double DEFAULT_DOUBLE_DELTA = 0.001; private static final float DEFAULT_FLOAT_DELTA = 0.001f; private static final List<String> DEFAULT_DATE_FORMAT; static { List<String> formats = new ArrayList<>(); formats.add( "strict_date_optional_time" ); formats.add( "epoch_millis" ); DEFAULT_DATE_FORMAT = CollectionHelper.toImmutableList( formats ); } private static final AnalysisParameterEquivalenceRegistry ANALYZER_EQUIVALENCES = new AnalysisParameterEquivalenceRegistry.Builder() .type( "keep_types" ) .param( "types" ).unorderedArray() .end() .build(); private static final AnalysisParameterEquivalenceRegistry CHAR_FILTER_EQUIVALENCES = new AnalysisParameterEquivalenceRegistry.Builder().build(); private static final AnalysisParameterEquivalenceRegistry TOKENIZER_EQUIVALENCES = new AnalysisParameterEquivalenceRegistry.Builder() .type( "edgeNGram" ) .param( "token_chars" ).unorderedArray() .end() .type( "nGram" ) .param( "token_chars" ).unorderedArray() .end() .type( "stop" ) .param( "stopwords" ).unorderedArray() .end() .type( "word_delimiter" ) .param( "protected_words" ).unorderedArray() .end() .type( "keyword_marker" ) .param( "keywords" ).unorderedArray() .end() .type( "pattern_capture" ) .param( "patterns" ).unorderedArray() .end() .type( "common_grams" ) .param( "common_words" ).unorderedArray() .end() .type( "cjk_bigram" ) .param( "ignored_scripts" ).unorderedArray() .end() .build(); private static final AnalysisParameterEquivalenceRegistry TOKEN_FILTER_EQUIVALENCES = new AnalysisParameterEquivalenceRegistry.Builder() .type( "keep_types" ) .param( "types" ).unorderedArray() .end() .build(); private final ElasticsearchSchemaAccessor schemaAccessor; private final Validator<TypeMapping> typeMappingValidator = new TypeMappingValidator( new PropertyMappingValidator() ); private final Validator<AnalyzerDefinition> analyzerDefinitionValidator = new AnalyzerDefinitionValidator( ANALYZER_EQUIVALENCES ); private final Validator<CharFilterDefinition> charFilterDefinitionValidator = new AnalysisDefinitionValidator<>( CHAR_FILTER_EQUIVALENCES ); private final Validator<TokenizerDefinition> tokenizerDefinitionValidator = new AnalysisDefinitionValidator<>( TOKENIZER_EQUIVALENCES ); private final Validator<TokenFilterDefinition> tokenFilterDefinitionValidator = new AnalysisDefinitionValidator<>( TOKEN_FILTER_EQUIVALENCES ); public Elasticsearch2SchemaValidator(ElasticsearchSchemaAccessor schemaAccessor) { super(); this.schemaAccessor = schemaAccessor; } @Override public void validate(IndexMetadata expectedIndexMetadata, ExecutionOptions executionOptions) { URLEncodedString indexName = expectedIndexMetadata.getName(); IndexMetadata actualIndexMetadata = schemaAccessor.getCurrentIndexMetadata( indexName ); ValidationErrorCollector errorCollector = new ValidationErrorCollector(); errorCollector.push( ValidationContextType.INDEX, indexName.original ); try { validate( errorCollector, expectedIndexMetadata, actualIndexMetadata ); } finally { errorCollector.pop(); } Map<ValidationContext, List<String>> messagesByContext = errorCollector.getMessagesByContext(); if ( messagesByContext.isEmpty() ) { return; } StringBuilder builder = new StringBuilder(); for ( Map.Entry<ValidationContext, List<String>> entry : messagesByContext.entrySet() ) { ValidationContext context = entry.getKey(); List<String> messages = entry.getValue(); builder.append( "\n" ).append( formatContext( context ) ); for ( String message : messages ) { builder.append( "\n\t" ).append( message ); } } throw LOG.schemaValidationFailed( builder.toString() ); } @Override public boolean isSettingsValid(IndexMetadata expectedIndexMetadata, ExecutionOptions executionOptions) { URLEncodedString indexName = expectedIndexMetadata.getName(); IndexMetadata actualIndexMetadata = schemaAccessor.getCurrentIndexMetadata( indexName ); ValidationErrorCollector errorCollector = new ValidationErrorCollector(); errorCollector.push( ValidationContextType.INDEX, indexName.original ); try { validateIndexSettings( errorCollector, expectedIndexMetadata.getSettings(), actualIndexMetadata.getSettings() ); } finally { errorCollector.pop(); } return errorCollector.getMessagesByContext().isEmpty(); } /** * Format the validation context using the following format: * {@code contextElement1, contextElement2, ... , contextElementN:}. * <p> * Each element is rendered using {@link #MESSAGES}. * <p> * Multiple consecutive property contexts are squeezed into a single path * (e.g. "foo" followed by "bar" becomes "foo.bar". * * @param context The validation context to format. * @return The validation context rendered as a string. */ private String formatContext(ValidationContext context) { StringBuilder builder = new StringBuilder(); StringBuilder pathBuilder = new StringBuilder(); for ( ValidationContextElement element : context.getElements() ) { String name = element.getName(); if ( ValidationContextType.MAPPING_PROPERTY.equals( element.getType() ) ) { // Try to concatenate property names into a path before we actually append them if ( pathBuilder.length() > 0 ) { pathBuilder.append( "." ); } pathBuilder.append( name ); } else { if ( pathBuilder.length() > 0 ) { // Append the accumulated path appendContextElement( builder, ValidationContextType.MAPPING_PROPERTY, pathBuilder.toString() ); pathBuilder.setLength( 0 ); // Clear } appendContextElement( builder, element.getType(), element.getName() ); } } if ( pathBuilder.length() > 0 ) { // Append the remaining accumulated path appendContextElement( builder, ValidationContextType.MAPPING_PROPERTY, pathBuilder.toString() ); } builder.append( ":" ); return builder.toString(); } private void appendContextElement(StringBuilder builder, ValidationContextType type, String name) { String formatted; switch ( type ) { case INDEX: formatted = MESSAGES.indexContext( name ); break; case MAPPING: formatted = MESSAGES.mappingContext( name ); break; case MAPPING_PROPERTY: formatted = MESSAGES.mappingPropertyContext( name ); break; case MAPPING_PROPERTY_FIELD: formatted = MESSAGES.mappingPropertyFieldContext( name ); break; case ANALYZER: formatted = MESSAGES.analyzerContext( name ); break; case CHAR_FILTER: formatted = MESSAGES.charFilterContext( name ); break; case TOKENIZER: formatted = MESSAGES.tokenizerContext( name ); break; case TOKEN_FILTER: formatted = MESSAGES.tokenFilterContext( name ); break; default: throw new AssertionFailure( "Unexpected validation context element type: " + type ); } if ( builder.length() > 0 ) { builder.append( ", " ); } builder.append( formatted ); } private void validate(ValidationErrorCollector errorCollector, IndexMetadata expectedIndexMetadata, IndexMetadata actualIndexMetadata) { validateIndexSettings( errorCollector, expectedIndexMetadata.getSettings(), actualIndexMetadata.getSettings() ); validateAll( errorCollector, ValidationContextType.MAPPING, MESSAGES.mappingMissing(), typeMappingValidator, expectedIndexMetadata.getMappings(), actualIndexMetadata.getMappings() ); } private void validateIndexSettings(ValidationErrorCollector errorCollector, IndexSettings expectedSettings, IndexSettings actualSettings) { IndexSettings.Analysis expectedAnalysis = expectedSettings.getAnalysis(); if ( expectedAnalysis == null ) { // No expectation return; } IndexSettings.Analysis actualAnalysis = actualSettings.getAnalysis(); validateAll( errorCollector, ValidationContextType.ANALYZER, MESSAGES.analyzerMissing(), analyzerDefinitionValidator, expectedAnalysis.getAnalyzers(), actualAnalysis == null ? null : actualAnalysis.getAnalyzers() ); validateAll( errorCollector, ValidationContextType.CHAR_FILTER, MESSAGES.charFilterMissing(), charFilterDefinitionValidator, expectedAnalysis.getCharFilters(), actualAnalysis == null ? null : actualAnalysis.getCharFilters() ); validateAll( errorCollector, ValidationContextType.TOKENIZER, MESSAGES.tokenizerMissing(), tokenizerDefinitionValidator, expectedAnalysis.getTokenizers(), actualAnalysis == null ? null : actualAnalysis.getTokenizers() ); validateAll( errorCollector, ValidationContextType.TOKEN_FILTER, MESSAGES.tokenFilterMissing(), tokenFilterDefinitionValidator, expectedAnalysis.getTokenFilters(), actualAnalysis == null ? null : actualAnalysis.getTokenFilters() ); } /* * Validate that two values are equal, using a given default value when null is encountered on either value. * Useful to take into account the fact that Elasticsearch has default values for attributes. */ protected <T> void validateEqualWithDefault(ValidationErrorCollector errorCollector, String attributeName, T expectedValue, T actualValue, T defaultValueForNulls) { Object defaultedExpectedValue = expectedValue == null ? defaultValueForNulls : expectedValue; Object defaultedActualValue = actualValue == null ? defaultValueForNulls : actualValue; if ( ! Objects.equals( defaultedExpectedValue, defaultedActualValue ) ) { // Don't show the defaulted actual value, this might confuse users errorCollector.addError( MESSAGES.invalidAttributeValue( attributeName, defaultedExpectedValue, actualValue ) ); } } /* * Variation of validateEqualWithDefault() for floats. */ protected <T> void validateEqualWithDefault(ValidationErrorCollector errorCollector, String attributeName, Float expectedValue, Float actualValue, float delta, Float defaultValueForNulls) { Float defaultedExpectedValue = expectedValue == null ? defaultValueForNulls : expectedValue; Float defaultedActualValue = actualValue == null ? defaultValueForNulls : actualValue; if ( defaultedExpectedValue == null || defaultedActualValue == null ) { if ( defaultedExpectedValue == defaultedActualValue ) { // Both null return; } else { // One null and one non-null // Don't show the defaulted actual value, this might confuse users errorCollector.addError( MESSAGES.invalidAttributeValue( attributeName, defaultedExpectedValue, actualValue ) ); } } else { if ( Float.compare( defaultedExpectedValue, defaultedActualValue ) == 0 ) { return; } if ( Math.abs( defaultedExpectedValue - defaultedActualValue ) > delta ) { // Don't show the defaulted actual value, this might confuse users errorCollector.addError( MESSAGES.invalidAttributeValue( attributeName, defaultedExpectedValue, actualValue ) ); } } } /* * Variation of validateEqualWithDefault() for doubles. */ protected <T> void validateEqualWithDefault(ValidationErrorCollector errorCollector, String attributeName, Double expectedValue, Double actualValue, double delta, Double defaultValueForNulls) { Double defaultedExpectedValue = expectedValue == null ? defaultValueForNulls : expectedValue; Double defaultedActualValue = actualValue == null ? defaultValueForNulls : actualValue; if ( defaultedExpectedValue == null || defaultedActualValue == null ) { if ( defaultedExpectedValue == defaultedActualValue ) { // Both null return; } else { // One null and one non-null // Don't show the defaulted actual value, this might confuse users errorCollector.addError( MESSAGES.invalidAttributeValue( attributeName, defaultedExpectedValue, actualValue ) ); } } if ( Double.compare( defaultedExpectedValue, defaultedActualValue ) == 0 ) { return; } if ( Math.abs( defaultedExpectedValue - defaultedActualValue ) > delta ) { // Don't show the defaulted actual value, this might confuse users errorCollector.addError( MESSAGES.invalidAttributeValue( attributeName, defaultedExpectedValue, actualValue ) ); } } /* * Special validation for an Elasticsearch format: * - Checks that the first element (the format used for output format in ES) is equal * - Checks all expected formats are present in the actual value */ protected <T> void validateFormatWithDefault(ValidationErrorCollector errorCollector, String attributeName, List<String> expectedValue, List<String> actualValue, List<String> defaultValueForNulls) { List<String> defaultedExpectedValue = expectedValue == null ? defaultValueForNulls : expectedValue; List<String> defaultedActualValue = actualValue == null ? defaultValueForNulls : actualValue; if ( defaultedExpectedValue.isEmpty() ) { return; } String expectedOutputFormat = defaultedExpectedValue.get( 0 ); String actualOutputFormat = defaultedActualValue.isEmpty() ? null : defaultedActualValue.get( 0 ); if ( ! Objects.equals( expectedOutputFormat, actualOutputFormat ) ) { // Don't show the defaulted actual value, this might confuse users errorCollector.addError( MESSAGES.invalidOutputFormat( attributeName, expectedOutputFormat, actualOutputFormat ) ); } List<String> missingFormats = new ArrayList<>(); missingFormats.addAll( defaultedExpectedValue ); missingFormats.removeAll( defaultedActualValue ); List<String> unexpectedFormats = new ArrayList<>(); unexpectedFormats.addAll( defaultedActualValue ); unexpectedFormats.removeAll( defaultedExpectedValue ); if ( !missingFormats.isEmpty() || !unexpectedFormats.isEmpty() ) { errorCollector.addError( MESSAGES.invalidInputFormat( attributeName, defaultedExpectedValue, defaultedActualValue, missingFormats, unexpectedFormats ) ); } } protected final void validateJsonPrimitive(ValidationErrorCollector errorCollector, DataType type, String attributeName, JsonPrimitive expectedValue, JsonPrimitive actualValue) { DataType defaultedType = type == null ? DataType.OBJECT : type; doValidateJsonPrimitive( errorCollector, defaultedType, attributeName, expectedValue, actualValue ); } @SuppressWarnings("deprecation") protected void doValidateJsonPrimitive(ValidationErrorCollector errorCollector, DataType type, String attributeName, JsonPrimitive expectedValue, JsonPrimitive actualValue) { // We can't just use equal, mainly because of floating-point numbers switch ( type ) { case DOUBLE: if ( expectedValue.isNumber() && actualValue.isNumber() ) { validateEqualWithDefault( errorCollector, attributeName, expectedValue.getAsDouble(), actualValue.getAsDouble(), DEFAULT_DOUBLE_DELTA, null ); } else { errorCollector.addError( MESSAGES.invalidAttributeValue( attributeName, expectedValue, actualValue ) ); } break; case FLOAT: if ( expectedValue.isNumber() && actualValue.isNumber() ) { validateEqualWithDefault( errorCollector, attributeName, expectedValue.getAsFloat(), actualValue.getAsFloat(), DEFAULT_FLOAT_DELTA, null ); } else { errorCollector.addError( MESSAGES.invalidAttributeValue( attributeName, expectedValue, actualValue ) ); } break; case INTEGER: case LONG: case DATE: case BOOLEAN: case STRING: case OBJECT: case GEO_POINT: default: validateEqualWithDefault( errorCollector, attributeName, expectedValue, actualValue, null ); break; } } private interface Validator<T> { void validate(ValidationErrorCollector errorCollector, T expected, T actual); } /* * Validate all elements in a map. * * Unexpected elements are ignored, we only validate expected elements. */ protected <T> void validateAll( ValidationErrorCollector errorCollector, ValidationContextType type, String messageIfMissing, Validator<T> validator, Map<String, T> expectedMap, Map<String, T> actualMap) { if ( expectedMap == null || expectedMap.isEmpty() ) { return; } if ( actualMap == null ) { actualMap = Collections.<String, T>emptyMap(); } for ( Map.Entry<String, T> entry : expectedMap.entrySet() ) { String name = entry.getKey(); T expected = entry.getValue(); T actual = actualMap.get( name ); errorCollector.push( type, name ); try { if ( actual == null ) { errorCollector.addError( messageIfMissing ); continue; } validator.validate( errorCollector, expected, actual ); } finally { errorCollector.pop(); } } } private class AnalysisDefinitionValidator<T extends AnalysisDefinition> implements Validator<T> { private final AnalysisParameterEquivalenceRegistry equivalences; public AnalysisDefinitionValidator(AnalysisParameterEquivalenceRegistry equivalences) { super(); this.equivalences = equivalences; } @Override public void validate(ValidationErrorCollector errorCollector, T expectedDefinition, T actualDefinition) { if ( ! Objects.equals( expectedDefinition.getType(), actualDefinition.getType() ) ) { errorCollector.addError( MESSAGES.invalidAnalysisDefinitionType( expectedDefinition.getType(), actualDefinition.getType() ) ); } Map<String, JsonElement> expectedParameters = expectedDefinition.getParameters(); if ( expectedParameters == null ) { expectedParameters = Collections.emptyMap(); } Map<String, JsonElement> actualParameters = actualDefinition.getParameters(); if ( actualParameters == null ) { actualParameters = Collections.emptyMap(); } // We also validate there isn't any unexpected parameters Set<String> parametersToValidate = new HashSet<>(); parametersToValidate.addAll( expectedParameters.keySet() ); parametersToValidate.addAll( actualParameters.keySet() ); String typeName = expectedDefinition.getType(); for ( String parameterName : parametersToValidate ) { JsonElement expected = expectedParameters.get( parameterName ); JsonElement actual = actualParameters.get( parameterName ); AnalysisJsonElementEquivalence parameterEquivalence = equivalences.get( typeName, parameterName ); if ( ! parameterEquivalence.isEquivalent( expected, actual ) ) { errorCollector.addError( MESSAGES.invalidAnalysisDefinitionParameter( parameterName, expected, actual ) ); } } } } private class AnalyzerDefinitionValidator extends AnalysisDefinitionValidator<AnalyzerDefinition> { public AnalyzerDefinitionValidator(AnalysisParameterEquivalenceRegistry equivalences) { super( equivalences ); } @Override public void validate(ValidationErrorCollector errorCollector, AnalyzerDefinition expectedDefinition, AnalyzerDefinition actualDefinition) { super.validate( errorCollector, expectedDefinition, actualDefinition ); if ( ! Objects.equals( expectedDefinition.getCharFilters(), actualDefinition.getCharFilters() ) ) { errorCollector.addError( MESSAGES.invalidAnalyzerCharFilters( expectedDefinition.getCharFilters(), actualDefinition.getCharFilters() ) ); } if ( ! Objects.equals( expectedDefinition.getTokenizer(), actualDefinition.getTokenizer() ) ) { errorCollector.addError( MESSAGES.invalidAnalyzerTokenizer( expectedDefinition.getTokenizer(), actualDefinition.getTokenizer() ) ); } if ( ! Objects.equals( expectedDefinition.getTokenFilters(), actualDefinition.getTokenFilters() ) ) { errorCollector.addError( MESSAGES.invalidAnalyzerTokenFilters( expectedDefinition.getTokenFilters(), actualDefinition.getTokenFilters() ) ); } } } private abstract class AbstractTypeMappingValidator<T extends TypeMapping> implements Validator<T> { protected abstract Validator<PropertyMapping> getPropertyMappingValidator(); @Override public void validate(ValidationErrorCollector errorCollector, T expectedMapping, T actualMapping) { DynamicType expectedDynamic = expectedMapping.getDynamic(); if ( expectedDynamic != null ) { // If not provided, we don't care validateEqualWithDefault( errorCollector, "dynamic", expectedDynamic, actualMapping.getDynamic(), DynamicType.TRUE ); } validateAll( errorCollector, ValidationContextType.MAPPING_PROPERTY, MESSAGES.propertyMissing(), getPropertyMappingValidator(), expectedMapping.getProperties(), actualMapping.getProperties() ); } } private class TypeMappingValidator extends AbstractTypeMappingValidator<TypeMapping> { private final Validator<PropertyMapping> propertyMappingValidator; public TypeMappingValidator(Validator<PropertyMapping> propertyMappingValidator) { super(); this.propertyMappingValidator = propertyMappingValidator; } @Override protected Validator<PropertyMapping> getPropertyMappingValidator() { return propertyMappingValidator; } } private class PropertyMappingValidator extends AbstractTypeMappingValidator<PropertyMapping> { @Override protected Validator<PropertyMapping> getPropertyMappingValidator() { return this; } @Override public void validate(ValidationErrorCollector errorCollector, PropertyMapping expectedMapping, PropertyMapping actualMapping) { validateEqualWithDefault( errorCollector, "type", expectedMapping.getType(), actualMapping.getType(), DataType.OBJECT ); List<String> formatDefault = DataType.DATE.equals( expectedMapping.getType() ) ? DEFAULT_DATE_FORMAT : Collections.<String>emptyList(); validateFormatWithDefault( errorCollector, "format", expectedMapping.getFormat(), actualMapping.getFormat(), formatDefault ); validateEqualWithDefault( errorCollector, "boost", expectedMapping.getBoost(), actualMapping.getBoost(), DEFAULT_FLOAT_DELTA, 1.0f ); validateIndexOptions( errorCollector, expectedMapping, actualMapping ); Boolean expectedStore = expectedMapping.getStore(); if ( Boolean.TRUE.equals( expectedStore ) ) { // If we don't need storage, we don't care validateEqualWithDefault( errorCollector, "store", expectedStore, actualMapping.getStore(), false ); } validateJsonPrimitive( errorCollector, expectedMapping.getType(), "null_value", expectedMapping.getNullValue(), actualMapping.getNullValue() ); validateEqualWithDefault( errorCollector, "analyzer", expectedMapping.getAnalyzer(), actualMapping.getAnalyzer(), "default" ); super.validate( errorCollector, expectedMapping, actualMapping ); // Validate fields with the same method as properties, since the content is about the same validateAll( errorCollector, ValidationContextType.MAPPING_PROPERTY_FIELD, MESSAGES.propertyFieldMissing(), getPropertyMappingValidator(), expectedMapping.getFields(), actualMapping.getFields() ); } } @SuppressWarnings("deprecation") protected void validateIndexOptions(ValidationErrorCollector errorCollector, PropertyMapping expectedMapping, PropertyMapping actualMapping) { IndexType expectedIndex = expectedMapping.getIndex(); if ( !IndexType.NO.equals( expectedIndex ) ) { // If we don't need an index, we don't care // See Elasticsearch doc: this attribute's default value depends on the data type. IndexType indexDefault = DataType.STRING.equals( expectedMapping.getType() ) ? IndexType.ANALYZED : IndexType.NOT_ANALYZED; validateEqualWithDefault( errorCollector, "index", expectedIndex, actualMapping.getIndex(), indexDefault ); } Boolean expectedDocValues = expectedMapping.getDocValues(); if ( Boolean.TRUE.equals( expectedDocValues ) ) { // If we don't need doc_values, we don't care /* * Elasticsearch documentation (2.3) says doc_values is true by default on fields * supporting it, but tests show it's wrong. */ validateEqualWithDefault( errorCollector, "doc_values", expectedDocValues, actualMapping.getDocValues(), false ); } } }