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;
+ }
+ }
+}