Skip to content

Commit 496df04

Browse files
authored
Fix disabling of bulk scoring in monitor (#16350)
The monitor wraps all queries in a ForceNoBulkScoringQuery to ensure that it runs everything doc-at-a-time and doesn't allocate large bulk buffers for searches over a tiny number of documents. Changes to the bulk scoring API in Weight and ScorerSupplier meant that this optimization was silently disabled. Add a new test to ensure that bulk scoring is properly disabled, and re-work ForceNoBulkScoringQuery so that it behaves as expected.
1 parent 8fe701c commit 496df04

3 files changed

Lines changed: 137 additions & 1 deletion

File tree

lucene/CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ Optimizations
327327

328328
Bug Fixes
329329
---------------------
330+
* GITHUB#16350: Disable bulk-scoring in monitor queries. (Alan Woodward)
330331

331332
* GITHUB#16295: SingletonSortedNumericDocValues now delegates rangeIntoBitSet
332333
to the wrapped NumericDocValues, enabling optimized range evaluation for

lucene/monitor/src/java/org/apache/lucene/monitor/ForceNoBulkScoringQuery.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
import java.io.IOException;
2121
import java.util.Objects;
2222
import org.apache.lucene.index.LeafReaderContext;
23+
import org.apache.lucene.search.BulkScorer;
2324
import org.apache.lucene.search.Explanation;
2425
import org.apache.lucene.search.IndexSearcher;
2526
import org.apache.lucene.search.Matches;
2627
import org.apache.lucene.search.Query;
2728
import org.apache.lucene.search.QueryVisitor;
2829
import org.apache.lucene.search.ScoreMode;
30+
import org.apache.lucene.search.Scorer;
2931
import org.apache.lucene.search.ScorerSupplier;
3032
import org.apache.lucene.search.Weight;
3133

@@ -87,7 +89,29 @@ public Explanation explain(LeafReaderContext leafReaderContext, int i) throws IO
8789

8890
@Override
8991
public ScorerSupplier scorerSupplier(LeafReaderContext leafReaderContext) throws IOException {
90-
return innerWeight.scorerSupplier(leafReaderContext);
92+
ScorerSupplier innerScorerSupplier = innerWeight.scorerSupplier(leafReaderContext);
93+
if (innerScorerSupplier == null) {
94+
return null;
95+
}
96+
return new ScorerSupplier() {
97+
98+
@Override
99+
public Scorer get(long leadCost) throws IOException {
100+
return innerScorerSupplier.get(leadCost);
101+
}
102+
103+
@Override
104+
public long cost() {
105+
return innerScorerSupplier.cost();
106+
}
107+
108+
@Override
109+
public BulkScorer bulkScorer() throws IOException {
110+
// force the use of the default doc-by-doc BulkScorer, rather than any
111+
// optimized bulk-scoring implementation the wrapped query may provide
112+
return new DefaultBulkScorer(get(Long.MAX_VALUE));
113+
}
114+
};
91115
}
92116

93117
@Override

lucene/monitor/src/test/org/apache/lucene/monitor/TestForceNoBulkScoringQuery.java

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@
2626
import org.apache.lucene.index.IndexReader;
2727
import org.apache.lucene.index.IndexWriter;
2828
import org.apache.lucene.index.IndexWriterConfig;
29+
import org.apache.lucene.index.LeafReaderContext;
2930
import org.apache.lucene.index.Term;
31+
import org.apache.lucene.search.BulkScorer;
32+
import org.apache.lucene.search.FilterWeight;
33+
import org.apache.lucene.search.IndexSearcher;
3034
import org.apache.lucene.search.PrefixQuery;
3135
import org.apache.lucene.search.Query;
36+
import org.apache.lucene.search.QueryVisitor;
37+
import org.apache.lucene.search.ScoreMode;
38+
import org.apache.lucene.search.Scorer;
39+
import org.apache.lucene.search.ScorerSupplier;
3240
import org.apache.lucene.search.TermQuery;
41+
import org.apache.lucene.search.Weight;
3342
import org.apache.lucene.store.Directory;
3443
import org.apache.lucene.tests.util.LuceneTestCase;
3544

@@ -73,4 +82,106 @@ public void testRewrite() throws IOException {
7382
}
7483
}
7584
}
85+
86+
public void testBulkScoringIsDisabled() throws IOException {
87+
88+
try (Directory dir = newDirectory();
89+
IndexWriter iw = new IndexWriter(dir, new IndexWriterConfig(new StandardAnalyzer()))) {
90+
91+
Document doc = new Document();
92+
doc.add(new TextField("field", "term1 term2 term3 term4", Field.Store.NO));
93+
iw.addDocument(doc);
94+
iw.commit();
95+
96+
try (IndexReader reader = DirectoryReader.open(dir)) {
97+
IndexSearcher searcher = newSearcher(reader);
98+
// disable query caching, so that we exercise the search path directly rather than
99+
// any bulk-scoring optimizations the cache may apply internally
100+
searcher.setQueryCache(null);
101+
102+
Query throwing = new ThrowsOnBulkScoreQuery(new TermQuery(new Term("field", "term1")));
103+
104+
// sanity check that the wrapped query really does throw when bulk-scored directly
105+
expectThrows(AssertionError.class, () -> searcher.search(throwing, 10));
106+
107+
Query wrapped = new ForceNoBulkScoringQuery(throwing);
108+
// should not throw, because bulk scoring has been disabled
109+
assertEquals(1, searcher.search(wrapped, 10).totalHits.value());
110+
}
111+
}
112+
}
113+
114+
/**
115+
* A query wrapper whose ScorerSupplier throws an AssertionError if bulkScorer() is called, used
116+
* to check that {@link ForceNoBulkScoringQuery} never calls bulkScorer() on the ScorerSupplier of
117+
* the query it wraps.
118+
*/
119+
private static class ThrowsOnBulkScoreQuery extends Query {
120+
121+
private final Query in;
122+
123+
private ThrowsOnBulkScoreQuery(Query in) {
124+
this.in = in;
125+
}
126+
127+
@Override
128+
public Query rewrite(IndexSearcher indexSearcher) throws IOException {
129+
Query rewritten = in.rewrite(indexSearcher);
130+
if (rewritten != in) {
131+
return new ThrowsOnBulkScoreQuery(rewritten);
132+
}
133+
return super.rewrite(indexSearcher);
134+
}
135+
136+
@Override
137+
public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost)
138+
throws IOException {
139+
Weight innerWeight = in.createWeight(searcher, scoreMode, boost);
140+
return new FilterWeight(innerWeight) {
141+
@Override
142+
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
143+
ScorerSupplier innerSupplier = super.scorerSupplier(context);
144+
if (innerSupplier == null) {
145+
return null;
146+
}
147+
return new ScorerSupplier() {
148+
@Override
149+
public Scorer get(long leadCost) throws IOException {
150+
return innerSupplier.get(leadCost);
151+
}
152+
153+
@Override
154+
public long cost() {
155+
return innerSupplier.cost();
156+
}
157+
158+
@Override
159+
public BulkScorer bulkScorer() {
160+
throw new AssertionError("bulkScorer() should not have been called");
161+
}
162+
};
163+
}
164+
};
165+
}
166+
167+
@Override
168+
public String toString(String field) {
169+
return "ThrowsOnBulkScore(" + in.toString(field) + ")";
170+
}
171+
172+
@Override
173+
public void visit(QueryVisitor visitor) {
174+
in.visit(visitor);
175+
}
176+
177+
@Override
178+
public boolean equals(Object o) {
179+
return sameClassAs(o) && in.equals(((ThrowsOnBulkScoreQuery) o).in);
180+
}
181+
182+
@Override
183+
public int hashCode() {
184+
return classHash() * 31 + in.hashCode();
185+
}
186+
}
76187
}

0 commit comments

Comments
 (0)