This is an automated email from the ASF dual-hosted git repository.

dsmiley pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/solr.git

commit 1e70c4579590ed49f68e1c16ad2f8e7f73d1beba
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)
    
    (cherry picked from commit 2775ad0d85a2e2b3e4d9fdcdad202829da7774a6)
---
 .../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}) {

Reply via email to