/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.index.mapper;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.Terms;
import org.apache.lucene.queries.TermsQuery;
import org.apache.lucene.search.*;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.action.fieldstats.FieldStats;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.fielddata.FieldDataType;
import org.elasticsearch.index.mapper.Mapper.CqlCollection;
import org.elasticsearch.index.mapper.Mapper.CqlStruct;
import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.similarity.SimilarityProvider;
import java.io.IOException;
import java.lang.IllegalArgumentException;
import java.util.List;
import java.util.Objects;
/**
* This defines the core properties and functions to operate on a field.
*/
public abstract class MappedFieldType extends FieldType {
private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(MappedFieldType.class));
public static class Names {
private final String indexName;
private final String originalIndexName;
private final String fullName;
public Names(String name) {
this(name, name, name);
}
public Names(String indexName, String originalIndexName, String fullName) {
this.indexName = indexName;
this.originalIndexName = originalIndexName;
this.fullName = fullName;
}
/**
* The indexed name of the field. This is the name under which we will
* store it in the index.
*/
public String indexName() {
return indexName;
}
/**
* The original index name, before any "path" modifications performed on it.
*/
public String originalIndexName() {
return originalIndexName;
}
/**
* The full name, including dot path.
*/
public String fullName() {
return fullName;
}
@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Names names = (Names) o;
if (!fullName.equals(names.fullName)) return false;
if (!indexName.equals(names.indexName)) return false;
if (!originalIndexName.equals(names.originalIndexName)) return false;
return true;
}
@Override
public int hashCode() {
int result = indexName.hashCode();
result = 31 * result + originalIndexName.hashCode();
result = 31 * result + fullName.hashCode();
return result;
}
}
public enum Loading {
LAZY {
@Override
public String toString() {
return LAZY_VALUE;
}
},
EAGER {
@Override
public String toString() {
return EAGER_VALUE;
}
},
EAGER_GLOBAL_ORDINALS {
@Override
public String toString() {
return EAGER_GLOBAL_ORDINALS_VALUE;
}
};
public static final String KEY = "loading";
public static final String EAGER_GLOBAL_ORDINALS_VALUE = "eager_global_ordinals";
public static final String EAGER_VALUE = "eager";
public static final String LAZY_VALUE = "lazy";
public static Loading parse(String loading, Loading defaultValue) {
if (Strings.isNullOrEmpty(loading)) {
return defaultValue;
} else if (EAGER_GLOBAL_ORDINALS_VALUE.equalsIgnoreCase(loading)) {
return EAGER_GLOBAL_ORDINALS;
} else if (EAGER_VALUE.equalsIgnoreCase(loading)) {
return EAGER;
} else if (LAZY_VALUE.equalsIgnoreCase(loading)) {
return LAZY;
} else {
throw new MapperParsingException("Unknown [" + KEY + "] value: [" + loading + "]");
}
}
}
private Names names;
private float boost;
// TODO: remove this docvalues flag and use docValuesType
private boolean docValues;
private NamedAnalyzer indexAnalyzer;
private NamedAnalyzer searchAnalyzer;
private NamedAnalyzer searchQuoteAnalyzer;
private SimilarityProvider similarity;
private Loading normsLoading;
private FieldDataType fieldDataType;
private Object nullValue;
private String nullValueAsString; // for sending null value to _all field
private CqlCollection cqlCollection = CqlCollection.LIST;
private CqlStruct cqlStruct = CqlStruct.UDT;
private boolean cqlPartialUpdate = true;
private boolean cqlPartitionKey = false;
private boolean cqlStaticColumn = false;
private int cqlPrimaryKeyOrder = -1;
protected MappedFieldType(MappedFieldType ref) {
super(ref);
this.names = ref.names();
this.boost = ref.boost();
this.docValues = ref.hasDocValues();
this.indexAnalyzer = ref.indexAnalyzer();
this.searchAnalyzer = ref.searchAnalyzer();
this.searchQuoteAnalyzer = ref.searchQuoteAnalyzer();
this.similarity = ref.similarity();
this.normsLoading = ref.normsLoading();
this.fieldDataType = ref.fieldDataType();
this.nullValue = ref.nullValue();
this.nullValueAsString = ref.nullValueAsString();
this.cqlCollection = ref.cqlCollection;
this.cqlStruct = ref.cqlStruct;
this.cqlPartialUpdate = ref.cqlPartialUpdate;
this.cqlPartitionKey = ref.cqlPartitionKey;
this.cqlStaticColumn = ref.cqlStaticColumn;
this.cqlPrimaryKeyOrder = ref.cqlPrimaryKeyOrder;
}
public MappedFieldType() {
setTokenized(true);
setStored(false);
setStoreTermVectors(false);
setOmitNorms(false);
setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
setBoost(1.0f);
fieldDataType = new FieldDataType(typeName());
}
@Override
public abstract MappedFieldType clone();
@Override
public boolean equals(Object o) {
if (!super.equals(o)) return false;
MappedFieldType fieldType = (MappedFieldType) o;
// check similarity first because we need to check the name, and it might be null
// TODO: SimilarityProvider should have equals?
if (similarity == null || fieldType.similarity == null) {
if (similarity != fieldType.similarity) {
return false;
}
} else {
if (Objects.equals(similarity.name(), fieldType.similarity.name()) == false) {
return false;
}
}
return boost == fieldType.boost &&
docValues == fieldType.docValues &&
Objects.equals(names, fieldType.names) &&
Objects.equals(indexAnalyzer, fieldType.indexAnalyzer) &&
Objects.equals(searchAnalyzer, fieldType.searchAnalyzer) &&
Objects.equals(searchQuoteAnalyzer(), fieldType.searchQuoteAnalyzer()) &&
Objects.equals(normsLoading, fieldType.normsLoading) &&
Objects.equals(fieldDataType, fieldType.fieldDataType) &&
Objects.equals(nullValue, fieldType.nullValue) &&
Objects.equals(cqlCollection, fieldType.cqlCollection) &&
Objects.equals(cqlStruct, fieldType.cqlStruct) &&
Objects.equals(cqlPartialUpdate, fieldType.cqlPartialUpdate) &&
Objects.equals(cqlPartitionKey, fieldType.cqlPartitionKey) &&
Objects.equals(cqlStaticColumn, fieldType.cqlStaticColumn) &&
Objects.equals(cqlPrimaryKeyOrder, fieldType.cqlPrimaryKeyOrder) &&
Objects.equals(nullValueAsString, fieldType.nullValueAsString);
}
public CqlCollection cqlCollection() {
return this.cqlCollection;
}
public void cqlCollection(CqlCollection cqlCollection) {
this.cqlCollection = cqlCollection;
}
public String cqlCollectionTag() {
if (this.cqlCollection.equals(CqlCollection.LIST)) return "list";
if (this.cqlCollection.equals(CqlCollection.SET)) return "set";
return "";
}
public CqlStruct cqlStruct() {
return this.cqlStruct;
}
public void cqlStruct(CqlStruct cqlStruct) {
this.cqlStruct = cqlStruct;
}
public boolean cqlPartialUpdate() {
return this.cqlPartialUpdate;
}
public void cqlPartialUpdate(boolean cqlPartialUpdate) {
this.cqlPartialUpdate = cqlPartialUpdate;
}
public boolean cqlPartitionKey() {
return this.cqlPartitionKey;
}
public void cqlPartitionKey(boolean cqlPartitionKey) {
this.cqlPartitionKey = cqlPartitionKey;
}
public boolean cqlStaticColumn() {
return this.cqlStaticColumn;
}
public void cqlStaticColumn(boolean cqlStaticColumn) {
this.cqlStaticColumn = cqlStaticColumn;
}
public int cqlPrimaryKeyOrder() {
return this.cqlPrimaryKeyOrder;
}
public void cqlPrimaryKeyOrder(int cqlPrimaryKeyOrder) {
this.cqlPrimaryKeyOrder = cqlPrimaryKeyOrder;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), names, boost, docValues, indexAnalyzer, searchAnalyzer, searchQuoteAnalyzer,
similarity == null ? null : similarity.name(), normsLoading, fieldDataType, nullValue, nullValueAsString,
cqlCollection, cqlStruct, cqlPartialUpdate, cqlPartitionKey, cqlStaticColumn, cqlPrimaryKeyOrder);
}
// we need to override freeze() and add safety checks that all settings are actually set
/** Returns the name of this type, as would be specified in mapping properties */
public abstract String typeName();
/** Checks this type is the same type as other. Adds a conflict if they are different. */
private final void checkTypeName(MappedFieldType other) {
if (typeName().equals(other.typeName()) == false) {
throw new IllegalArgumentException("mapper [" + names().fullName() + "] cannot be changed from type [" + typeName() + "] to [" + other.typeName() + "]");
} else if (getClass() != other.getClass()) {
throw new IllegalStateException("Type names equal for class " + getClass().getSimpleName() + " and " + other.getClass().getSimpleName());
}
}
/**
* Checks for any conflicts between this field type and other.
* If strict is true, all properties must be equal.
* Otherwise, only properties which must never change in an index are checked.
*/
public void checkCompatibility(MappedFieldType other, List<String> conflicts, boolean strict) {
checkTypeName(other);
boolean indexed = indexOptions() != IndexOptions.NONE;
boolean mergeWithIndexed = other.indexOptions() != IndexOptions.NONE;
// TODO: should be validating if index options go "up" (but "down" is ok)
if (indexed != mergeWithIndexed || tokenized() != other.tokenized()) {
conflicts.add("mapper [" + names().fullName() + "] has different [index] values");
}
if (stored() != other.stored()) {
conflicts.add("mapper [" + names().fullName() + "] has different [store] values");
}
if (hasDocValues() == false && other.hasDocValues()) {
// don't add conflict if this mapper has doc values while the mapper to merge doesn't since doc values are implicitly set
// when the doc_values field data format is configured
conflicts.add("mapper [" + names().fullName() + "] has different [doc_values] values, cannot change from disabled to enabled");
}
if (omitNorms() && !other.omitNorms()) {
conflicts.add("mapper [" + names().fullName() + "] has different [omit_norms] values, cannot change from disable to enabled");
}
if (storeTermVectors() != other.storeTermVectors()) {
conflicts.add("mapper [" + names().fullName() + "] has different [store_term_vector] values");
}
if (storeTermVectorOffsets() != other.storeTermVectorOffsets()) {
conflicts.add("mapper [" + names().fullName() + "] has different [store_term_vector_offsets] values");
}
if (storeTermVectorPositions() != other.storeTermVectorPositions()) {
conflicts.add("mapper [" + names().fullName() + "] has different [store_term_vector_positions] values");
}
if (storeTermVectorPayloads() != other.storeTermVectorPayloads()) {
conflicts.add("mapper [" + names().fullName() + "] has different [store_term_vector_payloads] values");
}
// null and "default"-named index analyzers both mean the default is used
if (indexAnalyzer() == null || "default".equals(indexAnalyzer().name())) {
if (other.indexAnalyzer() != null && "default".equals(other.indexAnalyzer().name()) == false) {
conflicts.add("mapper [" + names().fullName() + "] has different [analyzer]");
}
} else if (other.indexAnalyzer() == null || "default".equals(other.indexAnalyzer().name())) {
conflicts.add("mapper [" + names().fullName() + "] has different [analyzer]");
} else if (indexAnalyzer().name().equals(other.indexAnalyzer().name()) == false) {
conflicts.add("mapper [" + names().fullName() + "] has different [analyzer]");
}
if (!names().indexName().equals(other.names().indexName())) {
conflicts.add("mapper [" + names().fullName() + "] has different [index_name]");
}
if (Objects.equals(similarity(), other.similarity()) == false) {
conflicts.add("mapper [" + names().fullName() + "] has different [similarity]");
}
if (strict) {
if (omitNorms() != other.omitNorms()) {
conflicts.add("mapper [" + names().fullName() + "] is used by multiple types. Set update_all_types to true to update [omit_norms] across all types.");
}
if (boost() != other.boost()) {
conflicts.add("mapper [" + names().fullName() + "] is used by multiple types. Set update_all_types to true to update [boost] across all types.");
}
if (normsLoading() != other.normsLoading()) {
conflicts.add("mapper [" + names().fullName() + "] is used by multiple types. Set update_all_types to true to update [norms.loading] across all types.");
}
if (Objects.equals(searchAnalyzer(), other.searchAnalyzer()) == false) {
conflicts.add("mapper [" + names().fullName() + "] is used by multiple types. Set update_all_types to true to update [search_analyzer] across all types.");
}
if (Objects.equals(searchQuoteAnalyzer(), other.searchQuoteAnalyzer()) == false) {
conflicts.add("mapper [" + names().fullName() + "] is used by multiple types. Set update_all_types to true to update [search_quote_analyzer] across all types.");
}
if (Objects.equals(fieldDataType(), other.fieldDataType()) == false) {
conflicts.add("mapper [" + names().fullName() + "] is used by multiple types. Set update_all_types to true to update [fielddata] across all types.");
}
if (Objects.equals(nullValue(), other.nullValue()) == false) {
conflicts.add("mapper [" + names().fullName() + "] is used by multiple types. Set update_all_types to true to update [null_value] across all types.");
}
}
}
public boolean isNumeric() {
return false;
}
public boolean isSortable() {
return true;
}
public Names names() {
return names;
}
public void setNames(Names names) {
checkIfFrozen();
this.names = names;
}
public float boost() {
return boost;
}
public void setBoost(float boost) {
checkIfFrozen();
this.boost = boost;
}
public FieldDataType fieldDataType() {
return fieldDataType;
}
public void setFieldDataType(FieldDataType fieldDataType) {
checkIfFrozen();
this.fieldDataType = fieldDataType;
}
public boolean hasDocValues() {
return docValues;
}
public void setHasDocValues(boolean hasDocValues) {
checkIfFrozen();
this.docValues = hasDocValues;
}
public Loading normsLoading() {
return normsLoading;
}
public void setNormsLoading(Loading normsLoading) {
checkIfFrozen();
this.normsLoading = normsLoading;
}
public NamedAnalyzer indexAnalyzer() {
return indexAnalyzer;
}
public void setIndexAnalyzer(NamedAnalyzer analyzer) {
checkIfFrozen();
this.indexAnalyzer = analyzer;
}
public NamedAnalyzer searchAnalyzer() {
return searchAnalyzer;
}
public void setSearchAnalyzer(NamedAnalyzer analyzer) {
checkIfFrozen();
this.searchAnalyzer = analyzer;
}
public NamedAnalyzer searchQuoteAnalyzer() {
return searchQuoteAnalyzer == null ? searchAnalyzer : searchQuoteAnalyzer;
}
public void setSearchQuoteAnalyzer(NamedAnalyzer analyzer) {
checkIfFrozen();
this.searchQuoteAnalyzer = analyzer;
}
public SimilarityProvider similarity() {
return similarity;
}
public void setSimilarity(SimilarityProvider similarity) {
checkIfFrozen();
this.similarity = similarity;
}
/** Returns the value that should be added when JSON null is found, or null if no value should be added */
public Object nullValue() {
return nullValue;
}
/** Returns the null value stringified, so it can be used for e.g. _all field, or null if there is no null value */
public String nullValueAsString() {
return nullValueAsString;
}
/** Sets the null value and initializes the string version */
public void setNullValue(Object nullValue) {
checkIfFrozen();
this.nullValue = nullValue;
this.nullValueAsString = nullValue == null ? null : nullValue.toString();
}
/** Returns the actual value of the field. */
public Object value(Object value) {
return value;
}
/** Returns the value that will be used as a result for search. Can be only of specific types... */
public Object valueForSearch(Object value) {
return value;
}
/** Returns the indexed value used to construct search "values". */
public BytesRef indexedValueForSearch(Object value) {
return BytesRefs.toBytesRef(value);
}
/**
* Should the field query {@link #termQuery(Object, org.elasticsearch.index.query.QueryParseContext)} be used when detecting this
* field in query string.
*/
public boolean useTermQueryWithQueryString() {
return false;
}
/**
* Creates a term associated with the field of this mapper for the given
* value. Its important to use termQuery when building term queries because
* things like ParentFieldMapper override it to make more interesting
* queries.
*/
protected Term createTerm(Object value) {
return new Term(names().indexName(), indexedValueForSearch(value));
}
public Query termQuery(Object value, @Nullable QueryParseContext context) {
return new TermQuery(createTerm(value));
}
public Query termsQuery(List values, @Nullable QueryParseContext context) {
BytesRef[] bytesRefs = new BytesRef[values.size()];
for (int i = 0; i < bytesRefs.length; i++) {
bytesRefs[i] = indexedValueForSearch(values.get(i));
}
return new TermsQuery(names.indexName(), bytesRefs);
}
public Query rangeQuery(Object lowerTerm, Object upperTerm, boolean includeLower, boolean includeUpper) {
if (this instanceof StringFieldType == false) {
DEPRECATION_LOGGER.deprecated("Range query on field [{}] of type [{}] is deprecated. The next version will only support it " +
"on text/keyword/numeric/date/ip fields", names().fullName(), typeName());
}
return new TermRangeQuery(names().indexName(),
lowerTerm == null ? null : indexedValueForSearch(lowerTerm),
upperTerm == null ? null : indexedValueForSearch(upperTerm),
includeLower, includeUpper);
}
public Query fuzzyQuery(Object value, Fuzziness fuzziness, int prefixLength, int maxExpansions, boolean transpositions) {
if (this instanceof StringFieldType == false) {
DEPRECATION_LOGGER.deprecated("Fuzzy query on field [{}] of type [{}] is deprecated. The next version will only support it " +
"on text/keyword fields", names().fullName(), typeName());
}
return new FuzzyQuery(createTerm(value), fuzziness.asDistance(BytesRefs.toString(value)), prefixLength, maxExpansions, transpositions);
}
public Query prefixQuery(String value, @Nullable MultiTermQuery.RewriteMethod method, @Nullable QueryParseContext context) {
if (this instanceof StringFieldType == false) {
DEPRECATION_LOGGER.deprecated("Prefix query on field [{}] of type [{}] is deprecated. The next version will only support it " +
"on text/keyword fields", names().fullName(), typeName());
}
PrefixQuery query = new PrefixQuery(createTerm(value));
if (method != null) {
query.setRewriteMethod(method);
}
return query;
}
public Query regexpQuery(String value, int flags, int maxDeterminizedStates, @Nullable MultiTermQuery.RewriteMethod method, @Nullable QueryParseContext context) {
if (numericType() != null) {
throw new IllegalArgumentException("Cannot use regular expression to filter numeric field [" + names.fullName + "]");
}
if (this instanceof StringFieldType == false) {
DEPRECATION_LOGGER.deprecated("Regexp query on field [{}] of type [{}] is deprecated. The next version will only support it " +
"on text/keyword fields", names().fullName(), typeName());
}
RegexpQuery query = new RegexpQuery(createTerm(value), flags, maxDeterminizedStates);
if (method != null) {
query.setRewriteMethod(method);
}
return query;
}
public Query nullValueQuery() {
if (nullValue == null) {
return null;
}
return new ConstantScoreQuery(termQuery(nullValue, null));
}
/**
* @return a {@link FieldStats} instance that maps to the type of this field based on the provided {@link Terms} instance.
*/
public FieldStats stats(Terms terms, int maxDoc) throws IOException {
return new FieldStats.Text(
maxDoc, terms.getDocCount(), terms.getSumDocFreq(), terms.getSumTotalTermFreq(), terms.getMin(), terms.getMax()
);
}
/** A term query to use when parsing a query string. Can return <tt>null</tt>. */
@Nullable
public Query queryStringTermQuery(Term term) {
return null;
}
}