Skip to content

Commit d10dcea

Browse files
committed
Add tests for top_hits aggregation (#22754)
Add unit tests for `TopHitsAggregator` and convert some snippets in docs for `top_hits` aggregation to `// CONSOLE`. Relates to #22278 Relates to #18160
1 parent 9dd12bb commit d10dcea

File tree

5 files changed

+225
-129
lines changed

5 files changed

+225
-129
lines changed

buildSrc/src/main/resources/checkstyle_suppressions.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,6 @@
501501
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]aggregations[/\\]metrics[/\\]percentiles[/\\]tdigest[/\\]TDigestPercentilesAggregator.java" checks="LineLength" />
502502
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]aggregations[/\\]metrics[/\\]scripted[/\\]ScriptedMetricAggregator.java" checks="LineLength" />
503503
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]aggregations[/\\]metrics[/\\]stats[/\\]extended[/\\]ExtendedStatsAggregator.java" checks="LineLength" />
504-
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]aggregations[/\\]metrics[/\\]tophits[/\\]TopHitsAggregator.java" checks="LineLength" />
505504
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]aggregations[/\\]pipeline[/\\]bucketscript[/\\]BucketScriptPipelineAggregator.java" checks="LineLength" />
506505
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]aggregations[/\\]support[/\\]AggregationPath.java" checks="LineLength" />
507506
<suppress files="core[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]search[/\\]aggregations[/\\]support[/\\]ValuesSourceParser.java" checks="LineLength" />

core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregator.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,13 @@ public void collect(int docId, long bucket) throws IOException {
124124
// In the QueryPhase we don't need this protection, because it is build into the IndexSearcher,
125125
// but here we create collectors ourselves and we need prevent OOM because of crazy an offset and size.
126126
topN = Math.min(topN, subSearchContext.searcher().getIndexReader().maxDoc());
127-
TopDocsCollector<?> topLevelCollector = sort != null ? TopFieldCollector.create(sort.sort, topN, true, subSearchContext.trackScores(), subSearchContext.trackScores()) : TopScoreDocCollector.create(topN);
127+
TopDocsCollector<?> topLevelCollector;
128+
if (sort == null) {
129+
topLevelCollector = TopScoreDocCollector.create(topN);
130+
} else {
131+
topLevelCollector = TopFieldCollector.create(sort.sort, topN, true, subSearchContext.trackScores(),
132+
subSearchContext.trackScores());
133+
}
128134
collectors = new TopDocsAndLeafCollector(topLevelCollector);
129135
collectors.leafCollector = collectors.topLevelCollector.getLeafCollector(ctx);
130136
collectors.leafCollector.setScorer(scorer);
@@ -172,8 +178,8 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) {
172178
searchHitFields.sortValues(fieldDoc.fields, subSearchContext.sort().formats);
173179
}
174180
}
175-
topHits = new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), topDocs, fetchResult.hits(), pipelineAggregators(),
176-
metaData());
181+
topHits = new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), topDocs, fetchResult.hits(),
182+
pipelineAggregators(), metaData());
177183
}
178184
return topHits;
179185
}
@@ -186,7 +192,8 @@ public InternalTopHits buildEmptyAggregation() {
186192
} else {
187193
topDocs = Lucene.EMPTY_TOP_DOCS;
188194
}
189-
return new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), topDocs, InternalSearchHits.empty(), pipelineAggregators(), metaData());
195+
return new InternalTopHits(name, subSearchContext.from(), subSearchContext.size(), topDocs, InternalSearchHits.empty(),
196+
pipelineAggregators(), metaData());
190197
}
191198

192199
@Override

