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
This commit is contained in:
Clinton Gormley 2014-05-30 19:17:52 +02:00
parent 48ccb06160
commit 46a67b638d
11 changed files with 983 additions and 185 deletions

View File

@ -42,6 +42,36 @@ 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] [float]
==== Memory Considerations ==== Memory Considerations

View File

@ -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] [float]
==== Memory Considerations ==== Memory Considerations

View File

@ -32,6 +32,9 @@ public class HasChildFilterBuilder extends BaseFilterBuilder {
private String childType; private String childType;
private String filterName; private String filterName;
private Integer shortCircuitCutoff; private Integer shortCircuitCutoff;
private Integer minChildren;
private Integer maxChildren;
public HasChildFilterBuilder(String type, QueryBuilder queryBuilder) { public HasChildFilterBuilder(String type, QueryBuilder queryBuilder) {
this.childType = type; this.childType = type;
@ -53,6 +56,23 @@ public class HasChildFilterBuilder extends BaseFilterBuilder {
return this; 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. * This is a noop since has_child can't be cached.
*/ */
@ -87,6 +107,12 @@ public class HasChildFilterBuilder extends BaseFilterBuilder {
filterBuilder.toXContent(builder, params); filterBuilder.toXContent(builder, params);
} }
builder.field("child_type", childType); builder.field("child_type", childType);
if (minChildren != null) {
builder.field("min_children", minChildren);
}
if (maxChildren != null) {
builder.field("max_children", maxChildren);
}
if (filterName != null) { if (filterName != null) {
builder.field("_name", filterName); builder.field("_name", filterName);
} }

View File

@ -29,7 +29,9 @@ import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.internal.ParentFieldMapper; import org.elasticsearch.index.mapper.internal.ParentFieldMapper;
import org.elasticsearch.index.query.support.XContentStructure; import org.elasticsearch.index.query.support.XContentStructure;
import org.elasticsearch.index.search.child.ChildrenConstantScoreQuery; 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.CustomQueryWrappingFilter;
import org.elasticsearch.index.search.child.ScoreType;
import org.elasticsearch.index.search.nested.NonNestedDocsFilter; import org.elasticsearch.index.search.nested.NonNestedDocsFilter;
import java.io.IOException; import java.io.IOException;
@ -61,6 +63,8 @@ public class HasChildFilterParser implements FilterParser {
boolean filterFound = false; boolean filterFound = false;
String childType = null; String childType = null;
int shortCircuitParentDocSet = 8192; // Tests show a cut of point between 8192 and 16384. int shortCircuitParentDocSet = 8192; // Tests show a cut of point between 8192 and 16384.
int minChildren = 0;
int maxChildren = 0;
String filterName = null; String filterName = null;
String currentFieldName = null; String currentFieldName = null;
@ -97,6 +101,10 @@ public class HasChildFilterParser implements FilterParser {
// noop to be backwards compatible // noop to be backwards compatible
} else if ("short_circuit_cutoff".equals(currentFieldName)) { } else if ("short_circuit_cutoff".equals(currentFieldName)) {
shortCircuitParentDocSet = parser.intValue(); 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 { } else {
throw new QueryParsingException(parseContext.index(), "[has_child] filter does not support [" + currentFieldName + "]"); 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 + "]"); 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; Filter nonNestedDocsFilter = null;
if (parentDocMapper.hasNestedObjects()) { if (parentDocMapper.hasNestedObjects()) {
nonNestedDocsFilter = parseContext.cacheFilter(NonNestedDocsFilter.INSTANCE, null); nonNestedDocsFilter = parseContext.cacheFilter(NonNestedDocsFilter.INSTANCE, null);
@ -145,12 +157,18 @@ public class HasChildFilterParser implements FilterParser {
Filter parentFilter = parseContext.cacheFilter(parentDocMapper.typeFilter(), null); Filter parentFilter = parseContext.cacheFilter(parentDocMapper.typeFilter(), null);
ParentChildIndexFieldData parentChildIndexFieldData = parseContext.fieldData().getForField(parentFieldMapper); 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) { if (filterName != null) {
parseContext.addNamedFilter(filterName, new CustomQueryWrappingFilter(childrenConstantScoreQuery)); parseContext.addNamedFilter(filterName, new CustomQueryWrappingFilter(childrenQuery));
} }
return new CustomQueryWrappingFilter(childrenConstantScoreQuery); return new CustomQueryWrappingFilter(childrenQuery);
} }
} }

View File

@ -35,6 +35,10 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ
private String scoreType; private String scoreType;
private Integer minChildren;
private Integer maxChildren;
private Integer shortCircuitCutoff; private Integer shortCircuitCutoff;
private String queryName; private String queryName;
@ -61,6 +65,22 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ
return this; 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 * Configures at what cut off point only to evaluate parent documents that contain the matching parent id terms
* instead of evaluating all parent docs. * instead of evaluating all parent docs.
@ -90,6 +110,12 @@ public class HasChildQueryBuilder extends BaseQueryBuilder implements BoostableQ
if (scoreType != null) { if (scoreType != null) {
builder.field("score_type", scoreType); builder.field("score_type", scoreType);
} }
if (minChildren != null) {
builder.field("min_children", minChildren);
}
if (maxChildren != null) {
builder.field("max_children", maxChildren);
}
if (shortCircuitCutoff != null) { if (shortCircuitCutoff != null) {
builder.field("short_circuit_cutoff", shortCircuitCutoff); builder.field("short_circuit_cutoff", shortCircuitCutoff);
} }

View File

@ -63,7 +63,9 @@ public class HasChildQueryParser implements QueryParser {
boolean queryFound = false; boolean queryFound = false;
float boost = 1.0f; float boost = 1.0f;
String childType = null; String childType = null;
ScoreType scoreType = null; ScoreType scoreType = ScoreType.NONE;
int minChildren = 0;
int maxChildren = 0;
int shortCircuitParentDocSet = 8192; int shortCircuitParentDocSet = 8192;
String queryName = null; String queryName = null;
@ -88,19 +90,18 @@ public class HasChildQueryParser implements QueryParser {
if ("type".equals(currentFieldName) || "child_type".equals(currentFieldName) || "childType".equals(currentFieldName)) { if ("type".equals(currentFieldName) || "child_type".equals(currentFieldName) || "childType".equals(currentFieldName)) {
childType = parser.text(); childType = parser.text();
} else if ("_scope".equals(currentFieldName)) { } 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)) { } else if ("score_type".equals(currentFieldName) || "scoreType".equals(currentFieldName)) {
String scoreTypeValue = parser.text(); scoreType = ScoreType.fromString(parser.text());
if (!"none".equals(scoreTypeValue)) {
scoreType = ScoreType.fromString(scoreTypeValue);
}
} else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) { } else if ("score_mode".equals(currentFieldName) || "scoreMode".equals(currentFieldName)) {
String scoreModeValue = parser.text(); scoreType = ScoreType.fromString(parser.text());
if (!"none".equals(scoreModeValue)) {
scoreType = ScoreType.fromString(scoreModeValue);
}
} else if ("boost".equals(currentFieldName)) { } else if ("boost".equals(currentFieldName)) {
boost = parser.floatValue(); 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)) { } else if ("short_circuit_cutoff".equals(currentFieldName)) {
shortCircuitParentDocSet = parser.intValue(); shortCircuitParentDocSet = parser.intValue();
} else if ("_name".equals(currentFieldName)) { } else if ("_name".equals(currentFieldName)) {
@ -140,7 +141,12 @@ public class HasChildQueryParser implements QueryParser {
String parentType = parentFieldMapper.type(); String parentType = parentFieldMapper.type();
DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType); DocumentMapper parentDocMapper = parseContext.mapperService().documentMapper(parentType);
if (parentDocMapper == null) { 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; Filter nonNestedDocsFilter = null;
@ -154,10 +160,12 @@ public class HasChildQueryParser implements QueryParser {
Query query; Query query;
Filter parentFilter = parseContext.cacheFilter(parentDocMapper.typeFilter(), null); Filter parentFilter = parseContext.cacheFilter(parentDocMapper.typeFilter(), null);
ParentChildIndexFieldData parentChildIndexFieldData = parseContext.fieldData().getForField(parentFieldMapper); ParentChildIndexFieldData parentChildIndexFieldData = parseContext.fieldData().getForField(parentFieldMapper);
if (scoreType != null) { if (minChildren > 1 || maxChildren > 0 || scoreType != ScoreType.NONE) {
query = new ChildrenQuery(parentChildIndexFieldData, parentType, childType, parentFilter, innerQuery, scoreType, shortCircuitParentDocSet, nonNestedDocsFilter); query = new ChildrenQuery(parentChildIndexFieldData, parentType, childType, parentFilter, innerQuery, scoreType, minChildren,
maxChildren, shortCircuitParentDocSet, nonNestedDocsFilter);
} else { } else {
query = new ChildrenConstantScoreQuery(parentChildIndexFieldData, innerQuery, parentType, childType, parentFilter, shortCircuitParentDocSet, nonNestedDocsFilter); query = new ChildrenConstantScoreQuery(parentChildIndexFieldData, innerQuery, parentType, childType, parentFilter,
shortCircuitParentDocSet, nonNestedDocsFilter);
} }
if (queryName != null) { if (queryName != null) {
parseContext.addNamedFilter(queryName, new CustomQueryWrappingFilter(query)); parseContext.addNamedFilter(queryName, new CustomQueryWrappingFilter(query));

View File

@ -56,19 +56,21 @@ import java.util.Set;
*/ */
public class ChildrenQuery extends Query { public class ChildrenQuery extends Query {
private final ParentChildIndexFieldData ifd; protected final ParentChildIndexFieldData ifd;
private final String parentType; protected final String parentType;
private final String childType; protected final String childType;
private final Filter parentFilter; protected final Filter parentFilter;
private final ScoreType scoreType; protected final ScoreType scoreType;
private Query originalChildQuery; protected Query originalChildQuery;
private final int shortCircuitParentDocSet; protected final int minChildren;
private final Filter nonNestedDocsFilter; protected final int maxChildren;
protected final int shortCircuitParentDocSet;
protected final Filter nonNestedDocsFilter;
private Query rewrittenChildQuery; protected Query rewrittenChildQuery;
private IndexReader rewriteIndexReader; 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.ifd = ifd;
this.parentType = parentType; this.parentType = parentType;
this.childType = childType; this.childType = childType;
@ -77,6 +79,9 @@ public class ChildrenQuery extends Query {
this.scoreType = scoreType; this.scoreType = scoreType;
this.shortCircuitParentDocSet = shortCircuitParentDocSet; this.shortCircuitParentDocSet = shortCircuitParentDocSet;
this.nonNestedDocsFilter = nonNestedDocsFilter; this.nonNestedDocsFilter = nonNestedDocsFilter;
assert maxChildren == 0 || minChildren <= maxChildren;
this.minChildren = minChildren > 1 ? minChildren : 0;
this.maxChildren = maxChildren;
} }
@Override @Override
@ -98,6 +103,12 @@ public class ChildrenQuery extends Query {
if (getBoost() != that.getBoost()) { if (getBoost() != that.getBoost()) {
return false; return false;
} }
if (minChildren != that.minChildren) {
return false;
}
if (maxChildren != that.maxChildren) {
return false;
}
return true; return true;
} }
@ -106,13 +117,16 @@ public class ChildrenQuery extends Query {
int result = originalChildQuery.hashCode(); int result = originalChildQuery.hashCode();
result = 31 * result + childType.hashCode(); result = 31 * result + childType.hashCode();
result = 31 * result + Float.floatToIntBits(getBoost()); result = 31 * result + Float.floatToIntBits(getBoost());
result = 31 * result + minChildren;
result = 31 * result + maxChildren;
return result; return result;
} }
@Override @Override
public String toString(String field) { public String toString(String field) {
return "ChildrenQuery[" + childType + "/" + parentType + "](" + originalChildQuery int max = maxChildren == 0 ? Integer.MAX_VALUE : maxChildren;
.toString(field) + ')' + ToStringUtils.boost(getBoost()); return "ChildrenQuery[min(" + Integer.toString(minChildren) + ") max(" + Integer.toString(max) + ")of " + childType + "/"
+ parentType + "](" + originalChildQuery.toString(field) + ')' + ToStringUtils.boost(getBoost());
} }
@Override @Override
@ -144,12 +158,13 @@ public class ChildrenQuery extends Query {
public Weight createWeight(IndexSearcher searcher) throws IOException { public Weight createWeight(IndexSearcher searcher) throws IOException {
SearchContext sc = SearchContext.current(); SearchContext sc = SearchContext.current();
assert rewrittenChildQuery != null; 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; final Query childQuery = rewrittenChildQuery;
IndexFieldData.WithOrdinals globalIfd = ifd.getGlobalParentChild(parentType, searcher.getIndexReader()); IndexFieldData.WithOrdinals globalIfd = ifd.getGlobalParentChild(parentType, searcher.getIndexReader());
if (globalIfd == null) { 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); return Queries.newMatchNoDocsQuery().createWeight(searcher);
} }
IndexSearcher indexSearcher = new IndexSearcher(searcher.getIndexReader()); IndexSearcher indexSearcher = new IndexSearcher(searcher.getIndexReader());
@ -157,8 +172,9 @@ public class ChildrenQuery extends Query {
boolean abort = true; boolean abort = true;
long numFoundParents; long numFoundParents;
ParentOrdAndScoreCollector collector = null; ParentCollector collector = null;
try { try {
if (minChildren == 0 && maxChildren == 0 && scoreType != ScoreType.NONE) {
switch (scoreType) { switch (scoreType) {
case MAX: case MAX:
collector = new MaxCollector(globalIfd, sc); collector = new MaxCollector(globalIfd, sc);
@ -166,12 +182,25 @@ public class ChildrenQuery extends Query {
case SUM: case SUM:
collector = new SumCollector(globalIfd, sc); collector = new SumCollector(globalIfd, sc);
break; break;
}
}
if (collector == null) {
switch (scoreType) {
case MAX:
collector = new MaxCountCollector(globalIfd, sc);
break;
case SUM:
case AVG: case AVG:
collector = new AvgCollector(globalIfd, sc); collector = new SumCountAndAvgCollector(globalIfd, sc);
break;
case NONE:
collector = new CountCollector(globalIfd, sc);
break; break;
default: default:
throw new RuntimeException("Are we missing a score type here? -- " + scoreType); throw new RuntimeException("Are we missing a score type here? -- " + scoreType);
} }
}
indexSearcher.search(childQuery, collector); indexSearcher.search(childQuery, collector);
numFoundParents = collector.foundParents(); numFoundParents = collector.foundParents();
if (numFoundParents == 0) { if (numFoundParents == 0) {
@ -186,28 +215,34 @@ public class ChildrenQuery extends Query {
sc.addReleasable(collector, Lifetime.COLLECTION); sc.addReleasable(collector, Lifetime.COLLECTION);
final Filter parentFilter; final Filter parentFilter;
if (numFoundParents <= shortCircuitParentDocSet) { if (numFoundParents <= shortCircuitParentDocSet) {
parentFilter = ParentIdsFilter.createShortCircuitFilter( parentFilter = ParentIdsFilter.createShortCircuitFilter(nonNestedDocsFilter, sc, parentType, collector.values,
nonNestedDocsFilter, sc, parentType, collector.values, collector.parentIdxs, numFoundParents collector.parentIdxs, numFoundParents);
);
} else { } else {
parentFilter = new ApplyAcceptedDocsFilter(this.parentFilter); 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; protected final Weight childWeight;
private final Filter parentFilter; protected final Filter parentFilter;
private final ParentOrdAndScoreCollector collector; 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.childWeight = childWeight;
this.parentFilter = parentFilter; this.parentFilter = parentFilter;
this.remaining = remaining; this.remaining = remaining;
this.collector = collector; this.collector = collector;
this.minChildren = minChildren;
this.maxChildren = maxChildren;
} }
@Override @Override
@ -221,14 +256,20 @@ public class ChildrenQuery extends Query {
} }
@Override @Override
public float getValueForNormalization() throws IOException { public void normalize(float norm, float topLevelBoost) {
float sum = childWeight.getValueForNormalization(); this.queryNorm = norm * topLevelBoost;
sum *= getBoost() * getBoost(); queryWeight *= this.queryNorm;
return sum;
} }
@Override @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 @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" // 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. // count down (short circuit) logic will then work as expected.
DocIdSetIterator parents = BitsFilteredDocIdSet.wrap(parentsSet, context.reader().getLiveDocs()).iterator(); DocIdSetIterator parents = BitsFilteredDocIdSet.wrap(parentsSet, context.reader().getLiveDocs()).iterator();
if (parents != null) {
BytesValues.WithOrdinals bytesValues = collector.globalIfd.load(context).getBytesValues(false); BytesValues.WithOrdinals bytesValues = collector.globalIfd.load(context).getBytesValues(false);
if (bytesValues == null) { if (bytesValues == null) {
return 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) { switch (scoreType) {
case AVG: case AVG:
return new AvgParentScorer(this, parents, collector, bytesValues.ordinals()); return new AvgParentScorer(this, parents, collector, globalOrdinals);
default: default:
return new ParentScorer(this, parents, collector, bytesValues.ordinals()); return new ParentScorer(this, parents, collector, globalOrdinals);
}
}
return null;
} }
} }
} protected abstract static class ParentCollector extends NoopCollector implements Releasable {
private abstract static class ParentOrdAndScoreCollector extends NoopCollector implements Releasable { protected final IndexFieldData.WithOrdinals globalIfd;
private final IndexFieldData.WithOrdinals globalIfd;
protected final LongHash parentIdxs; protected final LongHash parentIdxs;
protected final BigArrays bigArrays; protected final BigArrays bigArrays;
protected FloatArray scores;
protected final SearchContext searchContext; protected final SearchContext searchContext;
protected Ordinals.Docs globalOrdinals; protected Ordinals.Docs globalOrdinals;
protected BytesValues.WithOrdinals values; protected BytesValues.WithOrdinals values;
protected Scorer scorer; protected Scorer scorer;
private ParentOrdAndScoreCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) { protected ParentCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) {
this.globalIfd = globalIfd; this.globalIfd = globalIfd;
this.searchContext = searchContext;
this.bigArrays = searchContext.bigArrays(); this.bigArrays = searchContext.bigArrays();
this.parentIdxs = new LongHash(512, bigArrays); this.parentIdxs = new LongHash(512, bigArrays);
this.scores = bigArrays.newFloatArray(512, false);
this.searchContext = searchContext;
} }
@Override @Override
public void collect(int doc) throws IOException { public final void collect(int doc) throws IOException {
if (globalOrdinals != null) { if (globalOrdinals != null) {
final long globalOrdinal = globalOrdinals.getOrd(doc); final long globalOrdinal = globalOrdinals.getOrd(doc);
if (globalOrdinal != Ordinals.MISSING_ORDINAL) { if (globalOrdinal != Ordinals.MISSING_ORDINAL) {
long parentIdx = parentIdxs.add(globalOrdinal); long parentIdx = parentIdxs.add(globalOrdinal);
if (parentIdx >= 0) { if (parentIdx >= 0) {
scores = bigArrays.grow(scores, parentIdx + 1); newParent(parentIdx);
scores.set(parentIdx, scorer.score());
} else { } else {
parentIdx = -1 - parentIdx; 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 @Override
@ -302,11 +364,6 @@ public class ChildrenQuery extends Query {
if (values != null) { if (values != null) {
globalOrdinals = values.ordinals(); globalOrdinals = values.ordinals();
} }
}
public long foundParents() {
return parentIdxs.size();
} }
@Override @Override
@ -314,71 +371,133 @@ public class ChildrenQuery extends Query {
this.scorer = scorer; 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 @Override
public void close() throws ElasticsearchException { public void close() throws ElasticsearchException {
Releasables.close(parentIdxs, scores); 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) { private SumCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) {
super(globalIfd, searchContext); super(globalIfd, searchContext);
} }
@Override @Override
protected void doScore(long index) throws IOException { protected void existingParent(long parentIdx) throws IOException {
scores.increment(index, scorer.score()); 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) { private MaxCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) {
super(globalIfd, searchContext); super(globalIfd, searchContext);
} }
@Override @Override
protected void doScore(long index) throws IOException { protected void existingParent(long parentIdx) throws IOException {
float currentScore = scorer.score(); float currentScore = scorer.score();
if (currentScore > scores.get(index)) { if (currentScore > scores.get(parentIdx)) {
scores.set(index, currentScore); scores.set(parentIdx, currentScore);
} }
} }
} }
private final static class AvgCollector extends ParentOrdAndScoreCollector { private final static class MaxCountCollector extends ParentScoreCountCollector {
private IntArray occurrences; private MaxCountCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) {
AvgCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) {
super(globalIfd, searchContext); super(globalIfd, searchContext);
this.occurrences = bigArrays.newIntArray(512, false);
} }
@Override @Override
public void collect(int doc) throws IOException { protected void existingParent(long parentIdx) throws IOException {
if (globalOrdinals != null) { float currentScore = scorer.score();
final long globalOrdinal = globalOrdinals.getOrd(doc); if (currentScore > scores.get(parentIdx)) {
if (globalOrdinal != Ordinals.MISSING_ORDINAL) { scores.set(parentIdx, currentScore);
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); occurrences.increment(parentIdx, 1);
} }
} }
}
private final static class SumCountAndAvgCollector extends ParentScoreCountCollector {
SumCountAndAvgCollector(IndexFieldData.WithOrdinals globalIfd, SearchContext searchContext) {
super(globalIfd, searchContext);
} }
@Override @Override
public void close() throws ElasticsearchException { protected void existingParent(long parentIdx) throws IOException {
Releasables.close(parentIdxs, scores, occurrences); scores.increment(parentIdx, scorer.score());
occurrences.increment(parentIdx, 1);
} }
} }
@ -394,13 +513,13 @@ public class ChildrenQuery extends Query {
int currentDocId = -1; int currentDocId = -1;
float currentScore; float currentScore;
ParentScorer(ParentWeight parentWeight, DocIdSetIterator parentsIterator, ParentOrdAndScoreCollector collector, Ordinals.Docs globalOrdinals) { ParentScorer(ParentWeight parentWeight, DocIdSetIterator parentsIterator, ParentCollector collector, Ordinals.Docs globalOrdinals) {
super(parentWeight); super(parentWeight);
this.parentWeight = parentWeight; this.parentWeight = parentWeight;
this.globalOrdinals = globalOrdinals; this.globalOrdinals = globalOrdinals;
this.parentsIterator = parentsIterator; this.parentsIterator = parentsIterator;
this.parentIds = collector.parentIdxs; this.parentIds = collector.parentIdxs;
this.scores = collector.scores; this.scores = ((ParentScoreCollector) collector).scores;
} }
@Override @Override
@ -408,6 +527,11 @@ public class ChildrenQuery extends Query {
return currentScore; return currentScore;
} }
protected boolean acceptAndScore(long parentIdx) {
currentScore = scores.get(parentIdx);
return true;
}
@Override @Override
public int freq() throws IOException { public int freq() throws IOException {
// We don't have the original child query hit info here... // We don't have the original child query hit info here...
@ -439,12 +563,13 @@ public class ChildrenQuery extends Query {
final long parentIdx = parentIds.find(globalOrdinal); final long parentIdx = parentIds.find(globalOrdinal);
if (parentIdx != -1) { if (parentIdx != -1) {
currentScore = scores.get(parentIdx);
parentWeight.remaining--; parentWeight.remaining--;
if (acceptAndScore(parentIdx)) {
return currentDocId; return currentDocId;
} }
} }
} }
}
@Override @Override
public int advance(int target) throws IOException { public int advance(int target) throws IOException {
@ -464,13 +589,13 @@ public class ChildrenQuery extends Query {
final long parentIdx = parentIds.find(globalOrdinal); final long parentIdx = parentIds.find(globalOrdinal);
if (parentIdx != -1) { if (parentIdx != -1) {
currentScore = scores.get(parentIdx);
parentWeight.remaining--; parentWeight.remaining--;
if (acceptAndScore(parentIdx)) {
return currentDocId; return currentDocId;
} else {
return nextDoc();
} }
} }
return nextDoc();
}
@Override @Override
public long cost() { public long cost() {
@ -478,68 +603,104 @@ 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) { ParentCountScorer(ParentWeight parentWeight, DocIdSetIterator parentsIterator, ParentCollector collector, Ordinals.Docs globalOrdinals, int minChildren, int maxChildren) {
super(weight, parentsIterator, collector, globalOrdinals); super(parentWeight, parentsIterator, (ParentScoreCollector) collector, globalOrdinals);
this.occurrences = ((AvgCollector) collector).occurrences; 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 @Override
public int nextDoc() throws IOException { protected boolean acceptAndScore(long parentIdx) {
if (parentWeight.remaining == 0) {
return currentDocId = NO_MORE_DOCS;
}
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);
if (parentIdx != -1) {
currentScore = scores.get(parentIdx); currentScore = scores.get(parentIdx);
currentScore /= occurrences.get(parentIdx); currentScore /= occurrences.get(parentIdx);
parentWeight.remaining--; return true;
return currentDocId;
} }
} }
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 @Override
public int advance(int target) throws IOException { protected boolean acceptAndScore(long parentIdx) {
if (parentWeight.remaining == 0) { int count = occurrences.get(parentIdx);
return currentDocId = NO_MORE_DOCS; if (count < minChildren || count > maxChildren) {
return false;
} }
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 = scores.get(parentIdx);
currentScore /= occurrences.get(parentIdx); currentScore /= occurrences.get(parentIdx);
parentWeight.remaining--; return true;
return currentDocId;
} else {
return nextDoc();
} }
} }
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) {
try {
advance(DocIdSetIterator.NO_MORE_DOCS);
} catch (IOException e) {
throw new RuntimeException(e);
}
return false;
}
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;
}
} }
} }

View File

@ -24,24 +24,33 @@ import org.elasticsearch.ElasticsearchIllegalArgumentException;
* Defines how scores from child documents are mapped into the parent document. * Defines how scores from child documents are mapped into the parent document.
*/ */
public enum ScoreType { 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, 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, AVG,
/** /**
* The matching children scores is summed up and mapped into the parent. * 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) { public static ScoreType fromString(String type) {
if ("max".equals(type)) { if ("none".equals(type)) {
return NONE;
} else if ("max".equals(type)) {
return MAX; return MAX;
} else if ("avg".equals(type)) { } else if ("avg".equals(type)) {
return AVG; return AVG;

View File

@ -78,7 +78,10 @@ public class ChildrenQueryTests extends ElasticsearchLuceneTestCase {
ParentFieldMapper parentFieldMapper = SearchContext.current().mapperService().documentMapper("child").parentFieldMapper(); ParentFieldMapper parentFieldMapper = SearchContext.current().mapperService().documentMapper("child").parentFieldMapper();
ParentChildIndexFieldData parentChildIndexFieldData = SearchContext.current().fieldData().getForField(parentFieldMapper); ParentChildIndexFieldData parentChildIndexFieldData = SearchContext.current().fieldData().getForField(parentFieldMapper);
Filter parentFilter = new TermFilter(new Term(TypeFieldMapper.NAME, "parent")); 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); QueryUtils.check(query);
} }
@ -219,7 +222,13 @@ public class ChildrenQueryTests extends ElasticsearchLuceneTestCase {
int shortCircuitParentDocSet = random().nextInt(numParentDocs); int shortCircuitParentDocSet = random().nextInt(numParentDocs);
ScoreType scoreType = ScoreType.values()[random().nextInt(ScoreType.values().length)]; ScoreType scoreType = ScoreType.values()[random().nextInt(ScoreType.values().length)];
Filter nonNestedDocsFilter = random().nextBoolean() ? NonNestedDocsFilter.INSTANCE : null; 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); query = new XFilteredQuery(query, filterMe);
BitSetCollector collector = new BitSetCollector(indexReader.maxDoc()); BitSetCollector collector = new BitSetCollector(indexReader.maxDoc());
int numHits = 1 + random().nextInt(25); int numHits = 1 + random().nextInt(25);
@ -239,6 +248,8 @@ public class ChildrenQueryTests extends ElasticsearchLuceneTestCase {
TermsEnum termsEnum = terms.iterator(null); TermsEnum termsEnum = terms.iterator(null);
DocsEnum docsEnum = null; DocsEnum docsEnum = null;
for (Map.Entry<String, FloatArrayList> entry : parentIdToChildScores.entrySet()) { for (Map.Entry<String, FloatArrayList> entry : parentIdToChildScores.entrySet()) {
int count = entry.getValue().elementsCount;
if (count >= minChildren && (maxChildren == 0 || count <= maxChildren)) {
TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(Uid.createUidAsBytes("parent", entry.getKey())); TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(Uid.createUidAsBytes("parent", entry.getKey()));
if (seekStatus == TermsEnum.SeekStatus.FOUND) { if (seekStatus == TermsEnum.SeekStatus.FOUND) {
docsEnum = termsEnum.docs(slowAtomicReader.getLiveDocs(), docsEnum, DocsEnum.FLAG_NONE); docsEnum = termsEnum.docs(slowAtomicReader.getLiveDocs(), docsEnum, DocsEnum.FLAG_NONE);
@ -251,6 +262,7 @@ public class ChildrenQueryTests extends ElasticsearchLuceneTestCase {
} }
} }
} }
}
assertBitSet(actualResult, expectedResult, searcher); assertBitSet(actualResult, expectedResult, searcher);
assertTopDocs(actualTopDocsCollector.topDocs(), expectedTopDocsCollector.topDocs()); assertTopDocs(actualTopDocsCollector.topDocs(), expectedTopDocsCollector.topDocs());

View File

@ -35,6 +35,9 @@ class MockScorer extends Scorer {
@Override @Override
public float score() throws IOException { public float score() throws IOException {
if (scoreType == ScoreType.NONE) {
return 1.0f;
}
float aggregateScore = 0; float aggregateScore = 0;
for (int i = 0; i < scores.elementsCount; i++) { for (int i = 0; i < scores.elementsCount; i++) {
float score = scores.buffer[i]; float score = scores.buffer[i];

View File

@ -59,6 +59,7 @@ import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilde
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.query.FilterBuilders.*; import static org.elasticsearch.index.query.FilterBuilders.*;
import static org.elasticsearch.index.query.QueryBuilders.*; 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.index.query.functionscore.ScoreFunctionBuilders.scriptFunction;
import static org.elasticsearch.search.facet.FacetBuilders.termsFacet; import static org.elasticsearch.search.facet.FacetBuilders.termsFacet;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*;
@ -1999,6 +2000,480 @@ public class SimpleChildQuerySearchTests extends ElasticsearchIntegrationTest {
assertThat(response.getHits().getAt(0).id(), equalTo("1")); 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) { private static HasChildFilterBuilder hasChildFilter(String type, QueryBuilder queryBuilder) {
HasChildFilterBuilder hasChildFilterBuilder = FilterBuilders.hasChildFilter(type, queryBuilder); HasChildFilterBuilder hasChildFilterBuilder = FilterBuilders.hasChildFilter(type, queryBuilder);
hasChildFilterBuilder.setShortCircuitCutoff(randomInt(10)); hasChildFilterBuilder.setShortCircuitCutoff(randomInt(10));