diff options
Diffstat (limited to 'modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java')
-rw-r--r-- | modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java new file mode 100644 index 0000000000..494c5e498e --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java @@ -0,0 +1,474 @@ +/* + * 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.join.query; + +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.MultiDocValues; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.join.JoinUtil; +import org.apache.lucene.search.join.ScoreMode; +import org.apache.lucene.search.similarities.Similarity; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lucene.search.Queries; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.fielddata.IndexParentChildFieldData; +import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.ParentFieldMapper; +import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.InnerHitBuilder; +import org.elasticsearch.index.query.NestedQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.QueryShardContext; +import org.elasticsearch.index.query.QueryShardException; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +/** + * A query builder for <tt>has_child</tt> query. + */ +public class HasChildQueryBuilder extends AbstractQueryBuilder<HasChildQueryBuilder> { + public static final String NAME = "has_child"; + + /** + * The default maximum number of children that are required to match for the parent to be considered a match. + */ + public static final int DEFAULT_MAX_CHILDREN = Integer.MAX_VALUE; + /** + * The default minimum number of children that are required to match for the parent to be considered a match. + */ + public static final int DEFAULT_MIN_CHILDREN = 0; + + /** + * The default value for ignore_unmapped. + */ + public static final boolean DEFAULT_IGNORE_UNMAPPED = false; + + private static final ParseField QUERY_FIELD = new ParseField("query", "filter"); + private static final ParseField TYPE_FIELD = new ParseField("type", "child_type"); + private static final ParseField MAX_CHILDREN_FIELD = new ParseField("max_children"); + private static final ParseField MIN_CHILDREN_FIELD = new ParseField("min_children"); + private static final ParseField SCORE_MODE_FIELD = new ParseField("score_mode"); + private static final ParseField INNER_HITS_FIELD = new ParseField("inner_hits"); + private static final ParseField IGNORE_UNMAPPED_FIELD = new ParseField("ignore_unmapped"); + + private final QueryBuilder query; + private final String type; + private final ScoreMode scoreMode; + private InnerHitBuilder innerHitBuilder; + private int minChildren = DEFAULT_MIN_CHILDREN; + private int maxChildren = DEFAULT_MAX_CHILDREN; + private boolean ignoreUnmapped = false; + + public HasChildQueryBuilder(String type, QueryBuilder query, ScoreMode scoreMode) { + this(type, query, DEFAULT_MIN_CHILDREN, DEFAULT_MAX_CHILDREN, scoreMode, null); + } + + private HasChildQueryBuilder(String type, QueryBuilder query, int minChildren, int maxChildren, ScoreMode scoreMode, + InnerHitBuilder innerHitBuilder) { + this.type = requireValue(type, "[" + NAME + "] requires 'type' field"); + this.query = requireValue(query, "[" + NAME + "] requires 'query' field"); + this.scoreMode = requireValue(scoreMode, "[" + NAME + "] requires 'score_mode' field"); + this.innerHitBuilder = innerHitBuilder; + this.minChildren = minChildren; + this.maxChildren = maxChildren; + } + + /** + * Read from a stream. + */ + public HasChildQueryBuilder(StreamInput in) throws IOException { + super(in); + type = in.readString(); + minChildren = in.readInt(); + maxChildren = in.readInt(); + scoreMode = ScoreMode.values()[in.readVInt()]; + query = in.readNamedWriteable(QueryBuilder.class); + innerHitBuilder = in.readOptionalWriteable(InnerHitBuilder::new); + ignoreUnmapped = in.readBoolean(); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + out.writeString(type); + out.writeInt(minChildren); + out.writeInt(maxChildren); + out.writeVInt(scoreMode.ordinal()); + out.writeNamedWriteable(query); + out.writeOptionalWriteable(innerHitBuilder); + out.writeBoolean(ignoreUnmapped); + } + + /** + * Defines the minimum number of children that are required to match for the parent to be considered a match and + * the maximum number of children that are required to match for the parent to be considered a match. + */ + public HasChildQueryBuilder minMaxChildren(int minChildren, int maxChildren) { + if (minChildren < 0) { + throw new IllegalArgumentException("[" + NAME + "] requires non-negative 'min_children' field"); + } + if (maxChildren < 0) { + throw new IllegalArgumentException("[" + NAME + "] requires non-negative 'max_children' field"); + } + if (maxChildren < minChildren) { + throw new IllegalArgumentException("[" + NAME + "] 'max_children' is less than 'min_children'"); + } + this.minChildren = minChildren; + this.maxChildren = maxChildren; + return this; + } + + /** + * Returns inner hit definition in the scope of this query and reusing the defined type and query. + */ + public InnerHitBuilder innerHit() { + return innerHitBuilder; + } + + public HasChildQueryBuilder innerHit(InnerHitBuilder innerHit, boolean ignoreUnmapped) { + this.innerHitBuilder = new InnerHitBuilder(Objects.requireNonNull(innerHit), query, type, ignoreUnmapped); + return this; + } + + /** + * Returns the children query to execute. + */ + public QueryBuilder query() { + return query; + } + + /** + * Returns the child type + */ + public String childType() { + return type; + } + + /** + * Returns how the scores from the matching child documents are mapped into the parent document. + */ + public ScoreMode scoreMode() { + return scoreMode; + } + + /** + * Returns the minimum number of children that are required to match for the parent to be considered a match. + * The default is {@value #DEFAULT_MAX_CHILDREN} + */ + public int minChildren() { + return minChildren; + } + + /** + * Returns the maximum number of children that are required to match for the parent to be considered a match. + * The default is {@value #DEFAULT_MIN_CHILDREN} + */ + public int maxChildren() { return maxChildren; } + + /** + * Sets whether the query builder should ignore unmapped types (and run a + * {@link MatchNoDocsQuery} in place of this query) or throw an exception if + * the type is unmapped. + */ + public HasChildQueryBuilder ignoreUnmapped(boolean ignoreUnmapped) { + this.ignoreUnmapped = ignoreUnmapped; + return this; + } + + /** + * Gets whether the query builder will ignore unmapped types (and run a + * {@link MatchNoDocsQuery} in place of this query) or throw an exception if + * the type is unmapped. + */ + public boolean ignoreUnmapped() { + return ignoreUnmapped; + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(NAME); + builder.field(QUERY_FIELD.getPreferredName()); + query.toXContent(builder, params); + builder.field(TYPE_FIELD.getPreferredName(), type); + builder.field(SCORE_MODE_FIELD.getPreferredName(), NestedQueryBuilder.scoreModeAsString(scoreMode)); + builder.field(MIN_CHILDREN_FIELD.getPreferredName(), minChildren); + builder.field(MAX_CHILDREN_FIELD.getPreferredName(), maxChildren); + builder.field(IGNORE_UNMAPPED_FIELD.getPreferredName(), ignoreUnmapped); + printBoostAndQueryName(builder); + if (innerHitBuilder != null) { + builder.field(INNER_HITS_FIELD.getPreferredName(), innerHitBuilder, params); + } + builder.endObject(); + } + + public static HasChildQueryBuilder fromXContent(QueryParseContext parseContext) throws IOException { + XContentParser parser = parseContext.parser(); + float boost = AbstractQueryBuilder.DEFAULT_BOOST; + String childType = null; + ScoreMode scoreMode = ScoreMode.None; + int minChildren = HasChildQueryBuilder.DEFAULT_MIN_CHILDREN; + int maxChildren = HasChildQueryBuilder.DEFAULT_MAX_CHILDREN; + boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED; + String queryName = null; + InnerHitBuilder innerHitBuilder = null; + String currentFieldName = null; + XContentParser.Token token; + QueryBuilder iqb = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (parseContext.isDeprecatedSetting(currentFieldName)) { + // skip + } else if (token == XContentParser.Token.START_OBJECT) { + if (QUERY_FIELD.match(currentFieldName)) { + iqb = parseContext.parseInnerQueryBuilder(); + } else if (INNER_HITS_FIELD.match(currentFieldName)) { + innerHitBuilder = InnerHitBuilder.fromXContent(parseContext); + } else { + throw new ParsingException(parser.getTokenLocation(), "[has_child] query does not support [" + currentFieldName + "]"); + } + } else if (token.isValue()) { + if (TYPE_FIELD.match(currentFieldName)) { + childType = parser.text(); + } else if (SCORE_MODE_FIELD.match(currentFieldName)) { + scoreMode = NestedQueryBuilder.parseScoreMode(parser.text()); + } else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName)) { + boost = parser.floatValue(); + } else if (MIN_CHILDREN_FIELD.match(currentFieldName)) { + minChildren = parser.intValue(true); + } else if (MAX_CHILDREN_FIELD.match(currentFieldName)) { + maxChildren = parser.intValue(true); + } else if (IGNORE_UNMAPPED_FIELD.match(currentFieldName)) { + ignoreUnmapped = parser.booleanValue(); + } else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName)) { + queryName = parser.text(); + } else { + throw new ParsingException(parser.getTokenLocation(), "[has_child] query does not support [" + currentFieldName + "]"); + } + } + } + HasChildQueryBuilder hasChildQueryBuilder = new HasChildQueryBuilder(childType, iqb, scoreMode); + hasChildQueryBuilder.minMaxChildren(minChildren, maxChildren); + hasChildQueryBuilder.queryName(queryName); + hasChildQueryBuilder.boost(boost); + hasChildQueryBuilder.ignoreUnmapped(ignoreUnmapped); + if (innerHitBuilder != null) { + hasChildQueryBuilder.innerHit(innerHitBuilder, ignoreUnmapped); + } + return hasChildQueryBuilder; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + protected Query doToQuery(QueryShardContext context) throws IOException { + Query innerQuery; + final String[] previousTypes = context.getTypes(); + context.setTypes(type); + try { + innerQuery = query.toQuery(context); + } finally { + context.setTypes(previousTypes); + } + + DocumentMapper childDocMapper = context.documentMapper(type); + if (childDocMapper == null) { + if (ignoreUnmapped) { + return new MatchNoDocsQuery(); + } else { + throw new QueryShardException(context, "[" + NAME + "] no mapping found for type [" + type + "]"); + } + } + ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); + if (parentFieldMapper.active() == false) { + throw new QueryShardException(context, "[" + NAME + "] _parent field has no parent type configured"); + } + String parentType = parentFieldMapper.type(); + DocumentMapper parentDocMapper = context.getMapperService().documentMapper(parentType); + if (parentDocMapper == null) { + throw new QueryShardException(context, + "[" + NAME + "] Type [" + type + "] points to a non existent parent type [" + parentType + "]"); + } + + // wrap the query with type query + innerQuery = Queries.filtered(innerQuery, childDocMapper.typeFilter(context)); + + final ParentChildIndexFieldData parentChildIndexFieldData = context.getForField(parentFieldMapper.fieldType()); + return new LateParsingQuery(parentDocMapper.typeFilter(context), innerQuery, minChildren(), maxChildren(), + parentType, scoreMode, parentChildIndexFieldData, context.getSearchSimilarity()); + } + + /** + * A query that rewrites into another query using + * {@link JoinUtil#createJoinQuery(String, Query, Query, IndexSearcher, ScoreMode, MultiDocValues.OrdinalMap, int, int)} + * that executes the actual join. + * + * This query is exclusively used by the {@link HasChildQueryBuilder} and {@link HasParentQueryBuilder} to get access + * to the {@link DirectoryReader} used by the current search in order to retrieve the {@link MultiDocValues.OrdinalMap}. + * The {@link MultiDocValues.OrdinalMap} is required by {@link JoinUtil} to execute the join. + */ + // TODO: Find a way to remove this query and let doToQuery(...) just return the query from JoinUtil.createJoinQuery(...) + public static final class LateParsingQuery extends Query { + + private final Query toQuery; + private final Query innerQuery; + private final int minChildren; + private final int maxChildren; + private final String parentType; + private final ScoreMode scoreMode; + private final ParentChildIndexFieldData parentChildIndexFieldData; + private final Similarity similarity; + + LateParsingQuery(Query toQuery, Query innerQuery, int minChildren, int maxChildren, + String parentType, ScoreMode scoreMode, ParentChildIndexFieldData parentChildIndexFieldData, + Similarity similarity) { + this.toQuery = toQuery; + this.innerQuery = innerQuery; + this.minChildren = minChildren; + this.maxChildren = maxChildren; + this.parentType = parentType; + this.scoreMode = scoreMode; + this.parentChildIndexFieldData = parentChildIndexFieldData; + this.similarity = similarity; + } + + @Override + public Query rewrite(IndexReader reader) throws IOException { + Query rewritten = super.rewrite(reader); + if (rewritten != this) { + return rewritten; + } + if (reader instanceof DirectoryReader) { + String joinField = ParentFieldMapper.joinField(parentType); + IndexSearcher indexSearcher = new IndexSearcher(reader); + indexSearcher.setQueryCache(null); + indexSearcher.setSimilarity(similarity); + IndexParentChildFieldData indexParentChildFieldData = parentChildIndexFieldData.loadGlobal((DirectoryReader) reader); + MultiDocValues.OrdinalMap ordinalMap = ParentChildIndexFieldData.getOrdinalMap(indexParentChildFieldData, parentType); + return JoinUtil.createJoinQuery(joinField, innerQuery, toQuery, indexSearcher, scoreMode, + ordinalMap, minChildren, maxChildren); + } else { + if (reader.leaves().isEmpty() && reader.numDocs() == 0) { + // asserting reader passes down a MultiReader during rewrite which makes this + // blow up since for this query to work we have to have a DirectoryReader otherwise + // we can't load global ordinals - for this to work we simply check if the reader has no leaves + // and rewrite to match nothing + return new MatchNoDocsQuery(); + } + throw new IllegalStateException("can't load global ordinals for reader of type: " + + reader.getClass() + " must be a DirectoryReader"); + } + } + + @Override + public boolean equals(Object o) { + if (sameClassAs(o) == false) return false; + + LateParsingQuery that = (LateParsingQuery) o; + + if (minChildren != that.minChildren) return false; + if (maxChildren != that.maxChildren) return false; + if (!toQuery.equals(that.toQuery)) return false; + if (!innerQuery.equals(that.innerQuery)) return false; + if (!parentType.equals(that.parentType)) return false; + return scoreMode == that.scoreMode; + } + + @Override + public int hashCode() { + return Objects.hash(classHash(), toQuery, innerQuery, minChildren, maxChildren, parentType, scoreMode); + } + + @Override + public String toString(String s) { + return "LateParsingQuery {parentType=" + parentType + "}"; + } + + public int getMinChildren() { + return minChildren; + } + + public int getMaxChildren() { + return maxChildren; + } + + public ScoreMode getScoreMode() { + return scoreMode; + } + + public Query getInnerQuery() { + return innerQuery; + } + + public Similarity getSimilarity() { + return similarity; + } + } + + @Override + protected boolean doEquals(HasChildQueryBuilder that) { + return Objects.equals(query, that.query) + && Objects.equals(type, that.type) + && Objects.equals(scoreMode, that.scoreMode) + && Objects.equals(minChildren, that.minChildren) + && Objects.equals(maxChildren, that.maxChildren) + && Objects.equals(innerHitBuilder, that.innerHitBuilder) + && Objects.equals(ignoreUnmapped, that.ignoreUnmapped); + } + + @Override + protected int doHashCode() { + return Objects.hash(query, type, scoreMode, minChildren, maxChildren, innerHitBuilder, ignoreUnmapped); + } + + @Override + protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { + QueryBuilder rewrittenQuery = query.rewrite(queryRewriteContext); + if (rewrittenQuery != query) { + InnerHitBuilder rewrittenInnerHit = InnerHitBuilder.rewrite(innerHitBuilder, rewrittenQuery); + HasChildQueryBuilder hasChildQueryBuilder = + new HasChildQueryBuilder(type, rewrittenQuery, minChildren, maxChildren, scoreMode, rewrittenInnerHit); + hasChildQueryBuilder.ignoreUnmapped(ignoreUnmapped); + return hasChildQueryBuilder; + } + return this; + } + + @Override + protected void extractInnerHitBuilders(Map<String, InnerHitBuilder> innerHits) { + if (innerHitBuilder != null) { + innerHitBuilder.inlineInnerHits(innerHits); + } + } +} |