/* * 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; } }