/* * 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.internal; import org.apache.lucene.document.Field; import org.apache.lucene.document.SortedDocValuesField; import org.apache.lucene.index.DocValuesType; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.queries.TermsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.util.BytesRef; import org.elasticsearch.Version; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.loader.SettingsLoader; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.fielddata.FieldDataType; import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.MetadataFieldMapper; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.Uid; import org.elasticsearch.index.query.QueryParseContext; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import static org.elasticsearch.common.settings.Settings.settingsBuilder; import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeMapValue; /** * */ public class ParentFieldMapper extends MetadataFieldMapper { public static final String NAME = "_parent"; public static final String CONTENT_TYPE = "_parent"; public static final String CQL_PARENT_PK = "cql_parent_pk"; public static class Defaults { public static final String NAME = ParentFieldMapper.NAME; public static final MappedFieldType FIELD_TYPE = new ParentFieldType(); public static final MappedFieldType JOIN_FIELD_TYPE = new ParentFieldType(); static { FIELD_TYPE.setIndexOptions(IndexOptions.DOCS); FIELD_TYPE.setTokenized(false); FIELD_TYPE.setStored(true); FIELD_TYPE.setOmitNorms(true); FIELD_TYPE.setIndexAnalyzer(Lucene.KEYWORD_ANALYZER); FIELD_TYPE.setSearchAnalyzer(Lucene.KEYWORD_ANALYZER); FIELD_TYPE.setNames(new MappedFieldType.Names(NAME)); FIELD_TYPE.freeze(); JOIN_FIELD_TYPE.setHasDocValues(true); JOIN_FIELD_TYPE.setDocValuesType(DocValuesType.SORTED); JOIN_FIELD_TYPE.freeze(); } } public static class Builder extends MetadataFieldMapper.Builder<Builder, ParentFieldMapper> { private String parentType; protected String indexName; private final String documentType; private String pkColumns; private final MappedFieldType parentJoinFieldType = Defaults.JOIN_FIELD_TYPE.clone(); private final MappedFieldType childJoinFieldType = Defaults.JOIN_FIELD_TYPE.clone(); public Builder(String documentType) { super(Defaults.NAME, Defaults.FIELD_TYPE, Defaults.FIELD_TYPE); this.indexName = name; this.documentType = documentType; builder = this; } public Builder type(String type) { this.parentType = type; return builder; } public Builder pkColumns(String columns) { this.pkColumns = columns; return builder; } @Override public Builder fieldDataSettings(Settings fieldDataSettings) { Settings settings = Settings.builder().put(childJoinFieldType.fieldDataType().getSettings()).put(fieldDataSettings).build(); childJoinFieldType.setFieldDataType(new FieldDataType(childJoinFieldType.fieldDataType().getType(), settings)); return this; } @Override public ParentFieldMapper build(BuilderContext context) { if (parentType == null) { throw new MapperParsingException("[_parent] field mapping must contain the [type] option"); } parentJoinFieldType.setNames(new MappedFieldType.Names(joinField(documentType))); parentJoinFieldType.setFieldDataType(null); childJoinFieldType.setNames(new MappedFieldType.Names(joinField(parentType))); if (context.indexCreatedVersion().before(Version.V_2_0_0_beta1)) { childJoinFieldType.setHasDocValues(false); childJoinFieldType.setDocValuesType(DocValuesType.NONE); parentJoinFieldType.setHasDocValues(false); parentJoinFieldType.setDocValuesType(DocValuesType.NONE); } return new ParentFieldMapper(fieldType, parentJoinFieldType, childJoinFieldType, parentType, pkColumns, context.indexSettings()); } } public static class TypeParser implements MetadataFieldMapper.TypeParser { @Override public MetadataFieldMapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException { Builder builder = new Builder(parserContext.type()); for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) { Map.Entry<String, Object> entry = iterator.next(); String fieldName = Strings.toUnderscoreCase(entry.getKey()); Object fieldNode = entry.getValue(); if (fieldName.equals("type")) { builder.type(fieldNode.toString()); iterator.remove(); } else if (fieldName.equals(CQL_PARENT_PK)) { builder.pkColumns(fieldNode.toString()); iterator.remove(); } else if (fieldName.equals("postings_format") && parserContext.indexVersionCreated().before(Version.V_2_0_0_beta1)) { // ignore before 2.0, reject on and after 2.0 iterator.remove(); } else if (fieldName.equals("fielddata")) { // Only take over `loading`, since that is the only option now that is configurable: Map<String, String> fieldDataSettings = SettingsLoader.Helper.loadNestedFromMap(nodeMapValue(fieldNode, "fielddata")); if (fieldDataSettings.containsKey(MappedFieldType.Loading.KEY)) { Settings settings = settingsBuilder().put(MappedFieldType.Loading.KEY, fieldDataSettings.get(MappedFieldType.Loading.KEY)).build(); builder.fieldDataSettings(settings); } iterator.remove(); } } return builder; } @Override public MetadataFieldMapper getDefault(Settings indexSettings, MappedFieldType fieldType, String parentType) { return new ParentFieldMapper(indexSettings, fieldType, parentType, null); } } static final class ParentFieldType extends MappedFieldType { public ParentFieldType() { setFieldDataType(new FieldDataType("_parent", settingsBuilder().put(MappedFieldType.Loading.KEY, Loading.EAGER_VALUE))); } protected ParentFieldType(ParentFieldType ref) { super(ref); } @Override public MappedFieldType clone() { return new ParentFieldType(this); } @Override public String typeName() { return CONTENT_TYPE; } @Override public Uid value(Object value) { if (value == null) { return null; } return Uid.createUid(value.toString()); } @Override public Object valueForSearch(Object value) { if (value == null) { return null; } String sValue = value.toString(); if (sValue == null) { return null; } int index = sValue.indexOf(Uid.DELIMITER); if (index == -1) { return sValue; } return sValue.substring(index + 1); } /** * We don't need to analyzer the text, and we need to convert it to UID... */ @Override public boolean useTermQueryWithQueryString() { return true; } @Override public Query termQuery(Object value, @Nullable QueryParseContext context) { return termsQuery(Collections.singletonList(value), context); } @Override public Query termsQuery(List values, @Nullable QueryParseContext context) { if (context == null) { return super.termsQuery(values, context); } List<String> types = new ArrayList<>(context.mapperService().types().size()); for (DocumentMapper documentMapper : context.mapperService().docMappers(false)) { if (!documentMapper.parentFieldMapper().active()) { types.add(documentMapper.type()); } } List<BytesRef> bValues = new ArrayList<>(values.size()); for (Object value : values) { BytesRef bValue = BytesRefs.toBytesRef(value); if (Uid.hasDelimiter(bValue)) { bValues.add(bValue); } else { // we use all non child types, cause we don't know if its exact or not... for (String type : types) { bValues.add(Uid.createUidAsBytes(type, bValue)); } } } return new TermsQuery(names().indexName(), bValues); } } private final String parentType; private final String parentPkColumns; // determines the field data settings private MappedFieldType childJoinFieldType; // has no impact of field data settings, is just here for creating a join field, the parent field mapper in the child type pointing to this type determines the field data settings for this join field private final MappedFieldType parentJoinFieldType; private ParentFieldMapper(MappedFieldType fieldType, MappedFieldType parentJoinFieldType, MappedFieldType childJoinFieldType, String parentType, String parentPkColumns, Settings indexSettings) { super(NAME, fieldType, Defaults.FIELD_TYPE, indexSettings); this.parentType = parentType; this.parentPkColumns = parentPkColumns; this.parentJoinFieldType = parentJoinFieldType; this.parentJoinFieldType.freeze(); this.childJoinFieldType = childJoinFieldType; if (childJoinFieldType != null) { this.childJoinFieldType.freeze(); } } private ParentFieldMapper(Settings indexSettings, MappedFieldType existing, String parentType, String parentPkColumns) { this(existing == null ? Defaults.FIELD_TYPE.clone() : existing.clone(), joinFieldTypeForParentType(parentType, indexSettings), null, null, null, indexSettings); } private static MappedFieldType joinFieldTypeForParentType(String parentType, Settings indexSettings) { MappedFieldType parentJoinFieldType = Defaults.JOIN_FIELD_TYPE.clone(); parentJoinFieldType.setNames(new MappedFieldType.Names(joinField(parentType))); Version indexCreated = Version.indexCreated(indexSettings); if (indexCreated.before(Version.V_2_0_0_beta1)) { parentJoinFieldType.setHasDocValues(false); parentJoinFieldType.setDocValuesType(DocValuesType.NONE); } parentJoinFieldType.freeze(); return parentJoinFieldType; } public MappedFieldType getParentJoinFieldType() { return parentJoinFieldType; } public MappedFieldType getChildJoinFieldType() { return childJoinFieldType; } public String type() { return parentType; } /** * @return a comma separated list of pk columns names. */ public String pkColumns() { return parentPkColumns; } @Override public void preParse(ParseContext context) throws IOException { } @Override public void postParse(ParseContext context) throws IOException { if (context.sourceToParse().flyweight() == false) { parse(context); } } @Override public void createField(ParseContext context, Object value) throws IOException { String parentId = (String) value; boolean parent = context.docMapper().isParent(context.type()); if (parent) { addJoinFieldIfNeeded(context, parentJoinFieldType, context.id()); } if (!active()) { return; } Field field = new Field(fieldType().names().indexName(), Uid.createUid(context.stringBuilder(), parentType, parentId), fieldType()); context.doc().add(field); addJoinFieldIfNeeded(context, childJoinFieldType, parentId); // we have parent mapping, yet no value was set, ignore it... } @Override protected void parseCreateField(ParseContext context, List<Field> fields) throws IOException { boolean parent = context.docMapper().isParent(context.type()); if (parent) { addJoinFieldIfNeeded(fields, parentJoinFieldType, context.id()); } if (!active()) { return; } if (context.parser().currentName() != null && context.parser().currentName().equals(Defaults.NAME)) { // we are in the parsing of _parent phase String parentId = context.parser().text(); context.sourceToParse().parent(parentId); fields.add(new Field(fieldType().names().indexName(), Uid.createUid(context.stringBuilder(), parentType, parentId), fieldType())); addJoinFieldIfNeeded(fields, childJoinFieldType, parentId); } else { // otherwise, we are running it post processing of the xcontent String parsedParentId = context.doc().get(Defaults.NAME); if (context.sourceToParse().parent() != null) { String parentId = context.sourceToParse().parent(); if (parsedParentId == null) { if (parentId == null) { throw new MapperParsingException("No parent id provided, not within the document, and not externally"); } // we did not add it in the parsing phase, add it now fields.add(new Field(fieldType().names().indexName(), Uid.createUid(context.stringBuilder(), parentType, parentId), fieldType())); addJoinFieldIfNeeded(fields, childJoinFieldType, parentId); } else if (parentId != null && !parsedParentId.equals(Uid.createUid(context.stringBuilder(), parentType, parentId))) { throw new MapperParsingException("Parent id mismatch, document value is [" + Uid.createUid(parsedParentId).id() + "], while external value is [" + parentId + "]"); } } } // we have parent mapping, yet no value was set, ignore it... } private void addJoinFieldIfNeeded(ParseContext context, MappedFieldType fieldType, String id) { if (fieldType.hasDocValues()) { Field field = new SortedDocValuesField(fieldType.names().indexName(), new BytesRef(id)); if (!customBoost()) { field.setBoost(fieldType().boost()); } context.doc().add(field); } } private void addJoinFieldIfNeeded(List<Field> fields, MappedFieldType fieldType, String id) { if (fieldType.hasDocValues()) { fields.add(new SortedDocValuesField(fieldType.names().indexName(), new BytesRef(id))); } } public static String joinField(String parentType) { return ParentFieldMapper.NAME + "#" + parentType; } @Override protected String contentType() { return CONTENT_TYPE; } private boolean joinFieldHasCustomFieldDataSettings() { return childJoinFieldType != null && childJoinFieldType.fieldDataType() != null && childJoinFieldType.fieldDataType().equals(Defaults.JOIN_FIELD_TYPE.fieldDataType()) == false; } @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { if (!active()) { return builder; } boolean includeDefaults = params.paramAsBoolean("include_defaults", false); builder.startObject(CONTENT_TYPE); builder.field("type", parentType); if (this.parentPkColumns != null) { builder.field(CQL_PARENT_PK, parentPkColumns); } if (includeDefaults || joinFieldHasCustomFieldDataSettings()) { builder.field("fielddata", (Map) childJoinFieldType.fieldDataType().getSettings().getAsMap()); } builder.endObject(); return builder; } @Override protected void doMerge(Mapper mergeWith, boolean updateAllTypes) { super.doMerge(mergeWith, updateAllTypes); ParentFieldMapper fieldMergeWith = (ParentFieldMapper) mergeWith; if (Objects.equals(parentType, fieldMergeWith.parentType) == false) { throw new IllegalArgumentException("The _parent field's type option can't be changed: [" + parentType + "]->[" + fieldMergeWith.parentType + "]"); } List<String> conflicts = new ArrayList<>(); fieldType().checkCompatibility(fieldMergeWith.fieldType(), conflicts, true); // always strict, this cannot change parentJoinFieldType.checkCompatibility(fieldMergeWith.parentJoinFieldType, conflicts, true); // same here if (childJoinFieldType != null) { // TODO: this can be set to false when the old parent/child impl is removed, we can do eager global ordinals loading per type. childJoinFieldType.checkCompatibility(fieldMergeWith.childJoinFieldType, conflicts, updateAllTypes == false); } if (conflicts.isEmpty() == false) { throw new IllegalArgumentException("Merge conflicts: " + conflicts); } if (active()) { childJoinFieldType = fieldMergeWith.childJoinFieldType.clone(); } } /** * @return Whether the _parent field is actually configured. */ public boolean active() { return parentType != null; } }