浏览代码

Parent/Child: Added min_children/max_children to has_child query/filter

Added support for min_children and max_children parameters to
the has_child query and filter. A parent document will only
be considered if a match if the number of matching children
fall between the min/max bounds.

Closes #6019
Clinton Gormley 11 年之前
父节点
当前提交
46a67b638d

+ 32 - 2
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
 

+ 30 - 0
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
 

+ 26 - 0
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);
         }

+ 21 - 3
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);
 
+        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);
+        }
         if (filterName != null) {
-            parseContext.addNamedFilter(filterName, new CustomQueryWrappingFilter(childrenConstantScoreQuery));
+            parseContext.addNamedFilter(filterName, new CustomQueryWrappingFilter(childrenQuery));
         }
-        return new CustomQueryWrappingFilter(childrenConstantScoreQuery);
+        return new CustomQueryWrappingFilter(childrenQuery);
     }
 
 }

+ 26 - 0
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);
         }

+ 24 - 16
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.<type> 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));

+ 303 - 142
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;
-
-    private Query rewrittenChildQuery;
-    private IndexReader rewriteIndexReader;
-
-    public ChildrenQuery(ParentChildIndexFieldData ifd, String parentType, String childType, Filter parentFilter, Query childQuery, ScoreType scoreType, int shortCircuitParentDocSet, 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;
+
+    protected Query rewrittenChildQuery;
+    protected IndexReader rewriteIndexReader;
+
+    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) {
+
+            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, bytesValues.ordinals());
+                    return new AvgParentScorer(this, parents, collector, globalOrdinals);
                 default:
-                    return new ParentScorer(this, parents, collector, bytesValues.ordinals());
+                    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 IntArray occurrences;
+    private final static class MaxCountCollector extends ParentScoreCountCollector {
 
-        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;
         }
 
-        @Override
-        public int nextDoc() throws IOException {
-            if (parentWeight.remaining == 0) {
-                return currentDocId = NO_MORE_DOCS;
+        protected boolean acceptAndScore(long parentIdx) {
+            int count = occurrences.get(parentIdx);
+            if (count < minChildren || count > maxChildren) {
+                return false;
             }
+            return super.acceptAndScore(parentIdx);
+        }
+    }
 
-            while (true) {
-                currentDocId = parentsIterator.nextDoc();
-                if (currentDocId == DocIdSetIterator.NO_MORE_DOCS) {
-                    return currentDocId;
-                }
+    private static final class AvgParentScorer extends ParentCountScorer {
 
-                final long globalOrdinal = globalOrdinals.getOrd(currentDocId);
-                if (globalOrdinal == Ordinals.MISSING_ORDINAL) {
-                    continue;
-                }
+        AvgParentScorer(ParentWeight weight, DocIdSetIterator parentsIterator, ParentCollector collector, Ordinals.Docs globalOrdinals) {
+            super(weight, parentsIterator, collector, globalOrdinals, 0, 0);
+        }
 
-                final long parentIdx = parentIds.find(globalOrdinal);
-                if (parentIdx != -1) {
-                    currentScore = scores.get(parentIdx);
-                    currentScore /= occurrences.get(parentIdx);
-                    parentWeight.remaining--;
-                    return currentDocId;
-                }
-            }
+        @Override
+        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
-        public int advance(int target) throws IOException {
-            if (parentWeight.remaining == 0) {
-                return currentDocId = NO_MORE_DOCS;
+        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;
+        }
+    }
 
-            currentDocId = parentsIterator.advance(target);
-            if (currentDocId == DocIdSetIterator.NO_MORE_DOCS) {
-                return currentDocId;
-            }
+    private final static class CountParentOrdIterator extends FilteredDocIdSetIterator {
 
-            final long globalOrdinal = globalOrdinals.getOrd(currentDocId);
-            if (globalOrdinal == Ordinals.MISSING_ORDINAL) {
-                return nextDoc();
+        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) {
+                try {
+                    advance(DocIdSetIterator.NO_MORE_DOCS);
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+                return false;
             }
 
-            final long parentIdx = parentIds.find(globalOrdinal);
-            if (parentIdx != -1) {
-                currentScore = scores.get(parentIdx);
-                currentScore /= occurrences.get(parentIdx);
-                parentWeight.remaining--;
-                return currentDocId;
-            } else {
-                return nextDoc();
+            final long parentOrd = ordinals.getOrd(doc);
+            if (parentOrd != Ordinals.MISSING_ORDINAL) {
+                final long parentIdx = parentIds.find(parentOrd);
+                if (parentIdx != -1) {
+                    parentWeight.remaining--;
+                    int count = occurrences.get(parentIdx);
+                    if (count >= minChildren && count <= maxChildren) {
+                        return true;
+                    }
+                }
             }
+            return false;
         }
     }
 

+ 14 - 5
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;

+ 22 - 10
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<String, FloatArrayList> 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;
+                            }
                         }
                     }
                 }

+ 3 - 0
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];

+ 475 - 0
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<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;
+    }
+
+    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));