summaryrefslogtreecommitdiff
path: root/modules
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
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')
-rw-r--r--modules/parent-join/build.gradle24
-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
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/ParentChildClientYamlTestSuiteIT.java37
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenIT.java503
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenTests.java44
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/InternalChildrenTests.java47
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregatorTests.java190
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/query/ChildQuerySearchIT.java2213
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java326
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java245
-rw-r--r--modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java568
-rw-r--r--modules/parent-join/src/test/resources/rest-api-spec/test/10_basic.yaml48
-rw-r--r--modules/percolator/build.gradle4
-rw-r--r--modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java7
-rw-r--r--modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java8
-rw-r--r--modules/reindex/build.gradle2
-rw-r--r--modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexParentChildTests.java26
-rw-r--r--modules/reindex/src/test/resources/rest-api-spec/test/reindex/90_remote.yaml79
27 files changed, 5732 insertions, 88 deletions
diff --git a/modules/parent-join/build.gradle b/modules/parent-join/build.gradle
new file mode 100644
index 0000000000..67bcc9d54e
--- /dev/null
+++ b/modules/parent-join/build.gradle
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+esplugin {
+ description 'This module adds the support parent-child queries and aggregations'
+ classname 'org.elasticsearch.join.ParentJoinPlugin'
+ hasClientJar = true
+}
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);
+ }
+
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/ParentChildClientYamlTestSuiteIT.java b/modules/parent-join/src/test/java/org/elasticsearch/join/ParentChildClientYamlTestSuiteIT.java
new file mode 100644
index 0000000000..666fa736d4
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/ParentChildClientYamlTestSuiteIT.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.carrotsearch.randomizedtesting.annotations.Name;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
+
+import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
+import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase;
+
+public class ParentChildClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
+ public ParentChildClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) {
+ super(testCandidate);
+ }
+
+ @ParametersFactory
+ public static Iterable<Object[]> parameters() throws Exception {
+ return createParameters();
+ }
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenIT.java b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenIT.java
new file mode 100644
index 0000000000..8da6dbcdf6
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenIT.java
@@ -0,0 +1,503 @@
+/*
+ * 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.join.ScoreMode;
+import org.elasticsearch.action.index.IndexRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.update.UpdateResponse;
+import org.elasticsearch.client.Requests;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.join.ParentJoinPlugin;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.InternalAggregation;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.elasticsearch.search.aggregations.metrics.sum.Sum;
+import org.elasticsearch.search.aggregations.metrics.tophits.TopHits;
+import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
+import org.elasticsearch.test.ESIntegTestCase.Scope;
+import org.junit.Before;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+import static org.elasticsearch.join.aggregations.JoinAggregationBuilders.children;
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasChildQuery;
+import static org.elasticsearch.search.aggregations.AggregationBuilders.sum;
+import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
+import static org.elasticsearch.search.aggregations.AggregationBuilders.topHits;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchResponse;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+
+@ClusterScope(scope = Scope.SUITE)
+public class ChildrenIT extends ESIntegTestCase {
+ private static final Map<String, Control> categoryToControl = new HashMap<>();
+
+ @Override
+ protected boolean ignoreExternalCluster() {
+ return true;
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> nodePlugins() {
+ return Collections.singleton(ParentJoinPlugin.class);
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> transportClientPlugins() {
+ return nodePlugins();
+ }
+
+ @Before
+ public void setupCluster() throws Exception {
+ categoryToControl.clear();
+ assertAcked(
+ prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("article", "category", "type=keyword")
+ .addMapping("comment", "_parent", "type=article", "commenter", "type=keyword")
+ );
+
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ String[] uniqueCategories = new String[randomIntBetween(1, 25)];
+ for (int i = 0; i < uniqueCategories.length; i++) {
+ uniqueCategories[i] = Integer.toString(i);
+ }
+ int catIndex = 0;
+
+ int numParentDocs = randomIntBetween(uniqueCategories.length, uniqueCategories.length * 5);
+ for (int i = 0; i < numParentDocs; i++) {
+ String id = Integer.toString(i);
+
+ // TODO: this array is always of length 1, and testChildrenAggs fails if this is changed
+ String[] categories = new String[randomIntBetween(1,1)];
+ for (int j = 0; j < categories.length; j++) {
+ String category = categories[j] = uniqueCategories[catIndex++ % uniqueCategories.length];
+ Control control = categoryToControl.get(category);
+ if (control == null) {
+ categoryToControl.put(category, control = new Control(category));
+ }
+ control.articleIds.add(id);
+ }
+
+ requests.add(client()
+ .prepareIndex("test", "article", id).setCreate(true).setSource("category", categories, "randomized", true));
+ }
+
+ String[] commenters = new String[randomIntBetween(5, 50)];
+ for (int i = 0; i < commenters.length; i++) {
+ commenters[i] = Integer.toString(i);
+ }
+
+ int id = 0;
+ for (Control control : categoryToControl.values()) {
+ for (String articleId : control.articleIds) {
+ int numChildDocsPerParent = randomIntBetween(0, 5);
+ for (int i = 0; i < numChildDocsPerParent; i++) {
+ String commenter = commenters[id % commenters.length];
+ String idValue = Integer.toString(id++);
+ control.commentIds.add(idValue);
+ Set<String> ids = control.commenterToCommentId.get(commenter);
+ if (ids == null) {
+ control.commenterToCommentId.put(commenter, ids = new HashSet<>());
+ }
+ ids.add(idValue);
+ requests.add(client().prepareIndex("test", "comment", idValue)
+ .setCreate(true).setParent(articleId).setSource("commenter", commenter));
+ }
+ }
+ }
+
+ requests.add(client().prepareIndex("test", "article", "a")
+ .setSource("category", new String[]{"a"}, "randomized", false));
+ requests.add(client().prepareIndex("test", "article", "b")
+ .setSource("category", new String[]{"a", "b"}, "randomized", false));
+ requests.add(client().prepareIndex("test", "article", "c")
+ .setSource("category", new String[]{"a", "b", "c"}, "randomized", false));
+ requests.add(client().prepareIndex("test", "article", "d")
+ .setSource("category", new String[]{"c"}, "randomized", false));
+ requests.add(client().prepareIndex("test", "comment", "a")
+ .setParent("a").setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("test", "comment", "c")
+ .setParent("c").setSource("{}", XContentType.JSON));
+
+ indexRandom(true, requests);
+ ensureSearchable("test");
+ }
+
+ public void testChildrenAggs() throws Exception {
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(matchQuery("randomized", true))
+ .addAggregation(
+ terms("category").field("category").size(10000).subAggregation(children("to_comment", "comment")
+ .subAggregation(
+ terms("commenters").field("commenter").size(10000).subAggregation(
+ topHits("top_comments")
+ ))
+ )
+ ).get();
+ assertSearchResponse(searchResponse);
+
+ Terms categoryTerms = searchResponse.getAggregations().get("category");
+ assertThat(categoryTerms.getBuckets().size(), equalTo(categoryToControl.size()));
+ for (Map.Entry<String, Control> entry1 : categoryToControl.entrySet()) {
+ Terms.Bucket categoryBucket = categoryTerms.getBucketByKey(entry1.getKey());
+ assertThat(categoryBucket.getKeyAsString(), equalTo(entry1.getKey()));
+ assertThat(categoryBucket.getDocCount(), equalTo((long) entry1.getValue().articleIds.size()));
+
+ Children childrenBucket = categoryBucket.getAggregations().get("to_comment");
+ assertThat(childrenBucket.getName(), equalTo("to_comment"));
+ assertThat(childrenBucket.getDocCount(), equalTo((long) entry1.getValue().commentIds.size()));
+ assertThat((long) ((InternalAggregation)childrenBucket).getProperty("_count"),
+ equalTo((long) entry1.getValue().commentIds.size()));
+
+ Terms commentersTerms = childrenBucket.getAggregations().get("commenters");
+ assertThat((Terms) ((InternalAggregation)childrenBucket).getProperty("commenters"), sameInstance(commentersTerms));
+ assertThat(commentersTerms.getBuckets().size(), equalTo(entry1.getValue().commenterToCommentId.size()));
+ for (Map.Entry<String, Set<String>> entry2 : entry1.getValue().commenterToCommentId.entrySet()) {
+ Terms.Bucket commentBucket = commentersTerms.getBucketByKey(entry2.getKey());
+ assertThat(commentBucket.getKeyAsString(), equalTo(entry2.getKey()));
+ assertThat(commentBucket.getDocCount(), equalTo((long) entry2.getValue().size()));
+
+ TopHits topHits = commentBucket.getAggregations().get("top_comments");
+ for (SearchHit searchHit : topHits.getHits().getHits()) {
+ assertThat(entry2.getValue().contains(searchHit.getId()), is(true));
+ }
+ }
+ }
+ }
+
+ public void testParentWithMultipleBuckets() throws Exception {
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(matchQuery("randomized", false))
+ .addAggregation(
+ terms("category").field("category").size(10000).subAggregation(
+ children("to_comment", "comment").subAggregation(topHits("top_comments").sort("_uid", SortOrder.ASC))
+ )
+ ).get();
+ assertSearchResponse(searchResponse);
+
+ Terms categoryTerms = searchResponse.getAggregations().get("category");
+ assertThat(categoryTerms.getBuckets().size(), equalTo(3));
+
+ for (Terms.Bucket bucket : categoryTerms.getBuckets()) {
+ logger.info("bucket={}", bucket.getKey());
+ Children childrenBucket = bucket.getAggregations().get("to_comment");
+ TopHits topHits = childrenBucket.getAggregations().get("top_comments");
+ logger.info("total_hits={}", topHits.getHits().getTotalHits());
+ for (SearchHit searchHit : topHits.getHits()) {
+ logger.info("hit= {} {} {}", searchHit.getSortValues()[0], searchHit.getType(), searchHit.getId());
+ }
+ }
+
+ Terms.Bucket categoryBucket = categoryTerms.getBucketByKey("a");
+ assertThat(categoryBucket.getKeyAsString(), equalTo("a"));
+ assertThat(categoryBucket.getDocCount(), equalTo(3L));
+
+ Children childrenBucket = categoryBucket.getAggregations().get("to_comment");
+ assertThat(childrenBucket.getName(), equalTo("to_comment"));
+ assertThat(childrenBucket.getDocCount(), equalTo(2L));
+ TopHits topHits = childrenBucket.getAggregations().get("top_comments");
+ assertThat(topHits.getHits().getTotalHits(), equalTo(2L));
+ assertThat(topHits.getHits().getAt(0).getId(), equalTo("a"));
+ assertThat(topHits.getHits().getAt(0).getType(), equalTo("comment"));
+ assertThat(topHits.getHits().getAt(1).getId(), equalTo("c"));
+ assertThat(topHits.getHits().getAt(1).getType(), equalTo("comment"));
+
+ categoryBucket = categoryTerms.getBucketByKey("b");
+ assertThat(categoryBucket.getKeyAsString(), equalTo("b"));
+ assertThat(categoryBucket.getDocCount(), equalTo(2L));
+
+ childrenBucket = categoryBucket.getAggregations().get("to_comment");
+ assertThat(childrenBucket.getName(), equalTo("to_comment"));
+ assertThat(childrenBucket.getDocCount(), equalTo(1L));
+ topHits = childrenBucket.getAggregations().get("top_comments");
+ assertThat(topHits.getHits().getTotalHits(), equalTo(1L));
+ assertThat(topHits.getHits().getAt(0).getId(), equalTo("c"));
+ assertThat(topHits.getHits().getAt(0).getType(), equalTo("comment"));
+
+ categoryBucket = categoryTerms.getBucketByKey("c");
+ assertThat(categoryBucket.getKeyAsString(), equalTo("c"));
+ assertThat(categoryBucket.getDocCount(), equalTo(2L));
+
+ childrenBucket = categoryBucket.getAggregations().get("to_comment");
+ assertThat(childrenBucket.getName(), equalTo("to_comment"));
+ assertThat(childrenBucket.getDocCount(), equalTo(1L));
+ topHits = childrenBucket.getAggregations().get("top_comments");
+ assertThat(topHits.getHits().getTotalHits(), equalTo(1L));
+ assertThat(topHits.getHits().getAt(0).getId(), equalTo("c"));
+ assertThat(topHits.getHits().getAt(0).getType(), equalTo("comment"));
+ }
+
+ public void testWithDeletes() throws Exception {
+ String indexName = "xyz";
+ assertAcked(
+ prepareCreate(indexName)
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent", "count", "type=long")
+ );
+
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex(indexName, "parent", "1").setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex(indexName, "child", "0").setParent("1").setSource("count", 1));
+ requests.add(client().prepareIndex(indexName, "child", "1").setParent("1").setSource("count", 1));
+ requests.add(client().prepareIndex(indexName, "child", "2").setParent("1").setSource("count", 1));
+ requests.add(client().prepareIndex(indexName, "child", "3").setParent("1").setSource("count", 1));
+ indexRandom(true, requests);
+
+ for (int i = 0; i < 10; i++) {
+ SearchResponse searchResponse = client().prepareSearch(indexName)
+ .addAggregation(children("children", "child").subAggregation(sum("counts").field("count")))
+ .get();
+
+ assertNoFailures(searchResponse);
+ Children children = searchResponse.getAggregations().get("children");
+ assertThat(children.getDocCount(), equalTo(4L));
+
+ Sum count = children.getAggregations().get("counts");
+ assertThat(count.getValue(), equalTo(4.));
+
+ String idToUpdate = Integer.toString(randomInt(3));
+ /*
+ * The whole point of this test is to test these things with deleted
+ * docs in the index so we turn off detect_noop to make sure that
+ * the updates cause that.
+ */
+ UpdateResponse updateResponse = client().prepareUpdate(indexName, "child", idToUpdate)
+ .setParent("1")
+ .setDoc(Requests.INDEX_CONTENT_TYPE, "count", 1)
+ .setDetectNoop(false)
+ .get();
+ assertThat(updateResponse.getVersion(), greaterThan(1L));
+ refresh();
+ }
+ }
+
+ public void testNonExistingChildType() throws Exception {
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .addAggregation(
+ children("non-existing", "xyz")
+ ).get();
+ assertSearchResponse(searchResponse);
+
+ Children children = searchResponse.getAggregations().get("non-existing");
+ assertThat(children.getName(), equalTo("non-existing"));
+ assertThat(children.getDocCount(), equalTo(0L));
+ }
+
+ public void testPostCollection() throws Exception {
+ String indexName = "prodcatalog";
+ String masterType = "masterprod";
+ String childType = "variantsku";
+ assertAcked(
+ prepareCreate(indexName)
+ .setSettings("index.mapping.single_type", false)
+ .addMapping(masterType, "brand", "type=text", "name", "type=keyword", "material", "type=text")
+ .addMapping(childType, "_parent", "type=masterprod", "color", "type=keyword", "size", "type=keyword")
+ );
+
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex(indexName, masterType, "1")
+ .setSource("brand", "Levis", "name", "Style 501", "material", "Denim"));
+ requests.add(client().prepareIndex(indexName, childType, "0").setParent("1").setSource("color", "blue", "size", "32"));
+ requests.add(client().prepareIndex(indexName, childType, "1").setParent("1").setSource("color", "blue", "size", "34"));
+ requests.add(client().prepareIndex(indexName, childType, "2").setParent("1").setSource("color", "blue", "size", "36"));
+ requests.add(client().prepareIndex(indexName, childType, "3").setParent("1").setSource("color", "black", "size", "38"));
+ requests.add(client().prepareIndex(indexName, childType, "4").setParent("1").setSource("color", "black", "size", "40"));
+ requests.add(client().prepareIndex(indexName, childType, "5").setParent("1").setSource("color", "gray", "size", "36"));
+
+ requests.add(client().prepareIndex(indexName, masterType, "2")
+ .setSource("brand", "Wrangler", "name", "Regular Cut", "material", "Leather"));
+ requests.add(client().prepareIndex(indexName, childType, "6").setParent("2").setSource("color", "blue", "size", "32"));
+ requests.add(client().prepareIndex(indexName, childType, "7").setParent("2").setSource("color", "blue", "size", "34"));
+ requests.add(client().prepareIndex(indexName, childType, "8").setParent("2").setSource("color", "black", "size", "36"));
+ requests.add(client().prepareIndex(indexName, childType, "9").setParent("2").setSource("color", "black", "size", "38"));
+ requests.add(client().prepareIndex(indexName, childType, "10").setParent("2").setSource("color", "black", "size", "40"));
+ requests.add(client().prepareIndex(indexName, childType, "11").setParent("2").setSource("color", "orange", "size", "36"));
+ requests.add(client().prepareIndex(indexName, childType, "12").setParent("2").setSource("color", "green", "size", "44"));
+ indexRandom(true, requests);
+
+ SearchResponse response = client().prepareSearch(indexName).setTypes(masterType)
+ .setQuery(hasChildQuery(childType, termQuery("color", "orange"), ScoreMode.None))
+.addAggregation(children("my-refinements", childType)
+ .subAggregation(terms("my-colors").field("color"))
+ .subAggregation(terms("my-sizes").field("size"))
+ ).get();
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+
+ Children childrenAgg = response.getAggregations().get("my-refinements");
+ assertThat(childrenAgg.getDocCount(), equalTo(7L));
+
+ Terms termsAgg = childrenAgg.getAggregations().get("my-colors");
+ assertThat(termsAgg.getBuckets().size(), equalTo(4));
+ assertThat(termsAgg.getBucketByKey("black").getDocCount(), equalTo(3L));
+ assertThat(termsAgg.getBucketByKey("blue").getDocCount(), equalTo(2L));
+ assertThat(termsAgg.getBucketByKey("green").getDocCount(), equalTo(1L));
+ assertThat(termsAgg.getBucketByKey("orange").getDocCount(), equalTo(1L));
+
+ termsAgg = childrenAgg.getAggregations().get("my-sizes");
+ assertThat(termsAgg.getBuckets().size(), equalTo(6));
+ assertThat(termsAgg.getBucketByKey("36").getDocCount(), equalTo(2L));
+ assertThat(termsAgg.getBucketByKey("32").getDocCount(), equalTo(1L));
+ assertThat(termsAgg.getBucketByKey("34").getDocCount(), equalTo(1L));
+ assertThat(termsAgg.getBucketByKey("38").getDocCount(), equalTo(1L));
+ assertThat(termsAgg.getBucketByKey("40").getDocCount(), equalTo(1L));
+ assertThat(termsAgg.getBucketByKey("44").getDocCount(), equalTo(1L));
+ }
+
+ public void testHierarchicalChildrenAggs() {
+ String indexName = "geo";
+ String grandParentType = "continent";
+ String parentType = "country";
+ String childType = "city";
+ assertAcked(
+ prepareCreate(indexName)
+ .setSettings(Settings.builder()
+ .put(IndexMetaData.SETTING_NUMBER_OF_SHARDS, 1)
+ .put(IndexMetaData.SETTING_NUMBER_OF_REPLICAS, 0)
+ )
+ .setSettings("index.mapping.single_type", false)
+ .addMapping(grandParentType, "name", "type=keyword")
+ .addMapping(parentType, "_parent", "type=" + grandParentType)
+ .addMapping(childType, "_parent", "type=" + parentType)
+ );
+
+ client().prepareIndex(indexName, grandParentType, "1").setSource("name", "europe").get();
+ client().prepareIndex(indexName, parentType, "2").setParent("1").setSource("name", "belgium").get();
+ client().prepareIndex(indexName, childType, "3").setParent("2").setRouting("1").setSource("name", "brussels").get();
+ refresh();
+
+ SearchResponse response = client().prepareSearch(indexName)
+ .setQuery(matchQuery("name", "europe"))
+ .addAggregation(
+ children(parentType, parentType).subAggregation(children(childType, childType).subAggregation(
+ terms("name").field("name")
+ )
+ )
+ )
+ .get();
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+
+ Children children = response.getAggregations().get(parentType);
+ assertThat(children.getName(), equalTo(parentType));
+ assertThat(children.getDocCount(), equalTo(1L));
+ children = children.getAggregations().get(childType);
+ assertThat(children.getName(), equalTo(childType));
+ assertThat(children.getDocCount(), equalTo(1L));
+ Terms terms = children.getAggregations().get("name");
+ assertThat(terms.getBuckets().size(), equalTo(1));
+ assertThat(terms.getBuckets().get(0).getKey().toString(), equalTo("brussels"));
+ assertThat(terms.getBuckets().get(0).getDocCount(), equalTo(1L));
+ }
+
+ public void testPostCollectAllLeafReaders() throws Exception {
+ // The 'towns' and 'parent_names' aggs operate on parent docs and if child docs are in different segments we need
+ // to ensure those segments which child docs are also evaluated to in the post collect phase.
+
+ // Before we only evaluated segments that yielded matches in 'towns' and 'parent_names' aggs, which caused
+ // us to miss to evaluate child docs in segments we didn't have parent matches for.
+
+ assertAcked(
+ prepareCreate("index")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parentType", "name", "type=keyword", "town", "type=keyword")
+ .addMapping("childType", "_parent", "type=parentType", "name", "type=keyword", "age", "type=integer")
+ );
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex("index", "parentType", "1").setSource("name", "Bob", "town", "Memphis"));
+ requests.add(client().prepareIndex("index", "parentType", "2").setSource("name", "Alice", "town", "Chicago"));
+ requests.add(client().prepareIndex("index", "parentType", "3").setSource("name", "Bill", "town", "Chicago"));
+ requests.add(client().prepareIndex("index", "childType", "1").setSource("name", "Jill", "age", 5).setParent("1"));
+ requests.add(client().prepareIndex("index", "childType", "2").setSource("name", "Joey", "age", 3).setParent("1"));
+ requests.add(client().prepareIndex("index", "childType", "3").setSource("name", "John", "age", 2).setParent("2"));
+ requests.add(client().prepareIndex("index", "childType", "4").setSource("name", "Betty", "age", 6).setParent("3"));
+ requests.add(client().prepareIndex("index", "childType", "5").setSource("name", "Dan", "age", 1).setParent("3"));
+ indexRandom(true, requests);
+
+ SearchResponse response = client().prepareSearch("index")
+ .setSize(0)
+ .addAggregation(AggregationBuilders.terms("towns").field("town")
+ .subAggregation(AggregationBuilders.terms("parent_names").field("name")
+.subAggregation(children("child_docs", "childType"))
+ )
+ )
+ .get();
+
+ Terms towns = response.getAggregations().get("towns");
+ assertThat(towns.getBuckets().size(), equalTo(2));
+ assertThat(towns.getBuckets().get(0).getKeyAsString(), equalTo("Chicago"));
+ assertThat(towns.getBuckets().get(0).getDocCount(), equalTo(2L));
+
+ Terms parents = towns.getBuckets().get(0).getAggregations().get("parent_names");
+ assertThat(parents.getBuckets().size(), equalTo(2));
+ assertThat(parents.getBuckets().get(0).getKeyAsString(), equalTo("Alice"));
+ assertThat(parents.getBuckets().get(0).getDocCount(), equalTo(1L));
+ Children children = parents.getBuckets().get(0).getAggregations().get("child_docs");
+ assertThat(children.getDocCount(), equalTo(1L));
+
+ assertThat(parents.getBuckets().get(1).getKeyAsString(), equalTo("Bill"));
+ assertThat(parents.getBuckets().get(1).getDocCount(), equalTo(1L));
+ children = parents.getBuckets().get(1).getAggregations().get("child_docs");
+ assertThat(children.getDocCount(), equalTo(2L));
+
+ assertThat(towns.getBuckets().get(1).getKeyAsString(), equalTo("Memphis"));
+ assertThat(towns.getBuckets().get(1).getDocCount(), equalTo(1L));
+ parents = towns.getBuckets().get(1).getAggregations().get("parent_names");
+ assertThat(parents.getBuckets().size(), equalTo(1));
+ assertThat(parents.getBuckets().get(0).getKeyAsString(), equalTo("Bob"));
+ assertThat(parents.getBuckets().get(0).getDocCount(), equalTo(1L));
+ children = parents.getBuckets().get(0).getAggregations().get("child_docs");
+ assertThat(children.getDocCount(), equalTo(2L));
+ }
+
+ private static final class Control {
+
+ final String category;
+ final Set<String> articleIds = new HashSet<>();
+ final Set<String> commentIds = new HashSet<>();
+ final Map<String, Set<String>> commenterToCommentId = new HashMap<>();
+
+ private Control(String category) {
+ this.category = category;
+ }
+ }
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenTests.java b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenTests.java
new file mode 100644
index 0000000000..85a97c4b9b
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ChildrenTests.java
@@ -0,0 +1,44 @@
+/*
+ * 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.join.ParentJoinPlugin;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.search.aggregations.BaseAggregationTestCase;
+
+import java.util.Collection;
+import java.util.Collections;
+
+public class ChildrenTests extends BaseAggregationTestCase<ChildrenAggregationBuilder> {
+
+ @Override
+ protected Collection<Class<? extends Plugin>> getPlugins() {
+ return Collections.singleton(ParentJoinPlugin.class);
+ }
+
+ @Override
+ protected ChildrenAggregationBuilder createTestAggregatorBuilder() {
+ String name = randomAlphaOfLengthBetween(3, 20);
+ String childType = randomAlphaOfLengthBetween(5, 40);
+ ChildrenAggregationBuilder factory = new ChildrenAggregationBuilder(name, childType);
+ return factory;
+ }
+
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/InternalChildrenTests.java b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/InternalChildrenTests.java
new file mode 100644
index 0000000000..afcbc953eb
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/InternalChildrenTests.java
@@ -0,0 +1,47 @@
+/*
+ * 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.Writeable.Reader;
+import org.elasticsearch.search.aggregations.InternalAggregations;
+import org.elasticsearch.search.aggregations.InternalSingleBucketAggregationTestCase;
+import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
+
+import java.util.List;
+import java.util.Map;
+
+public class InternalChildrenTests extends InternalSingleBucketAggregationTestCase<InternalChildren> {
+ @Override
+ protected InternalChildren createTestInstance(String name, long docCount, InternalAggregations aggregations,
+ List<PipelineAggregator> pipelineAggregators, Map<String, Object> metaData) {
+ return new InternalChildren(name, docCount, aggregations, pipelineAggregators, metaData);
+ }
+
+ @Override
+ protected void extraAssertReduced(InternalChildren reduced, List<InternalChildren> inputs) {
+ // Nothing extra to assert
+ }
+
+ @Override
+ protected Reader<InternalChildren> instanceReader() {
+ return InternalChildren::new;
+ }
+
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregatorTests.java b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregatorTests.java
new file mode 100644
index 0000000000..0a00b2d1c2
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/aggregations/ParentToChildrenAggregatorTests.java
@@ -0,0 +1,190 @@
+/*
+ * 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.document.Field;
+import org.apache.lucene.document.SortedDocValuesField;
+import org.apache.lucene.document.SortedNumericDocValuesField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermInSetQuery;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.index.mapper.ContentPath;
+import org.elasticsearch.index.mapper.DocumentMapper;
+import org.elasticsearch.index.mapper.MappedFieldType;
+import org.elasticsearch.index.mapper.Mapper;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.NumberFieldMapper;
+import org.elasticsearch.index.mapper.ParentFieldMapper;
+import org.elasticsearch.index.mapper.TypeFieldMapper;
+import org.elasticsearch.index.mapper.Uid;
+import org.elasticsearch.index.mapper.UidFieldMapper;
+import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.search.aggregations.AggregatorTestCase;
+import org.elasticsearch.search.aggregations.metrics.min.InternalMin;
+import org.elasticsearch.search.aggregations.metrics.min.MinAggregationBuilder;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ParentToChildrenAggregatorTests extends AggregatorTestCase {
+
+ private static final String CHILD_TYPE = "child_type";
+ private static final String PARENT_TYPE = "parent_type";
+
+ public void testNoDocs() throws IOException {
+ Directory directory = newDirectory();
+
+ RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
+ // intentionally not writing any docs
+ indexWriter.close();
+ IndexReader indexReader = DirectoryReader.open(directory);
+
+ testCase(new MatchAllDocsQuery(), newSearcher(indexReader, false, true), parentToChild -> {
+ assertEquals(0, parentToChild.getDocCount());
+ assertEquals(Double.POSITIVE_INFINITY, ((InternalMin) parentToChild.getAggregations().get("in_child")).getValue(),
+ Double.MIN_VALUE);
+ });
+ indexReader.close();
+ directory.close();
+ }
+
+ public void testParentChild() throws IOException {
+ Directory directory = newDirectory();
+ RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
+
+ final Map<String, Tuple<Integer, Integer>> expectedParentChildRelations = setupIndex(indexWriter);
+ indexWriter.close();
+
+ IndexReader indexReader = ElasticsearchDirectoryReader.wrap(DirectoryReader.open(directory),
+ new ShardId(new Index("foo", "_na_"), 1));
+ // TODO set "maybeWrap" to true for IndexSearcher once #23338 is resolved
+ IndexSearcher indexSearcher = newSearcher(indexReader, false, true);
+
+ testCase(new MatchAllDocsQuery(), indexSearcher, child -> {
+ int expectedTotalChildren = 0;
+ int expectedMinValue = Integer.MAX_VALUE;
+ for (Tuple<Integer, Integer> expectedValues : expectedParentChildRelations.values()) {
+ expectedTotalChildren += expectedValues.v1();
+ expectedMinValue = Math.min(expectedMinValue, expectedValues.v2());
+ }
+ assertEquals(expectedTotalChildren, child.getDocCount());
+ assertEquals(expectedMinValue, ((InternalMin) child.getAggregations().get("in_child")).getValue(), Double.MIN_VALUE);
+ });
+
+ for (String parent : expectedParentChildRelations.keySet()) {
+ testCase(new TermInSetQuery(UidFieldMapper.NAME, new BytesRef(Uid.createUid(PARENT_TYPE, parent))), indexSearcher, child -> {
+ assertEquals((long) expectedParentChildRelations.get(parent).v1(), child.getDocCount());
+ assertEquals(expectedParentChildRelations.get(parent).v2(),
+ ((InternalMin) child.getAggregations().get("in_child")).getValue(), Double.MIN_VALUE);
+ });
+ }
+ indexReader.close();
+ directory.close();
+ }
+
+ private static Map<String, Tuple<Integer, Integer>> setupIndex(RandomIndexWriter iw) throws IOException {
+ Map<String, Tuple<Integer, Integer>> expectedValues = new HashMap<>();
+ int numParents = randomIntBetween(1, 10);
+ for (int i = 0; i < numParents; i++) {
+ String parent = "parent" + i;
+ iw.addDocument(createParentDocument(parent));
+ int numChildren = randomIntBetween(1, 10);
+ int minValue = Integer.MAX_VALUE;
+ for (int c = 0; c < numChildren; c++) {
+ int randomValue = randomIntBetween(0, 100);
+ minValue = Math.min(minValue, randomValue);
+ iw.addDocument(createChildDocument("child" + c + "_" + parent, parent, randomValue));
+ }
+ expectedValues.put(parent, new Tuple<>(numChildren, minValue));
+ }
+ return expectedValues;
+ }
+
+ private static List<Field> createParentDocument(String id) {
+ return Arrays.asList(new StringField(TypeFieldMapper.NAME, PARENT_TYPE, Field.Store.NO),
+ new StringField(UidFieldMapper.NAME, Uid.createUid(PARENT_TYPE, id), Field.Store.NO),
+ createJoinField(PARENT_TYPE, id));
+ }
+
+ private static List<Field> createChildDocument(String childId, String parentId, int value) {
+ return Arrays.asList(new StringField(TypeFieldMapper.NAME, CHILD_TYPE, Field.Store.NO),
+ new StringField(UidFieldMapper.NAME, Uid.createUid(CHILD_TYPE, childId), Field.Store.NO),
+ new SortedNumericDocValuesField("number", value),
+ createJoinField(PARENT_TYPE, parentId));
+ }
+
+ private static SortedDocValuesField createJoinField(String parentType, String id) {
+ return new SortedDocValuesField(ParentFieldMapper.joinField(parentType), new BytesRef(id));
+ }
+
+ @Override
+ protected MapperService mapperServiceMock() {
+ MapperService mapperService = mock(MapperService.class);
+ DocumentMapper childDocMapper = mock(DocumentMapper.class);
+ DocumentMapper parentDocMapper = mock(DocumentMapper.class);
+ ParentFieldMapper parentFieldMapper = createParentFieldMapper();
+ when(childDocMapper.parentFieldMapper()).thenReturn(parentFieldMapper);
+ when(parentDocMapper.parentFieldMapper()).thenReturn(parentFieldMapper);
+ when(mapperService.documentMapper(CHILD_TYPE)).thenReturn(childDocMapper);
+ when(mapperService.documentMapper(PARENT_TYPE)).thenReturn(parentDocMapper);
+ when(mapperService.docMappers(false)).thenReturn(Arrays.asList(new DocumentMapper[] { childDocMapper, parentDocMapper }));
+ when(parentDocMapper.typeFilter(Mockito.any())).thenReturn(new TypeFieldMapper.TypesQuery(new BytesRef(PARENT_TYPE)));
+ when(childDocMapper.typeFilter(Mockito.any())).thenReturn(new TypeFieldMapper.TypesQuery(new BytesRef(CHILD_TYPE)));
+ return mapperService;
+ }
+
+ private static ParentFieldMapper createParentFieldMapper() {
+ Settings settings = Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build();
+ return new ParentFieldMapper.Builder("parent").type(PARENT_TYPE).build(new Mapper.BuilderContext(settings, new ContentPath(0)));
+ }
+
+ private void testCase(Query query, IndexSearcher indexSearcher, Consumer<InternalChildren> verify)
+ throws IOException {
+
+ ChildrenAggregationBuilder aggregationBuilder = new ChildrenAggregationBuilder("_name", CHILD_TYPE);
+ aggregationBuilder.subAggregation(new MinAggregationBuilder("in_child").field("number"));
+
+ MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.LONG);
+ fieldType.setName("number");
+ InternalChildren result = search(indexSearcher, query, aggregationBuilder, fieldType);
+ verify.accept(result);
+ }
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/ChildQuerySearchIT.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/ChildQuerySearchIT.java
new file mode 100644
index 0000000000..ed910ac89e
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/ChildQuerySearchIT.java
@@ -0,0 +1,2213 @@
+/*
+ * 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.action.admin.indices.mapping.get.GetMappingsResponse;
+import org.elasticsearch.action.admin.indices.mapping.put.PutMappingResponse;
+import org.elasticsearch.action.bulk.BulkRequestBuilder;
+import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.explain.ExplainResponse;
+import org.elasticsearch.action.index.IndexRequestBuilder;
+import org.elasticsearch.action.search.SearchPhaseExecutionException;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.search.SearchType;
+import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.lucene.search.function.CombineFunction;
+import org.elasticsearch.common.lucene.search.function.FiltersFunctionScoreQuery;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.IndexModule;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.IdsQueryBuilder;
+import org.elasticsearch.index.query.InnerHitBuilder;
+import org.elasticsearch.index.query.MatchAllQueryBuilder;
+import org.elasticsearch.index.query.MatchQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
+import org.elasticsearch.join.ParentJoinPlugin;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.bucket.filter.Filter;
+import org.elasticsearch.search.aggregations.bucket.global.Global;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
+import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder.Field;
+import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
+import org.elasticsearch.search.sort.SortBuilders;
+import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
+import org.elasticsearch.test.ESIntegTestCase.Scope;
+import org.hamcrest.Matchers;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
+import static org.elasticsearch.index.query.QueryBuilders.constantScoreQuery;
+import static org.elasticsearch.index.query.QueryBuilders.idsQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
+import static org.elasticsearch.index.query.QueryBuilders.multiMatchQuery;
+import static org.elasticsearch.index.query.QueryBuilders.parentId;
+import static org.elasticsearch.index.query.QueryBuilders.prefixQuery;
+import static org.elasticsearch.index.query.QueryBuilders.queryStringQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termsQuery;
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasChildQuery;
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasParentQuery;
+import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.fieldValueFactorFunction;
+import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.weightFactorFunction;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHit;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.hasId;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+@ClusterScope(scope = Scope.SUITE)
+public class ChildQuerySearchIT extends ESIntegTestCase {
+
+ @Override
+ protected boolean ignoreExternalCluster() {
+ return true;
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> nodePlugins() {
+ return Collections.singleton(ParentJoinPlugin.class);
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> transportClientPlugins() {
+ return nodePlugins();
+ }
+
+ @Override
+ public Settings indexSettings() {
+ return Settings.builder().put(super.indexSettings())
+ // aggressive filter caching so that we can assert on the filter cache size
+ .put(IndexModule.INDEX_QUERY_CACHE_ENABLED_SETTING.getKey(), true)
+ .put(IndexModule.INDEX_QUERY_CACHE_EVERYTHING_SETTING.getKey(), true)
+ .build();
+ }
+
+ public void testSelfReferentialIsForbidden() {
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->
+ prepareCreate("test").addMapping("type", "_parent", "type=type").get());
+ assertThat(e.getMessage(), equalTo("The [_parent.type] option can't point to the same type"));
+ }
+
+ public void testMultiLevelChild() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent")
+ .addMapping("grandchild", "_parent", "type=child"));
+ ensureGreen();
+
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "c_value1").setParent("p1").get();
+ client().prepareIndex("test", "grandchild", "gc1").setSource("gc_field", "gc_value1")
+ .setParent("c1").setRouting("p1").get();
+ refresh();
+
+ SearchResponse searchResponse = client()
+ .prepareSearch("test")
+ .setQuery(
+ boolQuery()
+ .must(matchAllQuery())
+ .filter(hasChildQuery(
+ "child",
+ boolQuery().must(termQuery("c_field", "c_value1"))
+ .filter(hasChildQuery("grandchild", termQuery("gc_field", "gc_value1"), ScoreMode.None))
+ , ScoreMode.None))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery())
+ .filter(hasParentQuery("parent", termQuery("p_field", "p_value1"), false))).execute()
+ .actionGet();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("c1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery())
+ .filter(hasParentQuery("child", termQuery("c_field", "c_value1"), false))).execute()
+ .actionGet();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("gc1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(hasParentQuery("parent", termQuery("p_field", "p_value1"), false)).execute()
+ .actionGet();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("c1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(hasParentQuery("child", termQuery("c_field", "c_value1"), false)).execute()
+ .actionGet();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("gc1"));
+ }
+
+ // see #2744
+ public void test2744() throws IOException {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("foo")
+ .addMapping("test", "_parent", "type=foo"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "foo", "1").setSource("foo", 1).get();
+ client().prepareIndex("test", "test").setSource("foo", 1).setParent("1").get();
+ refresh();
+ SearchResponse searchResponse = client().prepareSearch("test").
+ setQuery(hasChildQuery("test", matchQuery("foo", 1), ScoreMode.None))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("1"));
+
+ }
+
+ public void testSimpleChildQuery() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "red").setParent("p1").get();
+ client().prepareIndex("test", "child", "c2").setSource("c_field", "yellow").setParent("p1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "blue").setParent("p2").get();
+ client().prepareIndex("test", "child", "c4").setSource("c_field", "red").setParent("p2").get();
+ refresh();
+
+ // TEST FETCHING _parent from child
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(idsQuery("child").addIds("c1")).storedFields("_parent").execute()
+ .actionGet();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("c1"));
+ assertThat(searchResponse.getHits().getAt(0).field("_parent").getValue().toString(), equalTo("p1"));
+
+ // TEST matching on parent
+ searchResponse = client().prepareSearch("test").setQuery(termQuery("_parent#parent", "p1")).storedFields("_parent").get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), anyOf(equalTo("c1"), equalTo("c2")));
+ assertThat(searchResponse.getHits().getAt(0).field("_parent").getValue().toString(), equalTo("p1"));
+ assertThat(searchResponse.getHits().getAt(1).getId(), anyOf(equalTo("c1"), equalTo("c2")));
+ assertThat(searchResponse.getHits().getAt(1).field("_parent").getValue().toString(), equalTo("p1"));
+
+ searchResponse = client().prepareSearch("test").setQuery(queryStringQuery("_parent#parent:p1")).storedFields("_parent").get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), anyOf(equalTo("c1"), equalTo("c2")));
+ assertThat(searchResponse.getHits().getAt(0).field("_parent").getValue().toString(), equalTo("p1"));
+ assertThat(searchResponse.getHits().getAt(1).getId(), anyOf(equalTo("c1"), equalTo("c2")));
+ assertThat(searchResponse.getHits().getAt(1).field("_parent").getValue().toString(), equalTo("p1"));
+
+ // HAS CHILD
+ searchResponse = client().prepareSearch("test").setQuery(randomHasChild("child", "c_field", "yellow"))
+ .get();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+
+ searchResponse = client().prepareSearch("test").setQuery(randomHasChild("child", "c_field", "blue")).execute()
+ .actionGet();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p2"));
+
+ searchResponse = client().prepareSearch("test").setQuery(randomHasChild("child", "c_field", "red")).get();
+ assertHitCount(searchResponse, 2L);
+ assertThat(searchResponse.getHits().getAt(0).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+ assertThat(searchResponse.getHits().getAt(1).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+
+ // HAS PARENT
+ searchResponse = client().prepareSearch("test")
+ .setQuery(randomHasParent("parent", "p_field", "p_value2")).get();
+ assertNoFailures(searchResponse);
+ assertHitCount(searchResponse, 2L);
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("c3"));
+ assertThat(searchResponse.getHits().getAt(1).getId(), equalTo("c4"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(randomHasParent("parent", "p_field", "p_value1")).get();
+ assertHitCount(searchResponse, 2L);
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("c1"));
+ assertThat(searchResponse.getHits().getAt(1).getId(), equalTo("c2"));
+ }
+
+ // Issue #3290
+ public void testCachingBugWithFqueryFilter() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+ List<IndexRequestBuilder> builders = new ArrayList<>();
+ // index simple data
+ for (int i = 0; i < 10; i++) {
+ builders.add(client().prepareIndex("test", "parent", Integer.toString(i)).setSource("p_field", i));
+ }
+ indexRandom(randomBoolean(), builders);
+ builders.clear();
+ for (int j = 0; j < 2; j++) {
+ for (int i = 0; i < 10; i++) {
+ builders.add(client().prepareIndex("test", "child", Integer.toString(i)).setSource("c_field", i).setParent("" + 0));
+ }
+ for (int i = 0; i < 10; i++) {
+ builders.add(client().prepareIndex("test", "child", Integer.toString(i + 10))
+ .setSource("c_field", i + 10).setParent(Integer.toString(i)));
+ }
+
+ if (randomBoolean()) {
+ break; // randomly break out and dont' have deletes / updates
+ }
+ }
+ indexRandom(true, builders);
+
+ for (int i = 1; i <= 10; i++) {
+ logger.info("Round {}", i);
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", matchAllQuery(), ScoreMode.Max)))
+ .get();
+ assertNoFailures(searchResponse);
+ searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasParentQuery("parent", matchAllQuery(), true)))
+ .get();
+ assertNoFailures(searchResponse);
+ }
+ }
+
+ public void testHasParentFilter() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+ Map<String, Set<String>> parentToChildren = new HashMap<>();
+ // Childless parent
+ client().prepareIndex("test", "parent", "p0").setSource("p_field", "p0").get();
+ parentToChildren.put("p0", new HashSet<>());
+
+ String previousParentId = null;
+ int numChildDocs = 32;
+ int numChildDocsPerParent = 0;
+ List<IndexRequestBuilder> builders = new ArrayList<>();
+ for (int i = 1; i <= numChildDocs; i++) {
+
+ if (previousParentId == null || i % numChildDocsPerParent == 0) {
+ previousParentId = "p" + i;
+ builders.add(client().prepareIndex("test", "parent", previousParentId).setSource("p_field", previousParentId));
+ numChildDocsPerParent++;
+ }
+
+ String childId = "c" + i;
+ builders.add(client().prepareIndex("test", "child", childId).setSource("c_field", childId).setParent(previousParentId));
+
+ if (!parentToChildren.containsKey(previousParentId)) {
+ parentToChildren.put(previousParentId, new HashSet<>());
+ }
+ assertThat(parentToChildren.get(previousParentId).add(childId), is(true));
+ }
+ indexRandom(true, builders.toArray(new IndexRequestBuilder[builders.size()]));
+
+ assertThat(parentToChildren.isEmpty(), equalTo(false));
+ for (Map.Entry<String, Set<String>> parentToChildrenEntry : parentToChildren.entrySet()) {
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasParentQuery("parent", termQuery("p_field", parentToChildrenEntry.getKey()), false)))
+ .setSize(numChildDocsPerParent).get();
+
+ assertNoFailures(searchResponse);
+ Set<String> childIds = parentToChildrenEntry.getValue();
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo((long) childIds.size()));
+ for (int i = 0; i < searchResponse.getHits().getTotalHits(); i++) {
+ assertThat(childIds.remove(searchResponse.getHits().getAt(i).getId()), is(true));
+ assertThat(searchResponse.getHits().getAt(i).getScore(), is(1.0f));
+ }
+ assertThat(childIds.size(), is(0));
+ }
+ }
+
+ public void testSimpleChildQueryWithFlush() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // index simple data with flushes, so we have many segments
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().admin().indices().prepareFlush().get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "red").setParent("p1").get();
+ client().admin().indices().prepareFlush().get();
+ client().prepareIndex("test", "child", "c2").setSource("c_field", "yellow").setParent("p1").get();
+ client().admin().indices().prepareFlush().get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().admin().indices().prepareFlush().get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "blue").setParent("p2").get();
+ client().admin().indices().prepareFlush().get();
+ client().prepareIndex("test", "child", "c4").setSource("c_field", "red").setParent("p2").get();
+ client().admin().indices().prepareFlush().get();
+ refresh();
+
+ // HAS CHILD QUERY
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", termQuery("c_field", "yellow"), ScoreMode.None))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", termQuery("c_field", "blue"), ScoreMode.None))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p2"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", termQuery("c_field", "red"), ScoreMode.None))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+ assertThat(searchResponse.getHits().getAt(1).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+
+ // HAS CHILD FILTER
+ searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "yellow"), ScoreMode.None)))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "blue"), ScoreMode.None)))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p2"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "red"), ScoreMode.None)))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+ assertThat(searchResponse.getHits().getAt(1).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+ }
+
+ public void testScopedFacet() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent", "c_field", "type=keyword"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "red").setParent("p1").get();
+ client().prepareIndex("test", "child", "c2").setSource("c_field", "yellow").setParent("p1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "blue").setParent("p2").get();
+ client().prepareIndex("test", "child", "c4").setSource("c_field", "red").setParent("p2").get();
+
+ refresh();
+
+ SearchResponse searchResponse = client()
+ .prepareSearch("test")
+ .setQuery(hasChildQuery("child",
+ boolQuery().should(termQuery("c_field", "red")).should(termQuery("c_field", "yellow")), ScoreMode.None))
+ .addAggregation(AggregationBuilders.global("global").subAggregation(
+ AggregationBuilders.filter("filter",
+ boolQuery().should(termQuery("c_field", "red")).should(termQuery("c_field", "yellow"))).subAggregation(
+ AggregationBuilders.terms("facet1").field("c_field")))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+ assertThat(searchResponse.getHits().getAt(1).getId(), anyOf(equalTo("p2"), equalTo("p1")));
+
+ Global global = searchResponse.getAggregations().get("global");
+ Filter filter = global.getAggregations().get("filter");
+ Terms termsFacet = filter.getAggregations().get("facet1");
+ assertThat(termsFacet.getBuckets().size(), equalTo(2));
+ assertThat(termsFacet.getBuckets().get(0).getKeyAsString(), equalTo("red"));
+ assertThat(termsFacet.getBuckets().get(0).getDocCount(), equalTo(2L));
+ assertThat(termsFacet.getBuckets().get(1).getKeyAsString(), equalTo("yellow"));
+ assertThat(termsFacet.getBuckets().get(1).getDocCount(), equalTo(1L));
+ }
+
+ public void testDeletedParent() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "red").setParent("p1").get();
+ client().prepareIndex("test", "child", "c2").setSource("c_field", "yellow").setParent("p1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "blue").setParent("p2").get();
+ client().prepareIndex("test", "child", "c4").setSource("c_field", "red").setParent("p2").get();
+
+ refresh();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "yellow"), ScoreMode.None))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+ assertThat(searchResponse.getHits().getAt(0).getSourceAsString(), containsString("\"p_value1\""));
+
+ // update p1 and see what that we get updated values...
+
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1_updated").get();
+ client().admin().indices().prepareRefresh().get();
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "yellow"), ScoreMode.None))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+ assertThat(searchResponse.getHits().getAt(0).getSourceAsString(), containsString("\"p_value1_updated\""));
+ }
+
+ public void testDfsSearchType() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "red").setParent("p1").get();
+ client().prepareIndex("test", "child", "c2").setSource("c_field", "yellow").setParent("p1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "blue").setParent("p2").get();
+ client().prepareIndex("test", "child", "c4").setSource("c_field", "red").setParent("p2").get();
+
+ refresh();
+
+ SearchResponse searchResponse = client().prepareSearch("test").setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
+ .setQuery(boolQuery().mustNot(hasChildQuery("child", boolQuery().should(queryStringQuery("c_field:*")), ScoreMode.None)))
+ .get();
+ assertNoFailures(searchResponse);
+
+ searchResponse = client().prepareSearch("test").setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
+ .setQuery(boolQuery().mustNot(hasParentQuery("parent",
+ boolQuery().should(queryStringQuery("p_field:*")), false))).execute()
+ .actionGet();
+ assertNoFailures(searchResponse);
+ }
+
+ public void testHasChildAndHasParentFailWhenSomeSegmentsDontContainAnyParentOrChildDocs() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ client().prepareIndex("test", "parent", "1").setSource("p_field", 1).get();
+ client().prepareIndex("test", "child", "1").setParent("1").setSource("c_field", 1).get();
+ client().admin().indices().prepareFlush("test").get();
+
+ client().prepareIndex("test", "type1", "1").setSource("p_field", 1).get();
+ client().admin().indices().prepareFlush("test").get();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery()).filter(hasChildQuery("child", matchAllQuery(), ScoreMode.None))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery()).filter(hasParentQuery("parent", matchAllQuery(), false))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ }
+
+ public void testCountApiUsage() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ String parentId = "p1";
+ client().prepareIndex("test", "parent", parentId).setSource("p_field", "1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "1").setParent(parentId).get();
+ refresh();
+
+ SearchResponse countResponse = client().prepareSearch("test").setSize(0)
+ .setQuery(hasChildQuery("child", termQuery("c_field", "1"), ScoreMode.Max))
+ .get();
+ assertHitCount(countResponse, 1L);
+
+ countResponse = client().prepareSearch("test").setSize(0)
+ .setQuery(hasParentQuery("parent", termQuery("p_field", "1"), true))
+ .get();
+ assertHitCount(countResponse, 1L);
+
+ countResponse = client().prepareSearch("test").setSize(0)
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "1"), ScoreMode.None)))
+ .get();
+ assertHitCount(countResponse, 1L);
+
+ countResponse = client().prepareSearch("test").setSize(0)
+ .setQuery(constantScoreQuery(hasParentQuery("parent", termQuery("p_field", "1"), false)))
+ .get();
+ assertHitCount(countResponse, 1L);
+ }
+
+ public void testExplainUsage() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ String parentId = "p1";
+ client().prepareIndex("test", "parent", parentId).setSource("p_field", "1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "1").setParent(parentId).get();
+ refresh();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setExplain(true)
+ .setQuery(hasChildQuery("child", termQuery("c_field", "1"), ScoreMode.Max))
+ .get();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getAt(0).getExplanation().getDescription(), containsString("join value p1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setExplain(true)
+ .setQuery(hasParentQuery("parent", termQuery("p_field", "1"), true))
+ .get();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getAt(0).getExplanation().getDescription(), containsString("join value p1"));
+
+ ExplainResponse explainResponse = client().prepareExplain("test", "parent", parentId)
+ .setQuery(hasChildQuery("child", termQuery("c_field", "1"), ScoreMode.Max))
+ .get();
+ assertThat(explainResponse.isExists(), equalTo(true));
+ assertThat(explainResponse.getExplanation().getDetails()[0].getDescription(), containsString("join value p1"));
+ }
+
+ List<IndexRequestBuilder> createDocBuilders() {
+ List<IndexRequestBuilder> indexBuilders = new ArrayList<>();
+ // Parent 1 and its children
+ indexBuilders.add(client().prepareIndex().setType("parent").setId("1").setIndex("test").setSource("p_field", "p_value1"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("1").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 0).setParent("1"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("2").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 0).setParent("1"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("3").setIndex("test")
+ .setSource("c_field1", 2, "c_field2", 0).setParent("1"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("4").setIndex("test")
+ .setSource("c_field1", 2, "c_field2", 0).setParent("1"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("5").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 1).setParent("1"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("6").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 2).setParent("1"));
+
+ // Parent 2 and its children
+ indexBuilders.add(client().prepareIndex().setType("parent").setId("2").setIndex("test").setSource("p_field", "p_value2"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("7").setIndex("test")
+ .setSource("c_field1", 3, "c_field2", 0).setParent("2"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("8").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 1).setParent("2"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("9").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 1).setParent("p")); // why
+ // "p"????
+ indexBuilders.add(client().prepareIndex().setType("child").setId("10").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 1).setParent("2"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("11").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 1).setParent("2"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("12").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 2).setParent("2"));
+
+ // Parent 3 and its children
+
+ indexBuilders.add(client().prepareIndex().setType("parent").setId("3").setIndex("test")
+ .setSource("p_field1", "p_value3", "p_field2", 5));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("13").setIndex("test")
+ .setSource("c_field1", 4, "c_field2", 0, "c_field3", 0).setParent("3"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("14").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 1, "c_field3", 1).setParent("3"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("15").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 2, "c_field3", 2).setParent("3")); // why
+ // "p"????
+ indexBuilders.add(client().prepareIndex().setType("child").setId("16").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 2, "c_field3", 3).setParent("3"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("17").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 2, "c_field3", 4).setParent("3"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("18").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 2, "c_field3", 5).setParent("3"));
+ indexBuilders.add(client().prepareIndex().setType("child1").setId("1").setIndex("test")
+ .setSource("c_field1", 1, "c_field2", 2, "c_field3", 6).setParent("3"));
+
+ return indexBuilders;
+ }
+
+ public void testScoreForParentChildQueriesWithFunctionScore() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent")
+ .addMapping("child1", "_parent", "type=parent"));
+ ensureGreen();
+
+ indexRandom(true, createDocBuilders().toArray(new IndexRequestBuilder[0]));
+ SearchResponse response = client()
+ .prepareSearch("test")
+ .setQuery(
+ hasChildQuery(
+ "child",
+ QueryBuilders.functionScoreQuery(matchQuery("c_field2", 0),
+ fieldValueFactorFunction("c_field1"))
+ .boostMode(CombineFunction.REPLACE), ScoreMode.Total)).get();
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("1"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(6f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(4f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(3f));
+
+ response = client()
+ .prepareSearch("test")
+ .setQuery(
+ hasChildQuery(
+ "child",
+ QueryBuilders.functionScoreQuery(matchQuery("c_field2", 0),
+ fieldValueFactorFunction("c_field1"))
+ .boostMode(CombineFunction.REPLACE), ScoreMode.Max)).get();
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(4f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("1"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(2f));
+
+ response = client()
+ .prepareSearch("test")
+ .setQuery(
+ hasChildQuery(
+ "child",
+ QueryBuilders.functionScoreQuery(matchQuery("c_field2", 0),
+ fieldValueFactorFunction("c_field1"))
+ .boostMode(CombineFunction.REPLACE), ScoreMode.Avg)).get();
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(4f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("1"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1.5f));
+
+ response = client()
+ .prepareSearch("test")
+ .setQuery(
+ hasParentQuery(
+ "parent",
+ QueryBuilders.functionScoreQuery(matchQuery("p_field1", "p_value3"),
+ fieldValueFactorFunction("p_field2"))
+ .boostMode(CombineFunction.REPLACE), true))
+ .addSort(SortBuilders.fieldSort("c_field3")).addSort(SortBuilders.scoreSort()).get();
+
+ assertThat(response.getHits().getTotalHits(), equalTo(7L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("13"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(5f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("14"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(5f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("15"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(5f));
+ assertThat(response.getHits().getHits()[3].getId(), equalTo("16"));
+ assertThat(response.getHits().getHits()[3].getScore(), equalTo(5f));
+ assertThat(response.getHits().getHits()[4].getId(), equalTo("17"));
+ assertThat(response.getHits().getHits()[4].getScore(), equalTo(5f));
+ assertThat(response.getHits().getHits()[5].getId(), equalTo("18"));
+ assertThat(response.getHits().getHits()[5].getScore(), equalTo(5f));
+ assertThat(response.getHits().getHits()[6].getId(), equalTo("1"));
+ assertThat(response.getHits().getHits()[6].getScore(), equalTo(5f));
+ }
+
+ // Issue #2536
+ public void testParentChildQueriesCanHandleNoRelevantTypesInIndex() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ SearchResponse response = client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", matchQuery("text", "value"), ScoreMode.None)).get();
+ assertNoFailures(response);
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ client().prepareIndex("test", "child1").setSource(jsonBuilder().startObject().field("text", "value").endObject())
+ .setRefreshPolicy(RefreshPolicy.IMMEDIATE).get();
+
+ response = client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", matchQuery("text", "value"), ScoreMode.None)).get();
+ assertNoFailures(response);
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ response = client().prepareSearch("test").setQuery(hasChildQuery("child", matchQuery("text", "value"), ScoreMode.Max))
+ .get();
+ assertNoFailures(response);
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ response = client().prepareSearch("test")
+ .setQuery(hasParentQuery("parent", matchQuery("text", "value"), false)).get();
+ assertNoFailures(response);
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ response = client().prepareSearch("test").setQuery(hasParentQuery("parent", matchQuery("text", "value"), true))
+ .get();
+ assertNoFailures(response);
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+ }
+
+ public void testHasChildAndHasParentFilter_withFilter() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ client().prepareIndex("test", "parent", "1").setSource("p_field", 1).get();
+ client().prepareIndex("test", "child", "2").setParent("1").setSource("c_field", 1).get();
+ client().admin().indices().prepareFlush("test").get();
+
+ client().prepareIndex("test", "type1", "3").setSource("p_field", 2).get();
+ client().admin().indices().prepareFlush("test").get();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery()).filter(hasChildQuery("child", termQuery("c_field", 1), ScoreMode.None)))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery())
+ .filter(hasParentQuery("parent", termQuery("p_field", 1), false))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("2"));
+ }
+
+ public void testHasChildInnerHitsHighlighting() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ client().prepareIndex("test", "parent", "1").setSource("p_field", 1).get();
+ client().prepareIndex("test", "child", "2").setParent("1").setSource("c_field", "foo bar").get();
+ client().admin().indices().prepareFlush("test").get();
+
+ SearchResponse searchResponse = client().prepareSearch("test").setQuery(
+ hasChildQuery("child", matchQuery("c_field", "foo"), ScoreMode.None)
+ .innerHit(new InnerHitBuilder().setHighlightBuilder(
+ new HighlightBuilder().field(new Field("c_field")
+ .highlightQuery(QueryBuilders.matchQuery("c_field", "bar")))), false))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("1"));
+ SearchHit[] searchHits = searchResponse.getHits().getHits()[0].getInnerHits().get("child").getHits();
+ assertThat(searchHits.length, equalTo(1));
+ assertThat(searchHits[0].getHighlightFields().get("c_field").getFragments().length, equalTo(1));
+ assertThat(searchHits[0].getHighlightFields().get("c_field").getFragments()[0].string(), equalTo("foo <em>bar</em>"));
+ }
+
+ public void testHasChildAndHasParentWrappedInAQueryFilter() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // query filter in case for p/c shouldn't execute per segment, but rather
+ client().prepareIndex("test", "parent", "1").setSource("p_field", 1).get();
+ client().admin().indices().prepareFlush("test").setForce(true).get();
+ client().prepareIndex("test", "child", "2").setParent("1").setSource("c_field", 1).get();
+ refresh();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery()).filter(hasChildQuery("child", matchQuery("c_field", 1), ScoreMode.None)))
+ .get();
+ assertSearchHit(searchResponse, 1, hasId("1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery()).filter(hasParentQuery("parent", matchQuery("p_field", 1), false))).get();
+ assertSearchHit(searchResponse, 1, hasId("2"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery())
+ .filter(boolQuery().must(hasChildQuery("child", matchQuery("c_field", 1), ScoreMode.None))))
+ .get();
+ assertSearchHit(searchResponse, 1, hasId("1"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchAllQuery())
+ .filter(boolQuery().must(hasParentQuery("parent", matchQuery("p_field", 1), false)))).get();
+ assertSearchHit(searchResponse, 1, hasId("2"));
+ }
+
+ public void testSimpleQueryRewrite() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent", "p_field", "type=keyword")
+ .addMapping("child", "_parent", "type=parent", "c_field", "type=keyword"));
+ ensureGreen();
+
+ // index simple data
+ int childId = 0;
+ for (int i = 0; i < 10; i++) {
+ String parentId = String.format(Locale.ROOT, "p%03d", i);
+ client().prepareIndex("test", "parent", parentId).setSource("p_field", parentId).get();
+ int j = childId;
+ for (; j < childId + 50; j++) {
+ String childUid = String.format(Locale.ROOT, "c%03d", j);
+ client().prepareIndex("test", "child", childUid).setSource("c_field", childUid).setParent(parentId).get();
+ }
+ childId = j;
+ }
+ refresh();
+
+ SearchType[] searchTypes = new SearchType[]{SearchType.QUERY_THEN_FETCH, SearchType.DFS_QUERY_THEN_FETCH};
+ for (SearchType searchType : searchTypes) {
+ SearchResponse searchResponse = client().prepareSearch("test").setSearchType(searchType)
+ .setQuery(hasChildQuery("child", prefixQuery("c_field", "c"), ScoreMode.Max))
+ .addSort("p_field", SortOrder.ASC)
+ .setSize(5).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(10L));
+ assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("p000"));
+ assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("p001"));
+ assertThat(searchResponse.getHits().getHits()[2].getId(), equalTo("p002"));
+ assertThat(searchResponse.getHits().getHits()[3].getId(), equalTo("p003"));
+ assertThat(searchResponse.getHits().getHits()[4].getId(), equalTo("p004"));
+
+ searchResponse = client().prepareSearch("test").setSearchType(searchType)
+ .setQuery(hasParentQuery("parent", prefixQuery("p_field", "p"), true)).addSort("c_field", SortOrder.ASC)
+ .setSize(5).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(500L));
+ assertThat(searchResponse.getHits().getHits()[0].getId(), equalTo("c000"));
+ assertThat(searchResponse.getHits().getHits()[1].getId(), equalTo("c001"));
+ assertThat(searchResponse.getHits().getHits()[2].getId(), equalTo("c002"));
+ assertThat(searchResponse.getHits().getHits()[3].getId(), equalTo("c003"));
+ assertThat(searchResponse.getHits().getHits()[4].getId(), equalTo("c004"));
+ }
+ }
+
+ // Issue #3144
+ public void testReIndexingParentAndChildDocuments() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "red").setParent("p1").get();
+ client().prepareIndex("test", "child", "c2").setSource("c_field", "yellow").setParent("p1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "x").setParent("p2").get();
+ client().prepareIndex("test", "child", "c4").setSource("c_field", "x").setParent("p2").get();
+
+ refresh();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", termQuery("c_field", "yellow"), ScoreMode.Total)).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+ assertThat(searchResponse.getHits().getAt(0).getSourceAsString(), containsString("\"p_value1\""));
+
+ searchResponse = client()
+ .prepareSearch("test")
+ .setQuery(
+ boolQuery().must(matchQuery("c_field", "x")).must(
+ hasParentQuery("parent", termQuery("p_field", "p_value2"), true))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("c3"));
+ assertThat(searchResponse.getHits().getAt(1).getId(), equalTo("c4"));
+
+ // re-index
+ for (int i = 0; i < 10; i++) {
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "d" + i).setSource("c_field", "red").setParent("p1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "x").setParent("p2").get();
+ client().admin().indices().prepareRefresh("test").get();
+ }
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", termQuery("c_field", "yellow"), ScoreMode.Total))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p1"));
+ assertThat(searchResponse.getHits().getAt(0).getSourceAsString(), containsString("\"p_value1\""));
+
+ searchResponse = client()
+ .prepareSearch("test")
+ .setQuery(
+ boolQuery().must(matchQuery("c_field", "x")).must(
+ hasParentQuery("parent", termQuery("p_field", "p_value2"), true))).get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), Matchers.anyOf(equalTo("c3"), equalTo("c4")));
+ assertThat(searchResponse.getHits().getAt(1).getId(), Matchers.anyOf(equalTo("c3"), equalTo("c4")));
+ }
+
+ // Issue #3203
+ public void testHasChildQueryWithMinimumScore() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "x").setParent("p1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c3").setSource("c_field", "x").setParent("p2").get();
+ client().prepareIndex("test", "child", "c4").setSource("c_field", "x").setParent("p2").get();
+ client().prepareIndex("test", "child", "c5").setSource("c_field", "x").setParent("p2").get();
+ refresh();
+
+ SearchResponse searchResponse = client()
+ .prepareSearch("test").setQuery(hasChildQuery("child", matchAllQuery(), ScoreMode.Total))
+ .setMinScore(3) // Score needs to be 3 or above!
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("p2"));
+ assertThat(searchResponse.getHits().getAt(0).getScore(), equalTo(3.0f));
+ }
+
+ public void testParentFieldQuery() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.refresh_interval", -1, "index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ SearchResponse response = client().prepareSearch("test").setQuery(termQuery("_parent", "p1"))
+ .get();
+ assertHitCount(response, 0L);
+
+ client().prepareIndex("test", "child", "c1").setSource("{}", XContentType.JSON).setParent("p1").get();
+ refresh();
+
+ response = client().prepareSearch("test").setQuery(termQuery("_parent#parent", "p1")).get();
+ assertHitCount(response, 1L);
+
+ response = client().prepareSearch("test").setQuery(queryStringQuery("_parent#parent:p1")).get();
+ assertHitCount(response, 1L);
+
+ client().prepareIndex("test", "child", "c2").setSource("{}", XContentType.JSON).setParent("p2").get();
+ refresh();
+ response = client().prepareSearch("test").setQuery(termsQuery("_parent#parent", "p1", "p2")).get();
+ assertHitCount(response, 2L);
+
+ response = client().prepareSearch("test")
+ .setQuery(boolQuery()
+ .should(termQuery("_parent#parent", "p1"))
+ .should(termQuery("_parent#parent", "p2"))
+ ).get();
+ assertHitCount(response, 2L);
+ }
+
+ public void testParentIdQuery() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.refresh_interval", -1, "index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ client().prepareIndex("test", "child", "c1").setSource("{}", XContentType.JSON).setParent("p1").get();
+ refresh();
+
+ SearchResponse response = client().prepareSearch("test").setQuery(parentId("child", "p1")).get();
+ assertHitCount(response, 1L);
+
+ client().prepareIndex("test", "child", "c2").setSource("{}", XContentType.JSON).setParent("p2").get();
+ refresh();
+
+ response = client().prepareSearch("test")
+ .setQuery(boolQuery()
+ .should(parentId("child", "p1"))
+ .should(parentId("child", "p2"))
+ ).get();
+ assertHitCount(response, 2L);
+ }
+
+ public void testHasChildNotBeingCached() throws IOException {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "parent", "p3").setSource("p_field", "p_value3").get();
+ client().prepareIndex("test", "parent", "p4").setSource("p_field", "p_value4").get();
+ client().prepareIndex("test", "parent", "p5").setSource("p_field", "p_value5").get();
+ client().prepareIndex("test", "parent", "p6").setSource("p_field", "p_value6").get();
+ client().prepareIndex("test", "parent", "p7").setSource("p_field", "p_value7").get();
+ client().prepareIndex("test", "parent", "p8").setSource("p_field", "p_value8").get();
+ client().prepareIndex("test", "parent", "p9").setSource("p_field", "p_value9").get();
+ client().prepareIndex("test", "parent", "p10").setSource("p_field", "p_value10").get();
+ client().prepareIndex("test", "child", "c1").setParent("p1").setSource("c_field", "blue").get();
+ client().admin().indices().prepareFlush("test").get();
+ client().admin().indices().prepareRefresh("test").get();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "blue"), ScoreMode.None)))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+
+ client().prepareIndex("test", "child", "c2").setParent("p2").setSource("c_field", "blue").get();
+ client().admin().indices().prepareRefresh("test").get();
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasChildQuery("child", termQuery("c_field", "blue"), ScoreMode.None)))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ }
+
+ private QueryBuilder randomHasChild(String type, String field, String value) {
+ if (randomBoolean()) {
+ if (randomBoolean()) {
+ return constantScoreQuery(hasChildQuery(type, termQuery(field, value), ScoreMode.None));
+ } else {
+ return boolQuery().must(matchAllQuery()).filter(hasChildQuery(type, termQuery(field, value), ScoreMode.None));
+ }
+ } else {
+ return hasChildQuery(type, termQuery(field, value), ScoreMode.None);
+ }
+ }
+
+ private QueryBuilder randomHasParent(String type, String field, String value) {
+ if (randomBoolean()) {
+ if (randomBoolean()) {
+ return constantScoreQuery(hasParentQuery(type, termQuery(field, value), false));
+ } else {
+ return boolQuery().must(matchAllQuery()).filter(hasParentQuery(type, termQuery(field, value), false));
+ }
+ } else {
+ return hasParentQuery(type, termQuery(field, value), false);
+ }
+ }
+
+ // Issue #3818
+ public void testHasChildQueryOnlyReturnsSingleChildType() {
+ assertAcked(prepareCreate("grandissue")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("grandparent", "name", "type=text")
+ .addMapping("parent", "_parent", "type=grandparent")
+ .addMapping("child_type_one", "_parent", "type=parent")
+ .addMapping("child_type_two", "_parent", "type=parent"));
+
+ client().prepareIndex("grandissue", "grandparent", "1").setSource("name", "Grandpa").get();
+ client().prepareIndex("grandissue", "parent", "2").setParent("1").setSource("name", "Dana").get();
+ client().prepareIndex("grandissue", "child_type_one", "3").setParent("2").setRouting("1")
+ .setSource("name", "William")
+ .get();
+ client().prepareIndex("grandissue", "child_type_two", "4").setParent("2").setRouting("1")
+ .setSource("name", "Kate")
+ .get();
+ refresh();
+
+ SearchResponse searchResponse = client().prepareSearch("grandissue").setQuery(
+ boolQuery().must(
+ hasChildQuery(
+ "parent",
+ boolQuery().must(
+ hasChildQuery(
+ "child_type_one",
+ boolQuery().must(
+ queryStringQuery("name:William*")
+ ),
+ ScoreMode.None)
+ ),
+ ScoreMode.None)
+ )
+ ).get();
+ assertHitCount(searchResponse, 1L);
+
+ searchResponse = client().prepareSearch("grandissue").setQuery(
+ boolQuery().must(
+ hasChildQuery(
+ "parent",
+ boolQuery().must(
+ hasChildQuery(
+ "child_type_two",
+ boolQuery().must(
+ queryStringQuery("name:William*")
+ ),
+ ScoreMode.None)
+ ),
+ ScoreMode.None)
+ )
+ ).get();
+ assertHitCount(searchResponse, 0L);
+ }
+
+ public void testIndexChildDocWithNoParentMapping() throws IOException {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child1"));
+ ensureGreen();
+
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ try {
+ client().prepareIndex("test", "child1", "c1").setParent("p1").setSource("c_field", "blue").get();
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.toString(), containsString("can't specify parent if no parent field has been configured"));
+ }
+ try {
+ client().prepareIndex("test", "child2", "c2").setParent("p1").setSource("c_field", "blue").get();
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.toString(), containsString("can't specify parent if no parent field has been configured"));
+ }
+
+ refresh();
+ }
+
+ public void testAddingParentToExistingMapping() throws IOException {
+ createIndex("test");
+ ensureGreen();
+
+ PutMappingResponse putMappingResponse = client().admin().indices()
+ .preparePutMapping("test").setType("child").setSource("number", "type=integer")
+ .get();
+ assertThat(putMappingResponse.isAcknowledged(), equalTo(true));
+
+ GetMappingsResponse getMappingsResponse = client().admin().indices().prepareGetMappings("test").get();
+ Map<String, Object> mapping = getMappingsResponse.getMappings().get("test").get("child").getSourceAsMap();
+ assertThat(mapping.size(), greaterThanOrEqualTo(1)); // there are potentially some meta fields configured randomly
+ assertThat(mapping.get("properties"), notNullValue());
+
+ try {
+ // Adding _parent metadata field to existing mapping is prohibited:
+ client().admin().indices().preparePutMapping("test").setType("child").setSource(jsonBuilder().startObject().startObject("child")
+ .startObject("_parent").field("type", "parent").endObject()
+ .endObject().endObject()).get();
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.toString(), containsString("The _parent field's type option can't be changed: [null]->[parent]"));
+ }
+ }
+
+ public void testHasChildQueryWithNestedInnerObjects() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent", "objects", "type=nested")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ client().prepareIndex("test", "parent", "p1")
+ .setSource(jsonBuilder().startObject().field("p_field", "1").startArray("objects")
+ .startObject().field("i_field", "1").endObject()
+ .startObject().field("i_field", "2").endObject()
+ .startObject().field("i_field", "3").endObject()
+ .startObject().field("i_field", "4").endObject()
+ .startObject().field("i_field", "5").endObject()
+ .startObject().field("i_field", "6").endObject()
+ .endArray().endObject())
+ .get();
+ client().prepareIndex("test", "parent", "p2")
+ .setSource(jsonBuilder().startObject().field("p_field", "2").startArray("objects")
+ .startObject().field("i_field", "1").endObject()
+ .startObject().field("i_field", "2").endObject()
+ .endArray().endObject())
+ .get();
+ client().prepareIndex("test", "child", "c1").setParent("p1").setSource("c_field", "blue").get();
+ client().prepareIndex("test", "child", "c2").setParent("p1").setSource("c_field", "red").get();
+ client().prepareIndex("test", "child", "c3").setParent("p2").setSource("c_field", "red").get();
+ refresh();
+
+ ScoreMode scoreMode = randomFrom(ScoreMode.values());
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(hasChildQuery("child", termQuery("c_field", "blue"), scoreMode))
+ .filter(boolQuery().mustNot(termQuery("p_field", "3"))))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(boolQuery().must(hasChildQuery("child", termQuery("c_field", "red"), scoreMode))
+ .filter(boolQuery().mustNot(termQuery("p_field", "3"))))
+ .get();
+ assertNoFailures(searchResponse);
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ }
+
+ public void testNamedFilters() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ String parentId = "p1";
+ client().prepareIndex("test", "parent", parentId).setSource("p_field", "1").get();
+ client().prepareIndex("test", "child", "c1").setSource("c_field", "1").setParent(parentId).get();
+ refresh();
+
+ SearchResponse searchResponse = client().prepareSearch("test").setQuery(hasChildQuery("child",
+ termQuery("c_field", "1"), ScoreMode.Max).queryName("test"))
+ .get();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries().length, equalTo(1));
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries()[0], equalTo("test"));
+
+ searchResponse = client().prepareSearch("test").setQuery(hasParentQuery("parent",
+ termQuery("p_field", "1"), true).queryName("test"))
+ .get();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries().length, equalTo(1));
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries()[0], equalTo("test"));
+
+ searchResponse = client().prepareSearch("test").setQuery(constantScoreQuery(hasChildQuery("child",
+ termQuery("c_field", "1"), ScoreMode.None).queryName("test")))
+ .get();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries().length, equalTo(1));
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries()[0], equalTo("test"));
+
+ searchResponse = client().prepareSearch("test").setQuery(constantScoreQuery(hasParentQuery("parent",
+ termQuery("p_field", "1"), false).queryName("test")))
+ .get();
+ assertHitCount(searchResponse, 1L);
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries().length, equalTo(1));
+ assertThat(searchResponse.getHits().getAt(0).getMatchedQueries()[0], equalTo("test"));
+ }
+
+ public void testParentChildQueriesNoParentType() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings(Settings.builder()
+ .put(indexSettings())
+ .put("index.refresh_interval", -1)));
+ ensureGreen();
+
+ String parentId = "p1";
+ client().prepareIndex("test", "parent", parentId).setSource("p_field", "1").get();
+ refresh();
+
+ try {
+ client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", termQuery("c_field", "1"), ScoreMode.None))
+ .get();
+ fail();
+ } catch (SearchPhaseExecutionException e) {
+ assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST));
+ }
+
+ try {
+ client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", termQuery("c_field", "1"), ScoreMode.Max))
+ .get();
+ fail();
+ } catch (SearchPhaseExecutionException e) {
+ assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST));
+ }
+
+ try {
+ client().prepareSearch("test")
+ .setPostFilter(hasChildQuery("child", termQuery("c_field", "1"), ScoreMode.None))
+ .get();
+ fail();
+ } catch (SearchPhaseExecutionException e) {
+ assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST));
+ }
+
+ try {
+ client().prepareSearch("test")
+ .setQuery(hasParentQuery("parent", termQuery("p_field", "1"), true))
+ .get();
+ fail();
+ } catch (SearchPhaseExecutionException e) {
+ assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST));
+ }
+
+ try {
+ client().prepareSearch("test")
+ .setPostFilter(hasParentQuery("parent", termQuery("p_field", "1"), false))
+ .get();
+ fail();
+ } catch (SearchPhaseExecutionException e) {
+ assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST));
+ }
+ }
+
+ public void testParentChildCaching() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.refresh_interval", -1, "index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ // index simple data
+ client().prepareIndex("test", "parent", "p1").setSource("p_field", "p_value1").get();
+ client().prepareIndex("test", "parent", "p2").setSource("p_field", "p_value2").get();
+ client().prepareIndex("test", "child", "c1").setParent("p1").setSource("c_field", "blue").get();
+ client().prepareIndex("test", "child", "c2").setParent("p1").setSource("c_field", "red").get();
+ client().prepareIndex("test", "child", "c3").setParent("p2").setSource("c_field", "red").get();
+ client().admin().indices().prepareForceMerge("test").setMaxNumSegments(1).setFlush(true).get();
+ client().prepareIndex("test", "parent", "p3").setSource("p_field", "p_value3").get();
+ client().prepareIndex("test", "parent", "p4").setSource("p_field", "p_value4").get();
+ client().prepareIndex("test", "child", "c4").setParent("p3").setSource("c_field", "green").get();
+ client().prepareIndex("test", "child", "c5").setParent("p3").setSource("c_field", "blue").get();
+ client().prepareIndex("test", "child", "c6").setParent("p4").setSource("c_field", "blue").get();
+ client().admin().indices().prepareFlush("test").get();
+ client().admin().indices().prepareRefresh("test").get();
+
+ for (int i = 0; i < 2; i++) {
+ SearchResponse searchResponse = client().prepareSearch()
+ .setQuery(boolQuery().must(matchAllQuery()).filter(boolQuery()
+ .must(hasChildQuery("child", matchQuery("c_field", "red"), ScoreMode.None))
+ .must(matchAllQuery())))
+ .get();
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(2L));
+ }
+
+
+ client().prepareIndex("test", "child", "c3").setParent("p2").setSource("c_field", "blue").get();
+ client().admin().indices().prepareRefresh("test").get();
+
+ SearchResponse searchResponse = client().prepareSearch()
+ .setQuery(boolQuery().must(matchAllQuery()).filter(boolQuery()
+ .must(hasChildQuery("child", matchQuery("c_field", "red"), ScoreMode.None))
+ .must(matchAllQuery())))
+ .get();
+
+ assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L));
+ }
+
+ public void testParentChildQueriesViaScrollApi() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+ for (int i = 0; i < 10; i++) {
+ client().prepareIndex("test", "parent", "p" + i).setSource("{}", XContentType.JSON).get();
+ client().prepareIndex("test", "child", "c" + i).setSource("{}", XContentType.JSON).setParent("p" + i).get();
+ }
+
+ refresh();
+
+ QueryBuilder[] queries = new QueryBuilder[]{
+ hasChildQuery("child", matchAllQuery(), ScoreMode.None),
+ boolQuery().must(matchAllQuery()).filter(hasChildQuery("child", matchAllQuery(), ScoreMode.None)),
+ hasParentQuery("parent", matchAllQuery(), false),
+ boolQuery().must(matchAllQuery()).filter(hasParentQuery("parent", matchAllQuery(), false))
+ };
+
+ for (QueryBuilder query : queries) {
+ SearchResponse scrollResponse = client().prepareSearch("test")
+ .setScroll(TimeValue.timeValueSeconds(30))
+ .setSize(1)
+ .addStoredField("_id")
+ .setQuery(query)
+ .execute()
+ .actionGet();
+
+ assertNoFailures(scrollResponse);
+ assertThat(scrollResponse.getHits().getTotalHits(), equalTo(10L));
+ int scannedDocs = 0;
+ do {
+ assertThat(scrollResponse.getHits().getTotalHits(), equalTo(10L));
+ scannedDocs += scrollResponse.getHits().getHits().length;
+ scrollResponse = client()
+ .prepareSearchScroll(scrollResponse.getScrollId())
+ .setScroll(TimeValue.timeValueSeconds(30)).get();
+ } while (scrollResponse.getHits().getHits().length > 0);
+ clearScroll(scrollResponse.getScrollId());
+ assertThat(scannedDocs, equalTo(10));
+ }
+ }
+
+ // Issue #5783
+ public void testQueryBeforeChildType() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("features")
+ .addMapping("posts", "_parent", "type=features")
+ .addMapping("specials"));
+ ensureGreen();
+
+ client().prepareIndex("test", "features", "1").setSource("field", "foo").get();
+ client().prepareIndex("test", "posts", "1").setParent("1").setSource("field", "bar").get();
+ refresh();
+
+ SearchResponse resp;
+ resp = client().prepareSearch("test")
+ .setSource(new SearchSourceBuilder().query(hasChildQuery("posts",
+ QueryBuilders.matchQuery("field", "bar"), ScoreMode.None)))
+ .get();
+ assertHitCount(resp, 1L);
+ }
+
+ // Issue #6256
+ public void testParentFieldInMultiMatchField() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("type1")
+ .addMapping("type2", "_parent", "type=type1")
+ );
+ ensureGreen();
+
+ client().prepareIndex("test", "type2", "1").setParent("1").setSource("field", "value").get();
+ refresh();
+
+ SearchResponse response = client().prepareSearch("test")
+ .setQuery(multiMatchQuery("1", "_parent#type1"))
+ .get();
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getAt(0).getId(), equalTo("1"));
+ }
+
+ public void testTypeIsAppliedInHasParentInnerQuery() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ List<IndexRequestBuilder> indexRequests = new ArrayList<>();
+ indexRequests.add(client().prepareIndex("test", "parent", "1").setSource("field1", "a"));
+ indexRequests.add(client().prepareIndex("test", "child", "1").setParent("1").setSource("{}", XContentType.JSON));
+ indexRequests.add(client().prepareIndex("test", "child", "2").setParent("1").setSource("{}", XContentType.JSON));
+ indexRandom(true, indexRequests);
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasParentQuery("parent", boolQuery().mustNot(termQuery("field1", "a")), false)))
+ .get();
+ assertHitCount(searchResponse, 0L);
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(hasParentQuery("parent", constantScoreQuery(boolQuery().mustNot(termQuery("field1", "a"))), false))
+ .get();
+ assertHitCount(searchResponse, 0L);
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(constantScoreQuery(hasParentQuery("parent", termQuery("field1", "a"), false)))
+ .get();
+ assertHitCount(searchResponse, 2L);
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(hasParentQuery("parent", constantScoreQuery(termQuery("field1", "a")), false))
+ .get();
+ assertHitCount(searchResponse, 2L);
+ }
+
+ private List<IndexRequestBuilder> createMinMaxDocBuilders() {
+ List<IndexRequestBuilder> indexBuilders = new ArrayList<>();
+ // Parent 1 and its children
+ indexBuilders.add(client().prepareIndex().setType("parent").setId("1").setIndex("test").setSource("id",1));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("10").setIndex("test")
+ .setSource("foo", "one").setParent("1"));
+
+ // Parent 2 and its children
+ indexBuilders.add(client().prepareIndex().setType("parent").setId("2").setIndex("test").setSource("id",2));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("11").setIndex("test")
+ .setSource("foo", "one").setParent("2"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("12").setIndex("test")
+ .setSource("foo", "one two").setParent("2"));
+
+ // Parent 3 and its children
+ indexBuilders.add(client().prepareIndex().setType("parent").setId("3").setIndex("test").setSource("id",3));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("13").setIndex("test")
+ .setSource("foo", "one").setParent("3"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("14").setIndex("test")
+ .setSource("foo", "one two").setParent("3"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("15").setIndex("test")
+ .setSource("foo", "one two three").setParent("3"));
+
+ // Parent 4 and its children
+ indexBuilders.add(client().prepareIndex().setType("parent").setId("4").setIndex("test").setSource("id",4));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("16").setIndex("test")
+ .setSource("foo", "one").setParent("4"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("17").setIndex("test")
+ .setSource("foo", "one two").setParent("4"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("18").setIndex("test")
+ .setSource("foo", "one two three").setParent("4"));
+ indexBuilders.add(client().prepareIndex().setType("child").setId("19").setIndex("test")
+ .setSource("foo", "one two three four").setParent("4"));
+
+ return indexBuilders;
+ }
+
+ private SearchResponse minMaxQuery(ScoreMode scoreMode, int minChildren, Integer maxChildren) throws SearchPhaseExecutionException {
+ HasChildQueryBuilder hasChildQuery = hasChildQuery(
+ "child",
+ QueryBuilders.functionScoreQuery(constantScoreQuery(QueryBuilders.termQuery("foo", "two")),
+ new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
+ new FunctionScoreQueryBuilder.FilterFunctionBuilder(weightFactorFunction(1)),
+ new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("foo", "three"),
+ weightFactorFunction(1)),
+ new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("foo", "four"),
+ weightFactorFunction(1))
+ }).boostMode(CombineFunction.REPLACE).scoreMode(FiltersFunctionScoreQuery.ScoreMode.SUM), scoreMode)
+ .minMaxChildren(minChildren, maxChildren != null ? maxChildren : HasChildQueryBuilder.DEFAULT_MAX_CHILDREN);
+
+ return client()
+ .prepareSearch("test")
+ .setQuery(hasChildQuery)
+ .addSort("_score", SortOrder.DESC).addSort("id", SortOrder.ASC).get();
+ }
+
+ public void testMinMaxChildren() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent", "id", "type=long")
+ .addMapping("child", "_parent", "type=parent"));
+ ensureGreen();
+
+ indexRandom(true, createMinMaxDocBuilders().toArray(new IndexRequestBuilder[0]));
+ SearchResponse response;
+
+ // Score mode = NONE
+ response = minMaxQuery(ScoreMode.None, 0, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.None, 1, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.None, 2, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.None, 3, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.None, 4, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ response = minMaxQuery(ScoreMode.None, 0, 4);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.None, 0, 3);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.None, 0, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.None, 2, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1f));
+
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> minMaxQuery(ScoreMode.None, 3, 2));
+ assertThat(e.getMessage(), equalTo("[has_child] 'max_children' is less than 'min_children'"));
+
+ // Score mode = SUM
+ response = minMaxQuery(ScoreMode.Total, 0, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(6f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Total, 1, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(6f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Total, 2, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(6f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(3f));
+
+ response = minMaxQuery(ScoreMode.Total, 3, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(6f));
+
+ response = minMaxQuery(ScoreMode.Total, 4, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ response = minMaxQuery(ScoreMode.Total, 0, 4);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(6f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Total, 0, 3);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(6f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Total, 0, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Total, 2, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+
+ e = expectThrows(IllegalArgumentException.class, () -> minMaxQuery(ScoreMode.Total, 3, 2));
+ assertThat(e.getMessage(), equalTo("[has_child] 'max_children' is less than 'min_children'"));
+
+ // Score mode = MAX
+ response = minMaxQuery(ScoreMode.Max, 0, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Max, 1, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Max, 2, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(2f));
+
+ response = minMaxQuery(ScoreMode.Max, 3, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+
+ response = minMaxQuery(ScoreMode.Max, 4, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ response = minMaxQuery(ScoreMode.Max, 0, 4);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Max, 0, 3);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(3f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Max, 0, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Max, 2, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+
+ e = expectThrows(IllegalArgumentException.class, () -> minMaxQuery(ScoreMode.Max, 3, 2));
+ assertThat(e.getMessage(), equalTo("[has_child] 'max_children' is less than 'min_children'"));
+
+ // Score mode = AVG
+ response = minMaxQuery(ScoreMode.Avg, 0, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1.5f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Avg, 1, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1.5f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Avg, 2, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1.5f));
+
+ response = minMaxQuery(ScoreMode.Avg, 3, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+
+ response = minMaxQuery(ScoreMode.Avg, 4, null);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(0L));
+
+ response = minMaxQuery(ScoreMode.Avg, 0, 4);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1.5f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Avg, 0, 3);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(3L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("4"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(2f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1.5f));
+ assertThat(response.getHits().getHits()[2].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[2].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Avg, 0, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(2L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1.5f));
+ assertThat(response.getHits().getHits()[1].getId(), equalTo("2"));
+ assertThat(response.getHits().getHits()[1].getScore(), equalTo(1f));
+
+ response = minMaxQuery(ScoreMode.Avg, 2, 2);
+
+ assertThat(response.getHits().getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getHits()[0].getId(), equalTo("3"));
+ assertThat(response.getHits().getHits()[0].getScore(), equalTo(1.5f));
+
+ e = expectThrows(IllegalArgumentException.class, () -> minMaxQuery(ScoreMode.Avg, 3, 2));
+ assertThat(e.getMessage(), equalTo("[has_child] 'max_children' is less than 'min_children'"));
+ }
+
+ public void testParentFieldToNonExistingType() {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent").addMapping("child", "_parent", "type=parent2"));
+ client().prepareIndex("test", "parent", "1").setSource("{}", XContentType.JSON).get();
+ client().prepareIndex("test", "child", "1").setParent("1").setSource("{}", XContentType.JSON).get();
+ refresh();
+
+ try {
+ client().prepareSearch("test")
+ .setQuery(hasChildQuery("child", matchAllQuery(), ScoreMode.None))
+ .get();
+ fail();
+ } catch (SearchPhaseExecutionException e) {
+ }
+ }
+
+ public void testHasParentInnerQueryType() {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent-type").addMapping("child-type", "_parent", "type=parent-type"));
+ client().prepareIndex("test", "child-type", "child-id").setParent("parent-id").setSource("{}", XContentType.JSON).get();
+ client().prepareIndex("test", "parent-type", "parent-id").setSource("{}", XContentType.JSON).get();
+ refresh();
+ //make sure that when we explicitly set a type, the inner query is executed in the context of the parent type instead
+ SearchResponse searchResponse = client().prepareSearch("test").setTypes("child-type").setQuery(
+ hasParentQuery("parent-type", new IdsQueryBuilder().addIds("parent-id"), false)).get();
+ assertSearchHits(searchResponse, "child-id");
+ }
+
+ public void testHasChildInnerQueryType() {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent-type").addMapping("child-type", "_parent", "type=parent-type"));
+ client().prepareIndex("test", "child-type", "child-id").setParent("parent-id").setSource("{}", XContentType.JSON).get();
+ client().prepareIndex("test", "parent-type", "parent-id").setSource("{}", XContentType.JSON).get();
+ refresh();
+ //make sure that when we explicitly set a type, the inner query is executed in the context of the child type instead
+ SearchResponse searchResponse = client().prepareSearch("test").setTypes("parent-type").setQuery(
+ hasChildQuery("child-type", new IdsQueryBuilder().addIds("child-id"), ScoreMode.None)).get();
+ assertSearchHits(searchResponse, "parent-id");
+ }
+
+ public void testHighlightersIgnoreParentChild() {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent-type", "searchText", "type=text,term_vector=with_positions_offsets,index_options=offsets")
+ .addMapping("child-type", "_parent", "type=parent-type", "searchText",
+ "type=text,term_vector=with_positions_offsets,index_options=offsets"));
+ client().prepareIndex("test", "parent-type", "parent-id")
+ .setSource("searchText", "quick brown fox").get();
+ client().prepareIndex("test", "child-type", "child-id")
+ .setParent("parent-id").setSource("searchText", "quick brown fox").get();
+ refresh();
+
+ String[] highlightTypes = new String[] {"plain", "fvh", "postings"};
+ for (String highlightType : highlightTypes) {
+ logger.info("Testing with highlight type [{}]", highlightType);
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(new BoolQueryBuilder()
+ .must(new MatchQueryBuilder("searchText", "fox"))
+ .must(new HasChildQueryBuilder("child-type", new MatchAllQueryBuilder(), ScoreMode.None))
+ )
+ .highlighter(new HighlightBuilder().field(new HighlightBuilder.Field("searchText").highlighterType(highlightType)))
+ .get();
+ assertHitCount(searchResponse, 1);
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("parent-id"));
+ HighlightField highlightField = searchResponse.getHits().getAt(0).getHighlightFields().get("searchText");
+ assertThat(highlightField.getFragments()[0].string(), equalTo("quick brown <em>fox</em>"));
+
+ searchResponse = client().prepareSearch("test")
+ .setQuery(new BoolQueryBuilder()
+ .must(new MatchQueryBuilder("searchText", "fox"))
+ .must(new HasParentQueryBuilder("parent-type", new MatchAllQueryBuilder(), false))
+ )
+ .highlighter(new HighlightBuilder().field(new HighlightBuilder.Field("searchText").highlighterType(highlightType)))
+ .get();
+ assertHitCount(searchResponse, 1);
+ assertThat(searchResponse.getHits().getAt(0).getId(), equalTo("child-id"));
+ highlightField = searchResponse.getHits().getAt(0).getHighlightFields().get("searchText");
+ assertThat(highlightField.getFragments()[0].string(), equalTo("quick brown <em>fox</em>"));
+ }
+ }
+
+ public void testAliasesFilterWithHasChildQuery() throws Exception {
+ assertAcked(prepareCreate("my-index")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child", "_parent", "type=parent")
+ );
+ client().prepareIndex("my-index", "parent", "1").setSource("{}", XContentType.JSON).get();
+ client().prepareIndex("my-index", "child", "2").setSource("{}", XContentType.JSON).setParent("1").get();
+ refresh();
+
+ assertAcked(admin().indices().prepareAliases().addAlias("my-index", "filter1",
+ hasChildQuery("child", matchAllQuery(), ScoreMode.None)));
+ assertAcked(admin().indices().prepareAliases().addAlias("my-index", "filter2",
+ hasParentQuery("parent", matchAllQuery(), false)));
+
+ SearchResponse response = client().prepareSearch("filter1").get();
+ assertHitCount(response, 1);
+ assertThat(response.getHits().getAt(0).getId(), equalTo("1"));
+ response = client().prepareSearch("filter2").get();
+ assertHitCount(response, 1);
+ assertThat(response.getHits().getAt(0).getId(), equalTo("2"));
+ }
+
+ /*
+ Test for https://github.com/elastic/elasticsearch/issues/3444
+ */
+ public void testBulkUpdateDocAsUpsertWithParent() throws Exception {
+ client().admin().indices().prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent", "{\"parent\":{}}", XContentType.JSON)
+ .addMapping("child", "{\"child\": {\"_parent\": {\"type\": \"parent\"}}}", XContentType.JSON)
+ .execute().actionGet();
+ ensureGreen();
+
+ BulkRequestBuilder builder = client().prepareBulk();
+
+ // It's important to use JSON parsing here and request objects: issue 3444 is related to incomplete option parsing
+ byte[] addParent = new BytesArray(
+ "{" +
+ " \"index\" : {" +
+ " \"_index\" : \"test\"," +
+ " \"_type\" : \"parent\"," +
+ " \"_id\" : \"parent1\"" +
+ " }" +
+ "}" +
+ "\n" +
+ "{" +
+ " \"field1\" : \"value1\"" +
+ "}" +
+ "\n").array();
+
+ byte[] addChild = new BytesArray(
+ "{" +
+ " \"update\" : {" +
+ " \"_index\" : \"test\"," +
+ " \"_type\" : \"child\"," +
+ " \"_id\" : \"child1\"," +
+ " \"parent\" : \"parent1\"" +
+ " }" +
+ "}" +
+ "\n" +
+ "{" +
+ " \"doc\" : {" +
+ " \"field1\" : \"value1\"" +
+ " }," +
+ " \"doc_as_upsert\" : \"true\"" +
+ "}" +
+ "\n").array();
+
+ builder.add(addParent, 0, addParent.length, XContentType.JSON);
+ builder.add(addChild, 0, addChild.length, XContentType.JSON);
+
+ BulkResponse bulkResponse = builder.get();
+ assertThat(bulkResponse.getItems().length, equalTo(2));
+ assertThat(bulkResponse.getItems()[0].isFailed(), equalTo(false));
+ assertThat(bulkResponse.getItems()[1].isFailed(), equalTo(false));
+
+ client().admin().indices().prepareRefresh("test").get();
+
+ //we check that the _parent field was set on the child document by using the has parent query
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(hasParentQuery("parent", QueryBuilders.matchAllQuery(), false))
+ .get();
+
+ assertNoFailures(searchResponse);
+ assertSearchHits(searchResponse, "child1");
+ }
+
+ /*
+ Test for https://github.com/elastic/elasticsearch/issues/3444
+ */
+ public void testBulkUpdateUpsertWithParent() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent", "{\"parent\":{}}", XContentType.JSON)
+ .addMapping("child", "{\"child\": {\"_parent\": {\"type\": \"parent\"}}}", XContentType.JSON));
+ ensureGreen();
+
+ BulkRequestBuilder builder = client().prepareBulk();
+
+ byte[] addParent = new BytesArray(
+ "{" +
+ " \"index\" : {" +
+ " \"_index\" : \"test\"," +
+ " \"_type\" : \"parent\"," +
+ " \"_id\" : \"parent1\"" +
+ " }" +
+ "}" +
+ "\n" +
+ "{" +
+ " \"field1\" : \"value1\"" +
+ "}" +
+ "\n").array();
+
+ byte[] addChild1 = new BytesArray(
+ "{" +
+ " \"update\" : {" +
+ " \"_index\" : \"test\"," +
+ " \"_type\" : \"child\"," +
+ " \"_id\" : \"child1\"," +
+ " \"parent\" : \"parent1\"" +
+ " }" +
+ "}" +
+ "\n" +
+ "{" +
+ " \"script\" : {" +
+ " \"inline\" : \"ctx._source.field2 = 'value2'\"" +
+ " }," +
+ " \"lang\" : \"" + InnerHitsIT.CustomScriptPlugin.NAME + "\"," +
+ " \"upsert\" : {" +
+ " \"field1\" : \"value1'\"" +
+ " }" +
+ "}" +
+ "\n").array();
+
+ byte[] addChild2 = new BytesArray(
+ "{" +
+ " \"update\" : {" +
+ " \"_index\" : \"test\"," +
+ " \"_type\" : \"child\"," +
+ " \"_id\" : \"child1\"," +
+ " \"parent\" : \"parent1\"" +
+ " }" +
+ "}" +
+ "\n" +
+ "{" +
+ " \"script\" : \"ctx._source.field2 = 'value2'\"," +
+ " \"upsert\" : {" +
+ " \"field1\" : \"value1'\"" +
+ " }" +
+ "}" +
+ "\n").array();
+
+ builder.add(addParent, 0, addParent.length, XContentType.JSON);
+ builder.add(addChild1, 0, addChild1.length, XContentType.JSON);
+ builder.add(addChild2, 0, addChild2.length, XContentType.JSON);
+
+ BulkResponse bulkResponse = builder.get();
+ assertThat(bulkResponse.getItems().length, equalTo(3));
+ assertThat(bulkResponse.getItems()[0].isFailed(), equalTo(false));
+ assertThat(bulkResponse.getItems()[1].isFailed(), equalTo(false));
+ assertThat(bulkResponse.getItems()[2].isFailed(), equalTo(true));
+ assertThat(bulkResponse.getItems()[2].getFailure().getCause().getCause().getMessage(),
+ equalTo("script_lang not supported [painless]"));
+
+ client().admin().indices().prepareRefresh("test").get();
+
+ SearchResponse searchResponse = client().prepareSearch("test")
+ .setQuery(hasParentQuery("parent", QueryBuilders.matchAllQuery(), false))
+ .get();
+
+ assertSearchHits(searchResponse, "child1");
+ }
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java
new file mode 100644
index 0000000000..8f4fc9d0c3
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasChildQueryBuilderTests.java
@@ -0,0 +1,326 @@
+/*
+ * 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 com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import org.apache.lucene.search.TermInSetQuery;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.ConstantScoreQuery;
+import org.apache.lucene.search.MatchNoDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.join.ScoreMode;
+import org.apache.lucene.search.similarities.PerFieldSimilarityWrapper;
+import org.apache.lucene.search.similarities.Similarity;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.Version;
+import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.mapper.TypeFieldMapper;
+import org.elasticsearch.index.mapper.Uid;
+import org.elasticsearch.index.mapper.UidFieldMapper;
+import org.elasticsearch.index.query.IdsQueryBuilder;
+import org.elasticsearch.index.query.InnerHitBuilder;
+import org.elasticsearch.index.query.MatchAllQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryShardContext;
+import org.elasticsearch.index.query.QueryShardException;
+import org.elasticsearch.index.query.TermQueryBuilder;
+import org.elasticsearch.index.query.WrapperQueryBuilder;
+import org.elasticsearch.index.similarity.SimilarityService;
+import org.elasticsearch.join.ParentJoinPlugin;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.search.fetch.subphase.InnerHitsContext;
+import org.elasticsearch.search.internal.SearchContext;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.test.AbstractQueryTestCase;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasChildQuery;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.notNullValue;
+
+public class HasChildQueryBuilderTests extends AbstractQueryTestCase<HasChildQueryBuilder> {
+ protected static final String PARENT_TYPE = "parent";
+ protected static final String CHILD_TYPE = "child";
+
+ private static String similarity;
+
+ boolean requiresRewrite = false;
+
+ @Override
+ protected Collection<Class<? extends Plugin>> getPlugins() {
+ return Collections.singletonList(ParentJoinPlugin.class);
+ }
+
+ @Override
+ protected void initializeAdditionalMappings(MapperService mapperService) throws IOException {
+ similarity = randomFrom("classic", "BM25");
+ mapperService.merge(PARENT_TYPE, new CompressedXContent(PutMappingRequest.buildFromSimplifiedDef(PARENT_TYPE,
+ STRING_FIELD_NAME, "type=text",
+ STRING_FIELD_NAME_2, "type=keyword",
+ INT_FIELD_NAME, "type=integer",
+ DOUBLE_FIELD_NAME, "type=double",
+ BOOLEAN_FIELD_NAME, "type=boolean",
+ DATE_FIELD_NAME, "type=date",
+ OBJECT_FIELD_NAME, "type=object"
+ ).string()), MapperService.MergeReason.MAPPING_UPDATE, false);
+ mapperService.merge(CHILD_TYPE, new CompressedXContent(PutMappingRequest.buildFromSimplifiedDef(CHILD_TYPE,
+ "_parent", "type=" + PARENT_TYPE,
+ STRING_FIELD_NAME, "type=text",
+ "custom_string", "type=text,similarity=" + similarity,
+ INT_FIELD_NAME, "type=integer",
+ DOUBLE_FIELD_NAME, "type=double",
+ BOOLEAN_FIELD_NAME, "type=boolean",
+ DATE_FIELD_NAME, "type=date",
+ OBJECT_FIELD_NAME, "type=object"
+ ).string()), MapperService.MergeReason.MAPPING_UPDATE, false);
+ }
+
+ /**
+ * @return a {@link HasChildQueryBuilder} with random values all over the place
+ */
+ @Override
+ protected HasChildQueryBuilder doCreateTestQueryBuilder() {
+ int min = randomIntBetween(0, Integer.MAX_VALUE / 2);
+ int max = randomIntBetween(min, Integer.MAX_VALUE);
+
+ QueryBuilder innerQueryBuilder = new MatchAllQueryBuilder();
+ if (randomBoolean()) {
+ requiresRewrite = true;
+ innerQueryBuilder = new WrapperQueryBuilder(innerQueryBuilder.toString());
+ }
+
+ HasChildQueryBuilder hqb = new HasChildQueryBuilder(CHILD_TYPE, innerQueryBuilder,
+ RandomPicks.randomFrom(random(), ScoreMode.values()));
+ hqb.minMaxChildren(min, max);
+ hqb.ignoreUnmapped(randomBoolean());
+ if (randomBoolean()) {
+ hqb.innerHit(new InnerHitBuilder()
+ .setName(randomAlphaOfLengthBetween(1, 10))
+ .setSize(randomIntBetween(0, 100))
+ .addSort(new FieldSortBuilder(STRING_FIELD_NAME_2).order(SortOrder.ASC)), hqb.ignoreUnmapped());
+ }
+ return hqb;
+ }
+
+ @Override
+ protected void doAssertLuceneQuery(HasChildQueryBuilder queryBuilder, Query query, SearchContext searchContext) throws IOException {
+ assertThat(query, instanceOf(HasChildQueryBuilder.LateParsingQuery.class));
+ HasChildQueryBuilder.LateParsingQuery lpq = (HasChildQueryBuilder.LateParsingQuery) query;
+ assertEquals(queryBuilder.minChildren(), lpq.getMinChildren());
+ assertEquals(queryBuilder.maxChildren(), lpq.getMaxChildren());
+ assertEquals(queryBuilder.scoreMode(), lpq.getScoreMode()); // WTF is this why do we have two?
+ if (queryBuilder.innerHit() != null) {
+ // have to rewrite again because the provided queryBuilder hasn't been rewritten (directly returned from
+ // doCreateTestQueryBuilder)
+ queryBuilder = (HasChildQueryBuilder) queryBuilder.rewrite(searchContext.getQueryShardContext());
+ Map<String, InnerHitBuilder> innerHitBuilders = new HashMap<>();
+ InnerHitBuilder.extractInnerHits(queryBuilder, innerHitBuilders);
+ for (InnerHitBuilder builder : innerHitBuilders.values()) {
+ builder.build(searchContext, searchContext.innerHits());
+ }
+ assertNotNull(searchContext.innerHits());
+ assertEquals(1, searchContext.innerHits().getInnerHits().size());
+ assertTrue(searchContext.innerHits().getInnerHits().containsKey(queryBuilder.innerHit().getName()));
+ InnerHitsContext.BaseInnerHits innerHits =
+ searchContext.innerHits().getInnerHits().get(queryBuilder.innerHit().getName());
+ assertEquals(innerHits.size(), queryBuilder.innerHit().getSize());
+ assertEquals(innerHits.sort().sort.getSort().length, 1);
+ assertEquals(innerHits.sort().sort.getSort()[0].getField(), STRING_FIELD_NAME_2);
+ }
+ }
+
+ public void testIllegalValues() {
+ QueryBuilder query = new MatchAllQueryBuilder();
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
+ () -> hasChildQuery(null, query, ScoreMode.None));
+ assertEquals("[has_child] requires 'type' field", e.getMessage());
+
+ e = expectThrows(IllegalArgumentException.class, () -> hasChildQuery("foo", null, ScoreMode.None));
+ assertEquals("[has_child] requires 'query' field", e.getMessage());
+
+ e = expectThrows(IllegalArgumentException.class, () -> hasChildQuery("foo", query, null));
+ assertEquals("[has_child] requires 'score_mode' field", e.getMessage());
+
+ int positiveValue = randomIntBetween(0, Integer.MAX_VALUE);
+ HasChildQueryBuilder foo = hasChildQuery("foo", query, ScoreMode.None); // all good
+ e = expectThrows(IllegalArgumentException.class, () -> foo.minMaxChildren(randomIntBetween(Integer.MIN_VALUE, -1), positiveValue));
+ assertEquals("[has_child] requires non-negative 'min_children' field", e.getMessage());
+
+ e = expectThrows(IllegalArgumentException.class, () -> foo.minMaxChildren(positiveValue, randomIntBetween(Integer.MIN_VALUE, -1)));
+ assertEquals("[has_child] requires non-negative 'max_children' field", e.getMessage());
+
+ e = expectThrows(IllegalArgumentException.class, () -> foo.minMaxChildren(positiveValue, positiveValue - 10));
+ assertEquals("[has_child] 'max_children' is less than 'min_children'", e.getMessage());
+ }
+
+ public void testFromJson() throws IOException {
+ String query =
+ "{\n" +
+ " \"has_child\" : {\n" +
+ " \"query\" : {\n" +
+ " \"range\" : {\n" +
+ " \"mapped_string\" : {\n" +
+ " \"from\" : \"agJhRET\",\n" +
+ " \"to\" : \"zvqIq\",\n" +
+ " \"include_lower\" : true,\n" +
+ " \"include_upper\" : true,\n" +
+ " \"boost\" : 1.0\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " \"type\" : \"child\",\n" +
+ " \"score_mode\" : \"avg\",\n" +
+ " \"min_children\" : 883170873,\n" +
+ " \"max_children\" : 1217235442,\n" +
+ " \"ignore_unmapped\" : false,\n" +
+ " \"boost\" : 2.0,\n" +
+ " \"_name\" : \"WNzYMJKRwePuRBh\",\n" +
+ " \"inner_hits\" : {\n" +
+ " \"name\" : \"inner_hits_name\",\n" +
+ " \"ignore_unmapped\" : false,\n" +
+ " \"from\" : 0,\n" +
+ " \"size\" : 100,\n" +
+ " \"version\" : false,\n" +
+ " \"explain\" : false,\n" +
+ " \"track_scores\" : false,\n" +
+ " \"sort\" : [ {\n" +
+ " \"mapped_string\" : {\n" +
+ " \"order\" : \"asc\"\n" +
+ " }\n" +
+ " } ]\n" +
+ " }\n" +
+ " }\n" +
+ "}";
+ HasChildQueryBuilder queryBuilder = (HasChildQueryBuilder) parseQuery(query);
+ checkGeneratedJson(query, queryBuilder);
+ assertEquals(query, queryBuilder.maxChildren(), 1217235442);
+ assertEquals(query, queryBuilder.minChildren(), 883170873);
+ assertEquals(query, queryBuilder.boost(), 2.0f, 0.0f);
+ assertEquals(query, queryBuilder.queryName(), "WNzYMJKRwePuRBh");
+ assertEquals(query, queryBuilder.childType(), "child");
+ assertEquals(query, queryBuilder.scoreMode(), ScoreMode.Avg);
+ assertNotNull(query, queryBuilder.innerHit());
+ InnerHitBuilder expected = new InnerHitBuilder(new InnerHitBuilder(), queryBuilder.query(), "child", false)
+ .setName("inner_hits_name")
+ .setSize(100)
+ .addSort(new FieldSortBuilder("mapped_string").order(SortOrder.ASC));
+ assertEquals(query, queryBuilder.innerHit(), expected);
+ }
+
+ public void testToQueryInnerQueryType() throws IOException {
+ String[] searchTypes = new String[]{PARENT_TYPE};
+ QueryShardContext shardContext = createShardContext();
+ shardContext.setTypes(searchTypes);
+ HasChildQueryBuilder hasChildQueryBuilder = hasChildQuery(CHILD_TYPE, new IdsQueryBuilder().addIds("id"), ScoreMode.None);
+ Query query = hasChildQueryBuilder.toQuery(shardContext);
+ //verify that the context types are still the same as the ones we previously set
+ assertThat(shardContext.getTypes(), equalTo(searchTypes));
+ assertLateParsingQuery(query, CHILD_TYPE, "id");
+ }
+
+ static void assertLateParsingQuery(Query query, String type, String id) throws IOException {
+ assertThat(query, instanceOf(HasChildQueryBuilder.LateParsingQuery.class));
+ HasChildQueryBuilder.LateParsingQuery lateParsingQuery = (HasChildQueryBuilder.LateParsingQuery) query;
+ assertThat(lateParsingQuery.getInnerQuery(), instanceOf(BooleanQuery.class));
+ BooleanQuery booleanQuery = (BooleanQuery) lateParsingQuery.getInnerQuery();
+ assertThat(booleanQuery.clauses().size(), equalTo(2));
+ //check the inner ids query, we have to call rewrite to get to check the type it's executed against
+ assertThat(booleanQuery.clauses().get(0).getOccur(), equalTo(BooleanClause.Occur.MUST));
+ assertThat(booleanQuery.clauses().get(0).getQuery(), instanceOf(TermInSetQuery.class));
+ TermInSetQuery termsQuery = (TermInSetQuery) booleanQuery.clauses().get(0).getQuery();
+ Query rewrittenTermsQuery = termsQuery.rewrite(null);
+ assertThat(rewrittenTermsQuery, instanceOf(ConstantScoreQuery.class));
+ ConstantScoreQuery constantScoreQuery = (ConstantScoreQuery) rewrittenTermsQuery;
+ assertThat(constantScoreQuery.getQuery(), instanceOf(BooleanQuery.class));
+ BooleanQuery booleanTermsQuery = (BooleanQuery) constantScoreQuery.getQuery();
+ assertThat(booleanTermsQuery.clauses().toString(), booleanTermsQuery.clauses().size(), equalTo(1));
+ assertThat(booleanTermsQuery.clauses().get(0).getOccur(), equalTo(BooleanClause.Occur.SHOULD));
+ assertThat(booleanTermsQuery.clauses().get(0).getQuery(), instanceOf(TermQuery.class));
+ TermQuery termQuery = (TermQuery) booleanTermsQuery.clauses().get(0).getQuery();
+ assertThat(termQuery.getTerm().field(), equalTo(UidFieldMapper.NAME));
+ //we want to make sure that the inner ids query gets executed against the child type rather
+ // than the main type we initially set to the context
+ BytesRef[] ids = Uid.createUidsForTypesAndIds(Collections.singletonList(type), Collections.singletonList(id));
+ assertThat(termQuery.getTerm().bytes(), equalTo(ids[0]));
+ //check the type filter
+ assertThat(booleanQuery.clauses().get(1).getOccur(), equalTo(BooleanClause.Occur.FILTER));
+ assertEquals(new TypeFieldMapper.TypesQuery(new BytesRef(type)), booleanQuery.clauses().get(1).getQuery());
+ }
+
+ @Override
+ public void testMustRewrite() throws IOException {
+ try {
+ super.testMustRewrite();
+ } catch (UnsupportedOperationException e) {
+ if (requiresRewrite == false) {
+ throw e;
+ }
+ }
+ }
+
+ public void testNonDefaultSimilarity() throws Exception {
+ QueryShardContext shardContext = createShardContext();
+ HasChildQueryBuilder hasChildQueryBuilder =
+ hasChildQuery(CHILD_TYPE, new TermQueryBuilder("custom_string", "value"), ScoreMode.None);
+ HasChildQueryBuilder.LateParsingQuery query = (HasChildQueryBuilder.LateParsingQuery) hasChildQueryBuilder.toQuery(shardContext);
+ Similarity expected = SimilarityService.BUILT_IN.get(similarity)
+ .apply(similarity, Settings.EMPTY, Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT).build())
+ .get();
+ assertThat(((PerFieldSimilarityWrapper) query.getSimilarity()).get("custom_string"), instanceOf(expected.getClass()));
+ }
+
+ public void testIgnoreUnmapped() throws IOException {
+ final HasChildQueryBuilder queryBuilder = new HasChildQueryBuilder("unmapped", new MatchAllQueryBuilder(), ScoreMode.None);
+ queryBuilder.ignoreUnmapped(true);
+ Query query = queryBuilder.toQuery(createShardContext());
+ assertThat(query, notNullValue());
+ assertThat(query, instanceOf(MatchNoDocsQuery.class));
+
+ final HasChildQueryBuilder failingQueryBuilder = new HasChildQueryBuilder("unmapped", new MatchAllQueryBuilder(), ScoreMode.None);
+ failingQueryBuilder.ignoreUnmapped(false);
+ QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext()));
+ assertThat(e.getMessage(), containsString("[" + HasChildQueryBuilder.NAME + "] no mapping found for type [unmapped]"));
+ }
+
+ public void testIgnoreUnmappedWithRewrite() throws IOException {
+ // WrapperQueryBuilder makes sure we always rewrite
+ final HasChildQueryBuilder queryBuilder
+ = new HasChildQueryBuilder("unmapped", new WrapperQueryBuilder(new MatchAllQueryBuilder().toString()), ScoreMode.None);
+ queryBuilder.ignoreUnmapped(true);
+ QueryShardContext queryShardContext = createShardContext();
+ Query query = queryBuilder.rewrite(queryShardContext).toQuery(queryShardContext);
+ assertThat(query, notNullValue());
+ assertThat(query, instanceOf(MatchNoDocsQuery.class));
+ }
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java
new file mode 100644
index 0000000000..825dfede61
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/HasParentQueryBuilderTests.java
@@ -0,0 +1,245 @@
+/*
+ * 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.MatchNoDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.join.ScoreMode;
+import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest;
+import org.elasticsearch.common.compress.CompressedXContent;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.index.mapper.MapperService;
+import org.elasticsearch.index.query.IdsQueryBuilder;
+import org.elasticsearch.index.query.InnerHitBuilder;
+import org.elasticsearch.index.query.MatchAllQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryShardContext;
+import org.elasticsearch.index.query.QueryShardException;
+import org.elasticsearch.index.query.TermQueryBuilder;
+import org.elasticsearch.index.query.WrapperQueryBuilder;
+import org.elasticsearch.join.ParentJoinPlugin;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.search.fetch.subphase.InnerHitsContext;
+import org.elasticsearch.search.internal.SearchContext;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.test.AbstractQueryTestCase;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasParentQuery;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.notNullValue;
+
+public class HasParentQueryBuilderTests extends AbstractQueryTestCase<HasParentQueryBuilder> {
+ protected static final String PARENT_TYPE = "parent";
+ protected static final String CHILD_TYPE = "child";
+
+ boolean requiresRewrite = false;
+
+ @Override
+ protected Collection<Class<? extends Plugin>> getPlugins() {
+ return Collections.singletonList(ParentJoinPlugin.class);
+ }
+
+ @Override
+ protected void initializeAdditionalMappings(MapperService mapperService) throws IOException {
+ mapperService.merge(PARENT_TYPE, new CompressedXContent(PutMappingRequest.buildFromSimplifiedDef(PARENT_TYPE,
+ STRING_FIELD_NAME, "type=text",
+ STRING_FIELD_NAME_2, "type=keyword",
+ INT_FIELD_NAME, "type=integer",
+ DOUBLE_FIELD_NAME, "type=double",
+ BOOLEAN_FIELD_NAME, "type=boolean",
+ DATE_FIELD_NAME, "type=date",
+ OBJECT_FIELD_NAME, "type=object"
+ ).string()), MapperService.MergeReason.MAPPING_UPDATE, false);
+ mapperService.merge(CHILD_TYPE, new CompressedXContent(PutMappingRequest.buildFromSimplifiedDef(CHILD_TYPE,
+ "_parent", "type=" + PARENT_TYPE,
+ STRING_FIELD_NAME, "type=text",
+ STRING_FIELD_NAME_2, "type=keyword",
+ INT_FIELD_NAME, "type=integer",
+ DOUBLE_FIELD_NAME, "type=double",
+ BOOLEAN_FIELD_NAME, "type=boolean",
+ DATE_FIELD_NAME, "type=date",
+ OBJECT_FIELD_NAME, "type=object"
+ ).string()), MapperService.MergeReason.MAPPING_UPDATE, false);
+ mapperService.merge("just_a_type", new CompressedXContent(PutMappingRequest.buildFromSimplifiedDef("just_a_type"
+ ).string()), MapperService.MergeReason.MAPPING_UPDATE, false);
+ }
+
+ /**
+ * @return a {@link HasChildQueryBuilder} with random values all over the place
+ */
+ @Override
+ protected HasParentQueryBuilder doCreateTestQueryBuilder() {
+ QueryBuilder innerQueryBuilder = new MatchAllQueryBuilder();
+ if (randomBoolean()) {
+ requiresRewrite = true;
+ innerQueryBuilder = new WrapperQueryBuilder(innerQueryBuilder.toString());
+ }
+ HasParentQueryBuilder hqb = new HasParentQueryBuilder(PARENT_TYPE, innerQueryBuilder, randomBoolean());
+ hqb.ignoreUnmapped(randomBoolean());
+ if (randomBoolean()) {
+ hqb.innerHit(new InnerHitBuilder()
+ .setName(randomAlphaOfLengthBetween(1, 10))
+ .setSize(randomIntBetween(0, 100))
+ .addSort(new FieldSortBuilder(STRING_FIELD_NAME_2).order(SortOrder.ASC)), hqb.ignoreUnmapped());
+ }
+ return hqb;
+ }
+
+ @Override
+ protected void doAssertLuceneQuery(HasParentQueryBuilder queryBuilder, Query query, SearchContext searchContext) throws IOException {
+ assertThat(query, instanceOf(HasChildQueryBuilder.LateParsingQuery.class));
+ HasChildQueryBuilder.LateParsingQuery lpq = (HasChildQueryBuilder.LateParsingQuery) query;
+ assertEquals(queryBuilder.score() ? ScoreMode.Max : ScoreMode.None, lpq.getScoreMode());
+
+ if (queryBuilder.innerHit() != null) {
+ // have to rewrite again because the provided queryBuilder hasn't been rewritten (directly returned from
+ // doCreateTestQueryBuilder)
+ queryBuilder = (HasParentQueryBuilder) queryBuilder.rewrite(searchContext.getQueryShardContext());
+
+ assertNotNull(searchContext);
+ Map<String, InnerHitBuilder> innerHitBuilders = new HashMap<>();
+ InnerHitBuilder.extractInnerHits(queryBuilder, innerHitBuilders);
+ for (InnerHitBuilder builder : innerHitBuilders.values()) {
+ builder.build(searchContext, searchContext.innerHits());
+ }
+ assertNotNull(searchContext.innerHits());
+ assertEquals(1, searchContext.innerHits().getInnerHits().size());
+ assertTrue(searchContext.innerHits().getInnerHits().containsKey(queryBuilder.innerHit().getName()));
+ InnerHitsContext.BaseInnerHits innerHits = searchContext.innerHits()
+ .getInnerHits().get(queryBuilder.innerHit().getName());
+ assertEquals(innerHits.size(), queryBuilder.innerHit().getSize());
+ assertEquals(innerHits.sort().sort.getSort().length, 1);
+ assertEquals(innerHits.sort().sort.getSort()[0].getField(), STRING_FIELD_NAME_2);
+ }
+ }
+
+ public void testIllegalValues() throws IOException {
+ QueryBuilder query = new MatchAllQueryBuilder();
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
+ () -> hasParentQuery(null, query, false));
+ assertThat(e.getMessage(), equalTo("[has_parent] requires 'type' field"));
+
+ e = expectThrows(IllegalArgumentException.class,
+ () -> hasParentQuery("foo", null, false));
+ assertThat(e.getMessage(), equalTo("[has_parent] requires 'query' field"));
+
+ QueryShardContext context = createShardContext();
+ HasParentQueryBuilder qb = hasParentQuery("just_a_type", new MatchAllQueryBuilder(), false);
+ QueryShardException qse = expectThrows(QueryShardException.class, () -> qb.doToQuery(context));
+ assertThat(qse.getMessage(), equalTo("[has_parent] no child types found for type [just_a_type]"));
+ }
+
+ public void testDeprecatedXContent() throws IOException {
+ XContentBuilder builder = XContentFactory.jsonBuilder().prettyPrint();
+ builder.startObject();
+ builder.startObject("has_parent");
+ builder.field("query");
+ new TermQueryBuilder("a", "a").toXContent(builder, ToXContent.EMPTY_PARAMS);
+ builder.field("type", "foo"); // deprecated
+ builder.endObject();
+ builder.endObject();
+ HasParentQueryBuilder queryBuilder = (HasParentQueryBuilder) parseQuery(builder.string());
+ assertEquals("foo", queryBuilder.type());
+ assertWarnings("Deprecated field [type] used, expected [parent_type] instead");
+ }
+
+ public void testToQueryInnerQueryType() throws IOException {
+ String[] searchTypes = new String[]{CHILD_TYPE};
+ QueryShardContext shardContext = createShardContext();
+ shardContext.setTypes(searchTypes);
+ HasParentQueryBuilder hasParentQueryBuilder = new HasParentQueryBuilder(PARENT_TYPE, new IdsQueryBuilder().addIds("id"),
+ false);
+ Query query = hasParentQueryBuilder.toQuery(shardContext);
+ //verify that the context types are still the same as the ones we previously set
+ assertThat(shardContext.getTypes(), equalTo(searchTypes));
+ HasChildQueryBuilderTests.assertLateParsingQuery(query, PARENT_TYPE, "id");
+ }
+
+ @Override
+ public void testMustRewrite() throws IOException {
+ try {
+ super.testMustRewrite();
+ } catch (UnsupportedOperationException e) {
+ if (requiresRewrite == false) {
+ throw e;
+ }
+ }
+ }
+
+ public void testFromJson() throws IOException {
+ String json =
+ "{\n" +
+ " \"has_parent\" : {\n" +
+ " \"query\" : {\n" +
+ " \"term\" : {\n" +
+ " \"tag\" : {\n" +
+ " \"value\" : \"something\",\n" +
+ " \"boost\" : 1.0\n" +
+ " }\n" +
+ " }\n" +
+ " },\n" +
+ " \"parent_type\" : \"blog\",\n" +
+ " \"score\" : true,\n" +
+ " \"ignore_unmapped\" : false,\n" +
+ " \"boost\" : 1.0\n" +
+ " }\n" +
+ "}";
+ HasParentQueryBuilder parsed = (HasParentQueryBuilder) parseQuery(json);
+ checkGeneratedJson(json, parsed);
+ assertEquals(json, "blog", parsed.type());
+ assertEquals(json, "something", ((TermQueryBuilder) parsed.query()).value());
+ }
+
+ public void testIgnoreUnmapped() throws IOException {
+ final HasParentQueryBuilder queryBuilder = new HasParentQueryBuilder("unmapped", new MatchAllQueryBuilder(), false);
+ queryBuilder.ignoreUnmapped(true);
+ Query query = queryBuilder.toQuery(createShardContext());
+ assertThat(query, notNullValue());
+ assertThat(query, instanceOf(MatchNoDocsQuery.class));
+
+ final HasParentQueryBuilder failingQueryBuilder = new HasParentQueryBuilder("unmapped", new MatchAllQueryBuilder(), false);
+ failingQueryBuilder.ignoreUnmapped(false);
+ QueryShardException e = expectThrows(QueryShardException.class, () -> failingQueryBuilder.toQuery(createShardContext()));
+ assertThat(e.getMessage(),
+ containsString("[" + HasParentQueryBuilder.NAME + "] query configured 'parent_type' [unmapped] is not a valid type"));
+ }
+
+ public void testIgnoreUnmappedWithRewrite() throws IOException {
+ // WrapperQueryBuilder makes sure we always rewrite
+ final HasParentQueryBuilder queryBuilder =
+ new HasParentQueryBuilder("unmapped", new WrapperQueryBuilder(new MatchAllQueryBuilder().toString()), false);
+ queryBuilder.ignoreUnmapped(true);
+ QueryShardContext queryShardContext = createShardContext();
+ Query query = queryBuilder.rewrite(queryShardContext).toQuery(queryShardContext);
+ assertThat(query, notNullValue());
+ assertThat(query, instanceOf(MatchNoDocsQuery.class));
+ }
+}
diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java
new file mode 100644
index 0000000000..ad8e49e9f5
--- /dev/null
+++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java
@@ -0,0 +1,568 @@
+/*
+ * 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.apache.lucene.util.ArrayUtil;
+import org.elasticsearch.action.index.IndexRequestBuilder;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.InnerHitBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.join.ParentJoinPlugin;
+import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.script.MockScriptEngine;
+import org.elasticsearch.script.MockScriptPlugin;
+import org.elasticsearch.script.Script;
+import org.elasticsearch.script.ScriptType;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.SearchHits;
+import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
+import org.elasticsearch.search.sort.FieldSortBuilder;
+import org.elasticsearch.search.sort.SortBuilders;
+import org.elasticsearch.search.sort.SortOrder;
+import org.elasticsearch.test.ESIntegTestCase;
+import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
+import org.elasticsearch.test.ESIntegTestCase.Scope;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+
+import static org.elasticsearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE;
+import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
+import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
+import static org.elasticsearch.index.query.QueryBuilders.constantScoreQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
+import static org.elasticsearch.index.query.QueryBuilders.matchQuery;
+import static org.elasticsearch.index.query.QueryBuilders.nestedQuery;
+import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasChildQuery;
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasParentQuery;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHit;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits;
+import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.hasId;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+@ClusterScope(scope = Scope.SUITE)
+public class InnerHitsIT extends ESIntegTestCase {
+ @Override
+ protected boolean ignoreExternalCluster() {
+ return true;
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> nodePlugins() {
+ return Arrays.asList(ParentJoinPlugin.class, CustomScriptPlugin.class);
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> transportClientPlugins() {
+ return nodePlugins();
+ }
+
+ public static class CustomScriptPlugin extends MockScriptPlugin {
+ @Override
+ protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() {
+ return Collections.singletonMap("5", script -> "5");
+ }
+ }
+
+ public void testSimpleParentChild() throws Exception {
+ assertAcked(prepareCreate("articles")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("article", "title", "type=text")
+ .addMapping("comment", "_parent", "type=article", "message", "type=text,fielddata=true")
+ );
+
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex("articles", "article", "1").setSource("title", "quick brown fox"));
+ requests.add(client().prepareIndex("articles", "comment", "1").setParent("1").setSource("message", "fox eat quick"));
+ requests.add(client().prepareIndex("articles", "comment", "2").setParent("1").setSource("message", "fox ate rabbit x y z"));
+ requests.add(client().prepareIndex("articles", "comment", "3").setParent("1").setSource("message", "rabbit got away"));
+ requests.add(client().prepareIndex("articles", "article", "2").setSource("title", "big gray elephant"));
+ requests.add(client().prepareIndex("articles", "comment", "4").setParent("2").setSource("message", "elephant captured"));
+ requests.add(client().prepareIndex("articles", "comment", "5").setParent("2").setSource("message", "mice squashed by elephant x"));
+ requests.add(client().prepareIndex("articles", "comment", "6").setParent("2").setSource("message", "elephant scared by mice x y"));
+ indexRandom(true, requests);
+
+ SearchResponse response = client().prepareSearch("articles")
+ .setQuery(hasChildQuery("comment", matchQuery("message", "fox"), ScoreMode.None)
+ .innerHit(new InnerHitBuilder(), false))
+ .get();
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+ assertSearchHit(response, 1, hasId("1"));
+ assertThat(response.getHits().getAt(0).getShard(), notNullValue());
+
+ assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1));
+ SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment");
+ assertThat(innerHits.getTotalHits(), equalTo(2L));
+
+ assertThat(innerHits.getAt(0).getId(), equalTo("1"));
+ assertThat(innerHits.getAt(0).getType(), equalTo("comment"));
+ assertThat(innerHits.getAt(1).getId(), equalTo("2"));
+ assertThat(innerHits.getAt(1).getType(), equalTo("comment"));
+
+ response = client().prepareSearch("articles")
+ .setQuery(hasChildQuery("comment", matchQuery("message", "elephant"), ScoreMode.None)
+ .innerHit(new InnerHitBuilder(), false))
+ .get();
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+ assertSearchHit(response, 1, hasId("2"));
+
+ assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1));
+ innerHits = response.getHits().getAt(0).getInnerHits().get("comment");
+ assertThat(innerHits.getTotalHits(), equalTo(3L));
+
+ assertThat(innerHits.getAt(0).getId(), equalTo("4"));
+ assertThat(innerHits.getAt(0).getType(), equalTo("comment"));
+ assertThat(innerHits.getAt(1).getId(), equalTo("5"));
+ assertThat(innerHits.getAt(1).getType(), equalTo("comment"));
+ assertThat(innerHits.getAt(2).getId(), equalTo("6"));
+ assertThat(innerHits.getAt(2).getType(), equalTo("comment"));
+
+ response = client().prepareSearch("articles")
+ .setQuery(
+ hasChildQuery("comment", matchQuery("message", "fox"), ScoreMode.None).innerHit(
+ new InnerHitBuilder()
+ .addDocValueField("message")
+ .setHighlightBuilder(new HighlightBuilder().field("message"))
+ .setExplain(true).setSize(1)
+ .addScriptField("script", new Script(ScriptType.INLINE, MockScriptEngine.NAME, "5",
+ Collections.emptyMap())),
+ false)
+ ).get();
+ assertNoFailures(response);
+ innerHits = response.getHits().getAt(0).getInnerHits().get("comment");
+ assertThat(innerHits.getHits().length, equalTo(1));
+ assertThat(innerHits.getAt(0).getHighlightFields().get("message").getFragments()[0].string(), equalTo("<em>fox</em> eat quick"));
+ assertThat(innerHits.getAt(0).getExplanation().toString(), containsString("weight(message:fox"));
+ assertThat(innerHits.getAt(0).getFields().get("message").getValue().toString(), equalTo("eat"));
+ assertThat(innerHits.getAt(0).getFields().get("script").getValue().toString(), equalTo("5"));
+ }
+
+ public void testRandomParentChild() throws Exception {
+ assertAcked(prepareCreate("idx")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent")
+ .addMapping("child1", "_parent", "type=parent")
+ .addMapping("child2", "_parent", "type=parent")
+ );
+ int numDocs = scaledRandomIntBetween(5, 50);
+ List<IndexRequestBuilder> requestBuilders = new ArrayList<>();
+
+ int child1 = 0;
+ int child2 = 0;
+ int[] child1InnerObjects = new int[numDocs];
+ int[] child2InnerObjects = new int[numDocs];
+ for (int parent = 0; parent < numDocs; parent++) {
+ String parentId = String.format(Locale.ENGLISH, "%03d", parent);
+ requestBuilders.add(client().prepareIndex("idx", "parent", parentId).setSource("{}", XContentType.JSON));
+
+ int numChildDocs = child1InnerObjects[parent] = scaledRandomIntBetween(1, numDocs);
+ int limit = child1 + numChildDocs;
+ for (; child1 < limit; child1++) {
+ requestBuilders.add(client().prepareIndex("idx", "child1",
+ String.format(Locale.ENGLISH, "%04d", child1)).setParent(parentId).setSource("{}", XContentType.JSON));
+ }
+ numChildDocs = child2InnerObjects[parent] = scaledRandomIntBetween(1, numDocs);
+ limit = child2 + numChildDocs;
+ for (; child2 < limit; child2++) {
+ requestBuilders.add(client().prepareIndex("idx", "child2",
+ String.format(Locale.ENGLISH, "%04d", child2)).setParent(parentId).setSource("{}", XContentType.JSON));
+ }
+ }
+ indexRandom(true, requestBuilders);
+
+ int size = randomIntBetween(0, numDocs);
+ BoolQueryBuilder boolQuery = new BoolQueryBuilder();
+ boolQuery.should(constantScoreQuery(hasChildQuery("child1", matchAllQuery(), ScoreMode.None)
+ .innerHit(new InnerHitBuilder().setName("a")
+ .addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size), false)));
+ boolQuery.should(constantScoreQuery(hasChildQuery("child2", matchAllQuery(), ScoreMode.None)
+ .innerHit(new InnerHitBuilder().setName("b")
+ .addSort(new FieldSortBuilder("_uid").order(SortOrder.ASC)).setSize(size), false)));
+ SearchResponse searchResponse = client().prepareSearch("idx")
+ .setSize(numDocs)
+ .setTypes("parent")
+ .addSort("_uid", SortOrder.ASC)
+ .setQuery(boolQuery)
+ .get();
+
+ assertNoFailures(searchResponse);
+ assertHitCount(searchResponse, numDocs);
+ assertThat(searchResponse.getHits().getHits().length, equalTo(numDocs));
+
+ int offset1 = 0;
+ int offset2 = 0;
+ for (int parent = 0; parent < numDocs; parent++) {
+ SearchHit searchHit = searchResponse.getHits().getAt(parent);
+ assertThat(searchHit.getType(), equalTo("parent"));
+ assertThat(searchHit.getId(), equalTo(String.format(Locale.ENGLISH, "%03d", parent)));
+ assertThat(searchHit.getShard(), notNullValue());
+
+ SearchHits inner = searchHit.getInnerHits().get("a");
+ assertThat(inner.getTotalHits(), equalTo((long) child1InnerObjects[parent]));
+ for (int child = 0; child < child1InnerObjects[parent] && child < size; child++) {
+ SearchHit innerHit = inner.getAt(child);
+ assertThat(innerHit.getType(), equalTo("child1"));
+ String childId = String.format(Locale.ENGLISH, "%04d", offset1 + child);
+ assertThat(innerHit.getId(), equalTo(childId));
+ assertThat(innerHit.getNestedIdentity(), nullValue());
+ }
+ offset1 += child1InnerObjects[parent];
+
+ inner = searchHit.getInnerHits().get("b");
+ assertThat(inner.getTotalHits(), equalTo((long) child2InnerObjects[parent]));
+ for (int child = 0; child < child2InnerObjects[parent] && child < size; child++) {
+ SearchHit innerHit = inner.getAt(child);
+ assertThat(innerHit.getType(), equalTo("child2"));
+ String childId = String.format(Locale.ENGLISH, "%04d", offset2 + child);
+ assertThat(innerHit.getId(), equalTo(childId));
+ assertThat(innerHit.getNestedIdentity(), nullValue());
+ }
+ offset2 += child2InnerObjects[parent];
+ }
+ }
+
+ public void testInnerHitsOnHasParent() throws Exception {
+ assertAcked(prepareCreate("stack")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("question", "body", "type=text")
+ .addMapping("answer", "_parent", "type=question", "body", "type=text")
+ );
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex("stack", "question", "1").setSource("body", "I'm using HTTPS + Basic authentication "
+ + "to protect a resource. How can I throttle authentication attempts to protect against brute force attacks?"));
+ requests.add(client().prepareIndex("stack", "answer", "1").setParent("1").setSource("body",
+ "install fail2ban and enable rules for apache"));
+ requests.add(client().prepareIndex("stack", "question", "2").setSource("body",
+ "I have firewall rules set up and also denyhosts installed.\\ndo I also need to install fail2ban?"));
+ requests.add(client().prepareIndex("stack", "answer", "2").setParent("2").setSource("body",
+ "Denyhosts protects only ssh; Fail2Ban protects all daemons."));
+ indexRandom(true, requests);
+
+ SearchResponse response = client().prepareSearch("stack")
+ .setTypes("answer")
+ .addSort("_uid", SortOrder.ASC)
+ .setQuery(
+ boolQuery()
+ .must(matchQuery("body", "fail2ban"))
+ .must(hasParentQuery("question", matchAllQuery(), false).innerHit(new InnerHitBuilder(), false))
+ ).get();
+ assertNoFailures(response);
+ assertHitCount(response, 2);
+
+ SearchHit searchHit = response.getHits().getAt(0);
+ assertThat(searchHit.getId(), equalTo("1"));
+ assertThat(searchHit.getType(), equalTo("answer"));
+ assertThat(searchHit.getInnerHits().get("question").getTotalHits(), equalTo(1L));
+ assertThat(searchHit.getInnerHits().get("question").getAt(0).getType(), equalTo("question"));
+ assertThat(searchHit.getInnerHits().get("question").getAt(0).getId(), equalTo("1"));
+
+ searchHit = response.getHits().getAt(1);
+ assertThat(searchHit.getId(), equalTo("2"));
+ assertThat(searchHit.getType(), equalTo("answer"));
+ assertThat(searchHit.getInnerHits().get("question").getTotalHits(), equalTo(1L));
+ assertThat(searchHit.getInnerHits().get("question").getAt(0).getType(), equalTo("question"));
+ assertThat(searchHit.getInnerHits().get("question").getAt(0).getId(), equalTo("2"));
+ }
+
+ public void testParentChildMultipleLayers() throws Exception {
+ assertAcked(prepareCreate("articles")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("article", "title", "type=text")
+ .addMapping("comment", "_parent", "type=article", "message", "type=text")
+ .addMapping("remark", "_parent", "type=comment", "message", "type=text")
+ );
+
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex("articles", "article", "1").setSource("title", "quick brown fox"));
+ requests.add(client().prepareIndex("articles", "comment", "1").setParent("1").setSource("message", "fox eat quick"));
+ requests.add(client().prepareIndex("articles", "remark", "1").setParent("1").setRouting("1").setSource("message", "good"));
+ requests.add(client().prepareIndex("articles", "article", "2").setSource("title", "big gray elephant"));
+ requests.add(client().prepareIndex("articles", "comment", "2").setParent("2").setSource("message", "elephant captured"));
+ requests.add(client().prepareIndex("articles", "remark", "2").setParent("2").setRouting("2").setSource("message", "bad"));
+ indexRandom(true, requests);
+
+ SearchResponse response = client().prepareSearch("articles")
+ .setQuery(hasChildQuery("comment",
+ hasChildQuery("remark", matchQuery("message", "good"), ScoreMode.None).innerHit(new InnerHitBuilder(), false),
+ ScoreMode.None).innerHit(new InnerHitBuilder(), false))
+ .get();
+
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+ assertSearchHit(response, 1, hasId("1"));
+
+ assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1));
+ SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("comment");
+ assertThat(innerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerHits.getAt(0).getId(), equalTo("1"));
+ assertThat(innerHits.getAt(0).getType(), equalTo("comment"));
+
+ innerHits = innerHits.getAt(0).getInnerHits().get("remark");
+ assertThat(innerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerHits.getAt(0).getId(), equalTo("1"));
+ assertThat(innerHits.getAt(0).getType(), equalTo("remark"));
+
+ response = client().prepareSearch("articles")
+ .setQuery(hasChildQuery("comment",
+ hasChildQuery("remark", matchQuery("message", "bad"), ScoreMode.None).innerHit(new InnerHitBuilder(), false),
+ ScoreMode.None).innerHit(new InnerHitBuilder(), false))
+ .get();
+
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+ assertSearchHit(response, 1, hasId("2"));
+
+ assertThat(response.getHits().getAt(0).getInnerHits().size(), equalTo(1));
+ innerHits = response.getHits().getAt(0).getInnerHits().get("comment");
+ assertThat(innerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerHits.getAt(0).getId(), equalTo("2"));
+ assertThat(innerHits.getAt(0).getType(), equalTo("comment"));
+
+ innerHits = innerHits.getAt(0).getInnerHits().get("remark");
+ assertThat(innerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerHits.getAt(0).getId(), equalTo("2"));
+ assertThat(innerHits.getAt(0).getType(), equalTo("remark"));
+ }
+
+ public void testRoyals() throws Exception {
+ assertAcked(
+ prepareCreate("royals")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("king")
+ .addMapping("prince", "_parent", "type=king")
+ .addMapping("duke", "_parent", "type=prince")
+ .addMapping("earl", "_parent", "type=duke")
+ .addMapping("baron", "_parent", "type=earl")
+ );
+
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex("royals", "king", "king").setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "prince", "prince").setParent("king").setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "duke", "duke").setParent("prince").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "earl", "earl1").setParent("duke").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "earl", "earl2").setParent("duke").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "earl", "earl3").setParent("duke").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "earl", "earl4").setParent("duke").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "baron", "baron1").setParent("earl1").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "baron", "baron2").setParent("earl2").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "baron", "baron3").setParent("earl3").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("royals", "baron", "baron4").setParent("earl4").setRouting("king")
+ .setSource("{}", XContentType.JSON));
+ indexRandom(true, requests);
+
+ SearchResponse response = client().prepareSearch("royals")
+ .setTypes("duke")
+ .setQuery(boolQuery()
+ .filter(hasParentQuery("prince",
+ hasParentQuery("king", matchAllQuery(), false).innerHit(new InnerHitBuilder().setName("kings"), false),
+ false).innerHit(new InnerHitBuilder().setName("princes"), false)
+ )
+ .filter(hasChildQuery("earl",
+ hasChildQuery("baron", matchAllQuery(), ScoreMode.None)
+ .innerHit(new InnerHitBuilder().setName("barons"), false),
+ ScoreMode.None).innerHit(new InnerHitBuilder()
+ .addSort(SortBuilders.fieldSort("_uid").order(SortOrder.ASC))
+ .setName("earls")
+ .setSize(4), false)
+ )
+ )
+ .get();
+ assertHitCount(response, 1);
+ assertThat(response.getHits().getAt(0).getId(), equalTo("duke"));
+
+ SearchHits innerHits = response.getHits().getAt(0).getInnerHits().get("earls");
+ assertThat(innerHits.getTotalHits(), equalTo(4L));
+ assertThat(innerHits.getAt(0).getId(), equalTo("earl1"));
+ assertThat(innerHits.getAt(1).getId(), equalTo("earl2"));
+ assertThat(innerHits.getAt(2).getId(), equalTo("earl3"));
+ assertThat(innerHits.getAt(3).getId(), equalTo("earl4"));
+
+ SearchHits innerInnerHits = innerHits.getAt(0).getInnerHits().get("barons");
+ assertThat(innerInnerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerInnerHits.getAt(0).getId(), equalTo("baron1"));
+
+ innerInnerHits = innerHits.getAt(1).getInnerHits().get("barons");
+ assertThat(innerInnerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerInnerHits.getAt(0).getId(), equalTo("baron2"));
+
+ innerInnerHits = innerHits.getAt(2).getInnerHits().get("barons");
+ assertThat(innerInnerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerInnerHits.getAt(0).getId(), equalTo("baron3"));
+
+ innerInnerHits = innerHits.getAt(3).getInnerHits().get("barons");
+ assertThat(innerInnerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerInnerHits.getAt(0).getId(), equalTo("baron4"));
+
+ innerHits = response.getHits().getAt(0).getInnerHits().get("princes");
+ assertThat(innerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerHits.getAt(0).getId(), equalTo("prince"));
+
+ innerInnerHits = innerHits.getAt(0).getInnerHits().get("kings");
+ assertThat(innerInnerHits.getTotalHits(), equalTo(1L));
+ assertThat(innerInnerHits.getAt(0).getId(), equalTo("king"));
+ }
+
+ public void testMatchesQueriesParentChildInnerHits() throws Exception {
+ assertAcked(prepareCreate("index")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("child", "_parent", "type=parent"));
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex("index", "parent", "1").setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("index", "child", "1").setParent("1").setSource("field", "value1"));
+ requests.add(client().prepareIndex("index", "child", "2").setParent("1").setSource("field", "value2"));
+ requests.add(client().prepareIndex("index", "parent", "2").setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("index", "child", "3").setParent("2").setSource("field", "value1"));
+ indexRandom(true, requests);
+
+ SearchResponse response = client().prepareSearch("index")
+ .setQuery(hasChildQuery("child", matchQuery("field", "value1").queryName("_name1"), ScoreMode.None)
+ .innerHit(new InnerHitBuilder(), false))
+ .addSort("_uid", SortOrder.ASC)
+ .get();
+ assertHitCount(response, 2);
+ assertThat(response.getHits().getAt(0).getId(), equalTo("1"));
+ assertThat(response.getHits().getAt(0).getInnerHits().get("child").getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getAt(0).getInnerHits().get("child").getAt(0).getMatchedQueries().length, equalTo(1));
+ assertThat(response.getHits().getAt(0).getInnerHits().get("child").getAt(0).getMatchedQueries()[0], equalTo("_name1"));
+
+ assertThat(response.getHits().getAt(1).getId(), equalTo("2"));
+ assertThat(response.getHits().getAt(1).getInnerHits().get("child").getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getAt(1).getInnerHits().get("child").getAt(0).getMatchedQueries().length, equalTo(1));
+ assertThat(response.getHits().getAt(1).getInnerHits().get("child").getAt(0).getMatchedQueries()[0], equalTo("_name1"));
+
+ QueryBuilder query = hasChildQuery("child", matchQuery("field", "value2").queryName("_name2"), ScoreMode.None)
+ .innerHit(new InnerHitBuilder(), false);
+ response = client().prepareSearch("index")
+ .setQuery(query)
+ .addSort("_uid", SortOrder.ASC)
+ .get();
+ assertHitCount(response, 1);
+ assertThat(response.getHits().getAt(0).getId(), equalTo("1"));
+ assertThat(response.getHits().getAt(0).getInnerHits().get("child").getTotalHits(), equalTo(1L));
+ assertThat(response.getHits().getAt(0).getInnerHits().get("child").getAt(0).getMatchedQueries().length, equalTo(1));
+ assertThat(response.getHits().getAt(0).getInnerHits().get("child").getAt(0).getMatchedQueries()[0], equalTo("_name2"));
+ }
+
+ public void testDontExplode() throws Exception {
+ assertAcked(prepareCreate("index1")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("child", "_parent", "type=parent"));
+ List<IndexRequestBuilder> requests = new ArrayList<>();
+ requests.add(client().prepareIndex("index1", "parent", "1").setSource("{}", XContentType.JSON));
+ requests.add(client().prepareIndex("index1", "child", "1").setParent("1").setSource("field", "value1"));
+ indexRandom(true, requests);
+
+ QueryBuilder query = hasChildQuery("child", matchQuery("field", "value1"), ScoreMode.None)
+ .innerHit(new InnerHitBuilder().setSize(ArrayUtil.MAX_ARRAY_LENGTH - 1), false);
+ SearchResponse response = client().prepareSearch("index1")
+ .setQuery(query)
+ .get();
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+
+ assertAcked(prepareCreate("index2").addMapping("type", "nested", "type=nested"));
+ client().prepareIndex("index2", "type", "1").setSource(jsonBuilder().startObject()
+ .startArray("nested")
+ .startObject()
+ .field("field", "value1")
+ .endObject()
+ .endArray()
+ .endObject())
+ .setRefreshPolicy(IMMEDIATE)
+ .get();
+
+ query = nestedQuery("nested", matchQuery("nested.field", "value1"), ScoreMode.Avg)
+ .innerHit(new InnerHitBuilder().setSize(ArrayUtil.MAX_ARRAY_LENGTH - 1), false);
+ response = client().prepareSearch("index2")
+ .setQuery(query)
+ .get();
+ assertNoFailures(response);
+ assertHitCount(response, 1);
+ }
+
+ public void testNestedInnerHitWrappedInParentChildInnerhit() throws Exception {
+ assertAcked(prepareCreate("test")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("child_type", "_parent", "type=parent_type", "nested_type", "type=nested"));
+ client().prepareIndex("test", "parent_type", "1").setSource("key", "value").get();
+ client().prepareIndex("test", "child_type", "2").setParent("1").setSource("nested_type", Collections.singletonMap("key", "value"))
+ .get();
+ refresh();
+ SearchResponse response = client().prepareSearch("test")
+ .setQuery(boolQuery().must(matchQuery("key", "value"))
+ .should(hasChildQuery("child_type", nestedQuery("nested_type", matchAllQuery(), ScoreMode.None)
+ .innerHit(new InnerHitBuilder(), false), ScoreMode.None).innerHit(new InnerHitBuilder(), false)))
+ .get();
+ assertHitCount(response, 1);
+ SearchHit hit = response.getHits().getAt(0);
+ assertThat(hit.getInnerHits().get("child_type").getAt(0).field("_parent").getValue(), equalTo("1"));
+ assertThat(hit.getInnerHits().get("child_type").getAt(0).getInnerHits().get("nested_type").getAt(0).field("_parent"), nullValue());
+ }
+
+ public void testInnerHitsWithIgnoreUnmapped() throws Exception {
+ assertAcked(prepareCreate("index1")
+ .setSettings("index.mapping.single_type", false)
+ .addMapping("parent_type", "nested_type", "type=nested")
+ .addMapping("child_type", "_parent", "type=parent_type")
+ );
+ createIndex("index2");
+ client().prepareIndex("index1", "parent_type", "1").setSource("nested_type", Collections.singletonMap("key", "value")).get();
+ client().prepareIndex("index1", "child_type", "2").setParent("1").setSource("{}", XContentType.JSON).get();
+ client().prepareIndex("index2", "type", "3").setSource("key", "value").get();
+ refresh();
+
+ SearchResponse response = client().prepareSearch("index1", "index2")
+ .setQuery(boolQuery()
+ .should(hasChildQuery("child_type", matchAllQuery(), ScoreMode.None).ignoreUnmapped(true)
+ .innerHit(new InnerHitBuilder(), true))
+ .should(termQuery("key", "value"))
+ )
+ .get();
+ assertNoFailures(response);
+ assertHitCount(response, 2);
+ assertSearchHits(response, "1", "3");
+ }
+}
diff --git a/modules/parent-join/src/test/resources/rest-api-spec/test/10_basic.yaml b/modules/parent-join/src/test/resources/rest-api-spec/test/10_basic.yaml
new file mode 100644
index 0000000000..f5a5808012
--- /dev/null
+++ b/modules/parent-join/src/test/resources/rest-api-spec/test/10_basic.yaml
@@ -0,0 +1,48 @@
+setup:
+ - do:
+ indices.create:
+ index: test
+ body:
+ settings:
+ mapping.single_type: false
+ mappings:
+ type_2: {}
+ type_3:
+ _parent:
+ type: type_2
+
+---
+"Parent/child inner hits":
+ - skip:
+ version: " - 5.99.99"
+ reason: mapping.single_type was added in 6.0
+
+ - do:
+ index:
+ index: test
+ type: type_2
+ id: 1
+ body: {"foo": "bar"}
+
+ - do:
+ index:
+ index: test
+ type: type_3
+ id: 1
+ parent: 1
+ body: {"bar": "baz"}
+
+ - do:
+ indices.refresh: {}
+
+ - do:
+ search:
+ body: { "query" : { "has_child" : { "type" : "type_3", "query" : { "match_all" : {} }, "inner_hits" : {} } } }
+ - match: { hits.total: 1 }
+ - match: { hits.hits.0._index: "test" }
+ - match: { hits.hits.0._type: "type_2" }
+ - match: { hits.hits.0._id: "1" }
+ - is_false: hits.hits.0.inner_hits.type_3.hits.hits.0._index
+ - match: { hits.hits.0.inner_hits.type_3.hits.hits.0._type: "type_3" }
+ - match: { hits.hits.0.inner_hits.type_3.hits.hits.0._id: "1" }
+ - is_false: hits.hits.0.inner_hits.type_3.hits.hits.0._nested
diff --git a/modules/percolator/build.gradle b/modules/percolator/build.gradle
index 60fb82bdf4..cf55368861 100644
--- a/modules/percolator/build.gradle
+++ b/modules/percolator/build.gradle
@@ -23,5 +23,9 @@ esplugin {
hasClientJar = true
}
+dependencies {
+ // for testing hasChild and hasParent rejections
+ testCompile project(path: ':modules:parent-join', configuration: 'runtime')
+}
compileJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes"
compileTestJava.options.compilerArgs << "-Xlint:-deprecation,-rawtypes"
diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java
index f33aca55bc..1865f68158 100644
--- a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java
+++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java
@@ -57,8 +57,6 @@ import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.BoostingQueryBuilder;
import org.elasticsearch.index.query.ConstantScoreQueryBuilder;
import org.elasticsearch.index.query.DisMaxQueryBuilder;
-import org.elasticsearch.index.query.HasChildQueryBuilder;
-import org.elasticsearch.index.query.HasParentQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryParseContext;
import org.elasticsearch.index.query.QueryShardContext;
@@ -372,15 +370,16 @@ public class PercolatorFieldMapper extends FieldMapper {
return CONTENT_TYPE;
}
+
/**
* Fails if a percolator contains an unsupported query. The following queries are not supported:
* 1) a has_child query
* 2) a has_parent query
*/
static void verifyQuery(QueryBuilder queryBuilder) {
- if (queryBuilder instanceof HasChildQueryBuilder) {
+ if (queryBuilder.getName().equals("has_child")) {
throw new IllegalArgumentException("the [has_child] query is unsupported inside a percolator query");
- } else if (queryBuilder instanceof HasParentQueryBuilder) {
+ } else if (queryBuilder.getName().equals("has_parent")) {
throw new IllegalArgumentException("the [has_parent] query is unsupported inside a percolator query");
} else if (queryBuilder instanceof BoolQueryBuilder) {
BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) queryBuilder;
diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java
index ae585dc9dc..5a150349ed 100644
--- a/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java
+++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/PercolatorFieldMapperTests.java
@@ -52,13 +52,10 @@ import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.ParseContext;
import org.elasticsearch.index.mapper.ParsedDocument;
import org.elasticsearch.index.mapper.SourceToParse;
-import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.BoostingQueryBuilder;
import org.elasticsearch.index.query.ConstantScoreQueryBuilder;
import org.elasticsearch.index.query.DisMaxQueryBuilder;
-import org.elasticsearch.index.query.HasChildQueryBuilder;
-import org.elasticsearch.index.query.HasParentQueryBuilder;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryParseContext;
@@ -67,6 +64,9 @@ import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.RandomScoreFunctionBuilder;
import org.elasticsearch.indices.TermsLookup;
+import org.elasticsearch.join.ParentJoinPlugin;
+import org.elasticsearch.join.query.HasChildQueryBuilder;
+import org.elasticsearch.join.query.HasParentQueryBuilder;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.script.MockScriptPlugin;
import org.elasticsearch.script.Script;
@@ -109,7 +109,7 @@ public class PercolatorFieldMapperTests extends ESSingleNodeTestCase {
@Override
protected Collection<Class<? extends Plugin>> getPlugins() {
- return pluginList(InternalSettingsPlugin.class, PercolatorPlugin.class, FoolMeScriptPlugin.class);
+ return pluginList(InternalSettingsPlugin.class, PercolatorPlugin.class, FoolMeScriptPlugin.class, ParentJoinPlugin.class);
}
@Before
diff --git a/modules/reindex/build.gradle b/modules/reindex/build.gradle
index eba8b96461..b636c46d3a 100644
--- a/modules/reindex/build.gradle
+++ b/modules/reindex/build.gradle
@@ -39,6 +39,8 @@ dependencies {
compile "org.elasticsearch.client:rest:${version}"
// for http - testing reindex from remote
testCompile project(path: ':modules:transport-netty4', configuration: 'runtime')
+ // for parent/child testing
+ testCompile project(path: ':modules:parent-join', configuration: 'runtime')
}
dependencyLicenses {
diff --git a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexParentChildTests.java b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexParentChildTests.java
index d0eb1dcd75..8c4135f1f2 100644
--- a/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexParentChildTests.java
+++ b/modules/reindex/src/test/java/org/elasticsearch/index/reindex/ReindexParentChildTests.java
@@ -22,9 +22,16 @@ package org.elasticsearch.index.reindex;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.join.ParentJoinPlugin;
+import org.elasticsearch.plugins.Plugin;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
-import static org.elasticsearch.index.query.QueryBuilders.hasParentQuery;
import static org.elasticsearch.index.query.QueryBuilders.idsQuery;
+import static org.elasticsearch.join.query.JoinQueryBuilders.hasParentQuery;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertSearchHits;
import static org.hamcrest.Matchers.containsString;
@@ -40,6 +47,23 @@ public class ReindexParentChildTests extends ReindexTestCase {
QueryBuilder findsCity;
QueryBuilder findsNeighborhood;
+ @Override
+ protected boolean ignoreExternalCluster() {
+ return true;
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> nodePlugins() {
+ final List<Class<? extends Plugin>> plugins = new ArrayList<>(super.nodePlugins());
+ plugins.add(ParentJoinPlugin.class);
+ return Collections.unmodifiableList(plugins);
+ }
+
+ @Override
+ protected Collection<Class<? extends Plugin>> transportClientPlugins() {
+ return nodePlugins();
+ }
+
public void testParentChild() throws Exception {
createParentChildIndex("source");
createParentChildIndex("dest");
diff --git a/modules/reindex/src/test/resources/rest-api-spec/test/reindex/90_remote.yaml b/modules/reindex/src/test/resources/rest-api-spec/test/reindex/90_remote.yaml
index b30f263e86..32de51d022 100644
--- a/modules/reindex/src/test/resources/rest-api-spec/test/reindex/90_remote.yaml
+++ b/modules/reindex/src/test/resources/rest-api-spec/test/reindex/90_remote.yaml
@@ -158,85 +158,6 @@
metric: search
- match: {indices.source.total.search.open_contexts: 0}
----
-"Reindex from remote with parent/child":
- - do:
- indices.create:
- index: source
- body:
- settings:
- mapping.single_type: false
- mappings:
- foo: {}
- bar:
- _parent:
- type: foo
- - do:
- indices.create:
- index: dest
- body:
- settings:
- mapping.single_type: false
- mappings:
- foo: {}
- bar:
- _parent:
- type: foo
- - do:
- index:
- index: source
- type: foo
- id: 1
- body: { "text": "test" }
- - do:
- index:
- index: source
- type: bar
- id: 1
- parent: 1
- body: { "text": "test2" }
- - do:
- indices.refresh: {}
-
- # Fetch the http host. We use the host of the master because we know there will always be a master.
- - do:
- cluster.state: {}
- - set: { master_node: master }
- - do:
- nodes.info:
- metric: [ http ]
- - is_true: nodes.$master.http.publish_address
- - set: {nodes.$master.http.publish_address: host}
- - do:
- reindex:
- refresh: true
- body:
- source:
- remote:
- host: http://${host}
- index: source
- dest:
- index: dest
- - match: {created: 2}
-
- - do:
- search:
- index: dest
- body:
- query:
- has_parent:
- parent_type: foo
- query:
- match:
- text: test
- - match: {hits.total: 1}
-
- # Make sure reindex closed all the scroll contexts
- - do:
- indices.stats:
- index: source
- metric: search
- - match: {indices.source.total.search.open_contexts: 0}
---
"Reindex from remote with timeouts":