Convert KeywordFieldMapper to parametrized form (#60645)

This makes KeywordFieldMapper extend ParametrizedFieldMapper, with explicitly
defined parameters.

In addition, we add a new option to Parameter, restrictedStringParam, which
accepts a restricted set of string options.
This commit is contained in:
Alan Woodward 2020-08-12 11:24:22 +01:00 committed by Alan Woodward
parent 3a046e125d
commit c81dc2b8b7
11 changed files with 211 additions and 266 deletions

View File

@ -153,8 +153,6 @@ public class PercolatorFieldMapper extends FieldMapper {
static KeywordFieldMapper createExtractQueryFieldBuilder(String name, BuilderContext context) {
KeywordFieldMapper.Builder queryMetadataFieldBuilder = new KeywordFieldMapper.Builder(name);
queryMetadataFieldBuilder.docValues(false);
queryMetadataFieldBuilder.store(false);
queryMetadataFieldBuilder.indexOptions(IndexOptions.DOCS);
return queryMetadataFieldBuilder.build(context);
}

View File

@ -20,7 +20,6 @@
package org.elasticsearch.index.mapper;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.core.WhitespaceAnalyzer;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
@ -33,34 +32,27 @@ import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.lucene.Lucene;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.analysis.AnalyzerScope;
import org.elasticsearch.index.analysis.IndexAnalyzers;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.fielddata.plain.SortedSetOrdinalsIndexFieldData;
import org.elasticsearch.index.query.QueryShardContext;
import org.elasticsearch.index.similarity.SimilarityProvider;
import org.elasticsearch.index.similarity.SimilarityService;
import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.elasticsearch.index.mapper.TypeParsers.checkNull;
import static org.elasticsearch.index.mapper.TypeParsers.parseField;
/**
* A field mapper for keywords. This mapper accepts strings and indexes them as-is.
*/
public final class KeywordFieldMapper extends FieldMapper {
public final class KeywordFieldMapper extends ParametrizedFieldMapper {
public static final String CONTENT_TYPE = "keyword";
@ -73,11 +65,6 @@ public final class KeywordFieldMapper extends FieldMapper {
FIELD_TYPE.setIndexOptions(IndexOptions.DOCS);
FIELD_TYPE.freeze();
}
public static final String NULL_VALUE = null;
public static final int IGNORE_ABOVE = Integer.MAX_VALUE;
public static final boolean EAGER_GLOBAL_ORDINALS = false;
public static final boolean SPLIT_QUERIES_ON_WHITESPACE = false;
}
public static class KeywordField extends Field {
@ -86,147 +73,128 @@ public final class KeywordFieldMapper extends FieldMapper {
super(field, term, ft);
}
public KeywordField(String field, BytesRef term) {
super(field, term, Defaults.FIELD_TYPE);
}
}
public static class Builder extends FieldMapper.Builder<Builder> {
private static KeywordFieldMapper toType(FieldMapper in) {
return (KeywordFieldMapper) in;
}
protected String nullValue = Defaults.NULL_VALUE;
protected int ignoreAbove = Defaults.IGNORE_ABOVE;
private IndexAnalyzers indexAnalyzers;
private String normalizerName = "default";
private boolean eagerGlobalOrdinals = Defaults.EAGER_GLOBAL_ORDINALS;
private boolean splitQueriesOnWhitespace = Defaults.SPLIT_QUERIES_ON_WHITESPACE;
private SimilarityProvider similarity;
public static class Builder extends ParametrizedFieldMapper.Builder {
private final Parameter<Boolean> indexed = Parameter.indexParam(m -> toType(m).indexed, true);
private final Parameter<Boolean> hasDocValues = Parameter.docValuesParam(m -> toType(m).hasDocValues, true);
private final Parameter<Boolean> stored = Parameter.storeParam(m -> toType(m).fieldType.stored(), false);
private final Parameter<String> nullValue = Parameter.stringParam("null_value", false, m -> toType(m).nullValue, null);
private final Parameter<Boolean> eagerGlobalOrdinals
= Parameter.boolParam("eager_global_ordinals", true, m -> toType(m).eagerGlobalOrdinals, false);
private final Parameter<Integer> ignoreAbove
= Parameter.intParam("ignore_above", true, m -> toType(m).ignoreAbove, Integer.MAX_VALUE);
private final Parameter<String> indexOptions
= Parameter.restrictedStringParam("index_options", false, m -> toType(m).indexOptions, "docs", "freqs");
private final Parameter<Boolean> hasNorms
= Parameter.boolParam("norms", false, m -> toType(m).fieldType.omitNorms() == false, false);
private final Parameter<SimilarityProvider> similarity = new Parameter<>("similarity", false, () -> null,
(n, c, o) -> TypeParsers.resolveSimilarity(c, n, o.toString()), m -> toType(m).similarity);
private final Parameter<String> normalizer
= Parameter.stringParam("normalizer", false, m -> toType(m).normalizerName, "default");
private final Parameter<Boolean> splitQueriesOnWhitespace
= Parameter.boolParam("split_queries_on_whitespace", true, m -> toType(m).splitQueriesOnWhitespace, false);
private final Parameter<Map<String, String>> meta = Parameter.metaParam();
private final Parameter<Float> boost = Parameter.boostParam();
private final IndexAnalyzers indexAnalyzers;
public Builder(String name, IndexAnalyzers indexAnalyzers) {
super(name);
this.indexAnalyzers = indexAnalyzers;
}
public Builder(String name) {
super(name, Defaults.FIELD_TYPE);
builder = this;
}
@Override
public Builder omitNorms(boolean omitNorms) {
fieldType.setOmitNorms(omitNorms);
return builder;
}
public void similarity(SimilarityProvider similarity) {
this.similarity = similarity;
this(name, null);
}
public Builder ignoreAbove(int ignoreAbove) {
if (ignoreAbove < 0) {
throw new IllegalArgumentException("[ignore_above] must be positive, got " + ignoreAbove);
}
this.ignoreAbove = ignoreAbove;
this.ignoreAbove.setValue(ignoreAbove);
return this;
}
@Override
public Builder indexOptions(IndexOptions indexOptions) {
if (indexOptions.compareTo(IndexOptions.DOCS_AND_FREQS) > 0) {
throw new IllegalArgumentException("The [keyword] field does not support positions, got [index_options]="
+ indexOptionToString(indexOptions));
Builder normalizer(String normalizerName) {
this.normalizer.setValue(normalizerName);
return this;
}
Builder nullValue(String nullValue) {
this.nullValue.setValue(nullValue);
return this;
}
public Builder docValues(boolean hasDocValues) {
this.hasDocValues.setValue(hasDocValues);
return this;
}
private static IndexOptions toIndexOptions(boolean indexed, String in) {
if (indexed == false) {
return IndexOptions.NONE;
}
return super.indexOptions(indexOptions);
switch (in) {
case "docs":
return IndexOptions.DOCS;
case "freqs":
return IndexOptions.DOCS_AND_FREQS;
}
throw new MapperParsingException("Unknown index option [" + in + "]");
}
public Builder eagerGlobalOrdinals(boolean eagerGlobalOrdinals) {
this.eagerGlobalOrdinals = eagerGlobalOrdinals;
return builder;
@Override
protected List<Parameter<?>> getParameters() {
return Arrays.asList(indexed, hasDocValues, stored, nullValue, eagerGlobalOrdinals, ignoreAbove,
indexOptions, hasNorms, similarity, normalizer, splitQueriesOnWhitespace, boost, meta);
}
public Builder splitQueriesOnWhitespace(boolean splitQueriesOnWhitespace) {
this.splitQueriesOnWhitespace = splitQueriesOnWhitespace;
return builder;
}
public Builder normalizer(IndexAnalyzers indexAnalyzers, String name) {
this.indexAnalyzers = indexAnalyzers;
this.normalizerName = name;
return builder;
}
public Builder nullValue(String nullValue) {
this.nullValue = nullValue;
return builder;
}
private KeywordFieldType buildFieldType(BuilderContext context) {
private KeywordFieldType buildFieldType(BuilderContext context, FieldType fieldType) {
NamedAnalyzer normalizer = Lucene.KEYWORD_ANALYZER;
NamedAnalyzer searchAnalyzer = Lucene.KEYWORD_ANALYZER;
if (normalizerName == null || "default".equals(normalizerName) == false) {
String normalizerName = this.normalizer.getValue();
if (Objects.equals(normalizerName, "default") == false) {
assert indexAnalyzers != null;
normalizer = indexAnalyzers.getNormalizer(normalizerName);
if (normalizer == null) {
throw new MapperParsingException("normalizer [" + normalizerName + "] not found for field [" + name + "]");
}
if (splitQueriesOnWhitespace) {
if (splitQueriesOnWhitespace.getValue()) {
searchAnalyzer = indexAnalyzers.getWhitespaceNormalizer(normalizerName);
} else {
searchAnalyzer = normalizer;
}
}
else if (splitQueriesOnWhitespace) {
// TODO should this be a Lucene global analyzer as well?
searchAnalyzer = new NamedAnalyzer("whitespace", AnalyzerScope.INDEX, new WhitespaceAnalyzer());
else if (splitQueriesOnWhitespace.getValue()) {
searchAnalyzer = Lucene.WHITESPACE_ANALYZER;
}
return new KeywordFieldType(buildFullName(context), hasDocValues, fieldType,
eagerGlobalOrdinals, normalizer, searchAnalyzer, similarity, meta, boost);
return new KeywordFieldType(buildFullName(context), hasDocValues.getValue(), fieldType,
eagerGlobalOrdinals.getValue(), normalizer, searchAnalyzer,
similarity.getValue(), boost.getValue(), meta.getValue());
}
@Override
public KeywordFieldMapper build(BuilderContext context) {
return new KeywordFieldMapper(name,
fieldType, buildFieldType(context), ignoreAbove, splitQueriesOnWhitespace, nullValue,
multiFieldsBuilder.build(this, context), copyTo);
FieldType fieldtype = new FieldType(Defaults.FIELD_TYPE);
fieldtype.setOmitNorms(this.hasNorms.getValue() == false);
fieldtype.setIndexOptions(toIndexOptions(this.indexed.getValue(), this.indexOptions.getValue()));
fieldtype.setStored(this.stored.getValue());
return new KeywordFieldMapper(name, fieldtype, buildFieldType(context, fieldtype),
multiFieldsBuilder.build(this, context), copyTo.build(), this);
}
}
public static class TypeParser implements Mapper.TypeParser {
@Override
public Mapper.Builder<?> parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
KeywordFieldMapper.Builder builder = new KeywordFieldMapper.Builder(name);
for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<String, Object> entry = iterator.next();
String propName = entry.getKey();
Object propNode = entry.getValue();
checkNull(propName, propNode);
if (propName.equals("null_value")) {
if (propNode == null) {
throw new MapperParsingException("Property [null_value] cannot be null.");
}
builder.nullValue(propNode.toString());
iterator.remove();
} else if (propName.equals("ignore_above")) {
builder.ignoreAbove(XContentMapValues.nodeIntegerValue(propNode, -1));
iterator.remove();
} else if (propName.equals("norms")) {
TypeParsers.parseNorms(builder, name, propNode);
iterator.remove();
} else if (propName.equals("eager_global_ordinals")) {
builder.eagerGlobalOrdinals(XContentMapValues.nodeBooleanValue(propNode, "eager_global_ordinals"));
iterator.remove();
} else if (propName.equals("normalizer")) {
if (propNode != null) {
builder.normalizer(parserContext.getIndexAnalyzers(), propNode.toString());
}
iterator.remove();
} else if (propName.equals("split_queries_on_whitespace")) {
builder.splitQueriesOnWhitespace(XContentMapValues.nodeBooleanValue(propNode, "split_queries_on_whitespace"));
iterator.remove();
} else if (propName.equals("similarity")) {
SimilarityProvider similarityProvider = TypeParsers.resolveSimilarity(parserContext, name, propNode.toString());
builder.similarity(similarityProvider);
iterator.remove();
}
}
parseField(builder, name, node, parserContext);
return builder;
}
}
public static final TypeParser PARSER
= new TypeParser((n, c) -> new Builder(n, c.getIndexAnalyzers()));
public static final class KeywordFieldType extends StringFieldType {
@ -234,7 +202,7 @@ public final class KeywordFieldMapper extends FieldMapper {
public KeywordFieldType(String name, boolean hasDocValues, FieldType fieldType,
boolean eagerGlobalOrdinals, NamedAnalyzer normalizer, NamedAnalyzer searchAnalyzer,
SimilarityProvider similarity, Map<String, String> meta, float boost) {
SimilarityProvider similarity, float boost, Map<String, String> meta) {
super(name, fieldType.indexOptions() != IndexOptions.NONE,
hasDocValues, new TextSearchInfo(fieldType, similarity, searchAnalyzer, searchAnalyzer), meta);
this.hasNorms = fieldType.omitNorms() == false;
@ -250,7 +218,7 @@ public final class KeywordFieldMapper extends FieldMapper {
public KeywordFieldType(String name) {
this(name, true, Defaults.FIELD_TYPE, false,
Lucene.KEYWORD_ANALYZER, Lucene.KEYWORD_ANALYZER, null, Collections.emptyMap(), 1f);
Lucene.KEYWORD_ANALYZER, Lucene.KEYWORD_ANALYZER, null, 1.0f, Collections.emptyMap());
}
public KeywordFieldType(String name, NamedAnalyzer analyzer) {
@ -314,18 +282,35 @@ public final class KeywordFieldMapper extends FieldMapper {
}
}
private int ignoreAbove;
private boolean splitQueriesOnWhitespace;
private String nullValue;
private final boolean indexed;
private final boolean hasDocValues;
private final String nullValue;
private final boolean eagerGlobalOrdinals;
private final int ignoreAbove;
private final String indexOptions;
private final FieldType fieldType;
private final SimilarityProvider similarity;
private final String normalizerName;
private final boolean splitQueriesOnWhitespace;
private final IndexAnalyzers indexAnalyzers;
protected KeywordFieldMapper(String simpleName, FieldType fieldType, MappedFieldType mappedFieldType,
int ignoreAbove, boolean splitQueriesOnWhitespace, String nullValue,
MultiFields multiFields, CopyTo copyTo) {
super(simpleName, fieldType, mappedFieldType, multiFields, copyTo);
MultiFields multiFields, CopyTo copyTo, Builder builder) {
super(simpleName, mappedFieldType, multiFields, copyTo);
assert fieldType.indexOptions().compareTo(IndexOptions.DOCS_AND_FREQS) <= 0;
this.ignoreAbove = ignoreAbove;
this.splitQueriesOnWhitespace = splitQueriesOnWhitespace;
this.nullValue = nullValue;
this.indexed = builder.indexed.getValue();
this.hasDocValues = builder.hasDocValues.getValue();
this.nullValue = builder.nullValue.getValue();
this.eagerGlobalOrdinals = builder.eagerGlobalOrdinals.getValue();
this.ignoreAbove = builder.ignoreAbove.getValue();
this.indexOptions = builder.indexOptions.getValue();
this.fieldType = fieldType;
this.similarity = builder.similarity.getValue();
this.normalizerName = builder.normalizer.getValue();
this.splitQueriesOnWhitespace = builder.splitQueriesOnWhitespace.getValue();
this.indexAnalyzers = builder.indexAnalyzers;
}
/** Values that have more chars than the return value of this method will
@ -437,56 +422,7 @@ public final class KeywordFieldMapper extends FieldMapper {
}
@Override
protected void mergeOptions(FieldMapper other, List<String> conflicts) {
KeywordFieldMapper k = (KeywordFieldMapper) other;
if (Objects.equals(fieldType().indexAnalyzer(), k.fieldType().indexAnalyzer()) == false) {
conflicts.add("mapper [" + name() + "] has different [normalizer]");
}
if (Objects.equals(mappedFieldType.getTextSearchInfo().getSimilarity(),
k.fieldType().getTextSearchInfo().getSimilarity()) == false) {
conflicts.add("mapper [" + name() + "] has different [similarity]");
}
this.ignoreAbove = k.ignoreAbove;
this.splitQueriesOnWhitespace = k.splitQueriesOnWhitespace;
this.fieldType().setBoost(k.fieldType().boost());
}
@Override
protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
super.doXContentBody(builder, includeDefaults, params);
if (fieldType.indexOptions() != IndexOptions.NONE
&& (includeDefaults || fieldType.indexOptions() != Defaults.FIELD_TYPE.indexOptions())) {
builder.field("index_options", indexOptionToString(fieldType.indexOptions()));
}
if (nullValue != null) {
builder.field("null_value", nullValue);
}
if (includeDefaults || ignoreAbove != Defaults.IGNORE_ABOVE) {
builder.field("ignore_above", ignoreAbove);
}
if (includeDefaults || fieldType.omitNorms() != Defaults.FIELD_TYPE.omitNorms()) {
builder.field("norms", fieldType.omitNorms() == false);
}
if (includeDefaults || fieldType().eagerGlobalOrdinals() != false) {
builder.field("eager_global_ordinals", fieldType().eagerGlobalOrdinals());
}
if (fieldType().normalizer() != null && fieldType().normalizer() != Lucene.KEYWORD_ANALYZER) {
builder.field("normalizer", fieldType().normalizer().name());
}
else if (includeDefaults) {
builder.field("normalizer", "default");
}
if (fieldType().getTextSearchInfo().getSimilarity() != null) {
builder.field("similarity", fieldType().getTextSearchInfo().getSimilarity().name());
} else if (includeDefaults) {
builder.field("similarity", SimilarityService.DEFAULT_SIMILARITY);
}
if (includeDefaults || splitQueriesOnWhitespace) {
builder.field("split_queries_on_whitespace", splitQueriesOnWhitespace);
}
public ParametrizedFieldMapper.Builder getMergeBuilder() {
return new Builder(simpleName(), indexAnalyzers).init(this);
}
}

View File

@ -37,6 +37,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -318,6 +319,28 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
}, initializer);
}
/**
* Defines a parameter that takes one of a restricted set of string values
* @param name the parameter name
* @param updateable whether the parameter can be changed by a mapping update
* @param initializer a function that reads the parameter value from an existing mapper
* @param values the set of values that the parameter can take. The first value in the list
* is the default value, to be used if the parameter is undefined in a mapping
*/
public static Parameter<String> restrictedStringParam(String name, boolean updateable,
Function<FieldMapper, String> initializer, String... values) {
assert values.length > 0;
Set<String> acceptedValues = new LinkedHashSet<>(Arrays.asList(values));
return stringParam(name, updateable, initializer, values[0])
.setValidator(v -> {
if (acceptedValues.contains(v)) {
return;
}
throw new MapperParsingException("Unknown value [" + v + "] for field [" + name +
"] - accepted values are " + acceptedValues.toString());
});
}
/**
* Defines a parameter that takes an analyzer name
* @param name the parameter name

View File

@ -388,10 +388,11 @@ public class RootObjectMapper extends ObjectMapper {
continue;
}
Map<String, Object> fieldTypeConfig = dynamicTemplate.mappingForName("__dummy__", defaultDynamicType);
fieldTypeConfig.remove("type");
String templateName = "__dynamic__" + dynamicTemplate.name();
Map<String, Object> fieldTypeConfig = dynamicTemplate.mappingForName(templateName, defaultDynamicType);
try {
Mapper.Builder<?> dummyBuilder = typeParser.parse("__dummy__", fieldTypeConfig, parserContext);
Mapper.Builder<?> dummyBuilder = typeParser.parse(templateName, fieldTypeConfig, parserContext);
fieldTypeConfig.remove("type");
if (fieldTypeConfig.isEmpty()) {
Settings indexSettings = parserContext.mapperService().getIndexSettings().getSettings();
BuilderContext builderContext = new BuilderContext(indexSettings, new ContentPath(1));

View File

@ -127,7 +127,7 @@ public class IndicesModule extends AbstractModule {
mappers.put(nanoseconds.type(), DateFieldMapper.NANOS_PARSER);
mappers.put(IpFieldMapper.CONTENT_TYPE, IpFieldMapper.PARSER);
mappers.put(TextFieldMapper.CONTENT_TYPE, new TextFieldMapper.TypeParser());
mappers.put(KeywordFieldMapper.CONTENT_TYPE, new KeywordFieldMapper.TypeParser());
mappers.put(KeywordFieldMapper.CONTENT_TYPE, KeywordFieldMapper.PARSER);
mappers.put(ObjectMapper.CONTENT_TYPE, new ObjectMapper.TypeParser());
mappers.put(ObjectMapper.NESTED_CONTENT_TYPE, new ObjectMapper.TypeParser());
mappers.put(CompletionFieldMapper.CONTENT_TYPE, CompletionFieldMapper.PARSER);

View File

@ -115,7 +115,7 @@ public class ExternalFieldMapperTests extends ESSingleNodeTestCase {
Map<String, Mapper.TypeParser> mapperParsers = new HashMap<>();
mapperParsers.put(ExternalMapperPlugin.EXTERNAL, new ExternalMapper.TypeParser(ExternalMapperPlugin.EXTERNAL, "foo"));
mapperParsers.put(TextFieldMapper.CONTENT_TYPE, new TextFieldMapper.TypeParser());
mapperParsers.put(KeywordFieldMapper.CONTENT_TYPE, new KeywordFieldMapper.TypeParser());
mapperParsers.put(KeywordFieldMapper.CONTENT_TYPE, KeywordFieldMapper.PARSER);
MapperRegistry mapperRegistry = new MapperRegistry(mapperParsers, Collections.emptyMap(), MapperPlugin.NOOP_FIELD_FILTER);
Supplier<QueryShardContext> queryShardContext = () -> {

View File

@ -26,8 +26,6 @@ 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.search.similarities.BM25Similarity;
import org.apache.lucene.search.similarities.BooleanSimilarity;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.metadata.IndexMetadata;
@ -41,12 +39,12 @@ import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.analysis.PreConfiguredTokenFilter;
import org.elasticsearch.index.analysis.TokenizerFactory;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.index.similarity.SimilarityProvider;
import org.elasticsearch.index.termvectors.TermVectorsService;
import org.elasticsearch.indices.analysis.AnalysisModule;
import org.elasticsearch.plugins.AnalysisPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.search.lookup.SourceLookup;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.test.InternalSettingsPlugin;
import org.junit.Before;
@ -56,7 +54,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
@ -65,17 +62,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
public class KeywordFieldMapperTests extends FieldMapperTestCase<KeywordFieldMapper.Builder> {
@Override
protected KeywordFieldMapper.Builder newBuilder() {
return new KeywordFieldMapper.Builder("keyword");
}
@Override
protected Set<String> unsupportedProperties() {
return org.elasticsearch.common.collect.Set.of("analyzer");
}
public class KeywordFieldMapperTests extends ESSingleNodeTestCase {
/**
* Creates a copy of the lowercase token filter which we use for testing merge errors.
@ -99,11 +86,6 @@ public class KeywordFieldMapperTests extends FieldMapperTestCase<KeywordFieldMap
return pluginList(InternalSettingsPlugin.class, MockAnalysisPlugin.class);
}
@Override
protected Settings getIndexMapperSettings() {
return mapperSettings;
}
private static final Settings mapperSettings = Settings.builder()
.put("index.analysis.normalizer.my_lowercase.type", "custom")
.putList("index.analysis.normalizer.my_lowercase.filter", "lowercase")
@ -117,18 +99,6 @@ public class KeywordFieldMapperTests extends FieldMapperTestCase<KeywordFieldMap
public void setup() {
indexService = createIndex("test", mapperSettings);
parser = indexService.mapperService().documentMapperParser();
addModifier("normalizer", false, (a, b) -> {
a.normalizer(indexService.getIndexAnalyzers(), "my_lowercase");
});
addBooleanModifier("split_queries_on_whitespace", true, KeywordFieldMapper.Builder::splitQueriesOnWhitespace);
addModifier("index_options", false, (a, b) -> {
a.indexOptions(IndexOptions.DOCS);
b.indexOptions(IndexOptions.DOCS_AND_FREQS);
});
addModifier("similarity", false, (a, b) -> {
a.similarity(new SimilarityProvider("BM25", new BM25Similarity()));
b.similarity(new SimilarityProvider("boolean", new BooleanSimilarity()));
});
}
public void testDefaults() throws Exception {
@ -339,16 +309,17 @@ public class KeywordFieldMapperTests extends FieldMapperTestCase<KeywordFieldMap
.startObject("properties").startObject("field").field("type", "keyword")
.field("index_options", indexOptions).endObject().endObject()
.endObject().endObject());
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> parser.parse("type", new CompressedXContent(mapping2)));
assertEquals("The [keyword] field does not support positions, got [index_options]=" + indexOptions, e.getMessage());
assertEquals("Unknown value [" + indexOptions + "] for field [index_options] - accepted values are [docs, freqs]",
e.getMessage());
}
}
public void testBoost() throws IOException {
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
.startObject("properties").startObject("field").field("type", "keyword").field("boost", 2f).endObject().endObject()
.endObject().endObject());
.startObject("properties").startObject("field").field("type", "keyword").field("boost", 2f).endObject().endObject()
.endObject().endObject());
DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping));
@ -524,8 +495,8 @@ public class KeywordFieldMapperTests extends FieldMapperTestCase<KeywordFieldMap
() -> indexService.mapperService().merge("type",
new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE));
assertEquals(
"Mapper for [field] conflicts with existing mapping:\n" +
"[mapper [field] has different [analyzer], mapper [field] has different [normalizer]]",
"Mapper for [field] conflicts with existing mapper:\n" +
"\tCannot update parameter [normalizer] from [my_lowercase] to [my_other_lowercase]",
e.getMessage());
}
@ -650,8 +621,8 @@ public class KeywordFieldMapperTests extends FieldMapperTestCase<KeywordFieldMap
assertEquals("42", ignoreAboveMapper.parseSourceValue(42L, null));
assertEquals("true", ignoreAboveMapper.parseSourceValue(true, null));
KeywordFieldMapper normalizerMapper = new KeywordFieldMapper.Builder("field")
.normalizer(indexService.getIndexAnalyzers(), "lowercase")
KeywordFieldMapper normalizerMapper = new KeywordFieldMapper.Builder("field", indexService.getIndexAnalyzers())
.normalizer("lowercase")
.build(context);
assertEquals("value", normalizerMapper.parseSourceValue("VALUE", null));
assertEquals("42", normalizerMapper.parseSourceValue(42L, null));

View File

@ -122,6 +122,8 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
throw new IllegalArgumentException("field [required] must be specified");
}
});
final Parameter<String> restricted
= Parameter.restrictedStringParam("restricted", true, m -> toType(m).restricted, "foo", "bar");
protected Builder(String name) {
super(name);
@ -129,7 +131,7 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
@Override
protected List<Parameter<?>> getParameters() {
return Arrays.asList(fixed, fixed2, variable, index, wrapper, intValue, analyzer, searchAnalyzer, required);
return Arrays.asList(fixed, fixed2, variable, index, wrapper, intValue, analyzer, searchAnalyzer, required, restricted);
}
@Override
@ -160,6 +162,7 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
private final NamedAnalyzer searchAnalyzer;
private final boolean index;
private final String required;
private final String restricted;
protected TestMapper(String simpleName, String fullName, MultiFields multiFields, CopyTo copyTo,
ParametrizedMapperTests.Builder builder) {
@ -173,6 +176,7 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
this.searchAnalyzer = builder.searchAnalyzer.getValue();
this.index = builder.index.getValue();
this.required = builder.required.getValue();
this.restricted = builder.restricted.getValue();
}
@Override
@ -207,7 +211,7 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
when(mapperService.getIndexAnalyzers()).thenReturn(indexAnalyzers);
Mapper.TypeParser.ParserContext pc = new Mapper.TypeParser.ParserContext(s -> null, mapperService, s -> {
if (Objects.equals("keyword", s)) {
return new KeywordFieldMapper.TypeParser();
return KeywordFieldMapper.PARSER;
}
if (Objects.equals("binary", s)) {
return BinaryFieldMapper.PARSER;
@ -241,7 +245,7 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed\":true," +
"\"fixed2\":false,\"variable\":\"default\",\"index\":true," +
"\"wrapper\":\"default\",\"int_value\":5,\"analyzer\":\"_keyword\"," +
"\"search_analyzer\":\"_keyword\",\"required\":\"value\"}}",
"\"search_analyzer\":\"_keyword\",\"required\":\"value\",\"restricted\":\"foo\"}}",
Strings.toString(builder));
}
@ -439,4 +443,22 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
assertEquals("[required] on mapper [field] of type [test_mapper] must not have a [null] value", exc.getMessage());
}
}
public void testRestrictedField() {
{
String mapping = "{\"type\":\"test_mapper\",\"required\":\"a\",\"restricted\":\"baz\"}";
MapperParsingException e = expectThrows(MapperParsingException.class, () -> fromMapping(mapping));
assertEquals("Unknown value [baz] for field [restricted] - accepted values are [foo, bar]", e.getMessage());
}
{
String mapping = "{\"type\":\"test_mapper\",\"required\":\"a\",\"restricted\":\"bar\"}";
TestMapper mapper = fromMapping(mapping);
assertEquals("bar", mapper.restricted);
}
{
String mapping = "{\"type\":\"test_mapper\",\"required\":\"a\"}";
TestMapper mapper = fromMapping(mapping);
assertEquals("foo", mapper.restricted);
}
}
}

View File

@ -339,7 +339,8 @@ public class RootObjectMapperTests extends ESSingleNodeTestCase {
DocumentMapper mapper = mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE);
assertThat(mapper.mappingSource().toString(), containsString("\"foo\":\"bar\""));
assertWarnings("dynamic template [my_template] has invalid content [{\"match_mapping_type\":\"string\",\"mapping\":{" +
"\"foo\":\"bar\",\"type\":\"keyword\"}}], caused by [Unused mapping attributes [{foo=bar}]]");
"\"foo\":\"bar\",\"type\":\"keyword\"}}], " +
"caused by [unknown parameter [foo] on mapper [__dynamic__my_template] of type [keyword]]");
}
public void testIllegalDynamicTemplateInvalidAttribute() throws Exception {
@ -367,7 +368,7 @@ public class RootObjectMapperTests extends ESSingleNodeTestCase {
DocumentMapper mapper = mapperService.merge("type", new CompressedXContent(Strings.toString(mapping)), MergeReason.MAPPING_UPDATE);
assertThat(mapper.mappingSource().toString(), containsString("\"analyzer\":\"foobar\""));
assertWarnings("dynamic template [my_template] has invalid content [{\"match_mapping_type\":\"string\",\"mapping\":{" +
"\"analyzer\":\"foobar\",\"type\":\"text\"}}], caused by [analyzer [foobar] not found for field [__dummy__]]");
"\"analyzer\":\"foobar\",\"type\":\"text\"}}], caused by [analyzer [foobar] not found for field [__dynamic__my_template]]");
}
public void testIllegalDynamicTemplateNoMappingType() throws Exception {
@ -436,11 +437,11 @@ public class RootObjectMapperTests extends ESSingleNodeTestCase {
if (useMatchMappingType) {
assertWarnings("dynamic template [my_template] has invalid content [{\"match_mapping_type\":\"*\",\"mapping\":{" +
"\"foo\":\"bar\",\"type\":\"{dynamic_type}\"}}], " +
"caused by [unknown parameter [foo] on mapper [__dummy__] of type [null]]");
"caused by [unknown parameter [foo] on mapper [__dynamic__my_template] of type [binary]]");
} else {
assertWarnings("dynamic template [my_template] has invalid content [{\"match\":\"string_*\",\"mapping\":{" +
"\"foo\":\"bar\",\"type\":\"{dynamic_type}\"}}], " +
"caused by [unknown parameter [foo] on mapper [__dummy__] of type [null]]");
"caused by [unknown parameter [foo] on mapper [__dynamic__my_template] of type [binary]]");
}
}
}

View File

@ -182,19 +182,23 @@ public class TypeParsersTests extends ESTestCase {
.endObject()
.endObject();
Mapper.TypeParser typeParser = KeywordFieldMapper.PARSER;
Map<String, Object> fieldNode = XContentHelper.convertToMap(
BytesReference.bytes(mapping), true, mapping.contentType()).v2();
Mapper.TypeParser typeParser = new KeywordFieldMapper.TypeParser();
Mapper.TypeParser.ParserContext parserContext = new Mapper.TypeParser.ParserContext(
null, null, type -> typeParser, Version.CURRENT, null, null);
IndexAnalyzers indexAnalyzers = new IndexAnalyzers(defaultAnalyzers(), Collections.emptyMap(), Collections.emptyMap());
MapperService mapperService = mock(MapperService.class);
when(mapperService.getIndexAnalyzers()).thenReturn(indexAnalyzers);
Mapper.TypeParser.ParserContext olderContext = new Mapper.TypeParser.ParserContext(
null, mapperService, type -> typeParser, Version.CURRENT, null, null);
TypeParsers.parseField(builder, "some-field", fieldNode, parserContext);
assertWarnings("At least one multi-field, [sub-field], was " +
"encountered that itself contains a multi-field. Defining multi-fields within a multi-field is deprecated and will " +
"no longer be supported in 8.0. To resolve the issue, all instances of [fields] that occur within a [fields] block " +
"should be removed from the mappings, either by flattening the chained [fields] blocks into a single level, or " +
"switching to [copy_to] if appropriate.");
TypeParsers.parseField(builder, "some-field", fieldNode, olderContext);
assertWarnings("At least one multi-field, [sub-field], " +
"was encountered that itself contains a multi-field. Defining multi-fields within a multi-field is deprecated " +
"and will no longer be supported in 8.0. To resolve the issue, all instances of [fields] " +
"that occur within a [fields] block should be removed from the mappings, either by flattening the chained " +
"[fields] blocks into a single level, or switching to [copy_to] if appropriate.");
}
private Analyzer createAnalyzerWithMode(String name, AnalysisMode mode) {
@ -214,21 +218,15 @@ public class TypeParsersTests extends ESTestCase {
}
public void testParseMeta() {
FieldMapper.Builder<?> builder = new KeywordFieldMapper.Builder("foo");
Mapper.TypeParser.ParserContext parserContext = new Mapper.TypeParser.ParserContext(null, null, null, null, null, null);
{
Map<String, Object> mapping = new HashMap<>(Collections.singletonMap("meta", 3));
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> TypeParsers.parseField(builder, builder.name, mapping, parserContext));
() -> TypeParsers.parseMeta("foo", 3));
assertEquals("[meta] must be an object, got Integer[3] for field [foo]", e.getMessage());
}
{
Map<String, Object> mapping = new HashMap<>(Collections.singletonMap("meta",
Collections.singletonMap("veryloooooooooooongkey", 3L)));
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> TypeParsers.parseField(builder, builder.name, mapping, parserContext));
() -> TypeParsers.parseMeta("foo", Collections.singletonMap("veryloooooooooooongkey", 3L)));
assertEquals("[meta] keys can't be longer than 20 chars, but got [veryloooooooooooongkey] for field [foo]",
e.getMessage());
}
@ -241,18 +239,16 @@ public class TypeParsersTests extends ESTestCase {
meta.put("foo4", "3");
meta.put("foo5", "3");
meta.put("foo6", "3");
Map<String, Object> mapping = new HashMap<>(Collections.singletonMap("meta", meta));
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> TypeParsers.parseField(builder, builder.name, mapping, parserContext));
() -> TypeParsers.parseMeta("foo", meta));
assertEquals("[meta] can't have more than 5 entries, but got 6 on field [foo]",
e.getMessage());
}
{
Map<String, Object> mapping = new HashMap<>(Collections.singletonMap("meta",
Collections.singletonMap("foo", Collections.singletonMap("bar", "baz"))));
Map<String, Object> mapping = Collections.singletonMap("foo", Collections.singletonMap("bar", "baz"));
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> TypeParsers.parseField(builder, builder.name, mapping, parserContext));
() -> TypeParsers.parseMeta("foo", mapping));
assertEquals("[meta] values can only be strings, but got SingletonMap[{bar=baz}] for field [foo]",
e.getMessage());
}
@ -261,9 +257,8 @@ public class TypeParsersTests extends ESTestCase {
Map<String, Object> inner = new HashMap<>();
inner.put("bar", "baz");
inner.put("foo", 3);
Map<String, Object> mapping = new HashMap<>(Collections.singletonMap("meta", inner));
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> TypeParsers.parseField(builder, builder.name, mapping, parserContext));
() -> TypeParsers.parseMeta("foo", inner));
assertEquals("[meta] values can only be strings, but got Integer[3] for field [foo]",
e.getMessage());
}
@ -271,9 +266,8 @@ public class TypeParsersTests extends ESTestCase {
{
Map<String, String> meta = new HashMap<>();
meta.put("foo", null);
Map<String, Object> mapping = new HashMap<>(Collections.singletonMap("meta", meta));
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> TypeParsers.parseField(builder, builder.name, mapping, parserContext));
() -> TypeParsers.parseMeta("foo", meta));
assertEquals("[meta] values can't be null (field [foo])",
e.getMessage());
}
@ -282,9 +276,8 @@ public class TypeParsersTests extends ESTestCase {
String longString = IntStream.range(0, 51)
.mapToObj(Integer::toString)
.collect(Collectors.joining());
Map<String, Object> mapping = new HashMap<>(Collections.singletonMap("meta", Collections.singletonMap("foo", longString)));
MapperParsingException e = expectThrows(MapperParsingException.class,
() -> TypeParsers.parseField(builder, builder.name, mapping, parserContext));
() -> TypeParsers.parseMeta("foo", Collections.singletonMap("foo", longString)));
assertThat(e.getMessage(), Matchers.startsWith("[meta] values can't be longer than 50 chars"));
}
}

View File

@ -175,7 +175,7 @@ public class IndicesServiceTests extends ESSingleNodeTestCase {
@Override
public Map<String, Mapper.TypeParser> getMappers() {
return Collections.singletonMap("fake-mapper", new KeywordFieldMapper.TypeParser());
return Collections.singletonMap("fake-mapper", KeywordFieldMapper.PARSER);
}
@Override