Skip to content

Commit 3b054f5

Browse files
committed
TopHitsAggregator must propagate calls to setScorer. (#27138)
It is required in order to work correctly with bulk scorer implementations that change the scorer during the collection process. Otherwise sub collectors might call `Scorer.score()` on the wrong scorer. Closes #27131
1 parent 39ef2c4 commit 3b054f5

File tree

3 files changed

+59
-1
lines changed

3 files changed

+59
-1
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
package org.elasticsearch.search.aggregations.metrics.tophits;
2121

2222
import com.carrotsearch.hppc.LongObjectHashMap;
23+
import com.carrotsearch.hppc.cursors.ObjectCursor;
24+
2325
import org.apache.lucene.index.LeafReaderContext;
2426
import org.apache.lucene.search.FieldDoc;
2527
import org.apache.lucene.search.LeafCollector;
@@ -93,6 +95,9 @@ public LeafBucketCollector getLeafCollector(LeafReaderContext ctx, LeafBucketCol
9395
public void setScorer(Scorer scorer) throws IOException {
9496
this.scorer = scorer;
9597
super.setScorer(scorer);
98+
for (ObjectCursor<LeafCollector> cursor : leafCollectors.values()) {
99+
cursor.value.setScorer(scorer);
100+
}
96101
}
97102

98103
@Override

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

+51
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,22 @@
2121
import org.apache.lucene.analysis.core.KeywordAnalyzer;
2222
import org.apache.lucene.document.Document;
2323
import org.apache.lucene.document.Field;
24+
import org.apache.lucene.document.Field.Store;
2425
import org.apache.lucene.document.SortedSetDocValuesField;
26+
import org.apache.lucene.document.StringField;
2527
import org.apache.lucene.index.DirectoryReader;
2628
import org.apache.lucene.index.IndexReader;
29+
import org.apache.lucene.index.IndexWriter;
2730
import org.apache.lucene.index.RandomIndexWriter;
31+
import org.apache.lucene.index.Term;
2832
import org.apache.lucene.queryparser.classic.QueryParser;
33+
import org.apache.lucene.search.BooleanClause.Occur;
34+
import org.apache.lucene.search.BooleanQuery;
2935
import org.apache.lucene.search.IndexSearcher;
3036
import org.apache.lucene.search.MatchAllDocsQuery;
3137
import org.apache.lucene.search.MatchNoDocsQuery;
3238
import org.apache.lucene.search.Query;
39+
import org.apache.lucene.search.TermQuery;
3340
import org.apache.lucene.store.Directory;
3441
import org.apache.lucene.util.BytesRef;
3542
import org.elasticsearch.index.mapper.KeywordFieldMapper;
@@ -39,6 +46,7 @@
3946
import org.elasticsearch.search.SearchHits;
4047
import org.elasticsearch.search.aggregations.Aggregation;
4148
import org.elasticsearch.search.aggregations.AggregationBuilder;
49+
import org.elasticsearch.search.aggregations.AggregationBuilders;
4250
import org.elasticsearch.search.aggregations.AggregatorTestCase;
4351
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
4452
import org.elasticsearch.search.sort.SortOrder;
@@ -148,4 +156,47 @@ private Document document(String id, String... stringValues) {
148156
}
149157
return document;
150158
}
159+
160+
public void testSetScorer() throws Exception {
161+
Directory directory = newDirectory();
162+
IndexWriter w = new IndexWriter(directory, newIndexWriterConfig()
163+
// only merge adjacent segments
164+
.setMergePolicy(newLogMergePolicy()));
165+
// first window (see BooleanScorer) has matches on one clause only
166+
for (int i = 0; i < 2048; ++i) {
167+
Document doc = new Document();
168+
doc.add(new StringField("_id", Uid.encodeId(Integer.toString(i)), Store.YES));
169+
if (i == 1000) { // any doc in 0..2048
170+
doc.add(new StringField("string", "bar", Store.NO));
171+
}
172+
w.addDocument(doc);
173+
}
174+
// second window has matches in two clauses
175+
for (int i = 0; i < 2048; ++i) {
176+
Document doc = new Document();
177+
doc.add(new StringField("_id", Uid.encodeId(Integer.toString(2048 + i)), Store.YES));
178+
if (i == 500) { // any doc in 0..2048
179+
doc.add(new StringField("string", "baz", Store.NO));
180+
} else if (i == 1500) {
181+
doc.add(new StringField("string", "bar", Store.NO));
182+
}
183+
w.addDocument(doc);
184+
}
185+
186+
w.forceMerge(1); // we need all docs to be in the same segment
187+
188+
IndexReader reader = DirectoryReader.open(w);
189+
w.close();
190+
191+
IndexSearcher searcher = new IndexSearcher(reader);
192+
Query query = new BooleanQuery.Builder()
193+
.add(new TermQuery(new Term("string", "bar")), Occur.SHOULD)
194+
.add(new TermQuery(new Term("string", "baz")), Occur.SHOULD)
195+
.build();
196+
AggregationBuilder agg = AggregationBuilders.topHits("top_hits");
197+
TopHits result = searchAndReduce(searcher, query, agg, STRING_FIELD_TYPE);
198+
assertEquals(3, result.getHits().totalHits);
199+
reader.close();
200+
directory.close();
201+
}
151202
}

test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
public abstract class AggregatorTestCase extends ESTestCase {
9292
private static final String NESTEDFIELD_PREFIX = "nested_";
9393
private List<Releasable> releasables = new ArrayList<>();
94+
private static final String TYPE_NAME = "type";
9495

9596
/** Create a factory for the given aggregation builder. */
9697
protected AggregatorFactory<?> createAggregatorFactory(AggregationBuilder aggregationBuilder,
@@ -104,6 +105,7 @@ protected AggregatorFactory<?> createAggregatorFactory(AggregationBuilder aggreg
104105
MapperService mapperService = mapperServiceMock();
105106
when(mapperService.getIndexSettings()).thenReturn(indexSettings);
106107
when(mapperService.hasNested()).thenReturn(false);
108+
when(mapperService.types()).thenReturn(Collections.singleton(TYPE_NAME));
107109
when(searchContext.mapperService()).thenReturn(mapperService);
108110
IndexFieldDataService ifds = new IndexFieldDataService(indexSettings,
109111
new IndicesFieldDataCache(Settings.EMPTY, new IndexFieldDataCache.Listener() {
@@ -115,7 +117,7 @@ public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
115117
}
116118
});
117119

118-
SearchLookup searchLookup = new SearchLookup(mapperService, ifds::getForField, new String[]{"type"});
120+
SearchLookup searchLookup = new SearchLookup(mapperService, ifds::getForField, new String[]{TYPE_NAME});
119121
when(searchContext.lookup()).thenReturn(searchLookup);
120122

121123
QueryShardContext queryShardContext = queryShardContextMock(mapperService, fieldTypes, circuitBreakerService);

0 commit comments

Comments
 (0)