summaryrefslogtreecommitdiff
path: root/modules/parent-join/src/main/java/org
diff options
context:
space:
mode:
authorJim Ferenczi <jim.ferenczi@elastic.co>2017-05-12 15:58:06 +0200
committerGitHub <noreply@github.com>2017-05-12 15:58:06 +0200
commit279a18a527b9c23b06c8b75c2aa9321aefca9728 (patch)
tree111c9312cb41d09994a7fa7d86aeb9227a622a9f /modules/parent-join/src/main/java/org
parentbe2a6ce80b2282779159bc017352aa6f216349e2 (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')
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/ParentJoinPlugin.java54
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/Children.java28
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregationBuilder.java167
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ChildrenAggregatorFactory.java77
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/InternalChildren.java56
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/JoinAggregationBuilders.java29
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregator.java186
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java474
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/query/HasParentQueryBuilder.java328
-rw-r--r--modules/parent-join/src/main/java/org/elasticsearch/join/query/JoinQueryBuilders.java50
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);
+ }
+
+}