From 8ff8ac46e748a5a927d13b63ff158499e84e0f5f Mon Sep 17 00:00:00 2001 From: Serhiy Bzhezytskyy Date: Mon, 24 Aug 2026 17:36:33 +0300 Subject: [PATCH 1/4] SOLR-18363: remove EarlyTerminatingSortingCollector, use native TopFieldCollector Forces TopFieldCollectorManager's totalHitsThreshold down to `len` when segmentTerminateEarly=true, so TopFieldCollector does the same per-segment skip natively. A thin SegmentTerminatedEarlyObserver catches CollectionTerminatedException (only ever thrown from TopFieldCollector's sort-compatibility-gated path) to derive segmentTerminatedEarly precisely, without TopFieldCollector#isEarlyTerminated() (which also fires on routine hit-count-exceeded, unrelated to sort) or any package-private Lucene API. RankQuery + segmentTerminateEarly now logs "unsupported combination" explicitly instead of silently doing nothing (previously-untested combination, old wrapper-based code did attempt it). Also deprecates the public surface around this feature (the request param, response header, QueryCommand accessors) -- only the internal implementation class was ever marked deprecated before. --- .../solr/response/SolrQueryResponse.java | 2 + .../EarlyTerminatingSortingCollector.java | 130 ------------------ .../org/apache/solr/search/QueryCommand.java | 4 + .../apache/solr/search/SolrIndexSearcher.java | 128 +++++++++++++---- .../pages/common-query-parameters.adoc | 3 + .../solr/common/params/CommonParams.java | 12 +- 6 files changed, 120 insertions(+), 159 deletions(-) delete mode 100644 solr/core/src/java/org/apache/solr/search/EarlyTerminatingSortingCollector.java diff --git a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java index 5f2b67622d63..8492f14b6df6 100644 --- a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java +++ b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java @@ -66,6 +66,8 @@ public class SolrQueryResponse { public static final String NAME = "response"; public static final String RESPONSE_HEADER_PARTIAL_RESULTS_KEY = "partialResults"; public static final String RESPONSE_HEADER_PARTIAL_RESULTS_DETAILS_KEY = "partialResultsDetails"; + /** @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} */ + @Deprecated(since = "11.0") public static final String RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY = "segmentTerminatedEarly"; diff --git a/solr/core/src/java/org/apache/solr/search/EarlyTerminatingSortingCollector.java b/solr/core/src/java/org/apache/solr/search/EarlyTerminatingSortingCollector.java deleted file mode 100644 index 4153f1974321..000000000000 --- a/solr/core/src/java/org/apache/solr/search/EarlyTerminatingSortingCollector.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.lucene.index.IndexWriterConfig; -import org.apache.lucene.index.LeafReaderContext; -import org.apache.lucene.search.CollectionTerminatedException; -import org.apache.lucene.search.Collector; -import org.apache.lucene.search.FilterCollector; -import org.apache.lucene.search.FilterLeafCollector; -import org.apache.lucene.search.LeafCollector; -import org.apache.lucene.search.Sort; -import org.apache.lucene.search.SortField; -import org.apache.lucene.search.TopDocsCollector; -import org.apache.lucene.search.TopFieldCollector; -import org.apache.lucene.search.TotalHitCountCollector; - -/** - * A {@link Collector} that early terminates collection of documents on a per-segment basis, if the - * segment was sorted according to the given {@link Sort}. - * - *

