diff --git a/docs/reference/query-dsl/filters/has-child-filter.asciidoc b/docs/reference/query-dsl/filters/has-child-filter.asciidoc index 9f485c4e49e..2815ad45b80 100644 --- a/docs/reference/query-dsl/filters/has-child-filter.asciidoc +++ b/docs/reference/query-dsl/filters/has-child-filter.asciidoc @@ -16,7 +16,7 @@ the query. Here is an example: } } } -} +} -------------------------------------------------- The `type` is the child type to query against. The parent type to return @@ -39,9 +39,39 @@ The `has_child` filter also accepts a filter instead of a query: } } } -} +} -------------------------------------------------- +[float] +==== Min/Max Children + +coming[1.3.0] + +The `has_child` filter allows you to specify that a minimum and/or maximum +number of children are required to match for the parent doc to be considered +a match: + +[source,js] +-------------------------------------------------- +{ + "has_child" : { + "type" : "comment", + "min_children": 2, <1> + "max_children": 10, <1> + "filter" : { + "term" : { + "user" : "john" + } + } + } +} +-------------------------------------------------- +<1> Both `min_children` and `max_children` are optional. + +The execution speed of the `has_child` filter is equivalent +to that of the `has_child` query when `min_children` or `max_children` +is specified. + [float] ==== Memory Considerations diff --git a/docs/reference/query-dsl/queries/has-child-query.asciidoc b/docs/reference/query-dsl/queries/has-child-query.asciidoc index c8ebf9a44fb..9cae07e9e92 100644 --- a/docs/reference/query-dsl/queries/has-child-query.asciidoc +++ b/docs/reference/query-dsl/queries/has-child-query.asciidoc @@ -53,6 +53,36 @@ inside the `has_child` query: } -------------------------------------------------- +[float] +==== Min/Max Children + +coming[1.3.0] + +The `has_child` query allows you to specify that a minimum and/or maximum +number of children are required to match for the parent doc to be considered +a match: + +[source,js] +-------------------------------------------------- +{ + "has_child" : { + "type" : "blog_tag", + "score_mode" : "sum", + "min_children": 2, <1> + "max_children": 10, <1> + "query" : { + "term" : { + "tag" : "something" + } + } + } +} +-------------------------------------------------- +<1> Both `min_children` and `max_children` are optional. + +The `min_children` and `max_children` parameters can be combined with +the `score_mode` parameter. + [float] ==== Memory Considerations diff --git a/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java b/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java index 4417bcdb628..5524aa19e78 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildFilterBuilder.java @@ -32,6 +32,9 @@ public class HasChildFilterBuilder extends BaseFilterBuilder { private String childType; private String filterName; private Integer shortCircuitCutoff; + private Integer minChildren; + private Integer maxChildren; + public HasChildFilterBuilder(String type, QueryBuilder queryBuilder) { this.childType = type; @@ -53,6 +56,23 @@ public class HasChildFilterBuilder extends BaseFilterBuilder { return this; } + /** + * Defines the minimum number of children that are required to match for the parent to be considered a match. + */ + public HasChildFilterBuilder minChildren(int minChildren) { + this.minChildren = minChildren; + return this; + } + + /** + * Defines the maximum number of children that are required to match for the parent to be considered a match. + */ + public HasChildFilterBuilder maxChildren(int maxChildren) { + this.maxChildren = maxChildren; + return this; + } + + /** * This is a noop since has_child can't be cached. */ @@ -87,6 +107,12 @@ public class HasChildFilterBuilder extends BaseFilterBuilder { filterBuilder.toXContent(builder, params); } builder.field("child_type", childType); + if (minChildren != null) { + builder.field("min_children", minChildren); + } + if (maxChildren != null) { + builder.field("max_children", maxChildren); + } if (filterName != null) { builder.field("_name", filterName); } diff --git a/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java b/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java index 6328218ad76..dba47a754b9 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildFilterParser.java @@ -29,7 +29,9 @@ import org.elasticsearch.index.mapper.DocumentMapper; import org.elasticsearch.index.mapper.internal.ParentFieldMapper; import org.elasticsearch.index.query.support.XContentStructure; import org.elasticsearch.index.search.child.ChildrenConstantScoreQuery; +import org.elasticsearch.index.search.child.ChildrenQuery; import org.elasticsearch.index.search.child.CustomQueryWrappingFilter; +import org.elasticsearch.index.search.child.ScoreType; import org.elasticsearch.index.search.nested.NonNestedDocsFilter; import java.io.IOException; @@ -61,6 +63,8 @@ public class HasChildFilterParser implements FilterParser { boolean filterFound = false; String childType = null; int shortCircuitParentDocSet = 8192; // Tests show a cut of point between 8192 and 16384. + int minChildren = 0; + int maxChildren = 0; String filterName = null; String currentFieldName = null; @@ -97,6 +101,10 @@ public class HasChildFilterParser implements FilterParser { // noop to be backwards compatible } else if ("short_circuit_cutoff".equals(currentFieldName)) { shortCircuitParentDocSet = parser.intValue(); + } else if ("min_children".equals(currentFieldName) || "minChildren".equals(currentFieldName)) { + minChildren = parser.intValue(true); + } else if ("max_children".equals(currentFieldName) || "maxChildren".equals(currentFieldName)) { + maxChildren = parser.intValue(true); } else { throw new QueryParsingException(parseContext.index(), "[has_child] filter does not support [" + currentFieldName + "]"); } @@ -138,6 +146,10 @@ public class HasChildFilterParser implements FilterParser { throw new QueryParsingException(parseContext.index(), "[has_child] Type [" + childType + "] points to a non existent parent type [" + parentType + "]"); } + if (maxChildren > 0 && maxChildren < minChildren) { + throw new QueryParsingException(parseContext.index(), "[has_child] 'max_children' is less than 'min_children'"); + } + Filter nonNestedDocsFilter = null; if (parentDocMapper.hasNestedObjects()) { nonNestedDocsFilter = parseContext.cacheFilter(NonNestedDocsFilter.INSTANCE, null); @@ -145,12 +157,18 @@ public class HasChildFilterParser implements FilterParser { Filter parentFilter = parseContext.cacheFilter(parentDocMapper.typeFilter(), null); ParentChildIndexFieldData parentChildIndexFieldData = parseContext.fieldData().getForField(parentFieldMapper); - Query childrenConstantScoreQuery = new ChildrenConstantScoreQuery(parentChildIndexFieldData, query, parentType, childType, parentFilter, shortCircuitParentDocSet, nonNestedDocsFilter); - if (filterName != null) { - parseContext.addNamedFilter(filterName, new CustomQueryWrappingFilter(childrenConstantScoreQuery)); + Query childrenQuery; + if (minChildren > 1 || maxChildren > 0) { + childrenQuery = new ChildrenQuery(parentChildIndexFieldData, parentType, childType, parentFilter,query,ScoreType.NONE,minChildren, maxChildren, shortCircuitParentDocSet, nonNestedDocsFilter); + } else { + childrenQuery = new ChildrenConstantScoreQuery(parentChildIndexFieldData, query, parentType, childType, parentFilter, + shortCircuitParentDocSet, nonNestedDocsFilter); } - return new CustomQueryWrappingFilter(childrenConstantScoreQuery); + if (filterName != null) { + parseContext.addNamedFilter(filterName, new CustomQueryWrappingFilter(childrenQuery)); + } + return new CustomQueryWrappingFilter(childrenQuery); } } diff --git a/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java b/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java index ee9bdbb33f9..80795eb1369 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildQueryBuilder.java @@ -35,6 +35,10 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ private String scoreType; + private Integer minChildren; + + private Integer maxChildren; + private Integer shortCircuitCutoff; private String queryName; @@ -61,6 +65,22 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ return this; } + /** + * Defines the minimum number of children that are required to match for the parent to be considered a match. + */ + public HasChildQueryBuilder minChildren(int minChildren) { + this.minChildren = minChildren; + return this; + } + + /** + * Defines the maximum number of children that are required to match for the parent to be considered a match. + */ + public HasChildQueryBuilder maxChildren(int maxChildren) { + this.maxChildren = maxChildren; + return this; + } + /** * Configures at what cut off point only to evaluate parent documents that contain the matching parent id terms * instead of evaluating all parent docs. @@ -90,6 +110,12 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ if (scoreType != null) { builder.field("score_type", scoreType); } + if (minChildren != null) { + builder.field("min_children", minChildren); + } + if (maxChildren != null) { + builder.field("max_children", maxChildren); + } if (shortCircuitCutoff != null) { builder.field("short_circuit_cutoff", shortCircuitCutoff); } diff --git a/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java b/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java index aa6befd2622..48a390f6ec4 100644 --- a/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java +++ b/src/main/java/org/elasticsearch/index/query/HasChildQueryParser.java @@ -52,7 +52,7 @@ public class HasChildQueryParser implements QueryParser { @Override public String[] names() { - return new String[]{NAME, Strings.toCamelCase(NAME)}; + return new String[] { NAME, Strings.toCamelCase(NAME) }; } @Override @@ -63,7 +63,9 @@ public class HasChildQueryParser implements QueryParser { boolean queryFound = false; float boost = 1.0f; String childType = null; - ScoreType scoreType = null; + ScoreType scoreType = ScoreType.NONE; + int minChildren = 0; + int maxChildren = 0; int shortCircuitParentDocSet = 8192; String queryName = null; @@ -79,7 +81,7 @@ public class HasChildQueryParser implements QueryParser { // XContentStructure. facade to parse if available, // or delay parsing if not. if ("query".equals(currentFieldName)) { - iq = new XContentStructure.InnerQuery(parseContext, childType == null ? null : new String[] {childType}); + iq = new XContentStructure.InnerQuery(parseContext, childType == null ? null : new String[] { childType }); queryFound = true; } else { throw new QueryParsingException(parseContext.index(), "[has_child] query does not support [" + currentFieldName + "]"); @@ -88,19 +90,18 @@ public class HasChildQueryParser implements QueryParser { if ("type".equals(currentFieldName) || "child_type".equals(currentFieldName) || "childType".equals(currentFieldName)) { childType = parser.text(); } else if ("_scope".equals(currentFieldName)) { - throw new QueryParsingException(parseContext.index(), "the [_scope] support in [has_child] query has been removed, use a filter as a facet_filter in the relevant global facet"); + throw new QueryParsingException(parseContext.index(), + "the [_scope] support in [has_child] query has been removed, use a filter as a facet_filter in the relevant global facet"); } else if ("score_type".equals(currentFieldName) || "scoreType".equals(currentFieldName)) { - String scoreTypeValue = parser.text(); - if (!"none".equals(scoreTypeValue)) { - scoreType = ScoreType.fromString(scoreTypeValue); - } + scoreType = ScoreType.fromString(parser.text()); } else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) { - String scoreModeValue = parser.text(); - if (!"none".equals(scoreModeValue)) { - scoreType = ScoreType.fromString(scoreModeValue); - } + scoreType = ScoreType.fromString(parser.text()); } else if ("boost".equals(currentFieldName)) { boost = parser.floatValue(); + } else if ("min_children".equals(currentFieldName) || "minChildren".equals(currentFieldName)) { + minChildren = parser.intValue(true); + } else if ("max_children".equals(currentFieldName) || "maxChildren".equals(currentFieldName)) { + maxChildren = parser.intValue(true); } else if ("short_circuit_cutoff".equals(currentFieldName)) { shortCircuitParentDocSet = parser.intValue(); } else if ("_name".equals(currentFieldName)) { @@ -140,7 +141,12 @@ public class HasChildQueryParser implements QueryParser { String parentType = parentFieldMapper.type(); DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType); if (parentDocMapper == null) { - throw new QueryParsingException(parseContext.index(), "[has_child] Type [" + childType + "] points to a non existent parent type [" + parentType + "]"); + throw new QueryParsingException(parseContext.index(), "[has_child] Type [" + childType + + "] points to a non existent parent type [" + parentType + "]"); + } + + if (maxChildren > 0 && maxChildren < minChildren) { + throw new QueryParsingException(parseContext.index(), "[has_child] 'max_children' is less than 'min_children'"); } Filter nonNestedDocsFilter = null; @@ -154,10 +160,12 @@ public class HasChildQueryParser implements QueryParser { Query query; Filter parentFilter = parseContext.cacheFilter(parentDocMapper.typeFilter(), null); ParentChildIndexFieldData parentChildIndexFieldData = parseContext.fieldData().getForField(parentFieldMapper); - if (scoreType != null) { - query = new ChildrenQuery(parentChildIndexFieldData, parentType, childType, parentFilter, innerQuery, scoreType, shortCircuitParentDocSet, nonNestedDocsFilter); + if (minChildren > 1 || maxChildren > 0 || scoreType != ScoreType.NONE) { + query = new ChildrenQuery(parentChildIndexFieldData, parentType, childType, parentFilter, innerQuery, scoreType, minChildren, + maxChildren, shortCircuitParentDocSet, nonNestedDocsFilter); } else { - query = new ChildrenConstantScoreQuery(parentChildIndexFieldData, innerQuery, parentType, childType, parentFilter, shortCircuitParentDocSet, nonNestedDocsFilter); + query = new ChildrenConstantScoreQuery(parentChildIndexFieldData, innerQuery, parentType, childType, parentFilter, + shortCircuitParentDocSet, nonNestedDocsFilter); } if (queryName != null) { parseContext.addNamedFilter(queryName, new CustomQueryWrappingFilter(query)); diff --git a/src/main/java/org/elasticsearch/index/search/child/ChildrenQuery.java b/src/main/java/org/elasticsearch/index/search/child/ChildrenQuery.java index cdf68433add..64437d4b542 100644 --- a/src/main/java/org/elasticsearch/index/search/child/ChildrenQuery.java +++ b/src/main/java/org/elasticsearch/index/search/child/ChildrenQuery.java @@ -56,19 +56,21 @@ import java.util.Set; */ public class ChildrenQuery extends Query { - private final ParentChildIndexFieldData ifd; - private final String parentType; - private final String childType; - private final Filter parentFilter; - private final ScoreType scoreType; - private Query originalChildQuery; - private final int shortCircuitParentDocSet; - private final Filter nonNestedDocsFilter; + protected final ParentChildIndexFieldData ifd; + protected final String parentType; + protected final String childType; + protected final Filter parentFilter; + protected final ScoreType scoreType; + protected Query originalChildQuery; + protected final int minChildren; + protected final int maxChildren; + protected final int shortCircuitParentDocSet; + protected final Filter nonNestedDocsFilter; - private Query rewrittenChildQuery; - private IndexReader rewriteIndexReader; + protected Query rewrittenChildQuery; + protected IndexReader rewriteIndexReader; - public ChildrenQuery(ParentChildIndexFieldData ifd, String parentType, String childType, Filter parentFilter, Query childQuery, ScoreType scoreType, int shortCircuitParentDocSet, Filter nonNestedDocsFilter) { + public ChildrenQuery(ParentChildIndexFieldData ifd, String parentType, String childType, Filter parentFilter, Query childQuery, ScoreType scoreType, int minChildren, int maxChildren, int shortCircuitParentDocSet, Filter nonNestedDocsFilter) { this.ifd = ifd; this.parentType = parentType; this.childType = childType; @@ -77,6 +79,9 @@ public class ChildrenQuery extends Query { this.scoreType = scoreType; this.shortCircuitParentDocSet = shortCircuitParentDocSet; this.nonNestedDocsFilter = nonNestedDocsFilter; + assert maxChildren == 0 || minChildren <= maxChildren; + this.minChildren = minChildren > 1 ? minChildren : 0; + this.maxChildren = maxChildren; } @Override @@ -98,6 +103,12 @@ public class ChildrenQuery extends Query { if (getBoost() != that.getBoost()) { return false; } + if (minChildren != that.minChildren) { + return false; + } + if (maxChildren != that.maxChildren) { + return false; + } return true; } @@ -106,13 +117,16 @@ public class ChildrenQuery extends Query { int result = originalChildQuery.hashCode(); result = 31 * result + childType.hashCode(); result = 31 * result + Float.floatToIntBits(getBoost()); + result = 31 * result + minChildren; + result = 31 * result + maxChildren; return result; } @Override public String toString(String field) { - return "ChildrenQuery[" + childType + "/" + parentType + "](" + originalChildQuery - .toString(field) + ')' + ToStringUtils.boost(getBoost()); + int max = maxChildren == 0 ? Integer.MAX_VALUE : maxChildren; + return "ChildrenQuery[min(" + Integer.toString(minChildren) + ") max(" + Integer.toString(max) + ")of " + childType + "/" + + parentType + "](" + originalChildQuery.toString(field) + ')' + ToStringUtils.boost(getBoost()); } @Override @@ -144,12 +158,13 @@ public class ChildrenQuery extends Query { public Weight createWeight(IndexSearcher searcher) throws IOException { SearchContext sc = SearchContext.current(); assert rewrittenChildQuery != null; - assert rewriteIndexReader == searcher.getIndexReader() : "not equal, rewriteIndexReader=" + rewriteIndexReader + " searcher.getIndexReader()=" + searcher.getIndexReader(); + assert rewriteIndexReader == searcher.getIndexReader() : "not equal, rewriteIndexReader=" + rewriteIndexReader + + " searcher.getIndexReader()=" + searcher.getIndexReader(); final Query childQuery = rewrittenChildQuery; IndexFieldData.WithOrdinals globalIfd = ifd.getGlobalParentChild(parentType, searcher.getIndexReader()); if (globalIfd == null) { - // No docs of the specified type don't exist on this shard + // No docs of the specified type exist on this shard return Queries.newMatchNoDocsQuery().createWeight(searcher); } IndexSearcher indexSearcher = new IndexSearcher(searcher.getIndexReader()); @@ -157,21 +172,35 @@ public class ChildrenQuery extends Query { boolean abort = true; long numFoundParents; - ParentOrdAndScoreCollector collector = null; + ParentCollector collector = null; try { - switch (scoreType) { + if (minChildren == 0 && maxChildren == 0 && scoreType != ScoreType.NONE) { + switch (scoreType) { case MAX: collector = new MaxCollector(globalIfd, sc); break; case SUM: collector = new SumCollector(globalIfd, sc); break; + } + } + if (collector == null) { + switch (scoreType) { + case MAX: + collector = new MaxCountCollector(globalIfd, sc); + break; + case SUM: case AVG: - collector = new AvgCollector(globalIfd, sc); + collector = new SumCountAndAvgCollector(globalIfd, sc); + break; + case NONE: + collector = new CountCollector(globalIfd, sc); break; default: throw new RuntimeException("Are we missing a score type here? -- " + scoreType); + } } + indexSearcher.search(childQuery, collector); numFoundParents = collector.foundParents(); if (numFoundParents == 0) { @@ -186,28 +215,34 @@ public class ChildrenQuery extends Query { sc.addReleasable(collector, Lifetime.COLLECTION); final Filter parentFilter; if (numFoundParents <= shortCircuitParentDocSet) { - parentFilter = ParentIdsFilter.createShortCircuitFilter( - nonNestedDocsFilter, sc, parentType, collector.values, collector.parentIdxs, numFoundParents - ); + parentFilter = ParentIdsFilter.createShortCircuitFilter(nonNestedDocsFilter, sc, parentType, collector.values, + collector.parentIdxs, numFoundParents); } else { parentFilter = new ApplyAcceptedDocsFilter(this.parentFilter); } - return new ParentWeight(rewrittenChildQuery.createWeight(searcher), parentFilter, numFoundParents, collector); + return new ParentWeight(rewrittenChildQuery.createWeight(searcher), parentFilter, numFoundParents, collector, minChildren, + maxChildren); } - private final class ParentWeight extends Weight { + protected class ParentWeight extends Weight { - private final Weight childWeight; - private final Filter parentFilter; - private final ParentOrdAndScoreCollector collector; + protected final Weight childWeight; + protected final Filter parentFilter; + protected final ParentCollector collector; + protected final int minChildren; + protected final int maxChildren; - private long remaining; + protected long remaining; + protected float queryNorm; + protected float queryWeight; - private ParentWeight(Weight childWeight, Filter parentFilter, long remaining, ParentOrdAndScoreCollector collector) { + protected ParentWeight(Weight childWeight, Filter parentFilter, long remaining, ParentCollector collector, int minChildren, int maxChildren) { this.childWeight = childWeight; this.parentFilter = parentFilter; this.remaining = remaining; this.collector = collector; + this.minChildren = minChildren; + this.maxChildren = maxChildren; } @Override @@ -221,14 +256,20 @@ public class ChildrenQuery extends Query { } @Override - public float getValueForNormalization() throws IOException { - float sum = childWeight.getValueForNormalization(); - sum *= getBoost() * getBoost(); - return sum; + public void normalize(float norm, float topLevelBoost) { + this.queryNorm = norm * topLevelBoost; + queryWeight *= this.queryNorm; } @Override - public void normalize(float norm, float topLevelBoost) { + public float getValueForNormalization() throws IOException { + queryWeight = getBoost(); + if (scoreType == ScoreType.NONE) { + return queryWeight * queryWeight; + } + float sum = childWeight.getValueForNormalization(); + sum *= queryWeight * queryWeight; + return sum; } @Override @@ -241,59 +282,80 @@ public class ChildrenQuery extends Query { // We can't be sure of the fact that liveDocs have been applied, so we apply it here. The "remaining" // count down (short circuit) logic will then work as expected. DocIdSetIterator parents = BitsFilteredDocIdSet.wrap(parentsSet, context.reader().getLiveDocs()).iterator(); - BytesValues.WithOrdinals bytesValues = collector.globalIfd.load(context).getBytesValues(false); - if (bytesValues == null) { - return null; - } - switch (scoreType) { - case AVG: - return new AvgParentScorer(this, parents, collector, bytesValues.ordinals()); - default: - return new ParentScorer(this, parents, collector, bytesValues.ordinals()); - } - } + if (parents != null) { + BytesValues.WithOrdinals bytesValues = collector.globalIfd.load(context).getBytesValues(false); + if (bytesValues == null) { + return null; + } + + Ordinals.Docs globalOrdinals = bytesValues.ordinals(); + + if (minChildren > 0 || maxChildren != 0 || scoreType == ScoreType.NONE) { + switch (scoreType) { + case NONE: + DocIdSetIterator parentIdIterator = new CountParentOrdIterator(this, parents, collector, globalOrdinals, + minChildren, maxChildren); + return ConstantScorer.create(parentIdIterator, this, queryWeight); + case AVG: + return new AvgParentCountScorer(this, parents, collector, globalOrdinals, minChildren, maxChildren); + default: + return new ParentCountScorer(this, parents, collector, globalOrdinals, minChildren, maxChildren); + } + } + switch (scoreType) { + case AVG: + return new AvgParentScorer(this, parents, collector, globalOrdinals); + default: + return new ParentScorer(this, parents, collector, globalOrdinals); + } + } + return null; + } } - private abstract static class ParentOrdAndScoreCollector extends NoopCollector implements Releasable { + protected abstract static class ParentCollector extends NoopCollector implements Releasable { - private final IndexFieldData.WithOrdinals globalIfd; + protected final IndexFieldData.WithOrdinals globalIfd; protected final LongHash parentIdxs; protected final BigArrays bigArrays; - protected FloatArray scores; protected final SearchContext searchContext; protected Ordinals.Docs globalOrdinals; protected BytesValues.WithOrdinals values; protected Scorer scorer; - private ParentOrdAndScoreCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { + protected ParentCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { this.globalIfd = globalIfd; + this.searchContext = searchContext; this.bigArrays = searchContext.bigArrays(); this.parentIdxs = new LongHash(512, bigArrays); - this.scores = bigArrays.newFloatArray(512, false); - this.searchContext = searchContext; } - @Override - public void collect(int doc) throws IOException { + public final void collect(int doc) throws IOException { if (globalOrdinals != null) { final long globalOrdinal = globalOrdinals.getOrd(doc); if (globalOrdinal != Ordinals.MISSING_ORDINAL) { long parentIdx = parentIdxs.add(globalOrdinal); if (parentIdx >= 0) { - scores = bigArrays.grow(scores, parentIdx + 1); - scores.set(parentIdx, scorer.score()); + newParent(parentIdx); } else { parentIdx = -1 - parentIdx; - doScore(parentIdx); + existingParent(parentIdx); } } } } - protected void doScore(long index) throws IOException { + protected void newParent(long parentIdx) throws IOException { + } + + protected void existingParent(long parentIdx) throws IOException { + } + + public long foundParents() { + return parentIdxs.size(); } @Override @@ -302,11 +364,6 @@ public class ChildrenQuery extends Query { if (values != null) { globalOrdinals = values.ordinals(); } - - } - - public long foundParents() { - return parentIdxs.size(); } @Override @@ -314,71 +371,133 @@ public class ChildrenQuery extends Query { this.scorer = scorer; } + @Override + public void close() throws ElasticsearchException { + Releasables.close(parentIdxs); + } + } + + protected abstract static class ParentScoreCollector extends ParentCollector implements Releasable { + + protected FloatArray scores; + + protected ParentScoreCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { + super(globalIfd, searchContext); + this.scores = this.bigArrays.newFloatArray(512, false); + } + + protected void newParent(long parentIdx) throws IOException { + scores = bigArrays.grow(scores, parentIdx + 1); + scores.set(parentIdx, scorer.score()); + } + @Override public void close() throws ElasticsearchException { Releasables.close(parentIdxs, scores); } } - private final static class SumCollector extends ParentOrdAndScoreCollector { + protected abstract static class ParentScoreCountCollector extends ParentScoreCollector implements Releasable { + + protected IntArray occurrences; + + protected ParentScoreCountCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { + super(globalIfd, searchContext); + this.occurrences = bigArrays.newIntArray(512, false); + } + + protected void newParent(long parentIdx) throws IOException { + scores = bigArrays.grow(scores, parentIdx + 1); + scores.set(parentIdx, scorer.score()); + occurrences = bigArrays.grow(occurrences, parentIdx + 1); + occurrences.set(parentIdx, 1); + } + + @Override + public void close() throws ElasticsearchException { + Releasables.close(parentIdxs, scores, occurrences); + } + } + + private final static class CountCollector extends ParentCollector implements Releasable { + + protected IntArray occurrences; + + protected CountCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { + super(globalIfd, searchContext); + this.occurrences = bigArrays.newIntArray(512, false); + } + + @Override + protected void newParent(long parentIdx) throws IOException { + occurrences = bigArrays.grow(occurrences, parentIdx + 1); + occurrences.set(parentIdx, 1); + } + + @Override + protected void existingParent(long parentIdx) throws IOException { + occurrences.increment(parentIdx, 1); + } + + @Override + public void close() throws ElasticsearchException { + Releasables.close(parentIdxs, occurrences); + } + } + + private final static class SumCollector extends ParentScoreCollector { private SumCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { super(globalIfd, searchContext); } @Override - protected void doScore(long index) throws IOException { - scores.increment(index, scorer.score()); + protected void existingParent(long parentIdx) throws IOException { + scores.increment(parentIdx, scorer.score()); } } - private final static class MaxCollector extends ParentOrdAndScoreCollector { + private final static class MaxCollector extends ParentScoreCollector { private MaxCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { super(globalIfd, searchContext); } @Override - protected void doScore(long index) throws IOException { + protected void existingParent(long parentIdx) throws IOException { float currentScore = scorer.score(); - if (currentScore > scores.get(index)) { - scores.set(index, currentScore); + if (currentScore > scores.get(parentIdx)) { + scores.set(parentIdx, currentScore); } } } - private final static class AvgCollector extends ParentOrdAndScoreCollector { + private final static class MaxCountCollector extends ParentScoreCountCollector { - private IntArray occurrences; - - AvgCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { + private MaxCountCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { super(globalIfd, searchContext); - this.occurrences = bigArrays.newIntArray(512, false); } @Override - public void collect(int doc) throws IOException { - if (globalOrdinals != null) { - final long globalOrdinal = globalOrdinals.getOrd(doc); - if (globalOrdinal != Ordinals.MISSING_ORDINAL) { - long parentIdx = parentIdxs.add(globalOrdinal); - if (parentIdx >= 0) { - scores = bigArrays.grow(scores, parentIdx + 1); - occurrences = bigArrays.grow(occurrences, parentIdx + 1); - scores.set(parentIdx, scorer.score()); - occurrences.set(parentIdx, 1); - } else { - parentIdx = -1 - parentIdx; - scores.increment(parentIdx, scorer.score()); - occurrences.increment(parentIdx, 1); - } - } + protected void existingParent(long parentIdx) throws IOException { + float currentScore = scorer.score(); + if (currentScore > scores.get(parentIdx)) { + scores.set(parentIdx, currentScore); } + occurrences.increment(parentIdx, 1); + } + } + + private final static class SumCountAndAvgCollector extends ParentScoreCountCollector { + + SumCountAndAvgCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { + super(globalIfd, searchContext); } @Override - public void close() throws ElasticsearchException { - Releasables.close(parentIdxs, scores, occurrences); + protected void existingParent(long parentIdx) throws IOException { + scores.increment(parentIdx, scorer.score()); + occurrences.increment(parentIdx, 1); } } @@ -394,13 +513,13 @@ public class ChildrenQuery extends Query { int currentDocId = -1; float currentScore; - ParentScorer(ParentWeight parentWeight, DocIdSetIterator parentsIterator, ParentOrdAndScoreCollector collector, Ordinals.Docs globalOrdinals) { + ParentScorer(ParentWeight parentWeight, DocIdSetIterator parentsIterator, ParentCollector collector, Ordinals.Docs globalOrdinals) { super(parentWeight); this.parentWeight = parentWeight; this.globalOrdinals = globalOrdinals; this.parentsIterator = parentsIterator; this.parentIds = collector.parentIdxs; - this.scores = collector.scores; + this.scores = ((ParentScoreCollector) collector).scores; } @Override @@ -408,6 +527,11 @@ public class ChildrenQuery extends Query { return currentScore; } + protected boolean acceptAndScore(long parentIdx) { + currentScore = scores.get(parentIdx); + return true; + } + @Override public int freq() throws IOException { // We don't have the original child query hit info here... @@ -439,9 +563,10 @@ public class ChildrenQuery extends Query { final long parentIdx = parentIds.find(globalOrdinal); if (parentIdx != -1) { - currentScore = scores.get(parentIdx); parentWeight.remaining--; - return currentDocId; + if (acceptAndScore(parentIdx)) { + return currentDocId; + } } } } @@ -464,12 +589,12 @@ public class ChildrenQuery extends Query { final long parentIdx = parentIds.find(globalOrdinal); if (parentIdx != -1) { - currentScore = scores.get(parentIdx); parentWeight.remaining--; - return currentDocId; - } else { - return nextDoc(); + if (acceptAndScore(parentIdx)) { + return currentDocId; + } } + return nextDoc(); } @Override @@ -478,67 +603,103 @@ public class ChildrenQuery extends Query { } } - private static final class AvgParentScorer extends ParentScorer { + private static class ParentCountScorer extends ParentScorer { - private final IntArray occurrences; + protected final IntArray occurrences; + protected final int minChildren; + protected final int maxChildren; - AvgParentScorer(ParentWeight weight, DocIdSetIterator parentsIterator, ParentOrdAndScoreCollector collector, Ordinals.Docs globalOrdinals) { - super(weight, parentsIterator, collector, globalOrdinals); - this.occurrences = ((AvgCollector) collector).occurrences; + ParentCountScorer(ParentWeight parentWeight, DocIdSetIterator parentsIterator, ParentCollector collector, Ordinals.Docs globalOrdinals, int minChildren, int maxChildren) { + super(parentWeight, parentsIterator, (ParentScoreCollector) collector, globalOrdinals); + this.minChildren = minChildren; + this.maxChildren = maxChildren == 0 ? Integer.MAX_VALUE : maxChildren; + this.occurrences = ((ParentScoreCountCollector) collector).occurrences; + } + + protected boolean acceptAndScore(long parentIdx) { + int count = occurrences.get(parentIdx); + if (count < minChildren || count > maxChildren) { + return false; + } + return super.acceptAndScore(parentIdx); + } + } + + private static final class AvgParentScorer extends ParentCountScorer { + + AvgParentScorer(ParentWeight weight, DocIdSetIterator parentsIterator, ParentCollector collector, Ordinals.Docs globalOrdinals) { + super(weight, parentsIterator, collector, globalOrdinals, 0, 0); } @Override - public int nextDoc() throws IOException { + protected boolean acceptAndScore(long parentIdx) { + currentScore = scores.get(parentIdx); + currentScore /= occurrences.get(parentIdx); + return true; + } + + } + + private static final class AvgParentCountScorer extends ParentCountScorer { + + AvgParentCountScorer(ParentWeight weight, DocIdSetIterator parentsIterator, ParentCollector collector, Ordinals.Docs globalOrdinals, int minChildren, int maxChildren) { + super(weight, parentsIterator, collector, globalOrdinals, minChildren, maxChildren); + } + + @Override + protected boolean acceptAndScore(long parentIdx) { + int count = occurrences.get(parentIdx); + if (count < minChildren || count > maxChildren) { + return false; + } + currentScore = scores.get(parentIdx); + currentScore /= occurrences.get(parentIdx); + return true; + } + } + + private final static class CountParentOrdIterator extends FilteredDocIdSetIterator { + + private final LongHash parentIds; + protected final IntArray occurrences; + private final int minChildren; + private final int maxChildren; + private final Ordinals.Docs ordinals; + private final ParentWeight parentWeight; + + private CountParentOrdIterator(ParentWeight parentWeight, DocIdSetIterator innerIterator, ParentCollector collector, Ordinals.Docs ordinals, int minChildren, int maxChildren) { + super(innerIterator); + this.parentIds = ((CountCollector) collector).parentIdxs; + this.occurrences = ((CountCollector) collector).occurrences; + this.ordinals = ordinals; + this.parentWeight = parentWeight; + this.minChildren = minChildren; + this.maxChildren = maxChildren == 0 ? Integer.MAX_VALUE : maxChildren; + } + + @Override + protected boolean match(int doc) { if (parentWeight.remaining == 0) { - return currentDocId = NO_MORE_DOCS; + try { + advance(DocIdSetIterator.NO_MORE_DOCS); + } catch (IOException e) { + throw new RuntimeException(e); + } + return false; } - while (true) { - currentDocId = parentsIterator.nextDoc(); - if (currentDocId == DocIdSetIterator.NO_MORE_DOCS) { - return currentDocId; - } - - final long globalOrdinal = globalOrdinals.getOrd(currentDocId); - if (globalOrdinal == Ordinals.MISSING_ORDINAL) { - continue; - } - - final long parentIdx = parentIds.find(globalOrdinal); + final long parentOrd = ordinals.getOrd(doc); + if (parentOrd != Ordinals.MISSING_ORDINAL) { + final long parentIdx = parentIds.find(parentOrd); if (parentIdx != -1) { - currentScore = scores.get(parentIdx); - currentScore /= occurrences.get(parentIdx); parentWeight.remaining--; - return currentDocId; + int count = occurrences.get(parentIdx); + if (count >= minChildren && count <= maxChildren) { + return true; + } } } - } - - @Override - public int advance(int target) throws IOException { - if (parentWeight.remaining == 0) { - return currentDocId = NO_MORE_DOCS; - } - - currentDocId = parentsIterator.advance(target); - if (currentDocId == DocIdSetIterator.NO_MORE_DOCS) { - return currentDocId; - } - - final long globalOrdinal = globalOrdinals.getOrd(currentDocId); - if (globalOrdinal == Ordinals.MISSING_ORDINAL) { - return nextDoc(); - } - - final long parentIdx = parentIds.find(globalOrdinal); - if (parentIdx != -1) { - currentScore = scores.get(parentIdx); - currentScore /= occurrences.get(parentIdx); - parentWeight.remaining--; - return currentDocId; - } else { - return nextDoc(); - } + return false; } } diff --git a/src/main/java/org/elasticsearch/index/search/child/ScoreType.java b/src/main/java/org/elasticsearch/index/search/child/ScoreType.java index 5acdcb6b922..acb7ff62987 100644 --- a/src/main/java/org/elasticsearch/index/search/child/ScoreType.java +++ b/src/main/java/org/elasticsearch/index/search/child/ScoreType.java @@ -24,24 +24,33 @@ import org.elasticsearch.ElasticsearchIllegalArgumentException; * Defines how scores from child documents are mapped into the parent document. */ public enum ScoreType { - /** - * Only the highest score of all matching child documents is mapped into the parent. + * Only the highest score of all matching child documents is mapped into the + * parent. */ MAX, /** - * The average score based on all matching child documents are mapped into the parent. + * The average score based on all matching child documents are mapped into + * the parent. */ AVG, /** * The matching children scores is summed up and mapped into the parent. */ - SUM; + SUM, + + /** + * Scores are not taken into account + */ + NONE; + public static ScoreType fromString(String type) { - if ("max".equals(type)) { + if ("none".equals(type)) { + return NONE; + } else if ("max".equals(type)) { return MAX; } else if ("avg".equals(type)) { return AVG; diff --git a/src/test/java/org/elasticsearch/index/search/child/ChildrenQueryTests.java b/src/test/java/org/elasticsearch/index/search/child/ChildrenQueryTests.java index adee4173841..b9d50ed54fd 100644 --- a/src/test/java/org/elasticsearch/index/search/child/ChildrenQueryTests.java +++ b/src/test/java/org/elasticsearch/index/search/child/ChildrenQueryTests.java @@ -78,7 +78,10 @@ public class ChildrenQueryTests extends ElasticsearchLuceneTestCase { ParentFieldMapper parentFieldMapper = SearchContext.current().mapperService().documentMapper("child").parentFieldMapper(); ParentChildIndexFieldData parentChildIndexFieldData = SearchContext.current().fieldData().getForField(parentFieldMapper); Filter parentFilter = new TermFilter(new Term(TypeFieldMapper.NAME, "parent")); - Query query = new ChildrenQuery(parentChildIndexFieldData, "parent", "child", parentFilter, childQuery, scoreType, 12, NonNestedDocsFilter.INSTANCE); + int minChildren = random().nextInt(10); + int maxChildren = scaledRandomIntBetween(minChildren, 10); + Query query = new ChildrenQuery(parentChildIndexFieldData, "parent", "child", parentFilter, childQuery, scoreType, minChildren, + maxChildren, 12, NonNestedDocsFilter.INSTANCE); QueryUtils.check(query); } @@ -219,7 +222,13 @@ public class ChildrenQueryTests extends ElasticsearchLuceneTestCase { int shortCircuitParentDocSet = random().nextInt(numParentDocs); ScoreType scoreType = ScoreType.values()[random().nextInt(ScoreType.values().length)]; Filter nonNestedDocsFilter = random().nextBoolean() ? NonNestedDocsFilter.INSTANCE : null; - Query query = new ChildrenQuery(parentChildIndexFieldData, "parent", "child", parentFilter, childQuery, scoreType, shortCircuitParentDocSet, nonNestedDocsFilter); + + // leave min/max set to 0 half the time + int minChildren = random().nextInt(2) * scaledRandomIntBetween(0, 110); + int maxChildren = random().nextInt(2) * scaledRandomIntBetween(minChildren, 110); + + Query query = new ChildrenQuery(parentChildIndexFieldData, "parent", "child", parentFilter, childQuery, scoreType, minChildren, + maxChildren, shortCircuitParentDocSet, nonNestedDocsFilter); query = new XFilteredQuery(query, filterMe); BitSetCollector collector = new BitSetCollector(indexReader.maxDoc()); int numHits = 1 + random().nextInt(25); @@ -239,14 +248,17 @@ public class ChildrenQueryTests extends ElasticsearchLuceneTestCase { TermsEnum termsEnum = terms.iterator(null); DocsEnum docsEnum = null; for (Map.Entry entry : parentIdToChildScores.entrySet()) { - TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(Uid.createUidAsBytes("parent", entry.getKey())); - if (seekStatus == TermsEnum.SeekStatus.FOUND) { - docsEnum = termsEnum.docs(slowAtomicReader.getLiveDocs(), docsEnum, DocsEnum.FLAG_NONE); - expectedResult.set(docsEnum.nextDoc()); - mockScorer.scores = entry.getValue(); - expectedTopDocsCollector.collect(docsEnum.docID()); - } else if (seekStatus == TermsEnum.SeekStatus.END) { - break; + int count = entry.getValue().elementsCount; + if (count >= minChildren && (maxChildren == 0 || count <= maxChildren)) { + TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(Uid.createUidAsBytes("parent", entry.getKey())); + if (seekStatus == TermsEnum.SeekStatus.FOUND) { + docsEnum = termsEnum.docs(slowAtomicReader.getLiveDocs(), docsEnum, DocsEnum.FLAG_NONE); + expectedResult.set(docsEnum.nextDoc()); + mockScorer.scores = entry.getValue(); + expectedTopDocsCollector.collect(docsEnum.docID()); + } else if (seekStatus == TermsEnum.SeekStatus.END) { + break; + } } } } diff --git a/src/test/java/org/elasticsearch/index/search/child/MockScorer.java b/src/test/java/org/elasticsearch/index/search/child/MockScorer.java index 7fe3fd5931a..96fb12d3dac 100644 --- a/src/test/java/org/elasticsearch/index/search/child/MockScorer.java +++ b/src/test/java/org/elasticsearch/index/search/child/MockScorer.java @@ -35,6 +35,9 @@ class MockScorer extends Scorer { @Override public float score() throws IOException { + if (scoreType == ScoreType.NONE) { + return 1.0f; + } float aggregateScore = 0; for (int i = 0; i < scores.elementsCount; i++) { float score = scores.buffer[i]; diff --git a/src/test/java/org/elasticsearch/search/child/SimpleChildQuerySearchTests.java b/src/test/java/org/elasticsearch/search/child/SimpleChildQuerySearchTests.java index ab8dbe65e4d..4652b99bb37 100644 --- a/src/test/java/org/elasticsearch/search/child/SimpleChildQuerySearchTests.java +++ b/src/test/java/org/elasticsearch/search/child/SimpleChildQuerySearchTests.java @@ -59,6 +59,7 @@ import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilde import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.FilterBuilders.*; import static org.elasticsearch.index.query.QueryBuilders.*; +import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.factorFunction; import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.scriptFunction; import static org.elasticsearch.search.facet.FacetBuilders.termsFacet; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*; @@ -1999,6 +2000,480 @@ public class SimpleChildQuerySearchTests extends ElasticsearchIntegrationTest { assertThat(response.getHits().getAt(0).id(), equalTo("1")); } + List createMinMaxDocBuilders() { + List 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; + } + + SearchResponse MinMaxQuery(String scoreType, int minChildren, int maxChildren, int cutoff) throws SearchPhaseExecutionException { + return client() + .prepareSearch("test") + .setQuery( + QueryBuilders + .hasChildQuery( + "child", + QueryBuilders.functionScoreQuery(constantScoreQuery(FilterBuilders.termFilter("foo", "two"))).boostMode("replace").scoreMode("sum") + .add(FilterBuilders.matchAllFilter(), factorFunction(1)) + .add(FilterBuilders.termFilter("foo", "three"), factorFunction(1)) + .add(FilterBuilders.termFilter("foo", "four"), factorFunction(1))).scoreType(scoreType) + .minChildren(minChildren).maxChildren(maxChildren).setShortCircuitCutoff(cutoff)) + .addSort("_score", SortOrder.DESC).addSort("id", SortOrder.ASC).get(); + } + + SearchResponse MinMaxFilter( int minChildren, int maxChildren, int cutoff) throws SearchPhaseExecutionException { + return client() + .prepareSearch("test") + .setQuery( + QueryBuilders.constantScoreQuery(FilterBuilders.hasChildFilter("child", termFilter("foo", "two")) + .minChildren(minChildren).maxChildren(maxChildren).setShortCircuitCutoff(cutoff))) + .addSort("id", SortOrder.ASC).setTrackScores(true).get(); + } + + + @Test + public void testMinMaxChildren() throws Exception { + assertAcked(prepareCreate("test").addMapping("parent").addMapping("child", "_parent", "type=parent")); + ensureGreen(); + + indexRandom(true, createMinMaxDocBuilders().toArray(new IndexRequestBuilder[0])); + SearchResponse response; + int cutoff = getRandom().nextInt(4); + + // Score mode = NONE + response = MinMaxQuery("none", 0, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("none", 1, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("none", 2, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("4")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + + response = MinMaxQuery("none", 3, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + + response = MinMaxQuery("none", 4, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(0l)); + + response = MinMaxQuery("none", 0, 4, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("none", 0, 3, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("none", 0, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + + response = MinMaxQuery("none", 2, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + + try { + response = MinMaxQuery("none", 3, 2, cutoff); + fail(); + } catch (SearchPhaseExecutionException e) { + assertThat(e.getMessage(), containsString("[has_child] 'max_children' is less than 'min_children'")); + } + + // Score mode = SUM + response = MinMaxQuery("sum", 0, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(6f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(3f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("sum", 1, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(6f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(3f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("sum", 2, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(6f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(3f)); + + response = MinMaxQuery("sum", 3, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(6f)); + + response = MinMaxQuery("sum", 4, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(0l)); + + response = MinMaxQuery("sum", 0, 4, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(6f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(3f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("sum", 0, 3, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(6f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(3f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("sum", 0, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + assertThat(response.getHits().hits()[1].id(), equalTo("2")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + + response = MinMaxQuery("sum", 2, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + + try { + response = MinMaxQuery("sum", 3, 2, cutoff); + fail(); + } catch (SearchPhaseExecutionException e) { + assertThat(e.getMessage(), containsString("[has_child] 'max_children' is less than 'min_children'")); + } + + // Score mode = MAX + response = MinMaxQuery("max", 0, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(2f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("max", 1, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(2f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("max", 2, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(2f)); + + response = MinMaxQuery("max", 3, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + + response = MinMaxQuery("max", 4, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(0l)); + + response = MinMaxQuery("max", 0, 4, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(2f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("max", 0, 3, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(3f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(2f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("max", 0, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + assertThat(response.getHits().hits()[1].id(), equalTo("2")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + + response = MinMaxQuery("max", 2, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + + try { + response = MinMaxQuery("max", 3, 2, cutoff); + fail(); + } catch (SearchPhaseExecutionException e) { + assertThat(e.getMessage(), containsString("[has_child] 'max_children' is less than 'min_children'")); + } + + // Score mode = AVG + response = MinMaxQuery("avg", 0, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1.5f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("avg", 1, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1.5f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("avg", 2, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1.5f)); + + response = MinMaxQuery("avg", 3, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + + response = MinMaxQuery("avg", 4, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(0l)); + + response = MinMaxQuery("avg", 0, 4, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1.5f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("avg", 0, 3, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(2f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1.5f)); + assertThat(response.getHits().hits()[2].id(), equalTo("2")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxQuery("avg", 0, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(1.5f)); + assertThat(response.getHits().hits()[1].id(), equalTo("2")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + + response = MinMaxQuery("avg", 2, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(1.5f)); + + try { + response = MinMaxQuery("avg", 3, 2, cutoff); + fail(); + } catch (SearchPhaseExecutionException e) { + assertThat(e.getMessage(), containsString("[has_child] 'max_children' is less than 'min_children'")); + } + + // HasChildFilter + response = MinMaxFilter(0, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxFilter(1, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxFilter(2, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("4")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + + response = MinMaxFilter(3, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("4")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + + response = MinMaxFilter(4, 0, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(0l)); + + response = MinMaxFilter(0, 4, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxFilter(0, 3, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(3l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + assertThat(response.getHits().hits()[2].id(), equalTo("4")); + assertThat(response.getHits().hits()[2].score(), equalTo(1f)); + + response = MinMaxFilter(0, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(2l)); + assertThat(response.getHits().hits()[0].id(), equalTo("2")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + assertThat(response.getHits().hits()[1].id(), equalTo("3")); + assertThat(response.getHits().hits()[1].score(), equalTo(1f)); + + response = MinMaxFilter(2, 2, cutoff); + + assertThat(response.getHits().totalHits(), equalTo(1l)); + assertThat(response.getHits().hits()[0].id(), equalTo("3")); + assertThat(response.getHits().hits()[0].score(), equalTo(1f)); + + try { + response = MinMaxFilter(3, 2, cutoff); + fail(); + } catch (SearchPhaseExecutionException e) { + assertThat(e.getMessage(), containsString("[has_child] 'max_children' is less than 'min_children'")); + } + + } + + private static HasChildFilterBuilder hasChildFilter(String type, QueryBuilder queryBuilder) { HasChildFilterBuilder hasChildFilterBuilder = FilterBuilders.hasChildFilter(type, queryBuilder); hasChildFilterBuilder.setShortCircuitCutoff(randomInt(10));