core/src/test/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public boolean shouldCache(Query query) throws IOException {
9494

9595
CircuitBreakerService circuitBreakerService = new NoneCircuitBreakerService();
9696
SearchContext searchContext = mock(SearchContext.class);
97+
when(searchContext.numberOfShards()).thenReturn(1);
9798
when(searchContext.searcher()).thenReturn(contextIndexSearcher);
9899
when(searchContext.bigArrays()).thenReturn(new MockBigArrays(Settings.EMPTY, circuitBreakerService));
99100
when(searchContext.fetchPhase())
@@ -163,13 +164,17 @@ protected <A extends InternalAggregation, C extends Aggregator> A searchAndReduc
163164
List<InternalAggregation> aggs = new ArrayList<> ();
164165
Query rewritten = searcher.rewrite(query);
165166
Weight weight = searcher.createWeight(rewritten, true);
166-
try (C root = createAggregator(builder, searcher, fieldTypes)) {
167+
C root = createAggregator(builder, searcher, fieldTypes);
168+
try {
167169
for (ShardSearcher subSearcher : subSearchers) {
168-
try (C a = createAggregator(builder, subSearcher, fieldTypes)) {
170+
C a = createAggregator(builder, subSearcher, fieldTypes);
171+
try {
169172
a.preCollection();
170173
subSearcher.search(weight, a);
171174
a.postCollection();
172175
aggs.add(a.buildAggregation(0L));
176+
} finally {
177+
closeAgg(a);
173178
}
174179
}
175180
if (aggs.isEmpty()) {
@@ -179,6 +184,15 @@ protected <A extends InternalAggregation, C extends Aggregator> A searchAndReduc
179184
A internalAgg = (A) aggs.get(0).doReduce(aggs, new InternalAggregation.ReduceContext(root.context().bigArrays(), null));
180185
return internalAgg;
181186
}
187+
} finally {
188+
closeAgg(root);
189+
}
190+
}
191+
192+
private void closeAgg(Aggregator agg) {
193+
agg.close();
194+
for (Aggregator sub : ((AggregatorBase) agg).subAggregators) {
195+
closeAgg(sub);
182196
}
183197
}
184198

core/src/test/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorTests.java

Lines changed: 100 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,70 +18,133 @@
1818
*/
1919
package org.elasticsearch.search.aggregations.metrics.tophits;
2020

21+
import org.apache.lucene.analysis.core.KeywordAnalyzer;
2122
import org.apache.lucene.document.Document;
2223
import org.apache.lucene.document.Field;
2324
import org.apache.lucene.document.SortedSetDocValuesField;
2425
import org.apache.lucene.index.DirectoryReader;
2526
import org.apache.lucene.index.IndexReader;
2627
import org.apache.lucene.index.RandomIndexWriter;
28+
import org.apache.lucene.queryparser.classic.QueryParser;
2729
import org.apache.lucene.search.IndexSearcher;
2830
import org.apache.lucene.search.MatchAllDocsQuery;
31+
import org.apache.lucene.search.MatchNoDocsQuery;
32+
import org.apache.lucene.search.Query;
2933
import org.apache.lucene.store.Directory;
3034
import org.apache.lucene.util.BytesRef;
3135
import org.elasticsearch.index.mapper.KeywordFieldMapper;
3236
import org.elasticsearch.index.mapper.MappedFieldType;
3337
import org.elasticsearch.index.mapper.Uid;
3438
import org.elasticsearch.index.mapper.UidFieldMapper;
3539
import org.elasticsearch.search.SearchHits;
40+
import org.elasticsearch.search.aggregations.Aggregation;
41+
import org.elasticsearch.search.aggregations.AggregationBuilder;
3642
import org.elasticsearch.search.aggregations.AggregatorTestCase;
43+
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
3744
import org.elasticsearch.search.sort.SortOrder;
3845

46+
import java.io.IOException;
47+
48+
import static org.elasticsearch.search.aggregations.AggregationBuilders.terms;
49+
import static org.elasticsearch.search.aggregations.AggregationBuilders.topHits;
50+
3951
public class TopHitsAggregatorTests extends AggregatorTestCase {
52+
public void testTopLevel() throws Exception {
53+
Aggregation result;
54+
if (randomBoolean()) {
55+
result = testCase(new MatchAllDocsQuery(), topHits("_name").sort("string", SortOrder.DESC));
56+
} else {
57+
Query query = new QueryParser("string", new KeywordAnalyzer()).parse("d^1000 c^100 b^10 a^1");
58+
result = testCase(query, topHits("_name"));
59+
}
60+
SearchHits searchHits = ((TopHits) result).getHits();
61+
assertEquals(3L, searchHits.getTotalHits());
62+
assertEquals("3", searchHits.getAt(0).getId());
63+
assertEquals("type", searchHits.getAt(0).getType());
64+
assertEquals("2", searchHits.getAt(1).getId());
65+
assertEquals("type", searchHits.getAt(1).getType());
66+
assertEquals("1", searchHits.getAt(2).getId());
67+
assertEquals("type", searchHits.getAt(2).getType());
68+
}
69+
70+
public void testNoResults() throws Exception {
71+
TopHits result = (TopHits) testCase(new MatchNoDocsQuery(), topHits("_name").sort("string", SortOrder.DESC));
72+
SearchHits searchHits = ((TopHits) result).getHits();
73+
assertEquals(0L, searchHits.getTotalHits());
74+
}
75+
76+
/**
77+
* Tests {@code top_hits} inside of {@code terms}. While not strictly a unit test this is a fairly common way to run {@code top_hits}
78+
* and serves as a good example of running {@code top_hits} inside of another aggregation.
79+
*/
80+
public void testInsideTerms() throws Exception {
81+
Aggregation result;
82+
if (randomBoolean()) {
83+
result = testCase(new MatchAllDocsQuery(),
84+
terms("term").field("string")
85+
.subAggregation(topHits("top").sort("string", SortOrder.DESC)));
86+
} else {
87+
Query query = new QueryParser("string", new KeywordAnalyzer()).parse("d^1000 c^100 b^10 a^1");
88+
result = testCase(query,
89+
terms("term").field("string")
90+
.subAggregation(topHits("top")));
91+
}
92+
Terms terms = (Terms) result;
93+
94+
// The "a" bucket
95+
TopHits hits = (TopHits) terms.getBucketByKey("a").getAggregations().get("top");
96+
SearchHits searchHits = (hits).getHits();
97+
assertEquals(2L, searchHits.getTotalHits());
98+
assertEquals("2", searchHits.getAt(0).getId());
99+
assertEquals("1", searchHits.getAt(1).getId());
100+
101+
// The "b" bucket
102+
searchHits = ((TopHits) terms.getBucketByKey("b").getAggregations().get("top")).getHits();
103+
assertEquals(2L, searchHits.getTotalHits());
104+
assertEquals("3", searchHits.getAt(0).getId());
105+
assertEquals("1", searchHits.getAt(1).getId());
40106

41-
public void testTermsAggregator() throws Exception {
107+
// The "c" bucket
108+
searchHits = ((TopHits) terms.getBucketByKey("c").getAggregations().get("top")).getHits();
109+
assertEquals(1L, searchHits.getTotalHits());
110+
assertEquals("2", searchHits.getAt(0).getId());
111+
112+
// The "d" bucket
113+
searchHits = ((TopHits) terms.getBucketByKey("d").getAggregations().get("top")).getHits();
114+
assertEquals(1L, searchHits.getTotalHits());
115+
assertEquals("3", searchHits.getAt(0).getId());
116+
}
117+
118+
private static final MappedFieldType STRING_FIELD_TYPE = new KeywordFieldMapper.KeywordFieldType();
119+
static {
120+
STRING_FIELD_TYPE.setName("string");
121+
STRING_FIELD_TYPE.setHasDocValues(true);
122+
}
123+
124+
private Aggregation testCase(Query query, AggregationBuilder builder) throws IOException {
42125
Directory directory = newDirectory();
43-
RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
44-
Document document = new Document();
45-
document.add(new Field(UidFieldMapper.NAME, Uid.createUid("type", "1"), UidFieldMapper.Defaults.FIELD_TYPE));
46-
document.add(new SortedSetDocValuesField("string", new BytesRef("a")));
47-
document.add(new SortedSetDocValuesField("string", new BytesRef("b")));
48-
indexWriter.addDocument(document);
49-
document = new Document();
50-
document.add(new Field(UidFieldMapper.NAME, Uid.createUid("type", "2"), UidFieldMapper.Defaults.FIELD_TYPE));
51-
document.add(new SortedSetDocValuesField("string", new BytesRef("c")));
52-
document.add(new SortedSetDocValuesField("string", new BytesRef("a")));
53-
indexWriter.addDocument(document);
54-
document = new Document();
55-
document.add(new Field(UidFieldMapper.NAME, Uid.createUid("type", "3"), UidFieldMapper.Defaults.FIELD_TYPE));
56-
document.add(new SortedSetDocValuesField("string", new BytesRef("b")));
57-
document.add(new SortedSetDocValuesField("string", new BytesRef("d")));
58-
indexWriter.addDocument(document);
59-
indexWriter.close();
126+
RandomIndexWriter iw = new RandomIndexWriter(random(), directory);
127+
iw.addDocument(document("1", "a", "b"));
128+
iw.addDocument(document("2", "c", "a"));
129+
iw.addDocument(document("3", "b", "d"));
130+
iw.close();
60131

61132
IndexReader indexReader = DirectoryReader.open(directory);
62133
IndexSearcher indexSearcher = newSearcher(indexReader, true, true);
63134

64-
MappedFieldType fieldType = new KeywordFieldMapper.KeywordFieldType();
65-
fieldType.setName("string");
66-
fieldType.setHasDocValues(true );
67-
TopHitsAggregationBuilder aggregationBuilder = new TopHitsAggregationBuilder("_name");
68-
aggregationBuilder.sort("string", SortOrder.DESC);
69-
try (TopHitsAggregator aggregator = createAggregator(aggregationBuilder, indexSearcher, fieldType)){
70-
aggregator.preCollection();
71-
indexSearcher.search(new MatchAllDocsQuery(), aggregator);
72-
aggregator.postCollection();
73-
TopHits topHits = (TopHits) aggregator.buildAggregation(0L);
74-
SearchHits searchHits = topHits.getHits();
75-
assertEquals(3L, searchHits.getTotalHits());
76-
assertEquals("3", searchHits.getAt(0).getId());
77-
assertEquals("type", searchHits.getAt(0).getType());
78-
assertEquals("2", searchHits.getAt(1).getId());
79-
assertEquals("type", searchHits.getAt(1).getType());
80-
assertEquals("1", searchHits.getAt(2).getId());
81-
assertEquals("type", searchHits.getAt(2).getType());
82-
}
135+
Aggregation result = searchAndReduce(indexSearcher, query, builder, STRING_FIELD_TYPE);
83136
indexReader.close();
84137
directory.close();
138+
return result;
85139
}
86140

141+
private Document document(String id, String... stringValues) {
142+
Document document = new Document();
143+
document.add(new Field(UidFieldMapper.NAME, Uid.createUid("type", id), UidFieldMapper.Defaults.FIELD_TYPE));
144+
for (String stringValue : stringValues) {
145+
document.add(new Field("string", stringValue, STRING_FIELD_TYPE));
146+
document.add(new SortedSetDocValuesField("string", new BytesRef(stringValue)));
147+
}
148+
return document;
149+
}
87150
}

0 commit comments

Comments
 (0)