NOTE: the {@code Collector} detects segments sorted according to a an {@link - * IndexWriterConfig#setIndexSort}. Also, it collects up to a specified {@code numDocsToCollect} - * from each segment, and therefore is mostly suitable for use in conjunction with collectors such - * as {@link TopDocsCollector}, and not e.g. {@link TotalHitCountCollector}. - * - *

NOTE: If you wrap a {@code TopDocsCollector} that sorts in the same order as the index - * order, the returned {@link TopDocsCollector#topDocs() TopDocs} will be correct. However the total - * of {@link TopDocsCollector#getTotalHits() hit count} will be vastly underestimated since not all - * matching documents will have been collected. - * - * @deprecated Use {@link TopFieldCollector} and set trackTotalHits to false. - * @lucene.experimental - */ -@Deprecated -final class EarlyTerminatingSortingCollector extends FilterCollector { - - /** - * Returns whether collection can be early-terminated if it sorts with the provided {@link Sort} - * and if segments are merged with the provided {@link Sort}. - */ - public static boolean canEarlyTerminate(Sort searchSort, Sort mergePolicySort) { - final SortField[] fields1 = searchSort.getSort(); - final SortField[] fields2 = mergePolicySort.getSort(); - // early termination is possible if fields1 is a prefix of fields2 - if (fields1.length > fields2.length) { - return false; - } - return Arrays.asList(fields1).equals(Arrays.asList(fields2).subList(0, fields1.length)); - } - - /** Sort used to sort the search results */ - private final Sort sort; - - /** Number of documents to collect in each segment */ - private final int numDocsToCollect; - - private final AtomicBoolean terminatedEarly = new AtomicBoolean(false); - - /** - * Create a new {@link EarlyTerminatingSortingCollector} instance. - * - * @param in the collector to wrap - * @param sort the sort you are sorting the search results on - * @param numDocsToCollect the number of documents to collect on each segment. When wrapping a - * {@link TopDocsCollector}, this number should be the number of hits. - * @throws IllegalArgumentException if the sort order doesn't allow for early termination with the - * given merge policy. - */ - public EarlyTerminatingSortingCollector(Collector in, Sort sort, int numDocsToCollect) { - super(in); - if (numDocsToCollect <= 0) { - throw new IllegalArgumentException( - "numDocsToCollect must always be > 0, got " + numDocsToCollect); - } - this.sort = sort; - this.numDocsToCollect = numDocsToCollect; - } - - @Override - public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { - Sort segmentSort = context.reader().getMetaData().sort(); - if (segmentSort != null && canEarlyTerminate(sort, segmentSort) == false) { - throw new IllegalStateException( - "Cannot early terminate with sort order " - + sort - + " if segments are sorted with " - + segmentSort); - } - - if (segmentSort != null) { - // segment is sorted, can early-terminate - return new FilterLeafCollector(super.getLeafCollector(context)) { - private int numCollected; - - @Override - public void collect(int doc) throws IOException { - super.collect(doc); - if (++numCollected >= numDocsToCollect) { - terminatedEarly.set(true); - throw new CollectionTerminatedException(); - } - } - }; - } else { - return super.getLeafCollector(context); - } - } - - public boolean terminatedEarly() { - return terminatedEarly.get(); - } -} diff --git a/solr/core/src/java/org/apache/solr/search/QueryCommand.java b/solr/core/src/java/org/apache/solr/search/QueryCommand.java index 8442217464b7..1d3b115b003a 100755 --- a/solr/core/src/java/org/apache/solr/search/QueryCommand.java +++ b/solr/core/src/java/org/apache/solr/search/QueryCommand.java @@ -211,10 +211,14 @@ public QueryCommand setTerminateEarly(boolean segmentTerminateEarly) { } } + /** @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} */ + @Deprecated(since = "11.0") public boolean getSegmentTerminateEarly() { return (flags & SolrIndexSearcher.SEGMENT_TERMINATE_EARLY) != 0; } + /** @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} */ + @Deprecated(since = "11.0") public QueryCommand setSegmentTerminateEarly(boolean segmentSegmentTerminateEarly) { if (segmentSegmentTerminateEarly) { return setFlags(SolrIndexSearcher.SEGMENT_TERMINATE_EARLY); 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 e7786f71d3b0..1d9778bc26e4 100644 --- a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java +++ b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java @@ -33,6 +33,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.function.Function; @@ -54,10 +55,13 @@ import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.CollectionStatistics; +import org.apache.lucene.search.CollectionTerminatedException; import org.apache.lucene.search.Collector; import org.apache.lucene.search.DocIdSetIterator; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.FieldDoc; +import org.apache.lucene.search.FilterCollector; +import org.apache.lucene.search.FilterLeafCollector; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.LeafCollector; import org.apache.lucene.search.MatchAllDocsQuery; @@ -284,28 +288,6 @@ private Collector buildAndRunCollectorChain( DelegatingCollector postFilter) throws IOException { - EarlyTerminatingSortingCollector earlyTerminatingSortingCollector = null; - if (cmd.getSegmentTerminateEarly()) { - final Sort cmdSort = cmd.getSort(); - final int cmdLen = cmd.getLen(); - final Sort mergeSort = core.getSolrCoreState().getMergePolicySort(); - - if (cmdSort == null - || cmdLen <= 0 - || mergeSort == null - || !EarlyTerminatingSortingCollector.canEarlyTerminate(cmdSort, mergeSort)) { - log.warn( - "unsupported combination: segmentTerminateEarly=true cmdSort={} cmdLen={} mergeSort={}", - cmdSort, - cmdLen, - mergeSort); - } else { - collector = - earlyTerminatingSortingCollector = - new EarlyTerminatingSortingCollector(collector, cmdSort, cmd.getLen()); - } - } - if (cmd.shouldEarlyTerminateSearch()) { collector = new EarlyTerminatingCollector(collector, cmd.getMaxHitsAllowed()); } @@ -343,9 +325,6 @@ private Collector buildAndRunCollectorChain( qr.setPartialResultsDetails(etce.getMessage()); qr.setApproximateTotalHits(etce.getApproximateTotalHits(reader.maxDoc())); } finally { - if (earlyTerminatingSortingCollector != null) { - qr.setSegmentTerminatedEarly(earlyTerminatingSortingCollector.terminatedEarly()); - } if (cmd.isQueryCancellable()) { core.getCancellableQueryTracker().removeCancellableQuery(cmd.getQueryID()); } @@ -1787,6 +1766,24 @@ private void populateNextCursorMarkFromTopDocs(QueryResult qr, QueryCommand qc, */ TopDocsCollector buildTopDocsCollector(int len, QueryCommand cmd) throws IOException { + return buildTopDocsCollector(len, cmd, false); + } + + /** + * @param allowNativeSegmentTerminateEarly if true and {@code cmd} has a sort, forces {@link + * TopFieldCollectorManager}'s totalHitsThreshold down to {@code len} so {@link + * TopFieldCollector} early-terminates a segment itself once its index sort matches the + * search sort (the same effect {@code segmentTerminateEarly=true} used to get from the now- + * removed {@code EarlyTerminatingSortingCollector} wrapper). Callers that pass {@code true} + * must not further wrap the returned collector together with a sibling collector that + * doesn't itself early-terminate (e.g. via {@code MultiCollector}) -- see SOLR-18363: {@code + * MultiCollector} only propagates {@code CollectionTerminatedException} once *every* wrapped + * collector has thrown it, so an early-terminating {@link TopFieldCollector} paired with a + * non-terminating sibling collector would silently stop counting hits early while the + * sibling kept counting, desyncing the two. + */ + private TopDocsCollector buildTopDocsCollector( + int len, QueryCommand cmd, boolean allowNativeSegmentTerminateEarly) throws IOException { int minNumFound = cmd.getMinExactCount(); Query q = cmd.getQuery(); if (q instanceof RankQuery rq) { @@ -1802,11 +1799,74 @@ TopDocsCollector buildTopDocsCollector(int len, QueryCommand final CursorMark cursor = cmd.getCursorMark(); final FieldDoc searchAfter = (null != cursor ? cursor.getSearchAfterFieldDoc() : null); + if (allowNativeSegmentTerminateEarly) { + minNumFound = 0; + } return new TopFieldCollectorManager(weightedSort, len, searchAfter, minNumFound) .newCollector(); } } + /** + * Whether {@link #buildTopDocsCollector(int, QueryCommand, boolean)} should be asked to force + * native per-segment early termination for this command. Also logs the same "unsupported + * combination" warning the old wrapper-based check used to log. + */ + private boolean allowNativeSegmentTerminateEarly(QueryCommand cmd, int len) { + if (!cmd.getSegmentTerminateEarly()) { + return false; + } + final Sort cmdSort = cmd.getSort(); + if (cmdSort == null || len <= 0 || cmd.getQuery() instanceof RankQuery) { + log.warn( + "unsupported combination: segmentTerminateEarly=true cmdSort={} cmdLen={} query={}", + cmdSort, + len, + cmd.getQuery().getClass().getSimpleName()); + return false; + } + return true; + } + + /** + * Records whether a wrapped collector's per-segment collection was actually cut short by a + * {@link CollectionTerminatedException}. Used with {@link #buildTopDocsCollector(int, + * QueryCommand, boolean)}'s {@code allowNativeSegmentTerminateEarly=true} path: {@link + * TopFieldCollector} only ever throws that exception from its sort-compatibility-gated fast + * path (see {@code TopFieldCollector.TopFieldLeafCollector#thresholdCheck}), never from the + * routine "totalHits exceeded totalHitsThreshold" bookkeeping alone -- so observing the + * exception itself gives the same precise "a segment was actually skipped" signal the removed + * {@code EarlyTerminatingSortingCollector} tracked directly, without needing + * TopFieldCollector#isEarlyTerminated() (which conflates that signal with the routine case) or + * any of Lucene's package-private sort-compatibility check. + */ + private static final class SegmentTerminatedEarlyObserver extends FilterCollector { + private final AtomicBoolean terminatedEarly = new AtomicBoolean(false); + + SegmentTerminatedEarlyObserver(Collector in) { + super(in); + } + + boolean terminatedEarly() { + return terminatedEarly.get(); + } + + @Override + public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { + return new FilterLeafCollector(super.getLeafCollector(context)) { + @Override + public void collect(int doc) throws IOException { + try { + super.collect(doc); + } catch (CollectionTerminatedException e) { + terminatedEarly.set(true); + throw e; + } + } + }; + } + } + private void getDocListNC(QueryResult qr, QueryCommand cmd) throws IOException { final int len = cmd.getSupersetMaxDoc(); int last = len; @@ -1878,16 +1938,28 @@ public ScoreMode scoreMode() { final ScoreMode scoreModeUsed; if (!MultiThreadedSearcher.allowMT(pf.postFilter, cmd, getTaskExecutor())) { log.trace("SINGLE THREADED search, skipping collector manager in getDocListNC"); - final TopDocsCollector topCollector = buildTopDocsCollector(len, cmd); + final boolean nativeSegmentTerminateEarly = allowNativeSegmentTerminateEarly(cmd, len); + final TopDocsCollector topCollector = + buildTopDocsCollector(len, cmd, nativeSegmentTerminateEarly); + SegmentTerminatedEarlyObserver terminatedEarlyObserver = null; + Collector observedTopCollector = topCollector; + if (nativeSegmentTerminateEarly) { + observedTopCollector = + terminatedEarlyObserver = new SegmentTerminatedEarlyObserver(topCollector); + } MaxScoreCollector maxScoreCollector = null; - Collector collector = topCollector; + Collector collector = observedTopCollector; if (needScores) { maxScoreCollector = new MaxScoreCollector(); - collector = MultiCollector.wrap(topCollector, maxScoreCollector); + collector = MultiCollector.wrap(observedTopCollector, maxScoreCollector); } scoreModeUsed = buildAndRunCollectorChain(qr, query, collector, cmd, pf.postFilter).scoreMode(); + if (terminatedEarlyObserver != null) { + qr.setSegmentTerminatedEarly(terminatedEarlyObserver.terminatedEarly()); + } + totalHits = topCollector.getTotalHits(); topDocs = topCollector.topDocs(0, len); diff --git a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc index 7acd8024fee5..f3a557d8ad2f 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc @@ -435,6 +435,9 @@ of the distributed query processing. == segmentTerminateEarly Parameter +WARNING: Deprecated since 11.0. Its implementation relies on internal Lucene behavior that cannot +currently be replicated using Lucene's own public early-termination support (see SOLR-18363). + [%autowidth,frame=none] |=== |Optional |Default: `false` diff --git a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java index 3bed47aac571..6d131b769ad6 100644 --- a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java +++ b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java @@ -136,9 +136,19 @@ public interface CommonParams { /** another query to explain against */ String EXPLAIN_OTHER = "explainOther"; - /** Whether the search may be terminated early within a segment. */ + /** + * Whether the search may be terminated early within a segment. + * + * @deprecated relies on the internal, package-private {@code + * org.apache.solr.search.EarlyTerminatingSortingCollector}, which cannot be replaced by + * Lucene's own {@code TopFieldCollector} early-termination support without losing the + * specific "segment skipped due to matching sort" signal (see SOLR-18363). + */ + @Deprecated(since = "11.0") String SEGMENT_TERMINATE_EARLY = "segmentTerminateEarly"; + /** @deprecated see {@link #SEGMENT_TERMINATE_EARLY} */ + @Deprecated(since = "11.0") boolean SEGMENT_TERMINATE_EARLY_DEFAULT = false; /** From faab8dd2e9a68ff3fc2594cb80346ad527b12650 Mon Sep 17 00:00:00 2001 From: Serhiy Bzhezytskyy Date: Tue, 25 Aug 2026 13:01:07 +0300 Subject: [PATCH 2/4] SOLR-18363: fix spotless javadoc-wrap violation on the new @deprecated tags --- .../solr/response/SolrQueryResponse.java | 5 ++- .../org/apache/solr/search/QueryCommand.java | 8 +++-- .../apache/solr/search/SolrIndexSearcher.java | 34 +++++++++---------- .../solr/common/params/CommonParams.java | 4 ++- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java index 8492f14b6df6..e1351c54dfed 100644 --- a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java +++ b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java @@ -66,7 +66,10 @@ public class SolrQueryResponse { public static final String NAME = "response"; public static final String RESPONSE_HEADER_PARTIAL_RESULTS_KEY = "partialResults"; public static final String RESPONSE_HEADER_PARTIAL_RESULTS_DETAILS_KEY = "partialResultsDetails"; - /** @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} */ + + /** + * @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} + */ @Deprecated(since = "11.0") public static final String RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY = "segmentTerminatedEarly"; diff --git a/solr/core/src/java/org/apache/solr/search/QueryCommand.java b/solr/core/src/java/org/apache/solr/search/QueryCommand.java index 1d3b115b003a..dee6b0646d21 100755 --- a/solr/core/src/java/org/apache/solr/search/QueryCommand.java +++ b/solr/core/src/java/org/apache/solr/search/QueryCommand.java @@ -211,13 +211,17 @@ public QueryCommand setTerminateEarly(boolean segmentTerminateEarly) { } } - /** @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} */ + /** + * @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} + */ @Deprecated(since = "11.0") public boolean getSegmentTerminateEarly() { return (flags & SolrIndexSearcher.SEGMENT_TERMINATE_EARLY) != 0; } - /** @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} */ + /** + * @deprecated see {@link org.apache.solr.common.params.CommonParams#SEGMENT_TERMINATE_EARLY} + */ @Deprecated(since = "11.0") public QueryCommand setSegmentTerminateEarly(boolean segmentSegmentTerminateEarly) { if (segmentSegmentTerminateEarly) { 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 1d9778bc26e4..0d5cf902db88 100644 --- a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java +++ b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java @@ -1772,15 +1772,15 @@ TopDocsCollector buildTopDocsCollector(int len, QueryCommand /** * @param allowNativeSegmentTerminateEarly if true and {@code cmd} has a sort, forces {@link * TopFieldCollectorManager}'s totalHitsThreshold down to {@code len} so {@link - * TopFieldCollector} early-terminates a segment itself once its index sort matches the - * search sort (the same effect {@code segmentTerminateEarly=true} used to get from the now- - * removed {@code EarlyTerminatingSortingCollector} wrapper). Callers that pass {@code true} - * must not further wrap the returned collector together with a sibling collector that - * doesn't itself early-terminate (e.g. via {@code MultiCollector}) -- see SOLR-18363: {@code - * MultiCollector} only propagates {@code CollectionTerminatedException} once *every* wrapped - * collector has thrown it, so an early-terminating {@link TopFieldCollector} paired with a - * non-terminating sibling collector would silently stop counting hits early while the - * sibling kept counting, desyncing the two. + * TopFieldCollector} early-terminates a segment itself once its index sort matches the search + * sort (the same effect {@code segmentTerminateEarly=true} used to get from the now- removed + * {@code EarlyTerminatingSortingCollector} wrapper). Callers that pass {@code true} must not + * further wrap the returned collector together with a sibling collector that doesn't itself + * early-terminate (e.g. via {@code MultiCollector}) -- see SOLR-18363: {@code MultiCollector} + * only propagates {@code CollectionTerminatedException} once *every* wrapped collector has + * thrown it, so an early-terminating {@link TopFieldCollector} paired with a non-terminating + * sibling collector would silently stop counting hits early while the sibling kept counting, + * desyncing the two. */ private TopDocsCollector buildTopDocsCollector( int len, QueryCommand cmd, boolean allowNativeSegmentTerminateEarly) throws IOException { @@ -1829,14 +1829,14 @@ private boolean allowNativeSegmentTerminateEarly(QueryCommand cmd, int len) { } /** - * Records whether a wrapped collector's per-segment collection was actually cut short by a - * {@link CollectionTerminatedException}. Used with {@link #buildTopDocsCollector(int, - * QueryCommand, boolean)}'s {@code allowNativeSegmentTerminateEarly=true} path: {@link - * TopFieldCollector} only ever throws that exception from its sort-compatibility-gated fast - * path (see {@code TopFieldCollector.TopFieldLeafCollector#thresholdCheck}), never from the - * routine "totalHits exceeded totalHitsThreshold" bookkeeping alone -- so observing the - * exception itself gives the same precise "a segment was actually skipped" signal the removed - * {@code EarlyTerminatingSortingCollector} tracked directly, without needing + * Records whether a wrapped collector's per-segment collection was actually cut short by a {@link + * CollectionTerminatedException}. Used with {@link #buildTopDocsCollector(int, QueryCommand, + * boolean)}'s {@code allowNativeSegmentTerminateEarly=true} path: {@link TopFieldCollector} only + * ever throws that exception from its sort-compatibility-gated fast path (see {@code + * TopFieldCollector.TopFieldLeafCollector#thresholdCheck}), never from the routine "totalHits + * exceeded totalHitsThreshold" bookkeeping alone -- so observing the exception itself gives the + * same precise "a segment was actually skipped" signal the removed {@code + * EarlyTerminatingSortingCollector} tracked directly, without needing * TopFieldCollector#isEarlyTerminated() (which conflates that signal with the routine case) or * any of Lucene's package-private sort-compatibility check. */ diff --git a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java index 6d131b769ad6..74270d2fe06c 100644 --- a/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java +++ b/solr/solrj/src/java/org/apache/solr/common/params/CommonParams.java @@ -147,7 +147,9 @@ public interface CommonParams { @Deprecated(since = "11.0") String SEGMENT_TERMINATE_EARLY = "segmentTerminateEarly"; - /** @deprecated see {@link #SEGMENT_TERMINATE_EARLY} */ + /** + * @deprecated see {@link #SEGMENT_TERMINATE_EARLY} + */ @Deprecated(since = "11.0") boolean SEGMENT_TERMINATE_EARLY_DEFAULT = false; From 870f97035d3ce0ac62995f85a034171f472c3430 Mon Sep 17 00:00:00 2001 From: Serhiy Bzhezytskyy Date: Wed, 26 Aug 2026 16:29:04 +0300 Subject: [PATCH 3/4] SOLR-18363: add changelog entry per Eric's review --- .../SOLR-18363-native-early-termination.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 changelog/unreleased/SOLR-18363-native-early-termination.yml diff --git a/changelog/unreleased/SOLR-18363-native-early-termination.yml b/changelog/unreleased/SOLR-18363-native-early-termination.yml new file mode 100644 index 000000000000..4df832d89eeb --- /dev/null +++ b/changelog/unreleased/SOLR-18363-native-early-termination.yml @@ -0,0 +1,12 @@ +title: > + Removed the deprecated EarlyTerminatingSortingCollector in favor of Lucene's native + TopFieldCollector early termination. SEGMENT_TERMINATE_EARLY, its response header, and + QueryCommand's accessors for it are now deprecated too. Combining segmentTerminateEarly=true + with a RankQuery now logs an "unsupported combination" warning instead of silently doing + nothing. +type: removed +authors: + - name: Serhiy Bzhezytskyy +links: + - name: SOLR-18363 + url: https://issues.apache.org/jira/browse/SOLR-18363 From 6ee01fd70631ad66e23eb696ab9d16d073fec88a Mon Sep 17 00:00:00 2001 From: Serhiy Bzhezytskyy Date: Thu, 27 Aug 2026 07:56:30 +0300 Subject: [PATCH 4/4] SOLR-18363: address Houston's review Point the deprecation warning + changelog at minExactCount as the replacement, and use len instead of 0 for the forced totalHitsThreshold (Lucene clamps to max(totalHitsThreshold, numHits) internally, so it's equivalent, but len reads clearer). --- .../unreleased/SOLR-18363-native-early-termination.yml | 7 ++++--- .../src/java/org/apache/solr/search/SolrIndexSearcher.java | 2 +- .../modules/query-guide/pages/common-query-parameters.adoc | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/changelog/unreleased/SOLR-18363-native-early-termination.yml b/changelog/unreleased/SOLR-18363-native-early-termination.yml index 4df832d89eeb..eb0346b4a712 100644 --- a/changelog/unreleased/SOLR-18363-native-early-termination.yml +++ b/changelog/unreleased/SOLR-18363-native-early-termination.yml @@ -1,9 +1,10 @@ title: > Removed the deprecated EarlyTerminatingSortingCollector in favor of Lucene's native TopFieldCollector early termination. SEGMENT_TERMINATE_EARLY, its response header, and - QueryCommand's accessors for it are now deprecated too. Combining segmentTerminateEarly=true - with a RankQuery now logs an "unsupported combination" warning instead of silently doing - nothing. + QueryCommand's accessors for it are now deprecated too -- use minExactCount instead, which + drives the same native early termination without requiring a matching index sort. Combining + segmentTerminateEarly=true with a RankQuery now logs an "unsupported combination" warning + instead of silently doing nothing. type: removed authors: - name: Serhiy Bzhezytskyy 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 0d5cf902db88..6eb14113830f 100644 --- a/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java +++ b/solr/core/src/java/org/apache/solr/search/SolrIndexSearcher.java @@ -1800,7 +1800,7 @@ private TopDocsCollector buildTopDocsCollector( final FieldDoc searchAfter = (null != cursor ? cursor.getSearchAfterFieldDoc() : null); if (allowNativeSegmentTerminateEarly) { - minNumFound = 0; + minNumFound = len; } return new TopFieldCollectorManager(weightedSort, len, searchAfter, minNumFound) .newCollector(); diff --git a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc index f3a557d8ad2f..0960b701f46d 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc @@ -437,6 +437,9 @@ of the distributed query processing. WARNING: Deprecated since 11.0. Its implementation relies on internal Lucene behavior that cannot currently be replicated using Lucene's own public early-termination support (see SOLR-18363). +Use <> instead -- it drives the same native, +per-segment early termination Lucene now performs internally, and it works with any sort, not only +one that matches the collection's `mergePolicyFactory` index sort. [%autowidth,frame=none] |===