Migrate CompletionFieldMapper to parametrized format (#59691)

This adds a number of new optional parameters to Parameter, including:

* custom serialization (to handle analyzers)
* deprecated parameter names
* parameter validation
* allowing default values to be based on the values of other parameters

We preserve the previous serialization format of CompletionFieldMapper,
always emitting most fields, in order to meet mapping checks in mixed
version clusters, where the mapper service will check that mappings have
been correctly parsed and updated by checking their serialized outputs.
This commit is contained in:
Alan Woodward 2020-07-16 19:15:00 +01:00 committed by GitHub
parent 4089cbd767
commit 10be10c99b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 418 additions and 289 deletions

View File

@ -59,8 +59,7 @@ public class BinaryFieldMapper extends ParametrizedFieldMapper {
private final Parameter<Boolean> stored = Parameter.boolParam("store", false, m -> toType(m).stored, false);
private final Parameter<Boolean> hasDocValues = Parameter.boolParam("doc_values", false, m -> toType(m).hasDocValues, false);
private final Parameter<Map<String, String>> meta
= new Parameter<>("meta", true, Collections.emptyMap(), TypeParsers::parseMeta, m -> m.fieldType().meta());
private final Parameter<Map<String, String>> meta = Parameter.metaParam();
public Builder(String name) {
this(name, false);

View File

@ -79,12 +79,11 @@ public class BooleanFieldMapper extends ParametrizedFieldMapper {
private final Parameter<Boolean> indexed = Parameter.boolParam("index", false, m -> toType(m).indexed, true);
private final Parameter<Boolean> stored = Parameter.boolParam("store", false, m -> toType(m).stored, false);
private final Parameter<Boolean> nullValue
= new Parameter<>("null_value", false, null, (n, o) -> XContentMapValues.nodeBooleanValue(o), m -> toType(m).nullValue);
private final Parameter<Boolean> nullValue = new Parameter<>("null_value", false, () -> null,
(n, c, o) -> XContentMapValues.nodeBooleanValue(o), m -> toType(m).nullValue);
private final Parameter<Float> boost = Parameter.floatParam("boost", true, m -> m.fieldType().boost(), 1.0f);
private final Parameter<Map<String, String>> meta
= new Parameter<>("meta", true, Collections.emptyMap(), TypeParsers::parseMeta, m -> m.fieldType().meta());
private final Parameter<Map<String, String>> meta = Parameter.metaParam();
public Builder(String name) {
super(name);

View File

@ -34,13 +34,11 @@ import org.apache.lucene.search.suggest.document.RegexCompletionQuery;
import org.apache.lucene.search.suggest.document.SuggestField;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.lucene.Lucene;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParser.NumberType;
@ -53,16 +51,15 @@ import org.elasticsearch.search.suggest.completion.context.ContextMapping;
import org.elasticsearch.search.suggest.completion.context.ContextMappings;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static org.elasticsearch.index.mapper.TypeParsers.parseMultiField;
/**
* Mapper for completion field. The field values are indexed as a weighted FST for
* fast auto-completion/search-as-you-type functionality.<br>
@ -84,7 +81,7 @@ import static org.elasticsearch.index.mapper.TypeParsers.parseMultiField;
* This field can also be extended to add search criteria to suggestions
* for query-time filtering and boosting (see {@link ContextMappings}
*/
public class CompletionFieldMapper extends FieldMapper {
public class CompletionFieldMapper extends ParametrizedFieldMapper {
public static final String CONTENT_TYPE = "completion";
/**
@ -92,6 +89,11 @@ public class CompletionFieldMapper extends FieldMapper {
*/
static final int COMPLETION_CONTEXTS_LIMIT = 10;
@Override
public ParametrizedFieldMapper.Builder getMergeBuilder() {
return new Builder(simpleName(), defaultAnalyzer).init(this);
}
public static class Defaults {
public static final FieldType FIELD_TYPE = new FieldType();
static {
@ -108,20 +110,113 @@ public class CompletionFieldMapper extends FieldMapper {
}
public static class Fields {
// Mapping field names
public static final ParseField ANALYZER = new ParseField("analyzer");
public static final ParseField SEARCH_ANALYZER = new ParseField("search_analyzer");
public static final ParseField PRESERVE_SEPARATORS = new ParseField("preserve_separators");
public static final ParseField PRESERVE_POSITION_INCREMENTS = new ParseField("preserve_position_increments");
public static final ParseField TYPE = new ParseField("type");
public static final ParseField CONTEXTS = new ParseField("contexts");
public static final ParseField MAX_INPUT_LENGTH = new ParseField("max_input_length", "max_input_len");
// Content field names
public static final String CONTENT_FIELD_NAME_INPUT = "input";
public static final String CONTENT_FIELD_NAME_WEIGHT = "weight";
public static final String CONTENT_FIELD_NAME_CONTEXTS = "contexts";
}
private static CompletionFieldMapper toType(FieldMapper in) {
return (CompletionFieldMapper) in;
}
/**
* Builder for {@link CompletionFieldMapper}
*/
public static class Builder extends ParametrizedFieldMapper.Builder {
private final Parameter<NamedAnalyzer> analyzer;
private final Parameter<NamedAnalyzer> searchAnalyzer;
private final Parameter<Boolean> preserveSeparators = Parameter.boolParam("preserve_separators", false,
m -> toType(m).preserveSeparators, Defaults.DEFAULT_PRESERVE_SEPARATORS);
private final Parameter<Boolean> preservePosInc = Parameter.boolParam("preserve_position_increments", false,
m -> toType(m).preservePosInc, Defaults.DEFAULT_POSITION_INCREMENTS);
private final Parameter<ContextMappings> contexts = new Parameter<>("contexts", false, () -> null,
(n, c, o) -> ContextMappings.load(o, c.indexVersionCreated()), m -> toType(m).contexts)
.setSerializer((b, n, c) -> {
if (c == null) {
return;
}
b.startArray(n);
c.toXContent(b, ToXContent.EMPTY_PARAMS);
b.endArray();
});
private final Parameter<Integer> maxInputLength = Parameter.intParam("max_input_length", true,
m -> toType(m).maxInputLength, Defaults.DEFAULT_MAX_INPUT_LENGTH)
.addDeprecatedName("max_input_len")
.setValidator(Builder::validateInputLength);
private final Parameter<Map<String, String>> meta = Parameter.metaParam();
private final NamedAnalyzer defaultAnalyzer;
private static final DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(Builder.class));
/**
* @param name of the completion field to build
*/
public Builder(String name, NamedAnalyzer defaultAnalyzer) {
super(name);
this.defaultAnalyzer = defaultAnalyzer;
this.analyzer = Parameter.analyzerParam("analyzer", false, m -> toType(m).analyzer, () -> defaultAnalyzer);
this.searchAnalyzer
= Parameter.analyzerParam("search_analyzer", true, m -> toType(m).searchAnalyzer, analyzer::getValue);
}
private static void validateInputLength(int maxInputLength) {
if (maxInputLength <= 0) {
throw new IllegalArgumentException("[max_input_length] must be > 0 but was [" + maxInputLength + "]");
}
}
@Override
protected List<Parameter<?>> getParameters() {
return Arrays.asList(analyzer, searchAnalyzer, preserveSeparators, preservePosInc, contexts, maxInputLength, meta);
}
@Override
protected void toXContent(XContentBuilder builder, boolean includeDefaults) throws IOException {
builder.field("analyzer", this.analyzer.getValue().name());
if (Objects.equals(this.analyzer.getValue().name(), this.searchAnalyzer.getValue().name()) == false) {
builder.field("search_analyzer", this.searchAnalyzer.getValue().name());
}
builder.field(this.preserveSeparators.name, this.preserveSeparators.getValue());
builder.field(this.preservePosInc.name, this.preservePosInc.getValue());
builder.field(this.maxInputLength.name, this.maxInputLength.getValue());
if (this.contexts.getValue() != null) {
builder.startArray(this.contexts.name);
this.contexts.getValue().toXContent(builder, ToXContent.EMPTY_PARAMS);
builder.endArray();
}
}
@Override
public CompletionFieldMapper build(BuilderContext context) {
checkCompletionContextsLimit(context);
NamedAnalyzer completionAnalyzer = new NamedAnalyzer(this.searchAnalyzer.getValue().name(), AnalyzerScope.INDEX,
new CompletionAnalyzer(this.searchAnalyzer.getValue(), preserveSeparators.getValue(), preservePosInc.getValue()));
CompletionFieldType ft
= new CompletionFieldType(buildFullName(context), completionAnalyzer, meta.getValue());
ft.setContextMappings(contexts.getValue());
ft.setPreservePositionIncrements(preservePosInc.getValue());
ft.setPreserveSep(preserveSeparators.getValue());
ft.setIndexAnalyzer(analyzer.getValue());
return new CompletionFieldMapper(name, ft, defaultAnalyzer,
multiFieldsBuilder.build(this, context), copyTo.build(), this);
}
private void checkCompletionContextsLimit(BuilderContext context) {
if (this.contexts.getValue() != null && this.contexts.getValue().size() > COMPLETION_CONTEXTS_LIMIT) {
deprecationLogger.deprecatedAndMaybeLog("excessive_completion_contexts",
"You have defined more than [" + COMPLETION_CONTEXTS_LIMIT + "] completion contexts" +
" in the mapping for index [" + context.indexSettings().get(IndexMetadata.SETTING_INDEX_PROVIDED_NAME) + "]. " +
"The maximum allowed number of completion contexts in a mapping will be limited to " +
"[" + COMPLETION_CONTEXTS_LIMIT + "] starting in version [8.0].");
}
}
}
public static final Set<String> ALLOWED_CONTENT_FIELD_NAMES = Sets.newHashSet(Fields.CONTENT_FIELD_NAME_INPUT,
Fields.CONTENT_FIELD_NAME_WEIGHT, Fields.CONTENT_FIELD_NAME_CONTEXTS);
@ -130,60 +225,11 @@ public class CompletionFieldMapper extends FieldMapper {
@Override
public Mapper.Builder<?> parse(String name, Map<String, Object> node, ParserContext parserContext)
throws MapperParsingException {
CompletionFieldMapper.Builder builder = new CompletionFieldMapper.Builder(name);
NamedAnalyzer indexAnalyzer = null;
NamedAnalyzer searchAnalyzer = null;
for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<String, Object> entry = iterator.next();
String fieldName = entry.getKey();
Object fieldNode = entry.getValue();
if (fieldName.equals("type")) {
continue;
}
if (Fields.ANALYZER.match(fieldName, LoggingDeprecationHandler.INSTANCE)) {
indexAnalyzer = getNamedAnalyzer(parserContext, fieldNode.toString());
iterator.remove();
} else if (Fields.SEARCH_ANALYZER.match(fieldName, LoggingDeprecationHandler.INSTANCE)) {
searchAnalyzer = getNamedAnalyzer(parserContext, fieldNode.toString());
iterator.remove();
} else if (Fields.PRESERVE_SEPARATORS.match(fieldName, LoggingDeprecationHandler.INSTANCE)) {
builder.preserveSeparators(Boolean.parseBoolean(fieldNode.toString()));
iterator.remove();
} else if (Fields.PRESERVE_POSITION_INCREMENTS.match(fieldName, LoggingDeprecationHandler.INSTANCE)) {
builder.preservePositionIncrements(Boolean.parseBoolean(fieldNode.toString()));
iterator.remove();
} else if (Fields.MAX_INPUT_LENGTH.match(fieldName, LoggingDeprecationHandler.INSTANCE)) {
builder.maxInputLength(Integer.parseInt(fieldNode.toString()));
iterator.remove();
} else if (Fields.CONTEXTS.match(fieldName, LoggingDeprecationHandler.INSTANCE)) {
builder.contextMappings(ContextMappings.load(fieldNode, parserContext.indexVersionCreated()));
iterator.remove();
} else if (parseMultiField(builder::addMultiField, name, parserContext, fieldName, fieldNode)) {
iterator.remove();
}
}
if (indexAnalyzer == null) {
if (searchAnalyzer != null) {
throw new MapperParsingException("analyzer on completion field [" + name + "] must be set when search_analyzer is set");
}
indexAnalyzer = searchAnalyzer = parserContext.getIndexAnalyzers().get("simple");
} else if (searchAnalyzer == null) {
searchAnalyzer = indexAnalyzer;
}
builder.indexAnalyzer(indexAnalyzer);
builder.searchAnalyzer(searchAnalyzer);
CompletionFieldMapper.Builder builder
= new CompletionFieldMapper.Builder(name, parserContext.getIndexAnalyzers().get("simple"));
builder.parse(name, parserContext, node);
return builder;
}
private NamedAnalyzer getNamedAnalyzer(ParserContext parserContext, String name) {
NamedAnalyzer analyzer = parserContext.getIndexAnalyzers().get(name);
if (analyzer == null) {
throw new IllegalArgumentException("Can't find default or mapped analyzer with name [" + name + "]");
}
return analyzer;
}
}
public static final class CompletionFieldType extends TermBasedFieldType {
@ -194,17 +240,9 @@ public class CompletionFieldMapper extends FieldMapper {
private boolean preservePositionIncrements = Defaults.DEFAULT_POSITION_INCREMENTS;
private ContextMappings contextMappings = null;
public CompletionFieldType(String name, FieldType luceneFieldType,
NamedAnalyzer searchAnalyzer, NamedAnalyzer searchQuoteAnalyzer, Map<String, String> meta) {
public CompletionFieldType(String name, NamedAnalyzer searchAnalyzer, Map<String, String> meta) {
super(name, true, false,
new TextSearchInfo(luceneFieldType, null, searchAnalyzer, searchQuoteAnalyzer), meta);
}
public CompletionFieldType(String name) {
this(name, Defaults.FIELD_TYPE,
new NamedAnalyzer("completion", AnalyzerScope.INDEX, new CompletionAnalyzer(Lucene.STANDARD_ANALYZER)),
new NamedAnalyzer("completion", AnalyzerScope.INDEX, new CompletionAnalyzer(Lucene.STANDARD_ANALYZER)),
Collections.emptyMap());
new TextSearchInfo(Defaults.FIELD_TYPE, null, searchAnalyzer, searchAnalyzer), meta);
}
public void setPreserveSep(boolean preserveSep) {
@ -245,14 +283,6 @@ public class CompletionFieldMapper extends FieldMapper {
return contextMappings;
}
public boolean preserveSep() {
return preserveSep;
}
public boolean preservePositionIncrements() {
return preservePositionIncrements;
}
/**
* @return postings format to use for this field-type
*/
@ -302,100 +332,24 @@ public class CompletionFieldMapper extends FieldMapper {
}
/**
* Builder for {@link CompletionFieldMapper}
*/
public static class Builder extends FieldMapper.Builder<Builder> {
private final int maxInputLength;
private final boolean preserveSeparators;
private final boolean preservePosInc;
private final NamedAnalyzer defaultAnalyzer;
private final NamedAnalyzer analyzer;
private final NamedAnalyzer searchAnalyzer;
private final ContextMappings contexts;
private int maxInputLength = Defaults.DEFAULT_MAX_INPUT_LENGTH;
private ContextMappings contextMappings = null;
private boolean preserveSeparators = Defaults.DEFAULT_PRESERVE_SEPARATORS;
private boolean preservePositionIncrements = Defaults.DEFAULT_POSITION_INCREMENTS;
private static final DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(Builder.class));
/**
* @param name of the completion field to build
*/
public Builder(String name) {
super(name, Defaults.FIELD_TYPE);
builder = this;
}
/**
* @param maxInputLength maximum expected prefix length
* NOTE: prefixes longer than this will
* be truncated
*/
public Builder maxInputLength(int maxInputLength) {
if (maxInputLength <= 0) {
throw new IllegalArgumentException(Fields.MAX_INPUT_LENGTH.getPreferredName()
+ " must be > 0 but was [" + maxInputLength + "]");
}
this.maxInputLength = maxInputLength;
return this;
}
/**
* Add context mapping to this field
* @param contextMappings see {@link ContextMappings#load(Object, Version)}
*/
public Builder contextMappings(ContextMappings contextMappings) {
this.contextMappings = contextMappings;
return this;
}
public Builder preserveSeparators(boolean preserveSeparators) {
this.preserveSeparators = preserveSeparators;
return this;
}
public Builder preservePositionIncrements(boolean preservePositionIncrements) {
this.preservePositionIncrements = preservePositionIncrements;
return this;
}
@Override
public CompletionFieldMapper build(BuilderContext context) {
checkCompletionContextsLimit(context);
NamedAnalyzer searchAnalyzer = new NamedAnalyzer(this.searchAnalyzer.name(), AnalyzerScope.INDEX,
new CompletionAnalyzer(this.searchAnalyzer, preserveSeparators, preservePositionIncrements));
CompletionFieldType ft
= new CompletionFieldType(buildFullName(context), this.fieldType, searchAnalyzer, searchAnalyzer, meta);
ft.setContextMappings(contextMappings);
ft.setPreservePositionIncrements(preservePositionIncrements);
ft.setPreserveSep(preserveSeparators);
ft.setIndexAnalyzer(indexAnalyzer);
return new CompletionFieldMapper(name, this.fieldType, ft,
multiFieldsBuilder.build(this, context), copyTo, maxInputLength);
}
private void checkCompletionContextsLimit(BuilderContext context) {
if (this.contextMappings != null && this.contextMappings.size() > COMPLETION_CONTEXTS_LIMIT) {
deprecationLogger.deprecatedAndMaybeLog("excessive_completion_contexts",
"You have defined more than [" + COMPLETION_CONTEXTS_LIMIT + "] completion contexts" +
" in the mapping for index [" + context.indexSettings().get(IndexMetadata.SETTING_INDEX_PROVIDED_NAME) + "]. " +
"The maximum allowed number of completion contexts in a mapping will be limited to " +
"[" + COMPLETION_CONTEXTS_LIMIT + "] starting in version [8.0].");
}
}
@Override
public Builder index(boolean index) {
if (index == false) {
throw new MapperParsingException("Completion field type must be indexed");
}
return builder;
}
}
private int maxInputLength;
public CompletionFieldMapper(String simpleName, FieldType fieldType, MappedFieldType mappedFieldType,
MultiFields multiFields, CopyTo copyTo, int maxInputLength) {
super(simpleName, fieldType, mappedFieldType, multiFields, copyTo);
this.maxInputLength = maxInputLength;
public CompletionFieldMapper(String simpleName, MappedFieldType mappedFieldType, NamedAnalyzer defaultAnalyzer,
MultiFields multiFields, CopyTo copyTo, Builder builder) {
super(simpleName, mappedFieldType, multiFields, copyTo);
this.defaultAnalyzer = defaultAnalyzer;
this.maxInputLength = builder.maxInputLength.getValue();
this.preserveSeparators = builder.preserveSeparators.getValue();
this.preservePosInc = builder.preservePosInc.getValue();
this.analyzer = builder.analyzer.getValue();
this.searchAnalyzer = builder.searchAnalyzer.getValue();
this.contexts = builder.contexts.getValue();
}
@Override
@ -601,28 +555,6 @@ public class CompletionFieldMapper extends FieldMapper {
}
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(simpleName())
.field(Fields.TYPE.getPreferredName(), CONTENT_TYPE);
builder.field(Fields.ANALYZER.getPreferredName(), fieldType().indexAnalyzer().name());
if (fieldType().indexAnalyzer().name().equals(fieldType().getTextSearchInfo().getSearchAnalyzer().name()) == false) {
builder.field(Fields.SEARCH_ANALYZER.getPreferredName(), fieldType().getTextSearchInfo().getSearchAnalyzer().name());
}
builder.field(Fields.PRESERVE_SEPARATORS.getPreferredName(), fieldType().preserveSep());
builder.field(Fields.PRESERVE_POSITION_INCREMENTS.getPreferredName(), fieldType().preservePositionIncrements());
builder.field(Fields.MAX_INPUT_LENGTH.getPreferredName(), this.maxInputLength);
if (fieldType().hasContextMappings()) {
builder.startArray(Fields.CONTEXTS.getPreferredName());
fieldType().getContextMappings().toXContent(builder, params);
builder.endArray();
}
multiFields.toXContent(builder, params);
return builder.endObject();
}
@Override
protected void parseCreateField(ParseContext context) throws IOException {
// no-op
@ -633,23 +565,6 @@ public class CompletionFieldMapper extends FieldMapper {
return CONTENT_TYPE;
}
@Override
protected void mergeOptions(FieldMapper other, List<String> conflicts) {
CompletionFieldType c = (CompletionFieldType)other.fieldType();
if (fieldType().preservePositionIncrements != c.preservePositionIncrements) {
conflicts.add("mapper [" + name() + "] has different [preserve_position_increments] values");
}
if (fieldType().preserveSep != c.preserveSep) {
conflicts.add("mapper [" + name() + "] has different [preserve_separators] values");
}
if (fieldType().hasContextMappings() != c.hasContextMappings()) {
conflicts.add("mapper [" + name() + "] has different [context_mappings] values");
} else if (fieldType().hasContextMappings() && fieldType().contextMappings.equals(c.contextMappings) == false) {
conflicts.add("mapper [" + name() + "] has different [context_mappings] values");
}
this.maxInputLength = ((CompletionFieldMapper)other).maxInputLength;
}
}

View File

@ -22,14 +22,18 @@ package org.elasticsearch.index.mapper;
import org.apache.logging.log4j.LogManager;
import org.apache.lucene.document.FieldType;
import org.elasticsearch.Version;
import org.elasticsearch.common.TriFunction;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.mapper.Mapper.TypeParser.ParserContext;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@ -37,8 +41,9 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Defines how a particular field should be indexed and searched
@ -113,6 +118,13 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
return builder.endObject();
}
/**
* Serializes a parameter
*/
protected interface Serializer<T> {
void serialize(XContentBuilder builder, String name, T value) throws IOException;
}
/**
* A configurable parameter for a field mapper
* @param <T> the type of the value the parameter holds
@ -120,12 +132,16 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
public static final class Parameter<T> {
public final String name;
private final T defaultValue;
private final BiFunction<String, Object, T> parser;
private final List<String> deprecatedNames = new ArrayList<>();
private final Supplier<T> defaultValue;
private final TriFunction<String, ParserContext, Object, T> parser;
private final Function<FieldMapper, T> initializer;
private final boolean updateable;
private boolean acceptsNull = false;
private Consumer<T> validator = null;
private Serializer<T> serializer = XContentBuilder::field;
private T value;
private boolean isSet;
/**
* Creates a new Parameter
@ -135,11 +151,11 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
* @param parser a function that converts an object to a parameter value
* @param initializer a function that reads a parameter value from an existing mapper
*/
public Parameter(String name, boolean updateable, T defaultValue,
BiFunction<String, Object, T> parser, Function<FieldMapper, T> initializer) {
public Parameter(String name, boolean updateable, Supplier<T> defaultValue,
TriFunction<String, ParserContext, Object, T> parser, Function<FieldMapper, T> initializer) {
this.name = name;
this.defaultValue = defaultValue;
this.value = defaultValue;
this.defaultValue = Objects.requireNonNull(defaultValue);
this.value = null;
this.parser = parser;
this.initializer = initializer;
this.updateable = updateable;
@ -149,13 +165,14 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
* Returns the current value of the parameter
*/
public T getValue() {
return value;
return isSet ? value : defaultValue.get();
}
/**
* Sets the current value of the parameter
*/
public void setValue(T value) {
this.isSet = true;
this.value = value;
}
@ -167,26 +184,59 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
return this;
}
private void init(FieldMapper toInit) {
this.value = initializer.apply(toInit);
/**
* Adds a deprecated parameter name.
*
* If this parameter name is encountered during parsing, a deprecation warning will
* be emitted. The parameter will be serialized with its main name.
*/
public Parameter<T> addDeprecatedName(String deprecatedName) {
this.deprecatedNames.add(deprecatedName);
return this;
}
private void parse(String field, Object in) {
this.value = parser.apply(field, in);
/**
* Adds validation to a parameter, called after parsing and merging
*/
public Parameter<T> setValidator(Consumer<T> validator) {
this.validator = validator;
return this;
}
/**
* Configure a custom serializer for this parameter
*/
public Parameter<T> setSerializer(Serializer<T> serializer) {
this.serializer = serializer;
return this;
}
private void validate() {
if (validator != null && isSet) {
validator.accept(value);
}
}
private void init(FieldMapper toInit) {
setValue(initializer.apply(toInit));
}
private void parse(String field, ParserContext context, Object in) {
setValue(parser.apply(field, context, in));
}
private void merge(FieldMapper toMerge, Conflicts conflicts) {
T value = initializer.apply(toMerge);
if (updateable == false && Objects.equals(this.value, value) == false) {
if (updateable == false && isSet && Objects.equals(this.value, value) == false) {
conflicts.addConflict(name, this.value.toString(), value.toString());
} else {
this.value = value;
setValue(value);
}
}
private void toXContent(XContentBuilder builder, boolean includeDefaults) throws IOException {
if (includeDefaults || (Objects.equals(defaultValue, value) == false)) {
builder.field(name, value);
if (includeDefaults || Objects.equals(getValue(), defaultValue.get()) == false) {
serializer.serialize(builder, name, getValue());
}
}
@ -199,7 +249,7 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
*/
public static Parameter<Boolean> boolParam(String name, boolean updateable,
Function<FieldMapper, Boolean> initializer, boolean defaultValue) {
return new Parameter<>(name, updateable, defaultValue, (n, o) -> XContentMapValues.nodeBooleanValue(o), initializer);
return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeBooleanValue(o), initializer);
}
/**
@ -211,7 +261,19 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
*/
public static Parameter<Float> floatParam(String name, boolean updateable,
Function<FieldMapper, Float> initializer, float defaultValue) {
return new Parameter<>(name, updateable, defaultValue, (n, o) -> XContentMapValues.nodeFloatValue(o), initializer);
return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeFloatValue(o), initializer);
}
/**
* Defines a parameter that takes an integer value
* @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 defaultValue the default value, to be used if the parameter is undefined in a mapping
*/
public static Parameter<Integer> intParam(String name, boolean updateable,
Function<FieldMapper, Integer> initializer, int defaultValue) {
return new Parameter<>(name, updateable, () -> defaultValue, (n, c, o) -> XContentMapValues.nodeIntegerValue(o), initializer);
}
/**
@ -223,9 +285,38 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
*/
public static Parameter<String> stringParam(String name, boolean updateable,
Function<FieldMapper, String> initializer, String defaultValue) {
return new Parameter<>(name, updateable, defaultValue,
(n, o) -> XContentMapValues.nodeStringValue(o), initializer);
return new Parameter<>(name, updateable, () -> defaultValue,
(n, c, o) -> XContentMapValues.nodeStringValue(o), initializer);
}
/**
* Defines a parameter that takes an analyzer name
* @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 defaultAnalyzer the default value, to be used if the parameter is undefined in a mapping
*/
public static Parameter<NamedAnalyzer> analyzerParam(String name, boolean updateable,
Function<FieldMapper, NamedAnalyzer> initializer,
Supplier<NamedAnalyzer> defaultAnalyzer) {
return new Parameter<>(name, updateable, defaultAnalyzer, (n, c, o) -> {
String analyzerName = o.toString();
NamedAnalyzer a = c.getIndexAnalyzers().get(analyzerName);
if (a == null) {
throw new IllegalArgumentException("analyzer [" + analyzerName + "] has not been configured in mappings");
}
return a;
}, initializer).setSerializer((b, n, v) -> b.field(n, v.name()));
}
/**
* Declares a metadata parameter
*/
public static Parameter<Map<String, String>> metaParam() {
return new Parameter<>("meta", true, Collections::emptyMap,
(n, c, o) -> TypeParsers.parseMeta(n, o), m -> m.fieldType().meta());
}
}
private static final class Conflicts {
@ -288,6 +379,13 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
multiFieldsBuilder.update(newSubField, parentPath(newSubField.name()));
}
this.copyTo.reset(in.copyTo);
validate();
}
private void validate() {
for (Parameter<?> param : getParameters()) {
param.validate();
}
}
/**
@ -305,7 +403,10 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
return context.path().pathAsText(name);
}
private void toXContent(XContentBuilder builder, boolean includeDefaults) throws IOException {
/**
* Writes the current builder parameter values as XContent
*/
protected void toXContent(XContentBuilder builder, boolean includeDefaults) throws IOException {
for (Parameter<?> parameter : getParameters()) {
parameter.toXContent(builder, includeDefaults);
}
@ -317,10 +418,14 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
* @param parserContext the parser context
* @param fieldNode the root node of the map of mappings for this field
*/
public final void parse(String name, TypeParser.ParserContext parserContext, Map<String, Object> fieldNode) {
public final void parse(String name, ParserContext parserContext, Map<String, Object> fieldNode) {
Map<String, Parameter<?>> paramsMap = new HashMap<>();
Map<String, Parameter<?>> deprecatedParamsMap = new HashMap<>();
for (Parameter<?> param : getParameters()) {
paramsMap.put(param.name, param);
for (String deprecatedName : param.deprecatedNames) {
deprecatedParamsMap.put(deprecatedName, param);
}
}
String type = (String) fieldNode.remove("type");
for (Iterator<Map.Entry<String, Object>> iterator = fieldNode.entrySet().iterator(); iterator.hasNext();) {
@ -337,7 +442,13 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
iterator.remove();
continue;
}
Parameter<?> parameter = paramsMap.get(propName);
Parameter<?> parameter = deprecatedParamsMap.get(propName);
if (parameter != null) {
deprecationLogger.deprecatedAndMaybeLog(propName, "Parameter [{}] on mapper [{}] is deprecated, use [{}]",
propName, name, parameter.name);
} else {
parameter = paramsMap.get(propName);
}
if (parameter == null) {
if (isDeprecatedParameter(propName, parserContext.indexVersionCreated())) {
deprecationLogger.deprecatedAndMaybeLog(propName,
@ -352,9 +463,10 @@ public abstract class ParametrizedFieldMapper extends FieldMapper {
throw new MapperParsingException("[" + propName + "] on mapper [" + name
+ "] of type [" + type + "] must not have a [null] value");
}
parameter.parse(name, propNode);
parameter.parse(name, parserContext, propNode);
iterator.remove();
}
validate();
}
// These parameters were previously *always* parsed by TypeParsers#parseField(), even if they

View File

@ -18,7 +18,6 @@
*/
package org.elasticsearch.index.mapper;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.SortedSetDocValuesField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.search.Query;
@ -43,20 +42,16 @@ import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.analysis.AnalyzerScope;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.search.suggest.completion.context.ContextBuilder;
import org.elasticsearch.search.suggest.completion.context.ContextMappings;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.hamcrest.FeatureMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Before;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
@ -68,31 +63,7 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
public class CompletionFieldMapperTests extends FieldMapperTestCase<CompletionFieldMapper.Builder> {
@Before
public void addModifiers() {
addBooleanModifier("preserve_separators", false, CompletionFieldMapper.Builder::preserveSeparators);
addBooleanModifier("preserve_position_increments", false, CompletionFieldMapper.Builder::preservePositionIncrements);
addModifier("context_mappings", false, (a, b) -> {
ContextMappings contextMappings = new ContextMappings(Arrays.asList(ContextBuilder.category("foo").build(),
ContextBuilder.geo("geo").build()));
a.contextMappings(contextMappings);
});
}
@Override
protected Set<String> unsupportedProperties() {
return org.elasticsearch.common.collect.Set.of("doc_values", "index");
}
@Override
protected CompletionFieldMapper.Builder newBuilder() {
CompletionFieldMapper.Builder builder = new CompletionFieldMapper.Builder("completion");
builder.indexAnalyzer(new NamedAnalyzer("standard", AnalyzerScope.INDEX, new StandardAnalyzer()));
builder.searchAnalyzer(new NamedAnalyzer("standard", AnalyzerScope.INDEX, new StandardAnalyzer()));
return builder;
}
public class CompletionFieldMapperTests extends ESSingleNodeTestCase {
public void testDefaultConfiguration() throws IOException {
String mapping = Strings.toString(jsonBuilder().startObject().startObject("type1")
@ -155,6 +126,8 @@ public class CompletionFieldMapperTests extends FieldMapperTestCase<CompletionFi
assertThat(analyzer.preservePositionIncrements(), equalTo(true));
assertThat(analyzer.preserveSep(), equalTo(false));
assertEquals("{\"completion\":{\"type\":\"completion\",\"analyzer\":\"simple\",\"search_analyzer\":\"standard\"," +
"\"preserve_separators\":false,\"preserve_position_increments\":true,\"max_input_length\":50}}", Strings.toString(fieldMapper));
}
public void testTypeParsing() throws Exception {
@ -176,7 +149,8 @@ public class CompletionFieldMapperTests extends FieldMapperTestCase<CompletionFi
assertThat(fieldMapper, instanceOf(CompletionFieldMapper.class));
XContentBuilder builder = jsonBuilder().startObject();
fieldMapper.toXContent(builder, ToXContent.EMPTY_PARAMS).endObject();
fieldMapper.toXContent(builder,
new ToXContent.MapParams(Collections.singletonMap("include_defaults", "true"))).endObject();
builder.close();
Map<String, Object> serializedMap = createParser(JsonXContent.jsonXContent, BytesReference.bytes(builder)).map();
Map<String, Object> configMap = (Map<String, Object>) serializedMap.get("completion");

View File

@ -19,15 +19,20 @@
package org.elasticsearch.index.mapper;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.elasticsearch.Version;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.compress.CompressedXContent;
import org.elasticsearch.common.lucene.Lucene;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.analysis.AnalyzerScope;
import org.elasticsearch.index.analysis.IndexAnalyzers;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.mapper.ParametrizedFieldMapper.Parameter;
import org.elasticsearch.plugins.MapperPlugin;
import org.elasticsearch.plugins.Plugin;
@ -41,6 +46,10 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.hamcrest.Matchers.instanceOf;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ParametrizedMapperTests extends ESSingleNodeTestCase {
public static class TestPlugin extends Plugin implements MapperPlugin {
@ -55,6 +64,27 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
return Collections.singletonList(TestPlugin.class);
}
private static class StringWrapper {
final String name;
private StringWrapper(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StringWrapper that = (StringWrapper) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
private static TestMapper toType(Mapper in) {
return (TestMapper) in;
}
@ -64,9 +94,28 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
final Parameter<Boolean> fixed
= Parameter.boolParam("fixed", false, m -> toType(m).fixed, true);
final Parameter<Boolean> fixed2
= Parameter.boolParam("fixed2", false, m -> toType(m).fixed2, false);
= Parameter.boolParam("fixed2", false, m -> toType(m).fixed2, false)
.addDeprecatedName("fixed2_old");
final Parameter<String> variable
= Parameter.stringParam("variable", true, m -> toType(m).variable, "default").acceptsNull();
final Parameter<StringWrapper> wrapper
= new Parameter<>("wrapper", true, () -> new StringWrapper("default"),
(n, c, o) -> {
if (o == null) return null;
return new StringWrapper(o.toString());
},
m -> toType(m).wrapper).setSerializer((b, n, v) -> b.field(n, v.name));
final Parameter<Integer> intValue = Parameter.intParam("int_value", true, m -> toType(m).intValue, 5)
.setValidator(n -> {
if (n > 50) {
throw new IllegalArgumentException("Value of [n] cannot be greater than 50");
}
});
final Parameter<NamedAnalyzer> analyzer
= Parameter.analyzerParam("analyzer", true, m -> toType(m).analyzer, () -> Lucene.KEYWORD_ANALYZER);
final Parameter<NamedAnalyzer> searchAnalyzer
= Parameter.analyzerParam("search_analyzer", true, m -> toType(m).searchAnalyzer, analyzer::getValue);
final Parameter<Boolean> index = Parameter.boolParam("index", false, m -> toType(m).index, true);
protected Builder(String name) {
@ -75,7 +124,7 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
@Override
protected List<Parameter<?>> getParameters() {
return Arrays.asList(fixed, fixed2, variable, index);
return Arrays.asList(fixed, fixed2, variable, index, wrapper, intValue, analyzer, searchAnalyzer);
}
@Override
@ -100,6 +149,10 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
private final boolean fixed;
private final boolean fixed2;
private final String variable;
private final StringWrapper wrapper;
private final int intValue;
private final NamedAnalyzer analyzer;
private final NamedAnalyzer searchAnalyzer;
private final boolean index;
protected TestMapper(String simpleName, String fullName, MultiFields multiFields, CopyTo copyTo,
@ -108,6 +161,10 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
this.fixed = builder.fixed.getValue();
this.fixed2 = builder.fixed2.getValue();
this.variable = builder.variable.getValue();
this.wrapper = builder.wrapper.getValue();
this.intValue = builder.intValue.getValue();
this.analyzer = builder.analyzer.getValue();
this.searchAnalyzer = builder.searchAnalyzer.getValue();
this.index = builder.index.getValue();
}
@ -128,7 +185,15 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
}
private static TestMapper fromMapping(String mapping, Version version) {
Mapper.TypeParser.ParserContext pc = new Mapper.TypeParser.ParserContext(s -> null, null, s -> {
MapperService mapperService = mock(MapperService.class);
IndexAnalyzers indexAnalyzers = new IndexAnalyzers(
org.elasticsearch.common.collect.Map.of(
"_standard", Lucene.STANDARD_ANALYZER,
"_keyword", Lucene.KEYWORD_ANALYZER,
"default", new NamedAnalyzer("default", AnalyzerScope.INDEX, new StandardAnalyzer())),
Collections.emptyMap(), Collections.emptyMap());
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();
}
@ -162,7 +227,8 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
mapper.toXContent(builder, params);
builder.endObject();
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed\":true," +
"\"fixed2\":false,\"variable\":\"default\",\"index\":true}}",
"\"fixed2\":false,\"variable\":\"default\",\"index\":true," +
"\"wrapper\":\"default\",\"int_value\":5,\"analyzer\":\"_keyword\",\"search_analyzer\":\"_keyword\"}}",
Strings.toString(builder));
}
@ -255,6 +321,53 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
assertEquals(mapping, Strings.toString(indexService.mapperService().documentMapper()));
}
// test custom serializer
public void testCustomSerialization() {
String mapping = "{\"type\":\"test_mapper\",\"wrapper\":\"wrapped value\"}";
TestMapper mapper = fromMapping(mapping);
assertEquals("wrapped value", mapper.wrapper.name);
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
}
// test validator
public void testParameterValidation() {
String mapping = "{\"type\":\"test_mapper\",\"int_value\":10}";
TestMapper mapper = fromMapping(mapping);
assertEquals(10, mapper.intValue);
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> fromMapping("{\"type\":\"test_mapper\",\"int_value\":60}"));
assertEquals("Value of [n] cannot be greater than 50", e.getMessage());
}
// test deprecations
public void testDeprecatedParameterName() {
String mapping = "{\"type\":\"test_mapper\",\"fixed2_old\":true}";
TestMapper mapper = fromMapping(mapping);
assertTrue(mapper.fixed2);
assertWarnings("Parameter [fixed2_old] on mapper [field] is deprecated, use [fixed2]");
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"fixed2\":true}}", Strings.toString(mapper));
}
public void testAnalyzers() {
String mapping = "{\"type\":\"test_mapper\",\"analyzer\":\"_standard\"}";
TestMapper mapper = fromMapping(mapping);
assertEquals(mapper.analyzer, Lucene.STANDARD_ANALYZER);
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
String withDef = "{\"type\":\"test_mapper\",\"analyzer\":\"default\"}";
mapper = fromMapping(withDef);
assertEquals(mapper.analyzer.name(), "default");
assertThat(mapper.analyzer.analyzer(), instanceOf(StandardAnalyzer.class));
assertEquals("{\"field\":" + withDef + "}", Strings.toString(mapper));
String badAnalyzer = "{\"type\":\"test_mapper\",\"analyzer\":\"wibble\"}";
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> fromMapping(badAnalyzer));
assertEquals("analyzer [wibble] has not been configured in mappings", e.getMessage());
}
public void testDeprecatedParameters() throws IOException {
// 'index' is declared explicitly, 'store' is not, but is one of the previously always-accepted params
String mapping = "{\"type\":\"test_mapper\",\"index\":false,\"store\":true}";
@ -264,4 +377,25 @@ public class ParametrizedMapperTests extends ESSingleNodeTestCase {
assertEquals("{\"field\":{\"type\":\"test_mapper\",\"index\":false}}", Strings.toString(mapper));
}
public void testLinkedAnalyzers() {
String mapping = "{\"type\":\"test_mapper\",\"analyzer\":\"_standard\"}";
TestMapper mapper = fromMapping(mapping);
assertEquals("_standard", mapper.analyzer.name());
assertEquals("_standard", mapper.searchAnalyzer.name());
assertEquals("{\"field\":" + mapping + "}", Strings.toString(mapper));
String mappingWithSA = "{\"type\":\"test_mapper\",\"search_analyzer\":\"_standard\"}";
mapper = fromMapping(mappingWithSA);
assertEquals("_keyword", mapper.analyzer.name());
assertEquals("_standard", mapper.searchAnalyzer.name());
assertEquals("{\"field\":" + mappingWithSA + "}", Strings.toString(mapper));
String mappingWithBoth = "{\"type\":\"test_mapper\",\"analyzer\":\"default\",\"search_analyzer\":\"_standard\"}";
mapper = fromMapping(mappingWithBoth);
assertEquals("default", mapper.analyzer.name());
assertEquals("_standard", mapper.searchAnalyzer.name());
assertEquals("{\"field\":" + mappingWithBoth + "}", Strings.toString(mapper));
}
}

View File

@ -25,7 +25,6 @@ import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.index.analysis.AnalyzerScope;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.mapper.CompletionFieldMapper;
import org.elasticsearch.index.mapper.CompletionFieldMapper.CompletionFieldType;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.search.suggest.AbstractSuggestionBuilderTestCase;
@ -169,14 +168,11 @@ public class CompletionSuggesterBuilderTests extends AbstractSuggestionBuilderTe
@Override
protected MappedFieldType mockFieldType(String fieldName, boolean analyzerSet) {
if (analyzerSet == false) {
CompletionFieldType completionFieldType = new CompletionFieldType(fieldName,
CompletionFieldMapper.Defaults.FIELD_TYPE, null, null, Collections.emptyMap());
CompletionFieldType completionFieldType = new CompletionFieldType(fieldName, null, Collections.emptyMap());
completionFieldType.setContextMappings(new ContextMappings(contextMappings));
return completionFieldType;
}
CompletionFieldType completionFieldType = new CompletionFieldType(fieldName,
CompletionFieldMapper.Defaults.FIELD_TYPE,
new NamedAnalyzer("fieldSearchAnalyzer", AnalyzerScope.INDEX, new SimpleAnalyzer()),
new NamedAnalyzer("fieldSearchAnalyzer", AnalyzerScope.INDEX, new SimpleAnalyzer()),
Collections.emptyMap());
completionFieldType.setContextMappings(new ContextMappings(contextMappings));