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

dsmiley 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 9603aa22a53 SOLR-18136: fix multiThreaded=true with rerank & sort  
(#4164)
9603aa22a53 is described below

commit 9603aa22a5389a6ea7b8f63cb01d7ad5edfe41ae
Author: Shiming Li <[email protected]>
AuthorDate: Thu Mar 19 08:41:12 2026 +0800

    SOLR-18136: fix multiThreaded=true with rerank & sort  (#4164)
    
    When multi-threaded segment-parallel search is enabled 
(`indexSearcherExecutorThreads > 0` and `multiThreaded=true`)
    and a query uses both reranking (via `RankQuery` / `ReRankCollector`) and a 
sort, an `ArrayStoreException` is
    thrown during the merge phase if some segments have matching documents and 
others do not.
---
 ...36-rerank-multithreaded-arraystoreexception.yml |   7 +
 .../apache/solr/search/MultiThreadedSearcher.java  |   2 +-
 .../solr/search/TestMultiThreadedSearcher.java     | 199 +++++++++++++++++++++
 3 files changed, 207 insertions(+), 1 deletion(-)

diff --git 
a/changelog/unreleased/SOLR-18136-rerank-multithreaded-arraystoreexception.yml 
b/changelog/unreleased/SOLR-18136-rerank-multithreaded-arraystoreexception.yml
new file mode 100644
index 00000000000..78e59fc8562
--- /dev/null
+++ 
b/changelog/unreleased/SOLR-18136-rerank-multithreaded-arraystoreexception.yml
@@ -0,0 +1,7 @@
+title: Fix ArrayStoreException when combining rerank with sort under 
multi-threaded segment-parallel search
+type: fixed
+authors:
+  - name: Shiming Li
+links:
+  - name: SOLR-18136
+    url: https://issues.apache.org/jira/browse/SOLR-18136
diff --git 
a/solr/core/src/java/org/apache/solr/search/MultiThreadedSearcher.java 
b/solr/core/src/java/org/apache/solr/search/MultiThreadedSearcher.java
index 5121ab85553..3ad0712ff1a 100644
--- a/solr/core/src/java/org/apache/solr/search/MultiThreadedSearcher.java
+++ b/solr/core/src/java/org/apache/solr/search/MultiThreadedSearcher.java
@@ -349,7 +349,7 @@ public class MultiThreadedSearcher {
       TopDocs mergedTopDocs = null;
 
       if (topDocs.length > 0 && topDocs[0] != null) {
-        if (topDocs[0] instanceof TopFieldDocs) {
+        if (Arrays.stream(topDocs).allMatch(td -> td instanceof TopFieldDocs)) 
{
           TopFieldDocs[] topFieldDocs =
               Arrays.copyOf(topDocs, topDocs.length, TopFieldDocs[].class);
           mergedTopDocs = 
TopFieldDocs.merge(searcher.weightSort(cmd.getSort()), len, topFieldDocs);
diff --git 
a/solr/core/src/test/org/apache/solr/search/TestMultiThreadedSearcher.java 
b/solr/core/src/test/org/apache/solr/search/TestMultiThreadedSearcher.java
new file mode 100644
index 00000000000..2b83390158f
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/TestMultiThreadedSearcher.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.search;
+
+import java.io.IOException;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.QueryVisitor;
+import org.apache.lucene.search.Rescorer;
+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.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.TopDocsCollector;
+import org.apache.lucene.search.Weight;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.core.NodeConfig;
+import org.apache.solr.handler.component.MergeStrategy;
+import org.apache.solr.index.NoMergePolicyFactory;
+import org.apache.solr.update.UpdateShardHandlerConfig;
+import org.apache.solr.util.TestHarness;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+/** Tests for {@link MultiThreadedSearcher}. */
+public class TestMultiThreadedSearcher extends SolrTestCaseJ4 {
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    
systemSetPropertySolrTestsMergePolicyFactory(NoMergePolicyFactory.class.getName());
+
+    NodeConfig nodeConfig =
+        new NodeConfig.NodeConfigBuilder("testNode", TEST_PATH())
+            .setUseSchemaCache(Boolean.getBoolean("shareSchema"))
+            .setUpdateShardHandlerConfig(UpdateShardHandlerConfig.TEST_DEFAULT)
+            .setIndexSearcherExecutorThreads(4)
+            .build();
+    createCoreContainer(
+        nodeConfig,
+        new TestHarness.TestCoresLocator(
+            DEFAULT_TEST_CORENAME,
+            createTempDir("data").toAbsolutePath().toString(),
+            "solrconfig-minimal.xml",
+            "schema.xml"));
+    h.coreName = DEFAULT_TEST_CORENAME;
+
+    // Non-matching segments first, matching segment last.
+    // This ensures different slices see different result counts during 
parallel search.
+    for (int seg = 0; seg < 7; seg++) {
+      for (int i = 0; i < 10; i++) {
+        assertU(
+            adoc(
+                "id", String.valueOf(20000 + seg * 100 + i),
+                "field1_s", "nomatchterm",
+                "field4_t", "nomatchterm"));
+      }
+      assertU(commit());
+    }
+
+    // Matching segment last
+    for (int i = 0; i < 10; i++) {
+      assertU(
+          adoc(
+              "id", String.valueOf(10000 + i),
+              "field1_s", "xyzrareterm",
+              "field4_t", "xyzrareterm"));
+    }
+    assertU(commit());
+  }
+
+  @AfterClass
+  public static void afterClass() {
+    System.clearProperty(SYSTEM_PROPERTY_SOLR_TESTS_MERGEPOLICYFACTORY);
+  }
+
+  public void testReRankWithMultiThreadedSearch() throws Exception {
+    float fixedScore = 5.0f;
+    h.getCore()
+        .withSearcher(
+            searcher -> {
+              int numSegments = searcher.getTopReaderContext().leaves().size();
+              assertTrue("Expected > 5 segments, got " + numSegments, 
numSegments > 5);
+              assertTrue(
+                  "Expected > 1 slice, got " + searcher.getSlices().length,
+                  searcher.getSlices().length > 1);
+
+              final QueryCommand cmd = new QueryCommand();
+              cmd.setFlags(SolrIndexSearcher.GET_SCORES);
+              cmd.setLen(10);
+              cmd.setMultiThreaded(true);
+              cmd.setSort(
+                  new Sort(SortField.FIELD_SCORE, new SortField("id", 
SortField.Type.STRING)));
+              cmd.setQuery(
+                  new SimpleReRankQuery(
+                      new TermQuery(new Term("field1_s", "xyzrareterm")), 
fixedScore));
+
+              final QueryResult qr = searcher.search(cmd);
+
+              assertTrue(qr.getDocList().matches() >= 1);
+              final DocIterator iter = qr.getDocList().iterator();
+              assertTrue(iter.hasNext());
+              iter.next();
+              assertEquals(fixedScore, iter.score(), 0);
+              return null;
+            });
+  }
+
+  private static final class SimpleReRankQuery extends RankQuery {
+
+    private Query q;
+    private final float reRankScore;
+
+    SimpleReRankQuery(Query q, float reRankScore) {
+      this.q = q;
+      this.reRankScore = reRankScore;
+    }
+
+    @Override
+    public Weight createWeight(IndexSearcher indexSearcher, ScoreMode 
scoreMode, float boost)
+        throws IOException {
+      return q.createWeight(indexSearcher, scoreMode, boost);
+    }
+
+    @Override
+    public void visit(QueryVisitor visitor) {
+      q.visit(visitor);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return this == obj;
+    }
+
+    @Override
+    public int hashCode() {
+      return q.hashCode();
+    }
+
+    @Override
+    public String toString(String field) {
+      return q.toString(field);
+    }
+
+    @Override
+    public TopDocsCollector<? extends ScoreDoc> getTopDocsCollector(
+        int len, QueryCommand cmd, IndexSearcher searcher) throws IOException {
+      return new ReRankCollector(
+          len,
+          len,
+          new Rescorer() {
+            @Override
+            public TopDocs rescore(IndexSearcher searcher, TopDocs 
firstPassTopDocs, int topN) {
+              for (ScoreDoc scoreDoc : firstPassTopDocs.scoreDocs) {
+                scoreDoc.score = reRankScore;
+              }
+              return firstPassTopDocs;
+            }
+
+            @Override
+            public Explanation explain(
+                IndexSearcher searcher, Explanation firstPassExplanation, int 
docID) {
+              return firstPassExplanation;
+            }
+          },
+          cmd,
+          searcher,
+          null);
+    }
+
+    @Override
+    public MergeStrategy getMergeStrategy() {
+      return null;
+    }
+
+    @Override
+    public RankQuery wrap(Query q) {
+      this.q = q;
+      return this;
+    }
+  }
+}

Reply via email to