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

@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -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];

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