org.elasticsearch.index.mapper.MapperTestCase Maven / Gradle / Ivy
Show all versions of framework Show documentation
/*
* 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.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.IndexableFieldType;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.NoMergePolicy;
import org.apache.lucene.search.FieldExistsQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.analysis.MockAnalyzer;
import org.apache.lucene.tests.index.RandomIndexWriter;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.core.CheckedConsumer;
import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.index.IndexVersions;
import org.elasticsearch.index.fielddata.FieldDataContext;
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.fielddata.IndexFieldDataCache;
import org.elasticsearch.index.fielddata.LeafFieldData;
import org.elasticsearch.index.fieldvisitor.LeafStoredFieldLoader;
import org.elasticsearch.index.fieldvisitor.StoredFieldLoader;
import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.index.termvectors.TermVectorsService;
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptFactory;
import org.elasticsearch.script.field.DocValuesScriptFieldFactory;
import org.elasticsearch.search.DocValueFormat;
import org.elasticsearch.search.lookup.LeafStoredFieldsLookup;
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.search.lookup.Source;
import org.elasticsearch.search.lookup.SourceProvider;
import org.elasticsearch.test.ListMatcher;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.json.JsonXContent;
import org.hamcrest.Matcher;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static java.util.stream.Collectors.toList;
import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.DOC_VALUES;
import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.NONE;
import static org.elasticsearch.test.MapMatcher.assertMap;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Base class for testing {@link Mapper}s.
*/
public abstract class MapperTestCase extends MapperServiceTestCase {
public static final IndexVersion DEPRECATED_BOOST_INDEX_VERSION = IndexVersions.V_7_10_0;
protected abstract void minimalMapping(XContentBuilder b) throws IOException;
/**
* Provides a way for subclasses to define minimal mappings for previous index versions
* @param b content builder to use for building the minimal mapping
* @param indexVersion index version the mapping should be created for
* @throws IOException
*/
protected void minimalMapping(XContentBuilder b, IndexVersion indexVersion) throws IOException {
minimalMapping(b);
}
/**
* Writes the field and a sample value for it to the provided {@link XContentBuilder}.
* To be overridden in case the field should not be written at all in documents,
* like in the case of runtime fields.
*/
protected void writeField(XContentBuilder builder) throws IOException {
builder.field("field");
builder.value(getSampleValueForDocument());
}
/**
* Returns a sample value for the field, to be used in a document
*/
protected abstract Object getSampleValueForDocument();
/**
* Returns a sample object for the field or exception if the field does not support parsing objects
*/
protected Object getSampleObjectForDocument() {
throw new UnsupportedOperationException("Field doesn't support object parsing.");
}
/**
* Returns a sample value for the field, to be used when querying the field. Normally this is the same format as
* what is indexed as part of a document, and returned by {@link #getSampleValueForDocument()}, but there
* are cases where fields are queried differently frow how they are indexed e.g. token_count or runtime fields
*/
protected Object getSampleValueForQuery() {
return getSampleValueForDocument();
}
/**
* This test verifies that the exists query created is the appropriate one, and aligns with the data structures
* being created for a document with a value for the field. This can only be verified for the minimal mapping.
* Field types that allow configurable doc_values or norms should write their own tests that creates the different
* mappings combinations and invoke {@link #assertExistsQuery(MapperService)} to verify the behaviour.
*/
public final void testExistsQueryMinimalMapping() throws IOException {
MapperService mapperService = createMapperService(fieldMapping(this::minimalMapping));
assertExistsQuery(mapperService);
assertParseMinimalWarnings();
}
// TODO make this final once we've worked out what is happening with DenseVector
public void testAggregatableConsistency() throws IOException {
MapperService mapperService = createMapperService(fieldMapping(this::minimalMapping));
assertAggregatableConsistency(mapperService.fieldType("field"));
assertParseMinimalWarnings();
}
protected void assertAggregatableConsistency(MappedFieldType ft) {
if (ft.isAggregatable()) {
try {
ft.fielddataBuilder(FieldDataContext.noRuntimeFields("aggregation_test"));
} catch (Exception e) {
fail("Unexpected exception when fetching field data from aggregatable field type");
}
} else {
expectThrows(IllegalArgumentException.class, () -> ft.fielddataBuilder(FieldDataContext.noRuntimeFields("aggregation_test")));
}
}
/**
* Does this field mapper support {@code ignore_malformed}?
*/
protected abstract boolean supportsIgnoreMalformed();
/**
* Build an {@link ExampleMalformedValue} that parses a string.
*/
protected final ExampleMalformedValue exampleMalformedValue(String value) {
return exampleMalformedValue(b -> b.value(value));
}
/**
* Build an {@link ExampleMalformedValue} for arbitrary xcontent.
*/
protected final ExampleMalformedValue exampleMalformedValue(CheckedConsumer value) {
return new ExampleMalformedValue(this::minimalMapping, value, equalTo("unset"));
}
/**
* An example of a malformed value.
*/
public static class ExampleMalformedValue {
private final CheckedConsumer mapping;
private final CheckedConsumer value;
private final Matcher exceptionMessageMatcher;
private ExampleMalformedValue(
CheckedConsumer mapping,
CheckedConsumer value,
Matcher exceptionMessageMatcher
) {
this.mapping = mapping;
this.value = value;
this.exceptionMessageMatcher = exceptionMessageMatcher;
}
/**
* Set the mapping used for this value. If not called the default is
* {@link MapperTestCase#minimalMapping}.
*/
public ExampleMalformedValue mapping(CheckedConsumer newMapping) {
return new ExampleMalformedValue(newMapping, value, exceptionMessageMatcher);
}
/**
* Match error messages that contain a string.
*/
public ExampleMalformedValue errorMatches(String contains) {
return errorMatches(containsString(contains));
}
/**
* Match the error message in an arbitrary way.
*/
public ExampleMalformedValue errorMatches(Matcher newMatcher) {
return new ExampleMalformedValue(mapping, value, newMatcher);
}
}
/**
* Some example of malformed values and matches for exceptions that parsing them should create.
*/
protected List exampleMalformedValues() {
assertFalse("mappers that support ignore_malformed values most override exampleMalformedValues", supportsIgnoreMalformed());
return List.of();
}
public final void testIgnoreMalformedFalseByDefault() throws IOException {
for (ExampleMalformedValue example : exampleMalformedValues()) {
assertIgnoreMalformedFalse(example.mapping, example.value, example.exceptionMessageMatcher);
}
}
public final void testIgnoreMalformedExplicitlyFalse() throws IOException {
if (false == supportsIgnoreMalformed()) {
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> {
minimalMapping(b);
b.field("ignore_malformed", false);
})));
assertThat(e.getMessage(), containsString("unknown parameter [ignore_malformed] on mapper [field]"));
return;
}
for (ExampleMalformedValue example : exampleMalformedValues()) {
assertIgnoreMalformedFalse(b -> {
example.mapping.accept(b);
b.field("ignore_malformed", false);
}, example.value, example.exceptionMessageMatcher);
}
}
private void assertIgnoreMalformedFalse(
CheckedConsumer mapping,
CheckedConsumer value,
Matcher exceptionMessageMatcher
) throws IOException {
MapperService mapperService = createMapperService(fieldMapping(mapping));
FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field");
assertFalse(mapper.ignoreMalformed());
SourceToParse source = source(b -> {
b.field("field");
value.accept(b);
});
DocumentParsingException e = expectThrows(
DocumentParsingException.class,
"didn't throw while parsing " + source.source().utf8ToString(),
() -> mapperService.documentMapper().parse(source)
);
assertThat(
"incorrect exception while parsing " + source.source().utf8ToString(),
e.getCause().getMessage(),
exceptionMessageMatcher
);
}
public final void testIgnoreMalformedTrue() throws IOException {
if (false == supportsIgnoreMalformed()) {
Exception e = expectThrows(MapperParsingException.class, () -> createMapperService(fieldMapping(b -> {
minimalMapping(b);
b.field("ignore_malformed", true);
})));
assertThat(e.getMessage(), containsString("unknown parameter [ignore_malformed] on mapper [field]"));
return;
}
for (ExampleMalformedValue example : exampleMalformedValues()) {
XContentBuilder mapping = fieldMapping(b -> {
example.mapping.accept(b);
b.field("ignore_malformed", true);
});
MapperService mapperService = createMapperService(mapping);
FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field");
assertTrue(mapper.ignoreMalformed());
ParsedDocument doc = mapperService.documentMapper().parse(source(b -> {
b.field("field");
example.value.accept(b);
}));
List fields = doc.rootDoc().getFields("field");
assertThat(fields, empty());
assertThat(TermVectorsService.getValues(doc.rootDoc().getFields("_ignored")), contains("field"));
}
}
protected void assertExistsQuery(MapperService mapperService) throws IOException {
LuceneDocument fields = mapperService.documentMapper().parse(source(this::writeField)).rootDoc();
SearchExecutionContext searchExecutionContext = createSearchExecutionContext(mapperService);
MappedFieldType fieldType = mapperService.fieldType("field");
Query query = fieldType.existsQuery(searchExecutionContext);
assertExistsQuery(fieldType, query, fields);
}
protected void assertExistsQuery(MappedFieldType fieldType, Query query, LuceneDocument fields) {
if (fieldType.hasDocValues() || fieldType.getTextSearchInfo().hasNorms()) {
assertThat(query, instanceOf(FieldExistsQuery.class));
FieldExistsQuery fieldExistsQuery = (FieldExistsQuery) query;
assertEquals("field", fieldExistsQuery.getField());
assertNoFieldNamesField(fields);
} else {
assertThat(query, instanceOf(TermQuery.class));
TermQuery termQuery = (TermQuery) query;
assertEquals(FieldNamesFieldMapper.NAME, termQuery.getTerm().field());
// we always perform a term query against _field_names, even when the field
// is not added to _field_names because it is not indexed nor stored
assertEquals("field", termQuery.getTerm().text());
assertNoDocValuesField(fields, "field");
if (fieldType.isIndexed() || fieldType.isStored()) {
assertNotNull(fields.getField(FieldNamesFieldMapper.NAME));
} else {
assertNoFieldNamesField(fields);
}
}
}
protected static void assertNoFieldNamesField(LuceneDocument fields) {
assertNull(fields.getField(FieldNamesFieldMapper.NAME));
}
protected static void assertHasNorms(LuceneDocument doc, String field) {
List fields = doc.getFields(field);
for (IndexableField indexableField : fields) {
IndexableFieldType indexableFieldType = indexableField.fieldType();
if (indexableFieldType.indexOptions() != IndexOptions.NONE) {
assertFalse(indexableFieldType.omitNorms());
return;
}
}
fail("field [" + field + "] should be indexed but it isn't");
}
protected static void assertNoDocValuesField(LuceneDocument doc, String field) {
List fields = doc.getFields(field);
for (IndexableField indexableField : fields) {
assertEquals(DocValuesType.NONE, indexableField.fieldType().docValuesType());
}
}
protected void assertDimension(boolean isDimension, Function checker) throws IOException {
MapperService mapperService = createMapperService(fieldMapping(b -> {
minimalMapping(b);
b.field("time_series_dimension", isDimension);
}));
@SuppressWarnings("unchecked") // Syntactic sugar in tests
T fieldType = (T) mapperService.fieldType("field");
assertThat(checker.apply(fieldType), equalTo(isDimension));
}
protected void assertMetricType(String metricType, Function> checker) throws IOException {
MapperService mapperService = createMapperService(fieldMapping(b -> {
minimalMapping(b);
b.field("time_series_metric", metricType);
}));
@SuppressWarnings("unchecked") // Syntactic sugar in tests
T fieldType = (T) mapperService.fieldType("field");
assertThat(checker.apply(fieldType).toString(), equalTo(metricType));
}
public final void testEmptyName() {
MapperParsingException e = expectThrows(MapperParsingException.class, () -> createMapperService(mapping(b -> {
b.startObject("");
minimalMapping(b);
b.endObject();
})));
assertThat(e.getMessage(), containsString("field name cannot be an empty string"));
}
public final void testBlankName() {
IndexVersion version = getVersion();
assumeTrue("blank field names are rejected from 8.6.0 onwards", version.onOrAfter(IndexVersions.V_8_6_0));
MapperParsingException e = expectThrows(MapperParsingException.class, () -> createMapperService(version, mapping(b -> {
b.startObject(" ");
minimalMapping(b);
b.endObject();
})));
assertThat(e.getMessage(), containsString("field name cannot contain only whitespaces"));
}
public final void testMinimalSerializesToItself() throws IOException {
XContentBuilder orig = JsonXContent.contentBuilder().startObject();
createMapperService(fieldMapping(this::minimalMapping)).documentMapper().mapping().toXContent(orig, ToXContent.EMPTY_PARAMS);
orig.endObject();
XContentBuilder parsedFromOrig = JsonXContent.contentBuilder().startObject();
createMapperService(orig).documentMapper().mapping().toXContent(parsedFromOrig, ToXContent.EMPTY_PARAMS);
parsedFromOrig.endObject();
assertEquals(Strings.toString(orig), Strings.toString(parsedFromOrig));
assertParseMinimalWarnings();
}
public final void testMinimalToMaximal() throws IOException {
XContentBuilder orig = JsonXContent.contentBuilder().startObject();
createMapperService(fieldMapping(this::minimalMapping)).documentMapper().mapping().toXContent(orig, INCLUDE_DEFAULTS);
orig.endObject();
XContentBuilder parsedFromOrig = JsonXContent.contentBuilder().startObject();
createMapperService(orig).documentMapper().mapping().toXContent(parsedFromOrig, INCLUDE_DEFAULTS);
parsedFromOrig.endObject();
assertEquals(Strings.toString(orig), Strings.toString(parsedFromOrig));
assertParseMaximalWarnings();
}
public void testTotalFieldsCount() throws IOException {
MapperService mapperService = createMapperService(fieldMapping(this::minimalMapping));
assertEquals(1, mapperService.documentMapper().mapping().getRoot().getTotalFieldsCount());
}
protected final void assertParseMinimalWarnings() {
String[] warnings = getParseMinimalWarnings();
if (warnings.length > 0) {
assertWarnings(warnings);
}
}
protected final void assertParseMaximalWarnings() {
String[] warnings = getParseMaximalWarnings();
if (warnings.length > 0) {
assertWarnings(warnings);
}
}
protected String[] getParseMinimalWarnings() {
// Most mappers don't emit any warnings
return Strings.EMPTY_ARRAY;
}
protected String[] getParseMinimalWarnings(IndexVersion indexVersion) {
return getParseMinimalWarnings();
}
protected String[] getParseMaximalWarnings() {
// Most mappers don't emit any warnings
return Strings.EMPTY_ARRAY;
}
/**
* Override to disable testing {@code meta} in fields that don't support it.
*/
protected boolean supportsMeta() {
return true;
}
/**
* Override to disable testing {@code copy_to} in fields that don't support it.
*/
protected boolean supportsCopyTo() {
return true;
}
protected void metaMapping(XContentBuilder b) throws IOException {
minimalMapping(b);
}
public final void testMeta() throws IOException {
assumeTrue("Field doesn't support meta", supportsMeta());
XContentBuilder mapping = fieldMapping(b -> {
metaMapping(b);
b.field("meta", Collections.singletonMap("foo", "bar"));
});
MapperService mapperService = createMapperService(mapping);
assertEquals(
XContentHelper.convertToMap(BytesReference.bytes(mapping), false, mapping.contentType()).v2(),
XContentHelper.convertToMap(mapperService.documentMapper().mappingSource().uncompressed(), false, mapping.contentType()).v2()
);
mapping = fieldMapping(this::metaMapping);
merge(mapperService, mapping);
assertEquals(
XContentHelper.convertToMap(BytesReference.bytes(mapping), false, mapping.contentType()).v2(),
XContentHelper.convertToMap(mapperService.documentMapper().mappingSource().uncompressed(), false, mapping.contentType()).v2()
);
mapping = fieldMapping(b -> {
metaMapping(b);
b.field("meta", Collections.singletonMap("baz", "quux"));
});
merge(mapperService, mapping);
assertEquals(
XContentHelper.convertToMap(BytesReference.bytes(mapping), false, mapping.contentType()).v2(),
XContentHelper.convertToMap(mapperService.documentMapper().mappingSource().uncompressed(), false, mapping.contentType()).v2()
);
}
public final void testDeprecatedBoostWarning() throws IOException {
try {
createMapperService(DEPRECATED_BOOST_INDEX_VERSION, fieldMapping(b -> {
minimalMapping(b, DEPRECATED_BOOST_INDEX_VERSION);
b.field("boost", 2.0);
}));
String[] warnings = Strings.concatStringArrays(
getParseMinimalWarnings(DEPRECATED_BOOST_INDEX_VERSION),
new String[] { "Parameter [boost] on field [field] is deprecated and has no effect" }
);
assertWarnings(warnings);
} catch (MapperParsingException e) {
assertThat(e.getMessage(), anyOf(containsString("Unknown parameter [boost]"), containsString("[boost : 2.0]")));
}
}
public void testBoostNotAllowed() throws IOException {
MapperParsingException e = expectThrows(
MapperParsingException.class,
() -> createMapperService(boostNotAllowedIndexVersion(), fieldMapping(b -> {
minimalMapping(b);
b.field("boost", 2.0);
}))
);
assertThat(e.getMessage(), anyOf(containsString("Unknown parameter [boost]"), containsString("[boost : 2.0]")));
assertParseMinimalWarnings();
}
protected IndexVersion boostNotAllowedIndexVersion() {
return IndexVersions.V_8_0_0;
}
/**
* Use a {@linkplain ValueFetcher} to extract values from doc values.
*/
protected final List> fetchFromDocValues(MapperService mapperService, MappedFieldType ft, DocValueFormat format, Object sourceValue)
throws IOException {
SetOnce> result = new SetOnce<>();
withLuceneIndex(mapperService, iw -> {
iw.addDocument(mapperService.documentMapper().parse(source(b -> b.field(ft.name(), sourceValue))).rootDoc());
}, iw -> {
SearchLookup lookup = new SearchLookup(
mapperService::fieldType,
fieldDataLookup(mapperService),
SourceProvider.fromStoredFields()
);
ValueFetcher valueFetcher = new DocValueFetcher(format, lookup.getForField(ft, MappedFieldType.FielddataOperation.SEARCH));
IndexSearcher searcher = newSearcher(iw);
LeafReaderContext context = searcher.getIndexReader().leaves().get(0);
Source source = lookup.getSource(context, 0);
valueFetcher.setNextReader(context);
result.set(valueFetcher.fetchValues(source, 0, new ArrayList<>()));
});
return result.get();
}
protected static void assertScriptDocValues(MapperService mapperService, Object sourceValue, Matcher> dvMatcher)
throws IOException {
withLuceneIndex(mapperService, iw -> {
iw.addDocument(mapperService.documentMapper().parse(source(b -> b.field("field", sourceValue))).rootDoc());
}, iw -> {
IndexSearcher searcher = newSearcher(iw);
MappedFieldType ft = mapperService.fieldType("field");
SourceProvider sourceProvider = mapperService.mappingLookup().isSourceSynthetic() ? (ctx, doc) -> {
throw new IllegalArgumentException("Can't load source in scripts in synthetic mode");
} : SourceProvider.fromStoredFields();
SearchLookup searchLookup = new SearchLookup(null, null, sourceProvider);
IndexFieldData> sfd = ft.fielddataBuilder(
new FieldDataContext("", () -> searchLookup, Set::of, MappedFieldType.FielddataOperation.SCRIPT)
).build(null, null);
LeafFieldData lfd = sfd.load(getOnlyLeafReader(searcher.getIndexReader()).getContext());
DocValuesScriptFieldFactory sff = lfd.getScriptFieldFactory("field");
sff.setNextDocId(0);
assertThat(sff.toScriptDocValues(), dvMatcher);
});
}
private class UpdateCheck {
final XContentBuilder init;
final XContentBuilder update;
final Consumer check;
private UpdateCheck(CheckedConsumer update, Consumer check) throws IOException {
this.init = fieldMapping(MapperTestCase.this::minimalMapping);
this.update = fieldMapping(b -> {
minimalMapping(b);
update.accept(b);
});
this.check = check;
}
private UpdateCheck(
CheckedConsumer init,
CheckedConsumer update,
Consumer check
) throws IOException {
this.init = fieldMapping(init);
this.update = fieldMapping(update);
this.check = check;
}
}
private record ConflictCheck(XContentBuilder init, XContentBuilder update) {}
public class ParameterChecker {
List updateChecks = new ArrayList<>();
Map conflictChecks = new HashMap<>();
/**
* Register a check that a parameter can be updated, using the minimal mapping as a base
*
* @param update a field builder applied on top of the minimal mapping
* @param check a check that the updated parameter has been applied to the FieldMapper
*/
public void registerUpdateCheck(CheckedConsumer update, Consumer check)
throws IOException {
updateChecks.add(new UpdateCheck(update, check));
}
/**
* Register a check that a parameter can be updated
*
* @param init the initial mapping
* @param update the updated mapping
* @param check a check that the updated parameter has been applied to the FieldMapper
*/
public void registerUpdateCheck(
CheckedConsumer init,
CheckedConsumer update,
Consumer check
) throws IOException {
updateChecks.add(new UpdateCheck(init, update, check));
}
/**
* Register a check that a parameter update will cause a conflict, using the minimal mapping as a base
*
* @param param the parameter name, expected to appear in the error message
* @param update a field builder applied on top of the minimal mapping
*/
public void registerConflictCheck(String param, CheckedConsumer update) throws IOException {
conflictChecks.put(param, new ConflictCheck(fieldMapping(MapperTestCase.this::minimalMapping), fieldMapping(b -> {
minimalMapping(b);
update.accept(b);
})));
}
/**
* Register a check that a parameter update will cause a conflict
*
* @param param the parameter name, expected to appear in the error message
* @param init the initial mapping
* @param update the updated mapping
*/
public void registerConflictCheck(String param, XContentBuilder init, XContentBuilder update) {
conflictChecks.put(param, new ConflictCheck(init, update));
}
}
protected abstract void registerParameters(ParameterChecker checker) throws IOException;
public void testUpdates() throws IOException {
ParameterChecker checker = new ParameterChecker();
registerParameters(checker);
if (supportsIgnoreMalformed()) {
checker.registerUpdateCheck(b -> b.field("ignore_malformed", true), m -> assertTrue(m.ignoreMalformed()));
} else {
MapperService mapperService = createMapperService(fieldMapping(this::minimalMapping));
Exception e = expectThrows(
MapperParsingException.class,
"No conflict when setting parameter [ignore_malformed]",
() -> merge(mapperService, fieldMapping(b -> {
minimalMapping(b);
b.field("ignore_malformed", true);
}))
);
assertThat(e.getMessage(), containsString("unknown parameter [ignore_malformed] on mapper [field]"));
}
for (UpdateCheck updateCheck : checker.updateChecks) {
MapperService mapperService = createMapperService(updateCheck.init);
merge(mapperService, updateCheck.update);
FieldMapper mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field");
updateCheck.check.accept(mapper);
// do it again to ensure that we don't get conflicts the second time
merge(mapperService, updateCheck.update);
mapper = (FieldMapper) mapperService.documentMapper().mappers().getMapper("field");
updateCheck.check.accept(mapper);
}
for (String param : checker.conflictChecks.keySet()) {
MapperService mapperService = createMapperService(checker.conflictChecks.get(param).init);
// merging the same change is fine
merge(mapperService, checker.conflictChecks.get(param).init);
// merging the conflicting update should throw an exception
Exception e = expectThrows(
IllegalArgumentException.class,
"No conflict when updating parameter [" + param + "]",
() -> merge(mapperService, checker.conflictChecks.get(param).update)
);
assertThat(
e.getMessage(),
anyOf(
containsString("Cannot update parameter [" + param + "]"),
containsString("different [" + param + "]"),
containsString("[" + param + "] cannot be ")
)
);
}
assertParseMaximalWarnings();
}
public final void testTextSearchInfoConsistency() throws IOException {
MapperService mapperService = createMapperService(fieldMapping(this::minimalMapping));
MappedFieldType fieldType = mapperService.fieldType("field");
if (fieldType.getTextSearchInfo() == TextSearchInfo.NONE) {
expectThrows(IllegalArgumentException.class, () -> fieldType.termQuery(null, null));
} else {
SearchExecutionContext searchExecutionContext = createSearchExecutionContext(mapperService);
assertNotNull(fieldType.termQuery(getSampleValueForQuery(), searchExecutionContext));
}
assertSearchable(fieldType);
assertParseMinimalWarnings();
}
protected void assertSearchable(MappedFieldType fieldType) {
assertEquals(fieldType.isIndexed(), fieldType.getTextSearchInfo() != TextSearchInfo.NONE);
}
/**
* Asserts that fetching a single value from doc values and from the native
* {@link MappedFieldType#valueFetcher} produce the same results.
*
* Generally this method covers many many random cases but rarely. So if
* it fails its generally a good idea to capture its randomized
* parameters into a new method so we can be sure we consistently test
* any unique and interesting failure case. See the tests for
* {@link DateFieldMapper} for some examples.
*/
public final void testFetch() throws IOException {
MapperService mapperService = randomFetchTestMapper();
try {
MappedFieldType ft = mapperService.fieldType("field");
assertFetch(mapperService, "field", generateRandomInputValue(ft), randomFetchTestFormat());
} finally {
assertParseMinimalWarnings();
}
}
/**
* Asserts that fetching many values from doc values and from the native
* {@link MappedFieldType#valueFetcher} produce the same results.
*
* Generally this method covers many many random cases but rarely. So if
* it fails its generally a good idea to capture its randomized
* parameters into a new method so we can be sure we consistently test
* any unique and interesting failure case. See the tests for
* {@link DateFieldMapper} for some examples.
*/
public final void testFetchMany() throws IOException {
MapperService mapperService = randomFetchTestMapper();
try {
MappedFieldType ft = mapperService.fieldType("field");
int count = between(2, 10);
List