diff options
author | Jim Ferenczi <jim.ferenczi@elastic.co> | 2017-05-12 15:58:06 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-12 15:58:06 +0200 |
commit | 279a18a527b9c23b06c8b75c2aa9321aefca9728 (patch) | |
tree | 111c9312cb41d09994a7fa7d86aeb9227a622a9f /modules/parent-join/src/main/java/org | |
parent | be2a6ce80b2282779159bc017352aa6f216349e2 (diff) |
Add parent-join module (#24638)
* Add parent-join module
This change adds a new module named `parent-join`.
The goal of this module is to provide a replacement for the `_parent` field but as a first step this change only moves the `has_child`, `has_parent` queries and the `children` aggregation to this module.
These queries and aggregations are no longer in core but they are deployed by default as a module.
Relates #20257
Diffstat (limited to 'modules/parent-join/src/main/java/org')
10 files changed, 1449 insertions, 0 deletions
diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/ParentJoinPlugin.java b/modules/parent-join/src/main/java/org/elasticsearch/join/ParentJoinPlugin.java new file mode 100644 index 0000000000..dec3950836 --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/ParentJoinPlugin.java @@ -0,0 +1,54 @@ +/* + * 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; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.join.aggregations.ChildrenAggregationBuilder; +import org.elasticsearch.join.aggregations.InternalChildren; +import org.elasticsearch.join.query.HasChildQueryBuilder; +import org.elasticsearch.join.query.HasParentQueryBuilder; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ParentJoinPlugin extends Plugin implements SearchPlugin { + public ParentJoinPlugin(Settings settings) {} + + @Override + public List<QuerySpec<?>> getQueries() { + return Arrays.asList( + new QuerySpec<>(HasChildQueryBuilder.NAME, HasChildQueryBuilder::new, HasChildQueryBuilder::fromXContent), + new QuerySpec<>(HasParentQueryBuilder.NAME, HasParentQueryBuilder::new, HasParentQueryBuilder::fromXContent) + ); + } + + @Override + public List<AggregationSpec> getAggregations() { + return Collections.singletonList( + new AggregationSpec(ChildrenAggregationBuilder.NAME, ChildrenAggregationBuilder::new, ChildrenAggregationBuilder::parse) + .addResultReader(InternalChildren::new) + ); + } + + +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/Children.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/Children.java new file mode 100644 index 0000000000..394c690709 --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/Children.java @@ -0,0 +1,28 @@ +/* + * 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.aggregations; + +import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregation; + +/** + * An single bucket aggregation that translates parent documents to their children documents. + */ +public interface Children extends SingleBucketAggregation { +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregationBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregationBuilder.java new file mode 100644 index 0000000000..d04b1f0a66 --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregationBuilder.java @@ -0,0 +1,167 @@ +/* + * 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.aggregations; + +import org.apache.lucene.search.Query; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData; +import org.elasticsearch.index.mapper.DocumentMapper; +import org.elasticsearch.index.mapper.ParentFieldMapper; +import org.elasticsearch.index.query.QueryParseContext; +import org.elasticsearch.search.aggregations.AggregatorFactories.Builder; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.support.FieldContext; +import org.elasticsearch.search.aggregations.support.ValueType; +import org.elasticsearch.search.aggregations.support.ValuesSource.Bytes.ParentChild; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregationBuilder; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; +import org.elasticsearch.search.aggregations.support.ValuesSourceType; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; +import java.util.Objects; + +public class ChildrenAggregationBuilder extends ValuesSourceAggregationBuilder<ParentChild, ChildrenAggregationBuilder> { + public static final String NAME = "children"; + + private String parentType; + private final String childType; + private Query parentFilter; + private Query childFilter; + + /** + * @param name + * the name of this aggregation + * @param childType + * the type of children documents + */ + public ChildrenAggregationBuilder(String name, String childType) { + super(name, ValuesSourceType.BYTES, ValueType.STRING); + if (childType == null) { + throw new IllegalArgumentException("[childType] must not be null: [" + name + "]"); + } + this.childType = childType; + } + + /** + * Read from a stream. + */ + public ChildrenAggregationBuilder(StreamInput in) throws IOException { + super(in, ValuesSourceType.BYTES, ValueType.STRING); + childType = in.readString(); + } + + @Override + protected void innerWriteTo(StreamOutput out) throws IOException { + out.writeString(childType); + } + + @Override + protected ValuesSourceAggregatorFactory<ParentChild, ?> innerBuild(SearchContext context, + ValuesSourceConfig<ParentChild> config, AggregatorFactory<?> parent, Builder subFactoriesBuilder) throws IOException { + return new ChildrenAggregatorFactory(name, config, parentType, childFilter, parentFilter, context, parent, + subFactoriesBuilder, metaData); + } + + @Override + protected ValuesSourceConfig<ParentChild> resolveConfig(SearchContext context) { + ValuesSourceConfig<ParentChild> config = new ValuesSourceConfig<>(ValuesSourceType.BYTES); + DocumentMapper childDocMapper = context.mapperService().documentMapper(childType); + + if (childDocMapper != null) { + ParentFieldMapper parentFieldMapper = childDocMapper.parentFieldMapper(); + if (!parentFieldMapper.active()) { + throw new IllegalArgumentException("[children] no [_parent] field not configured that points to a parent type"); + } + parentType = parentFieldMapper.type(); + DocumentMapper parentDocMapper = context.mapperService().documentMapper(parentType); + if (parentDocMapper != null) { + parentFilter = parentDocMapper.typeFilter(context.getQueryShardContext()); + childFilter = childDocMapper.typeFilter(context.getQueryShardContext()); + ParentChildIndexFieldData parentChildIndexFieldData = context.fieldData() + .getForField(parentFieldMapper.fieldType()); + config.fieldContext(new FieldContext(parentFieldMapper.fieldType().name(), parentChildIndexFieldData, + parentFieldMapper.fieldType())); + } else { + config.unmapped(true); + } + } else { + config.unmapped(true); + } + return config; + } + + @Override + protected XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { + builder.field(ParentToChildrenAggregator.TYPE_FIELD.getPreferredName(), childType); + return builder; + } + + public static ChildrenAggregationBuilder parse(String aggregationName, QueryParseContext context) throws IOException { + String childType = null; + + XContentParser.Token token; + String currentFieldName = null; + XContentParser parser = context.parser(); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.VALUE_STRING) { + if ("type".equals(currentFieldName)) { + childType = parser.text(); + } else { + throw new ParsingException(parser.getTokenLocation(), + "Unknown key for a " + token + " in [" + aggregationName + "]: [" + currentFieldName + "]."); + } + } else { + throw new ParsingException(parser.getTokenLocation(), "Unexpected token " + token + " in [" + aggregationName + "]."); + } + } + + if (childType == null) { + throw new ParsingException(parser.getTokenLocation(), + "Missing [child_type] field for children aggregation [" + aggregationName + "]"); + } + + + return new ChildrenAggregationBuilder(aggregationName, childType); + } + + @Override + protected int innerHashCode() { + return Objects.hash(childType); + } + + @Override + protected boolean innerEquals(Object obj) { + ChildrenAggregationBuilder other = (ChildrenAggregationBuilder) obj; + return Objects.equals(childType, other.childType); + } + + @Override + public String getType() { + return NAME; + } +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java new file mode 100644 index 0000000000..800be74ba6 --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java @@ -0,0 +1,77 @@ +/* + * 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.aggregations; + +import org.apache.lucene.search.Query; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.AggregatorFactory; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.NonCollectingAggregator; +import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; +import org.elasticsearch.search.aggregations.support.ValuesSource; +import org.elasticsearch.search.aggregations.support.ValuesSource.Bytes.ParentChild; +import org.elasticsearch.search.aggregations.support.ValuesSourceAggregatorFactory; +import org.elasticsearch.search.aggregations.support.ValuesSourceConfig; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class ChildrenAggregatorFactory + extends ValuesSourceAggregatorFactory<ValuesSource.Bytes.WithOrdinals.ParentChild, ChildrenAggregatorFactory> { + + private final String parentType; + private final Query parentFilter; + private final Query childFilter; + + public ChildrenAggregatorFactory(String name, ValuesSourceConfig<ParentChild> config, String parentType, Query childFilter, + Query parentFilter, SearchContext context, AggregatorFactory<?> parent, AggregatorFactories.Builder subFactoriesBuilder, + Map<String, Object> metaData) throws IOException { + super(name, config, context, parent, subFactoriesBuilder, metaData); + this.parentType = parentType; + this.childFilter = childFilter; + this.parentFilter = parentFilter; + } + + @Override + protected Aggregator createUnmapped(Aggregator parent, List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) + throws IOException { + return new NonCollectingAggregator(name, context, parent, pipelineAggregators, metaData) { + + @Override + public InternalAggregation buildEmptyAggregation() { + return new InternalChildren(name, 0, buildEmptySubAggregations(), pipelineAggregators(), metaData()); + } + + }; + } + + @Override + protected Aggregator doCreateInternal(ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, Aggregator parent, + boolean collectsFromSingleBucket, List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) + throws IOException { + long maxOrd = valuesSource.globalMaxOrd(context.searcher(), parentType); + return new ParentToChildrenAggregator(name, factories, context, parent, parentType, childFilter, parentFilter, valuesSource, maxOrd, + pipelineAggregators, metaData); + } + +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/InternalChildren.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/InternalChildren.java new file mode 100644 index 0000000000..05cd40e3d3 --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/InternalChildren.java @@ -0,0 +1,56 @@ +/* + * 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.aggregations; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.search.aggregations.InternalAggregations; +import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; +import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Results of the {@link ParentToChildrenAggregator}. + */ +public class InternalChildren extends InternalSingleBucketAggregation implements Children { + public InternalChildren(String name, long docCount, InternalAggregations aggregations, List<PipelineAggregator> pipelineAggregators, + Map<String, Object> metaData) { + super(name, docCount, aggregations, pipelineAggregators, metaData); + } + + /** + * Read from a stream. + */ + public InternalChildren(StreamInput in) throws IOException { + super(in); + } + + @Override + public String getWriteableName() { + return ChildrenAggregationBuilder.NAME; + } + + @Override + protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { + return new InternalChildren(name, docCount, subAggregations, pipelineAggregators(), getMetaData()); + } +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/JoinAggregationBuilders.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/JoinAggregationBuilders.java new file mode 100644 index 0000000000..73522a68b4 --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/JoinAggregationBuilders.java @@ -0,0 +1,29 @@ +/* + * 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.aggregations; + +public abstract class JoinAggregationBuilders { + /** + * Create a new {@link Children} aggregation with the given name. + */ + public static ChildrenAggregationBuilder children(String name, String childType) { + return new ChildrenAggregationBuilder(name, childType); + } +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregator.java b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregator.java new file mode 100644 index 0000000000..c1ffb097ab --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregator.java @@ -0,0 +1,186 @@ +/* + * 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.aggregations; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.SortedDocValues; +import org.apache.lucene.search.ConstantScoreScorer; +import org.apache.lucene.search.DocIdSetIterator; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Scorer; +import org.apache.lucene.search.Weight; +import org.apache.lucene.util.Bits; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.lease.Releasables; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.util.LongArray; +import org.elasticsearch.common.util.LongObjectPagedHashMap; +import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.AggregatorFactories; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.LeafBucketCollector; +import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregator; +import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; +import org.elasticsearch.search.aggregations.support.ValuesSource; +import org.elasticsearch.search.internal.SearchContext; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +// The RecordingPerReaderBucketCollector assumes per segment recording which isn't the case for this +// aggregation, for this reason that collector can't be used +public class ParentToChildrenAggregator extends SingleBucketAggregator { + + static final ParseField TYPE_FIELD = new ParseField("type"); + + private final String parentType; + private final Weight childFilter; + private final Weight parentFilter; + private final ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource; + + // Maybe use PagedGrowableWriter? This will be less wasteful than LongArray, + // but then we don't have the reuse feature of BigArrays. + // Also if we know the highest possible value that a parent agg will create + // then we store multiple values into one slot + private final LongArray parentOrdToBuckets; + + // Only pay the extra storage price if the a parentOrd has multiple buckets + // Most of the times a parent doesn't have multiple buckets, since there is + // only one document per parent ord, + // only in the case of terms agg if a parent doc has multiple terms per + // field this is needed: + private final LongObjectPagedHashMap<long[]> parentOrdToOtherBuckets; + private boolean multipleBucketsPerParentOrd = false; + + public ParentToChildrenAggregator(String name, AggregatorFactories factories, + SearchContext context, Aggregator parent, String parentType, Query childFilter, + Query parentFilter, ValuesSource.Bytes.WithOrdinals.ParentChild valuesSource, + long maxOrd, List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) + throws IOException { + super(name, factories, context, parent, pipelineAggregators, metaData); + this.parentType = parentType; + // these two filters are cached in the parser + this.childFilter = context.searcher().createNormalizedWeight(childFilter, false); + this.parentFilter = context.searcher().createNormalizedWeight(parentFilter, false); + this.parentOrdToBuckets = context.bigArrays().newLongArray(maxOrd, false); + this.parentOrdToBuckets.fill(0, maxOrd, -1); + this.parentOrdToOtherBuckets = new LongObjectPagedHashMap<>(context.bigArrays()); + this.valuesSource = valuesSource; + } + + @Override + public InternalAggregation buildAggregation(long owningBucketOrdinal) throws IOException { + return new InternalChildren(name, bucketDocCount(owningBucketOrdinal), + bucketAggregations(owningBucketOrdinal), pipelineAggregators(), metaData()); + } + + @Override + public InternalAggregation buildEmptyAggregation() { + return new InternalChildren(name, 0, buildEmptySubAggregations(), pipelineAggregators(), + metaData()); + } + + @Override + public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, + final LeafBucketCollector sub) throws IOException { + if (valuesSource == null) { + return LeafBucketCollector.NO_OP_COLLECTOR; + } + + final SortedDocValues globalOrdinals = valuesSource.globalOrdinalsValues(parentType, ctx); + assert globalOrdinals != null; + Scorer parentScorer = parentFilter.scorer(ctx); + final Bits parentDocs = Lucene.asSequentialAccessBits(ctx.reader().maxDoc(), parentScorer); + return new LeafBucketCollector() { + + @Override + public void collect(int docId, long bucket) throws IOException { + if (parentDocs.get(docId) && globalOrdinals.advanceExact(docId)) { + long globalOrdinal = globalOrdinals.ordValue(); + if (globalOrdinal != -1) { + if (parentOrdToBuckets.get(globalOrdinal) == -1) { + parentOrdToBuckets.set(globalOrdinal, bucket); + } else { + long[] bucketOrds = parentOrdToOtherBuckets.get(globalOrdinal); + if (bucketOrds != null) { + bucketOrds = Arrays.copyOf(bucketOrds, bucketOrds.length + 1); + bucketOrds[bucketOrds.length - 1] = bucket; + parentOrdToOtherBuckets.put(globalOrdinal, bucketOrds); + } else { + parentOrdToOtherBuckets.put(globalOrdinal, new long[] { bucket }); + } + multipleBucketsPerParentOrd = true; + } + } + } + } + }; + } + + @Override + protected void doPostCollection() throws IOException { + IndexReader indexReader = context().searcher().getIndexReader(); + for (LeafReaderContext ctx : indexReader.leaves()) { + Scorer childDocsScorer = childFilter.scorer(ctx); + if (childDocsScorer == null) { + continue; + } + DocIdSetIterator childDocsIter = childDocsScorer.iterator(); + + final LeafBucketCollector sub = collectableSubAggregators.getLeafCollector(ctx); + final SortedDocValues globalOrdinals = valuesSource.globalOrdinalsValues(parentType, + ctx); + + // Set the scorer, since we now replay only the child docIds + sub.setScorer(new ConstantScoreScorer(null, 1f, childDocsIter)); + + final Bits liveDocs = ctx.reader().getLiveDocs(); + for (int docId = childDocsIter + .nextDoc(); docId != DocIdSetIterator.NO_MORE_DOCS; docId = childDocsIter + .nextDoc()) { + if (liveDocs != null && liveDocs.get(docId) == false) { + continue; + } + if (globalOrdinals.advanceExact(docId)) { + long globalOrdinal = globalOrdinals.ordValue(); + long bucketOrd = parentOrdToBuckets.get(globalOrdinal); + if (bucketOrd != -1) { + collectBucket(sub, docId, bucketOrd); + if (multipleBucketsPerParentOrd) { + long[] otherBucketOrds = parentOrdToOtherBuckets.get(globalOrdinal); + if (otherBucketOrds != null) { + for (long otherBucketOrd : otherBucketOrds) { + collectBucket(sub, docId, otherBucketOrd); + } + } + } + } + } + } + } + } + + @Override + protected void doClose() { + Releasables.close(parentOrdToBuckets, parentOrdToOtherBuckets); + } +} 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); + } + } +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java new file mode 100644 index 0000000000..ca0bfd623d --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java @@ -0,0 +1,328 @@ +/* + * 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.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.join.ScoreMode; +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.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.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.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Builder for the 'has_parent' query. + */ +public class HasParentQueryBuilder extends AbstractQueryBuilder<HasParentQueryBuilder> { + public static final String NAME = "has_parent"; + + /** + * 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 SCORE_MODE_FIELD = new ParseField("score_mode").withAllDeprecated("score"); + private static final ParseField TYPE_FIELD = new ParseField("parent_type", "type"); + private static final ParseField SCORE_FIELD = new ParseField("score"); + 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 boolean score; + private InnerHitBuilder innerHit; + private boolean ignoreUnmapped = false; + + public HasParentQueryBuilder(String type, QueryBuilder query, boolean score) { + this(type, query, score, null); + } + + private HasParentQueryBuilder(String type, QueryBuilder query, boolean score, InnerHitBuilder innerHit) { + this.type = requireValue(type, "[" + NAME + "] requires 'type' field"); + this.query = requireValue(query, "[" + NAME + "] requires 'query' field"); + this.score = score; + this.innerHit = innerHit; + } + + /** + * Read from a stream. + */ + public HasParentQueryBuilder(StreamInput in) throws IOException { + super(in); + type = in.readString(); + score = in.readBoolean(); + query = in.readNamedWriteable(QueryBuilder.class); + innerHit = in.readOptionalWriteable(InnerHitBuilder::new); + ignoreUnmapped = in.readBoolean(); + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + out.writeString(type); + out.writeBoolean(score); + out.writeNamedWriteable(query); + out.writeOptionalWriteable(innerHit); + out.writeBoolean(ignoreUnmapped); + } + + /** + * Returns the query to execute. + */ + public QueryBuilder query() { + return query; + } + + /** + * Returns <code>true</code> if the parent score is mapped into the child documents + */ + public boolean score() { + return score; + } + + /** + * Returns the parents type name + */ + public String type() { + return type; + } + + /** + * Returns inner hit definition in the scope of this query and reusing the defined type and query. + */ + public InnerHitBuilder innerHit() { + return innerHit; + } + + public HasParentQueryBuilder innerHit(InnerHitBuilder innerHit, boolean ignoreUnmapped) { + this.innerHit = new InnerHitBuilder(innerHit, query, type, ignoreUnmapped); + return this; + } + + /** + * 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 HasParentQueryBuilder 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 Query doToQuery(QueryShardContext context) throws IOException { + Query innerQuery; + String[] previousTypes = context.getTypes(); + context.setTypes(type); + try { + innerQuery = query.toQuery(context); + } finally { + context.setTypes(previousTypes); + } + + DocumentMapper parentDocMapper = context.documentMapper(type); + if (parentDocMapper == null) { + if (ignoreUnmapped) { + return new MatchNoDocsQuery(); + } else { + throw new QueryShardException(context, "[" + NAME + "] query configured 'parent_type' [" + type + "] is not a valid type"); + } + } + + Set<String> childTypes = new HashSet<>(); + ParentChildIndexFieldData parentChildIndexFieldData = null; + for (DocumentMapper documentMapper : context.getMapperService().docMappers(false)) { + ParentFieldMapper parentFieldMapper = documentMapper.parentFieldMapper(); + if (parentFieldMapper.active() && type.equals(parentFieldMapper.type())) { + childTypes.add(documentMapper.type()); + parentChildIndexFieldData = context.getForField(parentFieldMapper.fieldType()); + } + } + + if (childTypes.isEmpty()) { + throw new QueryShardException(context, "[" + NAME + "] no child types found for type [" + type + "]"); + } + + Query childrenQuery; + if (childTypes.size() == 1) { + DocumentMapper documentMapper = context.getMapperService().documentMapper(childTypes.iterator().next()); + childrenQuery = documentMapper.typeFilter(context); + } else { + BooleanQuery.Builder childrenFilter = new BooleanQuery.Builder(); + for (String childrenTypeStr : childTypes) { + DocumentMapper documentMapper = context.getMapperService().documentMapper(childrenTypeStr); + childrenFilter.add(documentMapper.typeFilter(context), BooleanClause.Occur.SHOULD); + } + childrenQuery = childrenFilter.build(); + } + + // wrap the query with type query + innerQuery = Queries.filtered(innerQuery, parentDocMapper.typeFilter(context)); + return new HasChildQueryBuilder.LateParsingQuery(childrenQuery, + innerQuery, + HasChildQueryBuilder.DEFAULT_MIN_CHILDREN, + HasChildQueryBuilder.DEFAULT_MAX_CHILDREN, + type, + score ? ScoreMode.Max : ScoreMode.None, + parentChildIndexFieldData, + context.getSearchSimilarity()); + } + + @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_FIELD.getPreferredName(), score); + builder.field(IGNORE_UNMAPPED_FIELD.getPreferredName(), ignoreUnmapped); + printBoostAndQueryName(builder); + if (innerHit != null) { + builder.field(INNER_HITS_FIELD.getPreferredName(), innerHit, params); + } + builder.endObject(); + } + + public static HasParentQueryBuilder fromXContent(QueryParseContext parseContext) throws IOException { + XContentParser parser = parseContext.parser(); + float boost = AbstractQueryBuilder.DEFAULT_BOOST; + String parentType = null; + boolean score = false; + String queryName = null; + InnerHitBuilder innerHits = null; + boolean ignoreUnmapped = DEFAULT_IGNORE_UNMAPPED; + + 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 (token == XContentParser.Token.START_OBJECT) { + if (QUERY_FIELD.match(currentFieldName)) { + iqb = parseContext.parseInnerQueryBuilder(); + } else if (INNER_HITS_FIELD.match(currentFieldName)) { + innerHits = InnerHitBuilder.fromXContent(parseContext); + } else { + throw new ParsingException(parser.getTokenLocation(), "[has_parent] query does not support [" + currentFieldName + "]"); + } + } else if (token.isValue()) { + if (TYPE_FIELD.match(currentFieldName)) { + parentType = parser.text(); + } else if (SCORE_MODE_FIELD.match(currentFieldName)) { + String scoreModeValue = parser.text(); + if ("score".equals(scoreModeValue)) { + score = true; + } else if ("none".equals(scoreModeValue)) { + score = false; + } else { + throw new ParsingException(parser.getTokenLocation(), "[has_parent] query does not support [" + + scoreModeValue + "] as an option for score_mode"); + } + } else if (SCORE_FIELD.match(currentFieldName)) { + score = parser.booleanValue(); + } else if (IGNORE_UNMAPPED_FIELD.match(currentFieldName)) { + ignoreUnmapped = parser.booleanValue(); + } else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName)) { + boost = parser.floatValue(); + } else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName)) { + queryName = parser.text(); + } else { + throw new ParsingException(parser.getTokenLocation(), "[has_parent] query does not support [" + currentFieldName + "]"); + } + } + } + HasParentQueryBuilder queryBuilder = new HasParentQueryBuilder(parentType, iqb, score) + .ignoreUnmapped(ignoreUnmapped) + .queryName(queryName) + .boost(boost); + if (innerHits != null) { + queryBuilder.innerHit(innerHits, ignoreUnmapped); + } + return queryBuilder; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + protected boolean doEquals(HasParentQueryBuilder that) { + return Objects.equals(query, that.query) + && Objects.equals(type, that.type) + && Objects.equals(score, that.score) + && Objects.equals(innerHit, that.innerHit) + && Objects.equals(ignoreUnmapped, that.ignoreUnmapped); + } + + @Override + protected int doHashCode() { + return Objects.hash(query, type, score, innerHit, ignoreUnmapped); + } + + @Override + protected QueryBuilder doRewrite(QueryRewriteContext queryShardContext) throws IOException { + QueryBuilder rewrittenQuery = query.rewrite(queryShardContext); + if (rewrittenQuery != query) { + InnerHitBuilder rewrittenInnerHit = InnerHitBuilder.rewrite(innerHit, rewrittenQuery); + HasParentQueryBuilder hasParentQueryBuilder = new HasParentQueryBuilder(type, rewrittenQuery, score, rewrittenInnerHit); + hasParentQueryBuilder.ignoreUnmapped(ignoreUnmapped); + return hasParentQueryBuilder; + } + return this; + } + + @Override + protected void extractInnerHitBuilders(Map<String, InnerHitBuilder> innerHits) { + if (innerHit!= null) { + innerHit.inlineInnerHits(innerHits); + } + } +} diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/JoinQueryBuilders.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/JoinQueryBuilders.java new file mode 100644 index 0000000000..af778f400f --- /dev/null +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/JoinQueryBuilders.java @@ -0,0 +1,50 @@ +/* + * 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.search.join.ScoreMode; +import org.elasticsearch.index.query.QueryBuilder; + +public abstract class JoinQueryBuilders { + /** + * Constructs a new has_child query, with the child type and the query to run on the child documents. The + * results of this query are the parent docs that those child docs matched. + * + * @param type The child type. + * @param query The query. + * @param scoreMode How the scores from the children hits should be aggregated into the parent hit. + */ + public static HasChildQueryBuilder hasChildQuery(String type, QueryBuilder query, ScoreMode scoreMode) { + return new HasChildQueryBuilder(type, query, scoreMode); + } + + /** + * Constructs a new parent query, with the parent type and the query to run on the parent documents. The + * results of this query are the children docs that those parent docs matched. + * + * @param type The parent type. + * @param query The query. + * @param score Whether the score from the parent hit should propagate to the child hit + */ + public static HasParentQueryBuilder hasParentQuery(String type, QueryBuilder query, boolean score) { + return new HasParentQueryBuilder(type, query, score); + } + +} |