Skip to content

Commit

Permalink
Parent/Child: Added min_children/max_children to has_child query/filter
Browse files Browse the repository at this point in the history
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
  • Loading branch information
clintongormley committed May 30, 2014
1 parent 7e88cdf commit 31bfe6d
Show file tree
Hide file tree
Showing 11 changed files with 976 additions and 178 deletions.
34 changes: 32 additions & 2 deletions docs/reference/query-dsl/filters/has-child-filter.asciidoc
Expand Up @@ -16,7 +16,7 @@ the query. Here is an example:
}
}
}
}
}
--------------------------------------------------

The `type` is the child type to query against. The parent type to return
Expand All @@ -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

Expand Down
30 changes: 30 additions & 0 deletions docs/reference/query-dsl/queries/has-child-query.asciidoc
Expand Up @@ -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

Expand Down
Expand Up @@ -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;
Expand All @@ -53,6 +56,23 @@ public HasChildFilterBuilder filterName(String filterName) {
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.
*/
Expand Down Expand Up @@ -87,6 +107,12 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
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);
}
Expand Down
Expand Up @@ -29,7 +29,9 @@
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;
Expand Down Expand Up @@ -61,6 +63,8 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
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;
Expand Down Expand Up @@ -97,6 +101,10 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
// 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 + "]");
}
Expand Down Expand Up @@ -138,19 +146,29 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
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);
}

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);
}

}
Expand Up @@ -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;
Expand All @@ -61,6 +65,22 @@ public HasChildQueryBuilder scoreType(String scoreType) {
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.
Expand Down Expand Up @@ -90,6 +110,12 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep
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);
}
Expand Down
Expand Up @@ -52,7 +52,7 @@ public HasChildQueryParser() {

@Override
public String[] names() {
return new String[]{NAME, Strings.toCamelCase(NAME)};
return new String[] { NAME, Strings.toCamelCase(NAME) };
}

@Override
Expand All @@ -63,7 +63,9 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
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;

Expand All @@ -79,7 +81,7 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
// 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 + "]");
Expand All @@ -88,19 +90,18 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
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)) {
Expand Down Expand Up @@ -140,7 +141,12 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
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;
Expand All @@ -154,10 +160,12 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
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));
Expand Down

0 comments on commit 31bfe6d

Please sign in to comment.