/*
* 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.Field;
import org.apache.lucene.document.SortedSetDocValuesField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermContext;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TermInSetQuery;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.action.fieldstats.FieldStats;
import org.elasticsearch.common.lucene.Lucene;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.fielddata.plain.DocValuesIndexFieldData;
import org.elasticsearch.index.fielddata.plain.ConstantIndexFieldData;
import org.elasticsearch.index.query.QueryShardContext;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
public class TypeFieldMapper extends MetadataFieldMapper {
public static final String NAME = "_type";
public static final String CONTENT_TYPE = "_type";
public static class Defaults {
public static final String NAME = TypeFieldMapper.NAME;
public static final MappedFieldType FIELD_TYPE = new TypeFieldType();
static {
FIELD_TYPE.setIndexOptions(IndexOptions.DOCS);
FIELD_TYPE.setTokenized(false);
FIELD_TYPE.setStored(false);
FIELD_TYPE.setOmitNorms(true);
FIELD_TYPE.setIndexAnalyzer(Lucene.KEYWORD_ANALYZER);
FIELD_TYPE.setSearchAnalyzer(Lucene.KEYWORD_ANALYZER);
FIELD_TYPE.setName(NAME);
FIELD_TYPE.freeze();
}
}
public static class TypeParser implements MetadataFieldMapper.TypeParser {
@Override
public MetadataFieldMapper.Builder<?,?> parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
throw new MapperParsingException(NAME + " is not configurable");
}
@Override
public MetadataFieldMapper getDefault(MappedFieldType fieldType, ParserContext context) {
final Settings indexSettings = context.mapperService().getIndexSettings().getSettings();
return new TypeFieldMapper(indexSettings, fieldType);
}
}
static final class TypeFieldType extends StringFieldType {
TypeFieldType() {
}
protected TypeFieldType(TypeFieldType ref) {
super(ref);
}
@Override
public MappedFieldType clone() {
return new TypeFieldType(this);
}
@Override
public String typeName() {
return CONTENT_TYPE;
}
@Override
public IndexFieldData.Builder fielddataBuilder() {
if (hasDocValues()) {
return new DocValuesIndexFieldData.Builder();
} else {
// means the index has a single type and the type field is implicit
Function<MapperService, String> typeFunction = mapperService -> {
Collection<String> types = mapperService.types();
if (types.size() > 1) {
throw new AssertionError();
}
// If we reach here, there is necessarily one type since we were able to find a `_type` field
String type = types.iterator().next();
return type;
};
return new ConstantIndexFieldData.Builder(typeFunction);
}
}
@Override
public FieldStats<?> stats(IndexReader reader) throws IOException {
if (reader.maxDoc() == 0) {
return null;
}
return new FieldStats.Text(reader.maxDoc(), reader.numDocs(), reader.maxDoc(), reader.maxDoc(),
isSearchable(), isAggregatable());
}
@Override
public boolean isSearchable() {
return true;
}
@Override
public Query termQuery(Object value, QueryShardContext context) {
return termsQuery(Arrays.asList(value), context);
}
@Override
public Query termsQuery(List<?> values, QueryShardContext context) {
if (context.getIndexSettings().isSingleType()) {
Collection<String> indexTypes = context.getMapperService().types();
if (indexTypes.isEmpty()) {
return new MatchNoDocsQuery("No types");
}
assert indexTypes.size() == 1;
BytesRef indexType = indexedValueForSearch(indexTypes.iterator().next());
if (values.stream()
.map(this::indexedValueForSearch)
.anyMatch(indexType::equals)) {
if (context.getMapperService().hasNested()) {
// type filters are expected not to match nested docs
return Queries.newNonNestedFilter();
} else {
return new MatchAllDocsQuery();
}
} else {
return new MatchNoDocsQuery("Type list does not contain the index type");
}
} else {
if (indexOptions() == IndexOptions.NONE) {
throw new AssertionError();
}
final BytesRef[] types = values.stream()
.map(this::indexedValueForSearch)
.toArray(size -> new BytesRef[size]);
return new TypesQuery(types);
}
}
}
/**
* Specialization for a disjunction over many _type
*/
public static class TypesQuery extends Query {
// Same threshold as TermInSetQuery
private static final int BOOLEAN_REWRITE_TERM_COUNT_THRESHOLD = 16;
private final BytesRef[] types;
public TypesQuery(BytesRef... types) {
if (types == null) {
throw new NullPointerException("types cannot be null.");
}
if (types.length == 0) {
throw new IllegalArgumentException("types must contains at least one value.");
}
this.types = types;
}
public BytesRef[] getTerms() {
return types;
}
@Override
public Query rewrite(IndexReader reader) throws IOException {
final int threshold = Math.min(BOOLEAN_REWRITE_TERM_COUNT_THRESHOLD, BooleanQuery.getMaxClauseCount());
if (types.length <= threshold) {
Set<BytesRef> uniqueTypes = new HashSet<>();
BooleanQuery.Builder bq = new BooleanQuery.Builder();
int totalDocFreq = 0;
for (BytesRef type : types) {
if (uniqueTypes.add(type)) {
Term term = new Term(CONTENT_TYPE, type);
TermContext context = TermContext.build(reader.getContext(), term);
if (context.docFreq() == 0) {
// this _type is not present in the reader
continue;
}
totalDocFreq += context.docFreq();
// strict equality should be enough ?
if (totalDocFreq >= reader.maxDoc()) {
assert totalDocFreq == reader.maxDoc();
// Matches all docs since _type is a single value field
// Using a match_all query will help Lucene perform some optimizations
// For instance, match_all queries as filter clauses are automatically removed
return new MatchAllDocsQuery();
}
bq.add(new TermQuery(term, context), BooleanClause.Occur.SHOULD);
}
}
return new ConstantScoreQuery(bq.build());
}
return new TermInSetQuery(CONTENT_TYPE, types);
}
@Override
public boolean equals(Object obj) {
if (sameClassAs(obj) == false) {
return false;
}
TypesQuery that = (TypesQuery) obj;
return Arrays.equals(types, that.types);
}
@Override
public int hashCode() {
return 31 * classHash() + Arrays.hashCode(types);
}
@Override
public String toString(String field) {
StringBuilder builder = new StringBuilder();
for (BytesRef type : types) {
if (builder.length() > 0) {
builder.append(' ');
}
builder.append(new Term(CONTENT_TYPE, type).toString());
}
return builder.toString();
}
}
private TypeFieldMapper(Settings indexSettings, MappedFieldType existing) {
this(existing == null ? defaultFieldType(indexSettings) : existing.clone(),
indexSettings);
}
private TypeFieldMapper(MappedFieldType fieldType, Settings indexSettings) {
super(NAME, fieldType, defaultFieldType(indexSettings), indexSettings);
}
private static MappedFieldType defaultFieldType(Settings indexSettings) {
MappedFieldType defaultFieldType = Defaults.FIELD_TYPE.clone();
if (MapperService.INDEX_MAPPING_SINGLE_TYPE_SETTING.get(indexSettings)) {
defaultFieldType.setIndexOptions(IndexOptions.NONE);
defaultFieldType.setHasDocValues(false);
} else {
defaultFieldType.setIndexOptions(IndexOptions.DOCS);
defaultFieldType.setHasDocValues(true);
}
return defaultFieldType;
}
@Override
public void preParse(ParseContext context) throws IOException {
super.parse(context);
}
@Override
public void postParse(ParseContext context) throws IOException {
}
@Override
public Mapper parse(ParseContext context) throws IOException {
// we parse in pre parse
return null;
}
@Override
protected void parseCreateField(ParseContext context, List<IndexableField> fields) throws IOException {
if (fieldType().indexOptions() == IndexOptions.NONE && !fieldType().stored()) {
return;
}
fields.add(new Field(fieldType().name(), context.sourceToParse().type(), fieldType()));
if (fieldType().hasDocValues()) {
fields.add(new SortedSetDocValuesField(fieldType().name(), new BytesRef(context.sourceToParse().type())));
}
}
@Override
protected String contentType() {
return CONTENT_TYPE;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder;
}
@Override
protected void doMerge(Mapper mergeWith, boolean updateAllTypes) {
// do nothing here, no merging, but also no exception
}
}