GroupingSearch#search(searcher, query, ...) hands its group sort and within-group sort to the grouping collectors without Sort#rewrite(searcher), the step IndexSearcher#search(query, n, sort) takes first. Sorts whose fields need the searcher then throw, or, for a legacy QueryValueSource view, score with the default Similarity.
- A
Sortcan hold fields that need the searcher before they can sort, such as a function sort fromValueSource#getSortFieldor a query-score sort fromDoubleValuesSource.fromQuery.Sort#rewrite(searcher)binds them to it. - Since 7.2.0,
IndexSearcher#search(query, n, sort)callssort.rewrite(this)itself, so callers can pass such a Sort as it is. GroupingSearchorders the groups by a group sort and the documents inside each group by a within-group sort, and runs its collectors on the searcher passed tosearch.
IndexSearcher#search orders doc1 (1.0), doc2 (1.0), doc0 (0.894), while GroupingSearch returns doc0 (0.101), doc1 (0.088), doc2 (0.088); sorted by the same Sort, the groups come back g0 first instead of g1, and the fix rewrites both Sorts. Select the still to open it at full size. Generated with AI, checked against the code.- You index two documents in one group
g: doc 0 =a a a a a a a a x x, doc 1 =a. The searcher usesClassicSimilarity, and you sort the documents inside each group by the score ofbody:a:new Sort(new QueryValueSource(body:a, 0).asDoubleValuesSource().getSortField(true)). - You expect
new GroupingSearch("group").setSortWithinGroup(sort)to list the group assearcher.search(matchAll, 10, sort)does for the same Sort: doc1 (1.0), then doc0 (0.894).IndexSearchergets there by callingsort.rewrite(this)first. - Instead the group comes back as doc0 (0.147), doc1 (0.125): the scores of the default BM25 Similarity, in the opposite order.
GroupingSearchnever rewrites its Sorts, so the score source has no searcher, andQueryValueSourcescoresbody:awith a fresh defaultIndexSearcher. - Most sorts that need the searcher fail loudly instead, and only in
GroupingSearch: aValueSource#getSortFieldsort throws "SortField needs to be rewritten ...", aDoubleValuesSource.fromQuerysort "This DoubleValuesSource must be rewritten". Group order and group heads go wrong the same way.
GroupingSearch users who sort groups or group documents by a function or a query score. Usually it throws on the first grouped search although IndexSearcher accepts the sort; the silent wrong order needs a legacy QueryValueSource sort and a non-default Similarity. Field sorts and relevance are unaffected. Any index size, since 7.2.0.
Mechanism, terms and numbers493 words
GroupingSearch takes a group sort and a within-group sort and runs its collectors on the searcher passed to search(searcher, query, ...). Since 7.2.0, IndexSearcher#search(query, n, sort) binds a Sort to the searcher with sort.rewrite(this) before sorting; GroupingSearch never does, although it rewrites its own groupEndDocs query against that searcher. A sort that works in IndexSearcher#search therefore throws in GroupingSearch ("SortField needs to be rewritten ..." for a ValueSource#getSortField sort, "This DoubleValuesSource must be rewritten" for a DoubleValuesSource.fromQuery sort), and a sort on a legacy QueryValueSource through asDoubleValuesSource() silently scores with the default Similarity: on a ClassicSimilarity searcher a two-document group comes back as doc0=0.147, doc1=0.125 (default-BM25 scores) instead of doc1=1.0, doc0=0.894. Group order and group heads go wrong the same way; the set of matching documents never changes. Every release from 7.2.0 to 10.6.0 and main is affected. Rewriting both Sorts at the start of the two grouping methods (4 added lines) fixes every case; all 56 existing grouping tests stay green.
IndexSearcherbinds the Sort.searchAfter, whichsearch(Query, int, Sort)uses, doesfinal Sort rewrittenSort = sort.rewrite(this);and builds itsTopFieldCollectorManagerfrom it (IndexSearcher.java:730-733).Sort.rewriterewrites each field and returnsthiswhen nothing changes (Sort.java:88-99). It is the onlySort.rewritecall in the non-test sources underlucene/.GroupingSearchdoes not.groupByFieldOrFunctionpasses the fieldgroupSorttoFirstPassGroupingCollectorManager(GroupingSearch.java:148),sortWithinGrouptoAllGroupHeadsCollectorManager(:160) and both toTopGroupsCollectorManager(:201-202).groupByDocBlockrewrites itsgroupEndDocsquery withsearcher.rewrite(this.groupEndDocs)(:223) and then passes both Sorts as given toBlockGroupingCollectorManager(:228,:233). The collector managers take only aSortand never see the searcher.- Loud path. The collectors build comparators with
SortField.getComparator(FirstPassGroupingCollector.java:116,AllGroupHeadsCollector.java:241,BlockGroupingCollector.java:237). AType.REWRITEABLEfield, whichValueSource#getSortFieldreturns (ValueSource.java:369-384), throws "SortField needs to be rewritten through Sort.rewrite(..) and SortField.rewrite(..)" (SortField.java:555-557); aDoubleValuesSource.fromQuerysource throws "This DoubleValuesSource must be rewritten" until it is rewritten (DoubleValuesSource.java:768). - Silent path.
ValueSource#asDoubleValuesSource()returns a wrapper with no searcher (ValueSource.java:184-185); its values get a function context whose searcher isnull(:198-200), and onlyrewrite(searcher)returns a wrapper that carries the searcher (:244-245).QueryValueSourcethen finds no searcher and scores its query withnew IndexSearcher(ReaderUtil.getTopLevelContext(readerContext))(QueryValueSource.java:116-123): the same reader, the default Similarity. - What does not change. The collectors only see documents the caller's query matched on the caller's searcher (
GroupingSearch.java:173,:175,:211,:237): the matching documents and groups are the same; only order, sort values and group heads differ, or the request throws.
On ClassicSimilarity, body:a scores sqrt(tf)/sqrt(length) with idf 1 (both documents match): doc 1 gets sqrt(1)/sqrt(1) = 1.0 and doc 0 gets sqrt(8)/sqrt(10) = 0.894. A plain IndexSearcher over the same reader (default BM25) scores them 0.14679137 and 0.12456752, and those are exactly the sort values GroupingSearch returns.
Exposure and likelihood201 words
- Who is hit. Applications that group with
GroupingSearch(field, function or doc-block grouping) and sort groups or the documents inside a group by a queries-module function sort (ValueSource#getSortField), a query-score sort (DoubleValuesSource.fromQuery) or anIndexReaderFunctionssort that needs the searcher, such asdocFreqorsumTotalTermFreq(all loud), or by a legacyQueryValueSourcethroughasDoubleValuesSource()(silent when the Similarity is not the default). - Default vs opt-in.
Sort.RELEVANCE(the default) and field sorts are unaffected. The silent form also needs a searcher Similarity other than the default BM25:ClassicSimilarity,BooleanSimilarityand BM25 with other parameters were all executed. No codec, analyzer or index setting is involved; assertions on or off. - Scale. Any index size (two documents are enough); deterministic: every grouped search with such a sort.
- Silent vs loud. The loud forms fail on the first grouped search with a message that names the rewrite. The silent form returns plausible but wrong orderings, sort values and group heads with no signal.
- How likely. Rare: upstream grouping tests and docs use only field sorts and relevance, and nobody has reported it since 7.2.0 (2017).
- Workaround.
setGroupSort(sort.rewrite(searcher))andsetSortWithinGroup(sort.rewrite(searcher)), once per searcher: on the example it gives doc1=1.0, doc0=0.894.
One JUnit test, one method, public API: two documents in one group, a ClassicSimilarity searcher and one sort by the score of body:a. It compares GroupingSearch's order inside the group with IndexSearcher#search for the same Sort. It fails with assertions on and off and passes with the fix.
/*
* 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.lucene.search.grouping;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.function.valuesource.QueryValueSource;
import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.similarities.ClassicSimilarity;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.BytesRef;
/**
* GroupingSearch must order the documents of a group like IndexSearcher#search(Query, int, Sort)
* orders them for the same Sort on the same searcher. IndexSearcher rewrites the Sort against
* itself; GroupingSearch passes it on as given.
*/
public class TestGroupingSearchSortRewrite extends LuceneTestCase {
public void testSortWithinGroupMatchesIndexSearcher() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig(new StandardAnalyzer()))) {
for (String body : new String[] {"a a a a a a a a x x", "a"}) { // doc 0, doc 1
Document doc = new Document();
doc.add(new TextField("body", body, Field.Store.NO));
doc.add(new SortedDocValuesField("group", new BytesRef("g")));
w.addDocument(doc);
}
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
IndexSearcher searcher = new IndexSearcher(reader);
searcher.setSimilarity(new ClassicSimilarity());
// sort by the score of body:a, best first
QueryValueSource score = new QueryValueSource(new TermQuery(new Term("body", "a")), 0f);
Sort sort = new Sort(score.asDoubleValuesSource().getSortField(true));
TopGroups<BytesRef> grouped =
new GroupingSearch("group")
.setSortWithinGroup(sort)
.setGroupDocsLimit(10)
.search(searcher, new MatchAllDocsQuery(), 0, 10);
ScoreDoc[] sorted = searcher.search(new MatchAllDocsQuery(), 10, sort).scoreDocs;
assertEquals(describe(sorted), describe(grouped.groups[0].scoreDocs()));
}
}
}
private static String describe(ScoreDoc[] hits) {
StringBuilder sb = new StringBuilder();
for (ScoreDoc hit : hits) {
sb.append("doc").append(hit.doc).append('=').append(((FieldDoc) hit).fields[0]).append(' ');
}
return sb.toString().trim();
}
}
./gradlew :lucene:grouping:test --tests org.apache.lucene.search.grouping.TestGroupingSearchSortRewrite -Ptests.seed=DEADBEEF
- Expected
- doc1=1.0 doc0=0.894: the order and sort values of
searcher.search(new MatchAllDocsQuery(), 10, sort)on the same searcher. - Observed
- doc0=0.147 doc1=0.125 (
expected:<doc[1=1.0 doc0=0.8944271802902222]> but was:<doc[0=0.14679136872291565 doc1=0.12456752359867096]>), with assertions on and off.
More files, environment, full output287 words
The test sits in org.apache.lucene.search.grouping only for locality; it uses only public API. ByteBuffersDirectory, core StandardAnalyzer, the default IndexWriterConfig, two documents (one segment), a plain IndexSearcher with ClassicSimilarity, nothing random: deterministic, single thread. It runs GroupingSearch before IndexSearcher#search, so no version can hand the searcher from one to the other through the shared Sort object. The same test is the suggested regression test.
On 9.1 to 9.12, GroupDocs has public fields: write grouped.groups[0].scoreDocs instead of scoreDocs() (one line).
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT); also fails on 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.1.0 |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 |
cp $REPRO/TestGroupingSearchSortRewrite.java $LUCENE/lucene/grouping/src/test/org/apache/lucene/search/grouping/
cd $LUCENE
./gradlew :lucene:grouping:test --tests org.apache.lucene.search.grouping.TestGroupingSearchSortRewrite -Ptests.seed=DEADBEEF
# production setting (assertions off): add -Ptests.asserts=false
# gradle, main 5adff78bb168, seed DEADBEEF, assertions on (the same with -Ptests.asserts=false)
TestGroupingSearchSortRewrite > testSortWithinGroupMatchesIndexSearcher FAILED
org.junit.ComparisonFailure: expected:<doc[1=1.0 doc0=0.8944271802902222]> but was:<doc[0=0.14679136872291565 doc1=0.12456752359867096]>
at org.apache.lucene.search.grouping.TestGroupingSearchSortRewrite.testSortWithinGroupMatchesIndexSearcher(TestGroupingSearchSortRewrite.java:72)
1 test completed, 1 failed
BUILD FAILED
# with the recommended fix, assertions on and off
:lucene:grouping:test (SUCCESS): 1 test
BUILD SUCCESSFUL
# JUnit runner on the released jars (seed DEADBEEF, -ea): the same message on 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.1.0
# exploration probe on the same two documents (main 5adff78bb168)
PROBE classic scores of body:a: doc1=1.0 doc0=0.8944272
PROBE default(BM25) scores of body:a: doc0=0.14679137 doc1=0.12456752
PROBE GroupingSearch within-group, raw sort: doc0=0.14679136872291565 doc1=0.12456752359867096
PROBE GroupingSearch within-group, sort.rewrite(searcher): doc1=1.0 doc0=0.8944271802902222
PROBE IndexSearcher#search(q, 10, sort): doc1=1.0 doc0=0.8944271802902222
PROBE IndexSearcher, ValueSource#getSortField: doc1=1.0 doc0=0.8944271802902222
PROBE GroupingSearch, ValueSource#getSortField: java.lang.IllegalStateException: SortField needs to be rewritten through Sort.rewrite(..) and SortField.rewrite(..)
PROBE IndexSearcher, DoubleValuesSource.fromQuery: doc1=1.0 doc0=0.8944271802902222
PROBE GroupingSearch, DoubleValuesSource.fromQuery: java.lang.UnsupportedOperationException: This DoubleValuesSource must be rewritten
PROBE control, FIELD_DOC within group: doc0=0 doc1=1
PROBE control, default-Similarity searcher, raw sort: doc0=0.14679136872291565 doc1=0.12456752359867096 vs IndexSearcher doc0=0.14679136872291565 doc1=0.12456752359867096GroupingSearch#searchis handed the searcher and rewrites its owngroupEndDocsquery with it (GroupingSearch.java:223), but not its Sorts.- Since 7.2.0 the class that holds the searcher rewrites the Sort (
IndexSearcher.java:730); a consumer missed then was fixed as a bug. - No javadoc asks callers to pre-rewrite, and an upstream test expects agreement with
IndexSearcher#searchfor the same Sort.
The full argument261 words
- The class that holds the searcher rewrites. LUCENE-7736 (7.2.0) moved the rewrite into
IndexSearcher(IndexSearcher.java:730); its author wrote thatSort.rewrite()"wasn't being called by IndexSearcher or SortRescorer" (#8787, 2017-04-10). A consumer left behind then was fixed as a bug: "LUCENE-7736: CoveringQuery needs to rewrite its ValuesSource." (6b621ea659bon branch_7x and3f0b240853don master, 2017-11-22). Earlier, Solr fixed another consumer that holds the searcher in the same way: SOLR-6118, "expand.sort bug for function queries; needed to sort.rewrite(searcher)" (6ffdda4545e, 2014). GroupingSearchholds the searcher.search(searcher, query, ...)is given theIndexSearcher, andgroupByDocBlockrewrites itsgroupEndDocsquery against it (GroupingSearch.java:223). Only the Sorts are left out.- No precondition is documented. The class javadoc (
:41-45),search(:115-129),setGroupSort("Specifies how groups are sorted",:285-294) andsetSortWithinGroup(:296-305) say nothing about pre-rewriting; neither do the package docs,MIGRATE.mdorCHANGES.txt. - An upstream test expects agreement.
BaseGroupSelectorTestCase#testSortGroupssets the sameSortonGroupingSearchand onsearcher.search(topLevel, 1, sort)and asserts (assertArrayEquals,:118) what its code comment states: "The top group should have sort values equal to the sort values of the top doc of a top-level search sorted by the same Sort" (BaseGroupSelectorTestCase.java:83-121, comment:112-114); it uses field sorts only. - No consistent semantics explains the current answers. One
Sortobject gives one order throughIndexSearcher#searchand another throughGroupingSearch; a sortIndexSearcheraccepts throws inGroupingSearch; and in the silent case the grouped order depends on a Similarity the application never chose.
The devil's-advocate check171 words
The strongest case for "works as designed": callers rewrite Sorts. That was the rule when GroupingSearch was written (LUCENE-3778, 2012, when IndexSearcher did not rewrite either); the REWRITEABLE javadoc and the error message tell you to call Sort.rewrite; upstream TestFunctionQuerySort still calls sf.rewrite(searcher) itself (TestFunctionQuerySort.java:65); Solr's grouping weights its sorts before building the collectors; the low-level collector managers have no searcher; #5619 describes caller-side rewriting as the status quo; and GroupingSearch is @lucene.experimental.
Why it fails: unlike the collector managers, GroupingSearch#search is handed the searcher and already rewrites its own query with it; since 7.2.0 the Lucene class that holds the searcher rewrites, and a missed consumer was fixed as a bug; no documentation states the precondition; and a silent fallback to a different Similarity is not a plausible design. The argument does fairly make the loud forms an API-consistency defect rather than corruption. Two devil's-advocate reviews (one by an independent reviewer from another model family, OpenAI GPT-5.6) rated it moderate and not persuasive; the adjudication agreed.
Recommended A. Rewrite both Sorts at the start of the two grouping methods (validated)
Rewrite both Sorts against the searcher at the start of groupByFieldOrFunction and groupByDocBlock, into local variables (4 added lines in GroupingSearch.java), so a reused GroupingSearch keeps its configured Sorts. The repro passes with assertions on and off, all 57 grouping-module tests pass, and the loud sorts stop throwing.
// groupByFieldOrFunction, before the first collector manager; groupByDocBlock, after endDocsQuery
final Sort groupSort = this.groupSort.rewrite(searcher);
final Sort sortWithinGroup = this.sortWithinGroup.rewrite(searcher);All options and trade-offs4 options ยท 517 words
Four options. A was built and validated on main 5adff78bb168; B to D were not built. Workaround until a fix ships: setGroupSort(sort.rewrite(searcher)) and setSortWithinGroup(sort.rewrite(searcher)) once per searcher.
In groupByFieldOrFunction, before the collector managers are built, and in groupByDocBlock, after the groupEndDocs rewrite, add two local variables that shadow the fields. The rest of both methods is unchanged. One file, +4 lines.
// groupByFieldOrFunction, before the first collector manager; groupByDocBlock, after endDocsQuery
final Sort groupSort = this.groupSort.rewrite(searcher);
final Sort sortWithinGroup = this.sortWithinGroup.rewrite(searcher);Trade-offs
- Cost:
Sort.rewritereturnsthiswhen no field changes, so field sorts and relevance get no new objects; a function sort is rewritten once persearch, as inIndexSearcher#search. Already-rewritten Sorts are unaffected (a second rewrite is harmless). - The returned
TopGroups#groupSort/withinGroupSortthen hold the rewrittenSortFields, asTopFieldDocs#fieldsdoes afterIndexSearcher#search. GroupingSearchthen shares the pathIndexSearcheruses for these sorts, with its two defects listed on this site: LFV-770 (a rewrittentoSortableLongDoubleValuesSource()sorts on(long)casts) and LFV-567 (aValueSourcesort context shared across the slices of an executor search). Fix LFV-770 with or before this.- Subclasses that override the
protectedgrouping methods do not get the rewrite.
What the checks validated
- Gradle in a scratch worktree of
5adff78bb168, OpenJDK 25.0.3, seed DEADBEEF: stock, the repro fails with assertions on and off; with the fix it passes, on and off. - Every test of the grouping module passes with the fix and the repro in place: 15 classes, 57 tests (56 existing + the repro), 0 failures.
checkGoogleJavaFormat,ecjLint,forbiddenApisMainandforbiddenApisTestoflucene/groupingpass.- With the patched class compiled over the stock jars, the probe's
ValueSource#getSortFieldandfromQuerysorts no longer throw, and the default-Similarity control is unchanged. A second probe on the same two documents, run separately with the fix applied, shows both sorts then return doc1=1.0, doc0=0.8944271802902222 inGroupingSearch, the same asIndexSearcher#search; on the stock jars both throw. - On
a9779ef3c193andea57360912edthe same 4 lines passed the finding's 3 tests, the independent reproducer's 6 tests and the cross-model reviewers' own tests.
Javadoc on setGroupSort and setSortWithinGroup: "rewrite the Sort against the searcher first".
Trade-offs
No code change, but it keeps the silent trap, ties a configured GroupingSearch to one searcher, and leaves it inconsistent with IndexSearcher#search.
Throw when the function context has no searcher, instead of creating a default-Similarity IndexSearcher (QueryValueSource.java:116-123).
Trade-offs
Turns the silent form into a loud one everywhere, not only in GroupingSearch, but changes behaviour for callers that rely on the fallback and does nothing for the loud forms. A separate question for maintainers.
The generic direction of #5619: every consumer rewrites.
Trade-offs
The managers take only a Sort and never see a searcher, so this needs an API change in every grouping manager; much broader than A.
A product that used the group order or group head of a query-function sort as a selection rule would pick a different, still matching document; no such use is known.
Semi-formal. A bytecode scan of the stock jars found the grouping collectors' sort-comparator calls with no Sort rewrite on the path from GroupingSearch#search. A bounded-exhaustive comparison then ran 23 Sort consumers with the raw Sort and with it pre-rewritten, over every index of 1-3 tiny documents, 3 Similarities and 18 value sources; GroupingSearch differed, IndexSearcher never did.
The method in full214 words
Method: semi-formal (bounded-exhaustive translation validation) of the APIs that consume a Sort or a values source.
- A ClassFile scan of the stock jars listed the grouping collectors'
getComparatorsites (FirstPassGroupingCollector:116, theTopGroups/SearchGroupcomparators,AllGroupHeadsCollector:241,BlockGroupingCollector:237) with noSortrewrite on the path fromGroupingSearch#search. - A test program on the stock main jars (assertions off) then enumerated every index of 1-3 documents over a 12-value per-document alphabet, in all 1-2 segment splits and deletion masks (42,648 index configurations), x 3 searcher Similarities (BM25, Classic, Boolean) x the 282 applicable pairs of 18 values sources and 23 consumer APIs (some consumers take only double or only long sources): 36,080,208 runs of a consumer fed the raw Sort or source, compared with the same consumer fed it pre-rewritten against the searcher.
- The
GroupingSearchgroup-sort and within-group consumers were silent (different results, no error) under Classic and Boolean similarity for every legacyquery()function source (group heads: under Boolean), and loud (an exception) forfromQuerysources and for theIndexReaderFunctionssources that need the searcher (docFreq,sumTotalTermFreq;termFreqgave the same result either way). TheIndexSearcher#searchcontrol was never silent. - The case was reduced to a 3-test public-API JUnit test (2026-10-27), which failed with assertions on and off.
New: no Lucene issue, PR, JIRA entry, list thread or commit, and nothing outside Lucene, describes this defect (searched 2026-10-27/28, re-checked 2026-11-03 and 2026-11-04). Related: #5619 asks generically that users not have to call Sort.rewrite(); SOLR-2642 fixed a similar collector error in Solr's own grouping code in 2011.
Searches3 searches
No report of this defect was found (re-checked 2026-11-03 in two independent passes, and again on 2026-11-04). Related prior items, none of them this defect:
- #5619 (LUCENE-4553, dsmiley, open since 2012, no comments), "Don't require users to call Sort.rewrite()": a generic request ("user code never calls Query.rewrite(), Lucene will call that one. Shouldn't Sort/SortField rewrite() work the same way?"). It names no consumer and no symptom, and predates
IndexSearcher's own rewrite. This defect isGroupingSearch's specific omission, which became an inconsistency only whenIndexSearcherstarted rewriting in 7.2.0, and its silent wrong order. - SOLR-2642 (2011, fixed in Solr 3.4), "Sorting by function fails when using result grouping": Solr's own grouping code built a first-pass grouping collector from unweighted function sorts and failed with "Unweighted use of sort". Solr fixed its caller by weighting the sorts first.
GroupingSearchwas written later (LUCENE-3778, 2012) as separate code; the report has no silent variant. Different code, different defect. - #8787 (LUCENE-7736, 2017): design context; it added the
IndexSearcherSort rewrite and namesIndexSearcherandSortRescorer, not grouping. - SOLR-6118 (2014,
6ffdda4545e), "expand.sort bug for function queries; needed to sort.rewrite(searcher)": Solr'sExpandComponentpassed a function sort to its collector without rewriting it and was fixed by addingsort.rewrite(searcher). A different consumer in a different code base, so not this defect; it shows that a consumer holding the searcher that skips the rewrite was treated as a bug. - Set aside: #4851 (adds
GroupingSearch), #8668, #7958, #8922 (a 2017 review remark on the caller-rewrites convention), #10491, #10060 and SOLR-6203 (other grouping defects or design work).
Same missing rewrite, other fix sites, in this list: LFV-814 (FacetsCollectorManager and DrillSideways sorted search) and LFV-815 (SortRescorer).
Limits: text search cannot rule out a report that describes only the symptom (a wrong grouped order with a function sort) without naming the API; private channels cannot be searched.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits (incl. the migrated JIRA archive); dev@, java-user@, issues@, commits@; JIRA; downstream repositories; web | 2026-10-27/28 | 313 logged read-only queries (three runs of the prior-report tool, 70 manual GitHub searches, web searches), the changed files of all 410 open PRs, 49 related threads read in full | only the related items; #16847 and #14996 change GroupingSearch.java for group-head bitsets, #16857 changes Sort/SortField without a consumer-side rewrite |
| Re-check: the same sources | 2026-11-03 | two independent passes over the earlier hits | the items found earlier are related but about a different defect |
| apache/lucene, lists, JIRA, downstream, web | 2026-11-04 | the prior-report tool again (51 queries incl. 4 for items created since 2026-10-26; web queries run by hand), GitHub searches for items updated since 2026-10-27, the commit history of GroupingSearch.java on upstream main | 217 hits; nothing new; upstream main f72004515ec2 still has the code (last change 3afffeef5a8d, 2026-07-08, unrelated) |
Introduced in 7.2.0 by LUCENE-7736 (03afeb7766a, committed 2017-11-15), which made IndexSearcher#search rewrite Sorts but did not touch GroupingSearch, which has used its Sorts as given since 2012. 7.1.0 is consistent; every release run from 7.2.0 to 10.6.0 and main diverges. Not fixed upstream.
Versions tested8 rows
Introduced by LUCENE-7736, "Expose some IndexReader stats via DoubleValuesSources" (commit subject "IndexReaderValues"; apache/lucene#8787; a JIRA patch, no PR; author romseygeek, authored 2017-03-28, committed 2017-11-15): 03afeb7766a on branch_7x (first released in 7.2.0) and 2a4dd499bbd on master (8.0.0). It added sort.rewrite(this) to IndexSearcher (7.1.0's IndexSearcher has none, 7.2.0's has it at line 545) and made DoubleValuesSource sort fields and the asDoubleValuesSource() view get their searcher only through rewrite. GroupingSearch was not changed.
The defect site is older. GroupingSearch has passed its Sorts as given since LUCENE-3778 (89b55566d83, 2012-03-22, first in 4.0.0-alpha), when every consumer, IndexSearcher included, needed a caller-rewritten Sort. Its only rewrite ever is of the groupEndDocs query.
Masking caveat for 7.2.0 to 7.7.1 and 8.0.0. There a Sort that an earlier IndexSearcher#search had rewritten kept that searcher (removed by LUCENE-8726 in 7.7.2 and 8.1.0), which hides the group-order failure when the same Sort object is reused; with separately built Sorts it fails there too. The minimal repro runs GroupingSearch first.
Not fixed upstream: GroupingSearch.java was last changed by 3afffeef5a8d (#16247, 2026-07-08, unrelated); upstream main f72004515ec2 (2026-11-03) has the code of 5adff78bb168.
| Version or commit | Verdict | Result |
|---|---|---|
| 6.6.6, 7.0.0, 7.1.0 | not affected | consistent: IndexSearcher did not rewrite Sorts either, so both APIs ignore the searcher's Similarity and IndexSearcher itself rejects the ValueSource sort |
| 7.2.0, 7.7.1, 8.0.0 | affected | within-group order wrong and the ValueSource group sort throws only in GroupingSearch; group order wrong with separately built Sorts |
| 7.7.2, 7.7.3, 8.1.0, 8.11.4, 9.0.0 | affected | all three history tests fail |
| 9.1.0, 9.12.3 | affected | the minimal repro fails (one-line scoreDocs field change) |
| 10.0.0, 10.5.2, 10.6.0 | affected | the minimal repro fails, same message |
main 5adff78bb168 | affected | the minimal repro fails, assertions on and off (gradle and JUnit runner); the 3-test and 6-test JUnits fail 3 of 3 and 5 of 6 |
main 5adff78bb168 + fix A | fix | the repro passes on and off; 57 of 57 grouping-module tests pass |
| 7.2.0 + fix A | commit-level check | with the fix applied to the 7.2.0 GroupingSearch, all history tests pass |
An independent reproducer, executing reviews by a model from another family (OpenAI GPT-5.6) and a devil's advocate each tested it and rated it real; an adjudication confirmed it with high confidence. This doc's runs reproduce it on 5adff78bb168, 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.1.0 and validate the fix with gradle.
Checks and verdicts363 words
The reviews, verifiers and judge ran on main a9779ef3c193 or ea57360912ed, whose GroupingSearch.java is identical to 5adff78bb168's. A re-verification on e1faeba08bde (2026-11-02) and this doc's runs on 5adff78bb168 reproduced it. Every control passed.
| Check | Verdict | What it found |
|---|---|---|
| finder | found | Bounded-exhaustive comparison, then a 3-test public-API JUnit: fails 3 of 3 on main (assertions on and off), 10.5.2 and 9.12.3; passes with the fix. Re-run on 5adff78bb168: 3 of 3 fail. |
| refute review (OpenAI GPT-5.6, executing) | real (high) | Own 4-test JUnit on a BooleanSimilarity searcher: the silent within-group mismatch and the loud fromQuery failure fail on main and 10.5.2, its 2 controls pass; with the fix 4 of 4 pass and 15 upstream grouping tests pass. |
| independent reproducer | real | Own 6-test JUnit on a BM25(k1=1.2, b=0) searcher with a self-checked oracle: 5 of 6 fail on main, 10.5.2 and 9.12.3 (within-group order, top group, group head, REWRITEABLE group sort, doc-block fromQuery); the default-Similarity control passes; a mechanism check shows each returned value is a fresh default searcher's score. Re-run on 5adff78bb168: 5 of 6 fail. |
| intended-behaviour review (OpenAI GPT-5.6, executing) | real (high) | Steelmanned the caller-rewrites convention and rated it moderate and failing; own 2-test JUnit fails 2 of 2 on main, 10.5.2 and 9.12.3, passes with the fix. |
| devil's-advocate verifier | real (medium-high) | Built the five-point case for "intended" and rebutted it; 4 intent probes pass: the caller workaround works, the default Similarity hides the silent case, a double rewrite is harmless, fromQuery throws only in GroupingSearch. |
| adjudication | confirmed (high) | Re-ran the reproducer's JUnit with a new seed: 5 of 6 fail with assertions on and off, the control passes. |
| re-verification on a newer main | still reproduces | On e1faeba08bde (2026-11-02): 3 of 3 fail with assertions on and off; the fix applies unchanged and passes, with 24 upstream grouping tests; 10.6.0 fails 3 of 3. |
| this doc's runs | reproduced, fix validated | The one-method repro fails on 5adff78bb168 (gradle and JUnit runner, on and off) and on 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.1.0; with the fix it passes and 57 of 57 grouping tests pass. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 517 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,486 words
- Registry title
- GroupingSearch#search never rewrites its group sort or within-group sort against the searcher, unlike IndexSearcher#search(Query, int, Sort): sorts that need a rewrite throw, and a legacy query() function sort silently orders groups, group documents and group heads with the default Similarity
- Area
- grouping / GroupingSearch (convenience API)
- Priority
- Medium, rank 218: GroupingSearch has had the same missing Sort rewrite since 7.2 (a loud failure, silent only on the rare legacy values-source bridge), so it gets the same Medium tier as LFV-815 and LFV-814.
- Impact class
- silent wrong results
- Impact note
- Applications that group with GroupingSearch and sort the groups or the documents inside a group by a Sort that needs the searcher are hit at any index size, in every release from 7.2.0 to 10.6.0: in the narrow silent case (a legacy QueryValueSource viewed through asDoubleValuesSource() on a searcher with a non-default Similarity such as ClassicSimilarity or BooleanSimilarity) the group order, group sort values, within-group order and all-group-heads follow default-BM25 scores with no error, and in the loud cases (ValueSource#getSortField, DoubleValuesSource.fromQuery and IndexReaderFunctions sorts) every grouped search throws a 'needs to be rewritten' / 'must be rewritten' error although IndexSearcher#search accepts the same Sort; field sorts, Sort.RELEVANCE and default-Similarity searchers are unaffected, and callers can work around it with setGroupSort(sort.rewrite(searcher)) and setSortWithinGroup(sort.rewrite(searcher)) per searcher.
- Summary
- You group with GroupingSearch and set a group sort or within-group sort that depends on the query or the searcher, for example a sort on new QueryValueSource(q, 0).asDoubleValuesSource(), and expect the groups and their documents in the order IndexSearcher#search(query, n, sort) gives for the same Sort on the same searcher, because IndexSearcher rewrites the Sort and GroupingSearch#search is handed that searcher. Instead GroupingSearch passes both Sorts to its collectors as given (GroupingSearch.java:148, 160, 201-202, 228-233): on a ClassicSimilarity searcher the documents of one group come back as doc0=0.101, doc1=0.088, doc2=0.088 (default-BM25 scores) instead of doc1=1.0, doc2=1.0, doc0=0.894, the first group is g0 instead of g1, and ValueSource sort fields or fromQuery sorts throw. Rewriting both Sorts against the searcher at the start of groupByFieldOrFunction and groupByDocBlock fixes every case.
- Introduced
- 03afeb7766a39996de3c85e8a6ab24d2a352dd1c on branch_7x (the copy first released, in 7.2.0); master equivalent 2a4dd499bbd2f5ed1f481e17eeaee4f32b927185. LUCENE-7736 'Expose some IndexReader stats via DoubleValuesSources' (apache/lucene#8787; JIRA patch, no GitHub PR; author romseygeek) made IndexSearcher#search(Query, int, Sort) call sort.rewrite(this) (IndexSearcher.java:730 on main) and gave DoubleValuesSource sort fields and ValueSource#asDoubleValuesSource() views their searcher only through rewrite, but did not touch GroupingSearch. A consumer left behind, not a line added to GroupingSearch: its Sort-as-given code dates from LUCENE-3778 (89b55566d83, 2012-03-22, first released in 4.0.0-alpha), when every consumer, IndexSearcher included, needed a caller-rewritten Sort; GroupingSearch's only rewrite in its whole history is of the groupEndDocs query (LUCENE-8242) [exec: release-level bisection, 7.1.0 consistent and 7.2.0 divergent, including with separately built Sort objects, plus the candidate fix applied to the 7.2.0 GroupingSearch, which passes all 4 history tests; no build at the parent commit] (2017-11-15 (LUCENE-7736 committed on master and branch_7x; author date 2017-03-28); first released in 7.2.0 (tag 2017-12-19). GroupingSearch itself: 2012-03-22 (LUCENE-3778))
- First affected version
- 7.2.0 [exec, -ea]: the within-group order diverges from IndexSearcher#search and a ValueSource group sort throws only in GroupingSearch; the group-order test passes there only because WrappedDoubleValuesSource kept the searcher of an earlier IndexSearcher#search in the shared Sort object (removed by LUCENE-8726 in 7.7.2 and 8.1.0), and with separately built Sort objects (TestGroupingSearchFreshSortProbe) it fails on 7.2.0, 7.7.1, 8.0.0 and 8.1.0. 7.1.0 is the last unaffected release [exec]: both APIs agree there, as on 6.6.6 and 7.0.0, because IndexSearcher did not rewrite Sorts either (it rejects the ValueSource sort itself); GroupingSearch does not exist before 4.0.0-alpha.
- Affected versions
- every release from 7.2.0 to 10.6.0 and main a9779ef3c193 (unreleased 11.0); the released 10.6.0 [exec, 2026-11-04: the one-method repro fails on its jars with -ea and -da]; not fixed upstream (upstream main 20ced71c1e6 has a byte-identical GroupingSearch.java; last upstream change 3afffeef5a8, author and committer date 2026-07-08, #16247, unrelated; checked 2026-10-28). [exec] the finder's JUnit (with API-adapted copies for 9.1-9.12, 8.x/9.0.0 and 6.6.6-7.7.3) fails 3 of 3 on main (-ea and -da), 10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.4, 8.1.0, 7.7.3 and 7.7.2, and 2 of 3 on 8.0.0, 7.7.1 and 7.2.0 (group-order test masked, see first_affected_version); the fresh-Sort probe fails on 7.2.0, 7.7.1, 8.0.0 and 8.1.0 and passes on 7.1.0; the independent reproduction's JUnit fails 5 of 6 on main (-ea and -da), 10.5.2 and 9.12.3; the intended-behaviour check's re-run of the finder's JUnit fails 3 of 3 on main (-ea and -da) and 10.5.2; the cross-model reviewers' own JUnits fail on main (ea57360912ed and a9779ef3c193), 10.5.2 and an API-adapted 9.12.3. [read] the releases between the executed ones, from code identity (IndexSearcher's sort.rewrite(this) unchanged since LUCENE-7736; GroupingSearch never rewrites a Sort). All runs JDK 25 on Linux aarch64.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: exhaustive check
- Upstream and prior reports
- related items only, no report of this defect (searched 2026-10-27 and 2026-10-28, re-checked 2026-11-03, read-only): nobody has reported the exception or the silent wrong order through GroupingSearch, and nothing upstream fixes it or is in flight. Related, other defects: #5619 (LUCENE-4553, open since 2012, dsmiley, 0 comments) 'Don't require users to call Sort.rewrite()', a generic request that covers the fix but names no consumer or symptom and predates IndexSearcher's own Sort rewrite; SOLR-2642 (2011, fixed in Solr 3.4) reported the same loud failure ('Unweighted use of sort' from SortField.getComparator <- AbstractFirstPassGroupingCollector.<init>) from Solr's grouping and fixed it in Solr by weighting the sorts before building the collectors; GroupingSearch (LUCENE-3778, 2012) was written without that step. Context, not reports: #8787 (LUCENE-7736), #4851 (LUCENE-3778), #8668 (LUCENE-7617), #7958 (LUCENE-6900), #8922 (LUCENE-7871 review comment on the caller-rewrites convention), #10491 (LUCENE-9451); #10060 (LUCENE-9017) and SOLR-6203 are unrelated grouping bugs. Searched: 313 queries (scripted prior-report searches over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web, plus own GitHub searches); the changed files of all 410 open apache/lucene PRs (only #16847 and #14996 change GroupingSearch.java, both for group-head bitsets; #16857 changes Sort.java/SortField.java but adds no consumer-side rewrite); the 53 PRs updated since 2026-10-26 (none touches lucene/grouping or QueryValueSource); upstream commit author and committer dates on GroupingSearch.java (latest 3afffeef5a8, 2026-07-08, unrelated); upstream main 20ced71c1e6 (2026-10-27) unchanged at the fix site
- Lucene commit tested
- main
5adff78bb168(2026-11-02): the one-method repro through gradle in a scratch worktree, stock and with the fix, assertions on and off, plus every test of the grouping module with the fix; through the standalone JUnit runner on the stock jars of main (-ea,-da) and of 10.6.0, 10.5.2, 10.0.0, 9.12.3 and 9.1.0 (2026-11-04). Earlier checks ran on maina9779ef3c193andea57360912ed(identicalGroupingSearch.java) and on 6.6.6 to 10.5.2 for the version history; a re-verification one1faeba08bdeand 10.6.0 on 2026-11-02.
- Finding record
lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:44 (@lucene.experimental), :148, :160, :201-202 (field/function path: groupSort and sortWithinGroup passed as given), :223 (searcher.rewrite of groupEndDocs), :228-233 (doc-block path), :286-305 (setGroupSort / setSortWithinGroup javadoc, no pre-rewrite requirement)lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:730 (sort.rewrite(this))lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:34-36 (client rewrite protocol), :768 ('This DoubleValuesSource must be rewritten')lucene/core/src/java/org/apache/lucene/search/SortField.java:113-117 (Type.REWRITEABLE), :526-528 ('SortField needs to be rewritten')lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/QueryValueSource.java:116-123 (no searcher in the context: a fresh default-Similarity IndexSearcher over the same reader)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:730-733; (at 5adff78bb168) Sort.java:88-99; SortField.java:113-117, 555-557; DoubleValuesSource.java:34-36, 741, 768lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:141-238 (at 5adff78bb168) (148, 160, 173, 175, 201-202, 211, 223, 228, 233, 237)lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollector.java:116; (at 5adff78bb168) AllGroupHeadsCollector.java:241; BlockGroupingCollector.java:237lucene/queries/src/java/org/apache/lucene/queries/function/ValueSource.java:74-77, (at 5adff78bb168) 184-185, 198-200, 244-245, 369-384; valuesource/QueryValueSource.java:116-123grep -rnE '[sS]ort[A-Za-z]*\.rewrite\(' lucene/*/src/java at 5adff78bb168 (only IndexSearcher.java:730)
- Who is affected
lucene/grouping/src/test (at 5adff78bb168) (no getSortField, fromQuery or QueryValueSource)
- Why it looks unintended
lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:41-45, (at 5adff78bb168) 115-129, 223, 285-305lucene/grouping/src/test/org/apache/lucene/search/grouping/BaseGroupSelectorTestCase.java:83-121 (at 5adff78bb168) (comment :112-114, assertArrayEquals :118-119)lucene/queries/src/test/org/apache/lucene/queries/function/TestFunctionQuerySort.java:65 (at 5adff78bb168)
- Suggested fix
lucene/core/src/java/org/apache/lucene/search/Sort.java:88-99; (at 5adff78bb168) lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroupsCollector.java:212-214; lucene/queries/src/java/org/apache/lucene/queries/function/valuesource/QueryValueSource.java:116-123
- Prior reports
GitHub API (read-only, 2026-11-04): issues/5619; commits?path=lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java; search/issues updated:>2026-10-27
- History
a Lucene clone tags releases/lucene-solr/7.1.0 and 7.2.0: IndexSearcher.java (sort.rewrite at 7.2.0 line 545 only)GitHub API (read-only, 2026-11-04): commits?path=lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java (latest 3afffeef5a8d); main head f72004515ec2
- TestGroupingSearchSortRewrite.java (one test method):
repro/LFV-816/TestGroupingSearchSortRewrite.java(84 lines)
Made in parallel with this doc from runs on main 5adff78bb168. It walks through the three-document test reduced from the finding: the two documents of the minimal repro in groups g0 and g1, plus a copy of doc 1 in g0. The minimal repro keeps two documents in one group, which changes the BM25 values (0.147 and 0.125 instead of 0.101 and 0.088), not the failure. The PNG is the summary still.
assets/LFV-816/bug.png(517 KB)
