diff options
author | Jim Ferenczi <jim.ferenczi@elastic.co> | 2017-05-12 15:58:06 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-12 15:58:06 +0200 |
commit | 279a18a527b9c23b06c8b75c2aa9321aefca9728 (patch) | |
tree | 111c9312cb41d09994a7fa7d86aeb9227a622a9f /modules | |
parent | be2a6ce80b2282779159bc017352aa6f216349e2 (diff) |
Add parent-join module (#24638)
* Add parent-join module
This change adds a new module named `parent-join`.
The goal of this module is to provide a replacement for the `_parent` field but as a first step this change only moves the `has_child`, `has_parent` queries and the `children` aggregation to this module.
These queries and aggregations are no longer in core but they are deployed by default as a module.
Relates #20257
Diffstat (limited to 'modules')
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": |