This is an automated email from the ASF dual-hosted git repository.
sigram pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 2775ad0d85a SOLR-18305: Ensure that when query limits are used the
correct similarity is used for different types of queries. (#4898)
2775ad0d85a is described below
commit 2775ad0d85a2e2b3e4d9fdcdad202829da7774a6
Author: Andrzej BiaĆecki <[email protected]>
AuthorDate: Sat Sep 12 10:28:23 2026 +0200
SOLR-18305: Ensure that when query limits are used the correct similarity
is used for different types of queries. (#4898)
---
.../solr-18305-query-limits-searcher-setup.yml | 7 ++
.../org/apache/solr/search/SolrIndexSearcher.java | 15 +++
.../apache/solr/search/SolrIndexSearcherTest.java | 107 +++++++++++++++++++++
3 files changed, 129 insertions(+)
diff --git a/changelog/unreleased/solr-18305-query-limits-searcher-setup.yml
b/changelog/unreleased/solr-18305-query-limits-searcher-setup.yml
new file mode 100644
index 00000000000..92ca4abf757
--- /dev/null
+++ b/changelog/unreleased/solr-18305-query-limits-searcher-setup.yml
@@ -0,0 +1,7 @@
+title: Ensure that when query limits are used the correct similarity is used
for all types of queries.
+type: changed
+authors:
+ - name: Andrzej Bialecki
+links:
+ - name: SOLR-18305
+ url: https://issues.apache.org/jira/browse/SOLR-18305
diff --git a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
index 0a5c9c7d2ad..de4e7c601c5 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java
@@ -82,6 +82,7 @@ import org.apache.lucene.search.TotalHitCountCollector;
import org.apache.lucene.search.TotalHits;
import org.apache.lucene.search.TotalHits.Relation;
import org.apache.lucene.search.Weight;
+import org.apache.lucene.search.similarities.Similarity;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.BytesRef;
@@ -791,6 +792,18 @@ public class SolrIndexSearcher extends IndexSearcher
implements Closeable, SolrI
return getDocListC(cmd);
}
+ // make sure that searchLeaf searches with QueryLimits never call the
+ // throwaway's IndexSearcher similarity
+ private static final Similarity GUARD_SIMILARITY =
+ new Similarity() {
+ @Override
+ public SimScorer scorer(
+ float boost, CollectionStatistics collectionStats,
TermStatistics... termStats) {
+ throw new SolrException(
+ ErrorCode.SERVER_ERROR, "Similarity.scorer(...) should never be
invoked here!");
+ }
+ };
+
/**
* Override the default behavior to proxy to an anonymous {@link
IndexSearcher} with {@link
* IndexSearcher#setTimeout} enabled, if and only if {@link
QueryLimits#getCurrentLimits} are
@@ -819,6 +832,8 @@ public class SolrIndexSearcher extends IndexSearcher
implements Closeable, SolrI
reader, core.getCoreContainer().getIndexSearcherExecutor()) { //
cheap, actually!
void searchWithTimeout() throws IOException {
setTimeout(queryLimits); // Lucene's method name is less than ideal
here...
+ // make sure no additional weights are produced using this searcher
+ setSimilarity(GUARD_SIMILARITY);
super.searchLeaf(ctx, minDocId, maxDocId, weight, collector);
if (timedOut()) {
throw new QueryLimitsExceededException(
diff --git
a/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java
b/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java
index 41dec913565..524e217bfe3 100644
--- a/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java
+++ b/solr/core/src/test/org/apache/solr/search/SolrIndexSearcherTest.java
@@ -17,9 +17,14 @@
package org.apache.solr.search;
import java.io.IOException;
+import java.util.List;
import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.Rescorer;
@@ -27,13 +32,22 @@ import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.TermInSetQuery;
import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopDocsCollector;
import org.apache.lucene.search.TotalHits;
import org.apache.lucene.search.Weight;
+import org.apache.lucene.search.WildcardQuery;
+import org.apache.lucene.util.BytesRef;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.params.CommonParams;
import org.apache.solr.handler.component.MergeStrategy;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestInfo;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.TestInjection;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -233,6 +247,99 @@ public class SolrIndexSearcherTest extends SolrTestCaseJ4 {
});
}
+ /**
+ * Regression test for SOLR-18305 / SOLR-18156: {@link
SolrIndexSearcher#searchLeaf} substitutes a
+ * throwaway {@link IndexSearcher} for {@code SolrIndexSearcher.this} when
{@link QueryLimits} are
+ * enabled. The substitute must not affect scoring: the {@code Weight}
passed into {@code
+ * searchLeaf} was already built - with SolrIndexSearcher's own {@code
Similarity} baked in -
+ * before the substitute searcher is ever created, and Lucene's default
{@code searchLeaf} body
+ * only consults that {@code Weight}, never the searcher's own {@code
Similarity} or query cache.
+ */
+ public void testSearchLeafVsQueryLimits() throws Exception {
+ TestInjection.queryTimeout = null;
+ h.getCore()
+ .withSearcher(
+ searcher -> {
+ // A mix of plain and MultiTermQuery-derived queries, the latter
needing a
+ // rewrite (PrefixQuery/WildcardQuery/FuzzyQuery/TermRangeQuery
-> BooleanQuery or
+ // ConstantScoreQuery) before a Weight can be created; the
BooleanQuery additionally
+ // nests one such rewritten clause. Query#rewrite() happens on
SolrIndexSearcher.this
+ // (via IndexSearcher#createWeight -> searcher.rewrite(query)),
well before
+ // searchLeaf ever sees the substitute IndexSearcher, so it must
not matter to the
+ // outcome checked here.
+ List<Query> testQueries =
+ List.of(
+ new TermQuery(new Term("field4_t", "5")),
+ new PrefixQuery(new Term("field4_t", "1")),
+ new WildcardQuery(new Term("field4_t", "2*")),
+ new FuzzyQuery(new Term("field4_t", "5")),
+ TermRangeQuery.newStringRange("field4_t", "10", "12",
true, true),
+ // Same shape of query ExpandComponent#getGroupQuery
builds to re-expand a
+ // collapsed group's members: a TermInSetQuery over the
collapse field's
+ // distinct values.
+ new TermInSetQuery(
+ "field4_t",
+ List.of(new BytesRef("5"), new BytesRef("10"), new
BytesRef("15"))),
+ new BooleanQuery.Builder()
+ .add(new TermQuery(new Term("field1_s", "foo")),
BooleanClause.Occur.MUST)
+ .add(
+ new PrefixQuery(new Term("field4_t", "1")),
+ BooleanClause.Occur.SHOULD)
+ .build(),
+ // search using ReRankCollector, same as a real
{!rerank} query
+ new FixedScoreReRankQuery(new TermQuery(new
Term("field4_t", "5")), 7.5f));
+
+ for (Query q : testQueries) {
+ float scoreWithoutLimits = topScore(searcher, q);
+ float scoreWithLimits =
topScoreWithQueryLimitsEnabled(searcher, q);
+
+ assertEquals(
+ "score must be identical whether or not QueryLimits swaps
in a substitute "
+ + "IndexSearcher for searchLeaf: the Weight (and its
baked-in Similarity) "
+ + "predates that substitute and must be reused
unchanged, for query: "
+ + q,
+ scoreWithoutLimits,
+ scoreWithLimits,
+ 0.0f);
+ }
+ return null;
+ });
+ }
+
+ private static float topScoreWithQueryLimitsEnabled(SolrIndexSearcher
searcher, Query q)
+ throws IOException {
+ SolrQueryResponse rsp = new SolrQueryResponse();
+ try (SolrQueryRequest req = req(CommonParams.TIME_ALLOWED, "1000000")) {
+ SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, rsp));
+ assertTrue(
+ "QueryLimits must actually be enabled here, otherwise this test
never exercises the "
+ + "substitute-IndexSearcher branch of
SolrIndexSearcher#searchLeaf",
+ QueryLimits.getCurrentLimits().isLimitsEnabled());
+ return topScore(searcher, q);
+ } finally {
+ SolrRequestInfo.clearRequestInfo();
+ }
+ }
+
+ // Goes through SolrIndexSearcher's own QueryCommand path (rather than raw
+ // IndexSearcher#search) so that a RankQuery is detected and routed through
its
+ // getTopDocsCollector, same as production request handling. The qcache
flags are cleared so
+ // each call actually re-executes the search instead of reusing the other
call's cached result.
+ private static float topScore(SolrIndexSearcher searcher, Query q) throws
IOException {
+ QueryCommand cmd = new QueryCommand();
+ cmd.setQuery(q);
+ cmd.setLen(10);
+ cmd.setFlags(
+ SolrIndexSearcher.GET_SCORES
+ | SolrIndexSearcher.NO_CHECK_QCACHE
+ | SolrIndexSearcher.NO_SET_QCACHE);
+ QueryResult qr = searcher.search(cmd);
+ assertTrue("expected at least one hit for query: " + q,
qr.getDocList().matches() > 0);
+ DocIterator iter = qr.getDocList().iterator();
+ iter.next();
+ return iter.score();
+ }
+
public void testReranking() throws Exception {
float fixedScore = 1.23f;
for (boolean doFilter : new boolean[] {false, true}) {