setIncludeMaxScore asks for each group's best score and is on by default. On the doc-block path it is never read: scores are computed only if the group sort or the within-group sort needs them. With two field sorts nothing is scored, so every max score is NaN.
- Two-pass grouping,
new GroupingSearch("field"), groups hits by a field value: a first pass finds the top groups, a second pass collects their documents. - Doc-block grouping,
new GroupingSearch(groupEndQuery), needs each group indexed as one block of adjacent documents with the last one marked; it groups in a single pass. - Both return
TopGroups: for each group a page of documents andmaxScore, andTopGroups.maxScoreover all hits.NaNthere is documented as "scores were not computed".
groupByDocBlock evaluates to needsScores = false, the NaN result next to two-pass grouping, and the fix options. Select the still to open it at full size. Generated with AI, checked against the code.- Index two groups as document blocks; a query
body:ascores their matching documents 0.19659248 and 0.15317328 (group 0) and 0.22094907 (group 1). Search withnew GroupingSearch(groupEndQuery)and sort the groups and the documents inside them by a numeric fieldid. - Leave
setIncludeMaxScoreat its default, true: "include the score of the most relevant document per group". You expect group maxima 0.19659248 and 0.22094907 andTopGroups.maxScore0.22094907. Two-pass grouping,new GroupingSearch("group"), returns exactly that. - Instead doc-block grouping returns
NaNfor both groups and forTopGroups.maxScore, whichTopGroupsdocuments as "scores were not computed".setIncludeMaxScore(true)or(false)makes no difference. - Why: the doc-block path asks for scores only when one of the two sorts needs them,
groupSort.needsScores() || sortWithinGroup.needsScores(), and never reads the flag. Field sorts need no scores, so none are computed and every maximum staysNaN. The two-pass path passes the flag on. - The hits, their order and the groups are right, and nothing fails. With a relevance sort on either side the values are right too. It ships in every release since 8.0.0.
Applications that use doc-block grouping through GroupingSearch, sort both the groups and the documents inside them by fields, and read maxScore. For them it is NaN on every such search, at any index size, since 8.0.0. Rare: no use of block grouping was found in Solr, Elasticsearch or OpenSearch, and nobody has reported it.
Mechanism, terms and numbers517 words
GroupingSearch.setIncludeMaxScore ("Whether to include the score of the most relevant document per group", on by default) is ignored when grouping by doc block (new GroupingSearch(Query groupEndDocs)). groupByDocBlock passes needsScores = groupSort.needsScores() || sortWithinGroup.needsScores() to BlockGroupingCollectorManager and never reads includeMaxScore, so with a field group sort and a field within-group sort the collector runs with ScoreMode.COMPLETE_NO_SCORES and TopGroups.maxScore and every GroupDocs.maxScore are NaN, the value TopGroups documents as "scores were not computed". Two-pass grouping in the same class passes the flag on and reports the real values (0.22094907 overall in the example; groups 0.19659248 and 0.22094907). The hits, their order and the groups are unaffected, nothing throws, and a relevance sort on either side hides it. It dates from LUCENE-8412 (8.0.0), which removed the separate setIncludeScores option that had kept doc-block scoring on by default; the doc-block path never read includeMaxScore in any version. The fix is one line: add includeMaxScore to that expression, the rule the two-pass path already follows.
- Doc-block grouping: each group is indexed with one
IndexWriter.addDocumentscall and a query (groupEndDocs) matches the last document of each block.new GroupingSearch(Query groupEndDocs)(GroupingSearch.java:106) dispatches togroupByDocBlock(:132-133). - Two-pass grouping:
new GroupingSearch(String groupField)(or a function or group selector); its second pass isTopGroupsCollectorManager. setIncludeMaxScore: "Whether to include the score of the most relevant document per group" (GroupingSearch.java:329-338), defaulttrue(:56).TopGroups.maxScore: "Highest score across all hits, orFloat.NaNif scores were not computed" (TopGroups.java:59).
groupByDocBlock(GroupingSearch.java:221-237) buildsnew BlockGroupingCollectorManager<>(groupSort, groupOffset, groupLimit, groupSort.needsScores() || sortWithinGroup.needsScores(), groupEndDocs, ...); the fourth argument (:231) isneedsScores. The fieldincludeMaxScoreis read only at:205(two-pass); its setter (:335-336) writes it.- With two field sorts
needsScoresisfalse.BlockGroupingCollector.scoreMode()then returnsCOMPLETE_NO_SCORES(BlockGroupingCollector.java:506), so no scores are computed. - In
getTopGroups,groupMaxScore = needsScores ? Float.NEGATIVE_INFINITY : Float.NaN(:302) and the maximum is taken onlyif (needsScores)(:307-311): every group keepsNaN, andTopGroups.maxScore, aMath.maxover them (:339-341), isNaNtoo. BlockGroupingCollectorManager.reduce(:148-159) passes the collectors' results toTopGroups.mergeBlockGroups, which keeps theirGroupDocs(TopGroups.java:430-432) and merges the overall maximum fromNaNwithnonNANmax, which staysNaNwhen every input isNaN(:106-110,:404,:413).- The two-pass path passes
includeMaxScoretoTopGroupsCollectorManager(GroupingSearch.java:197-205), andTopGroupsCollectorscores whengetMaxScores || withinGroupSort.needsScores()(TopGroupsCollector.java:128).
request (both sorts by id unless stated) | TopGroups.maxScore | group maxima |
|---|---|---|
doc-block, setIncludeMaxScore(true) or default | NaN | NaN, NaN |
doc-block, setIncludeMaxScore(false) | NaN | NaN, NaN |
two-pass, setIncludeMaxScore(true) | 0.22094907 | 0.19659248, 0.22094907 |
two-pass, setIncludeMaxScore(false) | NaN | NaN, NaN |
| doc-block, relevance within groups | 0.22094907 | 0.19659248, 0.22094907 |
BlockGroupingCollectorManager, needsScores=true | 0.22094907 | 0.19659248, 0.22094907 |
In these runs the hits, their order and the groups are the same on both paths. The minimal repro is one block of one document under MatchAllDocsQuery (score 1.0): NaN instead of 1.0.
Exposure and likelihood228 words
- Who is hit. Code that groups block-indexed documents through
GroupingSearch(Query groupEndDocs)with a field group sort and a field within-group sort (for example products grouped by parent and sorted by price) and readsTopGroups.maxScoreorGroupDocs.maxScore, to show a relevance figure or normalise scores per group. - Default or opt-in. Doc-block grouping and field sorts are opt-in (both sorts default to relevance,
GroupingSearch.java:51-52); the ignored option is at its default,true(set in code at:56; the setter's javadoc does not state the default).GroupingSearchis@lucene.experimental(:44). - Effect. A requested statistic is missing (
NaN), visibly, not a plausible wrong number. Nothing throws. Code that compares, thresholds or divides by it getsNaNarithmetic. - Scale. None needed: one document is enough. It needs at least one returned group; with no matching group there is no score to report, so
NaNis right there anyway. - Workaround. Build
BlockGroupingCollectorManagerdirectly withneedsScores=true(executed: real values). It is not a drop-in replacement: the caller builds the end-of-blockWeightitself (BlockGroupingCollectorManager.java:51-53,:91) and, with intra-segment concurrency, must keep each document block within one search slice (:36-43). - How likely: rare. The hunt's downstream checks found no use of
BlockGroupingCollectorin Solr, Elasticsearch or OpenSearch, and their issue and PR trackers have nothing on it; private applications are not visible to such searches.
doc-block GroupingSearch ignores setIncludeMaxScore(true) when the group sort and the within-group sort are both field sorts, so TopGroups.maxScore is NaN.
/*
* 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.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StringField;
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.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestBlockGroupingIncludeMaxScore extends LuceneTestCase {
public void testMaxScoreWithFieldSorts() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document(); // a block of one document
doc.add(new NumericDocValuesField("id", 0));
doc.add(new StringField("groupEnd", "x", Field.Store.NO));
w.addDocument(doc);
}
try (DirectoryReader r = DirectoryReader.open(dir)) {
Sort byId = new Sort(new SortField("id", SortField.Type.INT));
GroupingSearch grouping = new GroupingSearch(new TermQuery(new Term("groupEnd", "x")));
grouping.setGroupSort(byId).setSortWithinGroup(byId).setIncludeMaxScore(true);
TopGroups<?> groups = grouping.search(new IndexSearcher(r), new MatchAllDocsQuery(), 0, 10);
// expected the matching document's score 1.0 (setIncludeMaxScore(true)), actual NaN
assertEquals(1f, groups.maxScore, 0f);
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-881/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/grouping/build/libs/lucene-grouping-$V.jar
CP=$CP:$LUCENE/lucene/queries/build/libs/lucene-queries-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.search.grouping.TestBlockGroupingIncludeMaxScore
- Expected vs actual
- Expected: the matching document's score 1.0 (
MatchAllDocsQuery, one one-document block); actual: NaN (expected:<1.0> but was:<NaN>), with-eaand-da.
More files, environment, full output34 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<1.0> but was:<NaN>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:577)
org.junit.Assert.assertEquals(Assert.java:701)
org.apache.lucene.search.grouping.TestBlockGroupingIncludeMaxScore.testMaxScoreWithFieldSorts(TestBlockGroupingIncludeMaxScore.java:52)- The
setIncludeMaxScorejavadoc names no exception, while its neighbourssetAllGroupsandsetAllGroupHeadsdocument theirs for doc-block grouping. - The package docs' doc-block
GroupingSearchexample callssetIncludeMaxScore(needsScores), and two-pass grouping in the same class honours the flag. - LUCENE-8405 kept explicit grouping max scores when requested; LUCENE-8412, which dropped them here, was a
TopFieldCollectorcleanup whose records never mentionGroupingSearchor doc-block grouping.
The full argument283 words
- Contract.
GroupingSearch.java:329-338: "Whether to include the score of the most relevant document per group", no restriction.setAllGroups: "When grouping by doc block ... this option doesn't have any influence" (:344-345);setAllGroupHeads: "This feature isn't enabled when grouping by doc block" (:371). - Package docs.
package-info.java:143-151: "Or alternatively use theGroupingSearchconvenience utility" withnew GroupingSearch(groupEndDocs)andgroupingSearch.setIncludeMaxScore(needsScores). (As written it sets no within-group sort, so its default relevance sort scores anyway: it shows the mismatch, it is not itself broken.) - The collector's own javadoc.
BlockGroupingCollector.java:205-207:needsScoresis true "because relevance is included in the withinGroupSort or because you plan to pass true for either getSscores or getMaxScores" (2011 text; those parameters no longer exist). - Lucene 8 intent. LUCENE-8405 (#9452, 5 days before the cause) removed
TopDocs.maxScorebut added an explicit group maximum toBlockGroupingCollectorand aMaxScoreCollectorto the two-pass collector; its MIGRATE text limits the removal to "IndexSearcher and TopFieldCollector" (grouping max scores survived the change; no design commitment is stated). LUCENE-8412 (#9459), the cause, is aboutTopFieldCollector'strackDocScores; its CHANGES entry (lucene/CHANGES.txt:6676) and MIGRATE entry mention neitherGroupingSearchnor the removal ofsetIncludeScores. Its discussion touches grouping only once: Steven Rowe reported a SolrTestDistributedGroupingfailure (.grouped[a_i1].doclist.maxScore:1.0!=null) bisected to55bfadb, and the follow-upd010cdd87eechanged only Solr'sTopGroupsFieldCommand. Nothing there mentionsGroupingSearch, doc-block grouping orincludeMaxScore. - Sibling path. Two-pass grouping passes the flag (
GroupingSearch.java:205,TopGroupsCollector.java:128). - Why tests missed it. No test calls
setIncludeMaxScore;TestGrouping.java:1632-1633has the per-group maxScore check as a commented-out TODO since 2011;TestBlockGroupingandTestGroupingSearchuse a relevance sort on at least one side.
The devil's-advocate check236 words
The strongest case for "intended" (the refute reviewer from another model family, OpenAI GPT-5.6, the intended-behaviour devil's advocate and the review; all rate it moderate): Lucene 8 stopped computing scores for field-sorted searches, and MIGRATE suggests a separate query for the top score; LUCENE-8412 switched TestGrouping's own block collector to the same sort-only expression, so the change looks deliberate; includeMaxScore never controlled the doc-block path (on 7.7.3, setIncludeScores(false) with setIncludeMaxScore(true) also gives NaN, executed); NaN is a documented value; the 2026 package-info line came from a mechanical rename in #16508; and the BlockGroupingCollectorManager javadoc defines needsScores by the sorts only.
Why it fails: LUCENE-8405 kept explicit grouping max scores when scores were requested, and the two-pass path still honours the flag; the TestGrouping edit changed no assertion, and the records inspected (the commits, CHANGES, MIGRATE, code comments and the #9459 discussion) document no decision to drop doc-block max scores; an option of the same class, documented and on by default, is ignored on one path while the class documents such exceptions for its other options; and NaN means "not computed" when the caller asked for them. The TestGrouping edit is some evidence of intent, which is why the case is moderate. The 7.7.3 point weakens the word "regression" (do not call it a regression of setIncludeMaxScore itself), not the bug. Verdict: a defect; upstream may still prefer a documentation fix (see Suggested fix).
Recommended A. Honour includeMaxScore on the doc-block path (recommended, validated)
Pass the flag through: in GroupingSearch.groupByDocBlock, use groupSort.needsScores() || sortWithinGroup.needsScores() || includeMaxScore, the rule two-pass grouping already follows. One line, validated with the repro and the grouping module's tests. Cost: default doc-block searches with field sorts now compute scores; setIncludeMaxScore(false) turns that off.
--- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
+++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
@@ -228,7 +228,7 @@
groupSort,
groupOffset,
groupLimit,
- groupSort.needsScores() || sortWithinGroup.needsScores(),
+ groupSort.needsScores() || sortWithinGroup.needsScores() || includeMaxScore,
groupEndDocs,
sortWithinGroup,
groupDocsOffset,All options and trade-offs3 options · 407 words
Option A was built and validated; B and C were not built. The adjudication's confidence is medium only because upstream may prefer B.
Add includeMaxScore to the needsScores argument of BlockGroupingCollectorManager in groupByDocBlock (GroupingSearch.java:231), the rule the two-pass path follows (TopGroupsCollector.java:128). Both paths then behave the same, and setIncludeMaxScore(false) starts to mean something on the doc-block path too.
--- a/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
+++ b/lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java
@@ -228,7 +228,7 @@
groupSort,
groupOffset,
groupLimit,
- groupSort.needsScores() || sortWithinGroup.needsScores(),
+ groupSort.needsScores() || sortWithinGroup.needsScores() || includeMaxScore,
groupEndDocs,
sortWithinGroup,
groupDocsOffset,Trade-offs
- Cost. A doc-block search with field-only sorts that leaves the flag at its default now runs with
ScoreMode.COMPLETEinstead ofCOMPLETE_NO_SCORESand computes scores; two-pass grouping already pays this by default. Not measured. Callers who do not need max scores get the old behaviour withsetIncludeMaxScore(false); worth a line in CHANGES. - Compatible with the separate sort-rewrite change to the same method (LFV-816).
What the checks validated
- In a scratch worktree of
645d4fe14a20with gradle and JDK 25.0.3: stock + the repro test fails (1 of 1) with assertions on and off; with the fix the repro passes, the whole:lucene:grouping:testpasses (15 suites, 57 tests, 0 failed, with-Ptests.asserts=trueandfalse), and:lucene:grouping:check -x test(google-java-format, ecjLint, forbiddenApis) is clean. - With the fixed class shadowed in the standalone runner: the finder's 2 tests (incl. the two-pass control) and the independent reproduction's 5 tests (incl. its 3 controls) pass; earlier, on
a9779ef3c193, 51 upstream grouping tests in 11 classes passed.
Say in the setIncludeMaxScore javadoc that it has no influence when grouping by doc block (as setAllGroups and setAllGroupHeads do for theirs), drop or correct setIncludeMaxScore(needsScores) in the doc-block example (package-info.java:149), point to BlockGroupingCollectorManager with needsScores=true, and fix the stale BlockGroupingCollector constructor javadoc (:205-207).
Trade-offs
No runtime cost and no behaviour change, but GroupingSearch then still offers no way to get doc-block max scores with field-only sorts, and its default-on flag keeps meaning different things on its two paths.
Score on the doc-block path only when setIncludeMaxScore(true) was called explicitly (a tri-state field).
Trade-offs
Avoids A's cost change for callers who never touched the flag, but makes the default of one option mean different things on the two paths, the inconsistency the issue is about.
A code search found no use of block grouping in Solr, Elasticsearch or OpenSearch; applications that use GroupingSearch doc-block grouping directly are the ones affected.
A hunt for searches that run without scores although a consumer wants them. The finder built a 561-row bytecode inventory of score-related call sites in the stock jars, 72 of them places that hand a no-score mode to a consumer; two of those rows led to the code that ignores the flag. A JUnit with a two-pass control confirmed it.
The method in full146 words
Method: semi-formal (the finder's tier; the decisive step was code reading of two rows of a mechanical inventory).
- Inventory. With the JDK 25 ClassFile API the finder scanned 20 non-test jars for score-related call sites: 561 rows of six kinds, 72 of them sites that pass
ScoreMode.COMPLETE_NO_SCORESorTOP_DOCSto a consumer (the others are score calls, weight creations, null scorers, scorer-derived modes and conditional modes). - Triage. Two of those rows,
GroupingSearch.groupByDocBlock:224(theCOMPLETE_NO_SCORESweight for the end-of-block query) andBlockGroupingCollector.scoreMode:506(COMPLETE_NO_SCORESwhenneedsScoresis false), led to readinggroupByDocBlock, whoseneedsScoresargument (:231) ignoresincludeMaxScore, unlike the two-pass path. The unit's dynamic sweep could not see it, because no score is ever requested there. - Repro. A public-API JUnit with two blocks, field sorts, a plain-search oracle and a two-pass control (found 2026-10-28); this doc's one-document test is a further reduction.
No prior report found (searched 2026-10-28, rechecked 2026-10-30): 51 tool queries over GitHub, commits, downstream trackers, four mailing lists, JIRA and the web, the finder's 45, manual searches for the exact names, and the files of all 410 open PRs. Closest: LUCENE-8412 itself, whose only grouping mention is a Solr test failure fixed in Solr.
Searches4 searches
Related, not reports of this bug: #9459 / LUCENE-8412 (the change that produced it; its discussion touches grouping only through a Solr distributed-grouping maxScore test failure, fixed in Solr by d010cdd87ee); #16508 (renamed the package-info example to setIncludeMaxScore(needsScores), no discussion); #15936 (BlockGroupingCollectorManager, kept the expression); #15942 (a block max-score performance change, different code); #15872/#15873 (two-pass NaN maxScore for a group without hits, fixed); #10039 / LUCENE-8996 (shard-merge NaN maxScore, fixed 2019); open #16569 (two-pass collector only) and #14996 (touches groupByFieldOrFunction only). Gaps: issue search does not index PR diffs (open PRs were checked by file list); list and JIRA search are word-based; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene); downstream repositories | 2026-10-28 | a scripted prior-report search (16 GitHub, 3 commit, 2 downstream queries) and the finder's 45-query run; manual searches for GroupingSearch, BlockGroupingCollector, includeMaxScore, setIncludeMaxScore, groupEndDocs, "doc block" grouping, TopGroups.maxScore, LUCENE-8412; the changed files of all 410 open PRs; Solr, Elasticsearch, OpenSearch and lucene-solr | no report; the related items above; 0 downstream hits |
| dev@, java-user@, issues@ and commits@ lists; JIRA archive; web | 2026-10-28 | 20 list, 4 JIRA and 6 web queries through the tool, plus direct list and JIRA searches | nothing relevant (two Stack Overflow questions about NaN scores in plain sorted search and in Solr) |
| GitHub (recheck for this doc) | 2026-10-30 | upstream heads and file history on main and branch_10_6; searches for includeMaxScore, setIncludeMaxScore, "GroupingSearch maxScore", "BlockGroupingCollector NaN", groupByDocBlock; grouping items updated since 2026-10-28 | still unfixed; nothing new |
| GitHub, Maven Central (independent fact check) | 2026-10-30 | read-only: upstream heads and the 10.6.0 tag, GroupingSearch.java at the tag and on branch_10_6, the #9459 comments, searches for setIncludeMaxScore, includeMaxScore and groupByDocBlock, the files of #14996 and #16569 | no report; 10.6.0 released with the expression unchanged |
Introduced by LUCENE-8412 (55bfadbce11, 2018-07-23), first released in 8.0.0: it removed setIncludeScores (default true), which had kept doc-block scoring on, and put in the sort-only expression. Fails on 8.0.0 to 10.6.0 and main; 7.x passes with default settings. Commit-level builds bracket it.
Versions tested7 rows
Introduced by 55bfadbce11, LUCENE-8412 "TopFieldCollector no longer takes a trackDocScores option" (Adrien Grand, author date 2018-07-23, master; a JIRA patch with +1 from two committers, migrated as #9459). It removed GroupingSearch.includeScores / setIncludeScores and replaced the doc-block path's includeScores argument with groupSort.needsScores() || sortWithinGroup.needsScores(), while passing includeMaxScore to the two-pass collector; git log -S finds it as the only commit that introduces the expression. Later reformatted (2cbf261032d) and moved into BlockGroupingCollectorManager's arguments by #15936 (10.5.0), unchanged in meaning.
Nuance. No version read includeMaxScore on the doc-block path: from 4.0.0-alpha to 7.7.3 its max scores came from includeScores, default true, and on 7.7.3 setIncludeScores(false) with setIncludeMaxScore(true) gives NaN too (executed). LUCENE-8412 made that gap the default behaviour.
Commit level. A javac build of the parent 34686c00dde passes (-ea) and of 55bfadbce11 fails (-ea, -da); swapping only that argument flips the result both ways (7.7.3 with the new expression fails, 8.0.0 with the old value passes; -ea). Upstream: 10.6.0 was tagged on 2026-10-29 at 4ae946e5a9b and is affected; main and branch_10_6 unchanged on 2026-10-30.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20, a9779ef3c193 | affected | NaN (this doc's test: gradle and standalone, -ea and -da; the finder's, the independent reproduction's and the intended-behaviour check's tests) |
10.6.0 (release commit 4ae946e5a9b) | affected | NaN (this doc's test on the release jars, -ea and -da; the finder's test on branch_10_6 at the same commit) |
| 10.5.2, 9.12.3 | affected | NaN (also this doc's test) |
| 10.0.0, 9.0.0, 8.11.4, 8.0.0 | affected | NaN (-ea; 8.0.0 also -da) |
55bfadbce11 vs its parent 34686c00dde | bisect | the commit fails, the parent passes |
| 7.7.3, 7.5.0, 7.4.0, 7.0.0 | defaults only | real max scores with default settings; NaN on 7.7.3 with setIncludeScores(false) |
| fix A on main | not affected | 1.0 (the repro passes) |
An independent reproducer, an intended-behaviour devil's advocate and an adjudication confirm it (adjudication: medium confidence, because upstream may prefer a doc fix); reviews by another model family (OpenAI GPT-5.6) agree on the root cause but could only read code. This doc re-ran a one-method repro on main and releases and validated the fix.
Checks and verdicts334 words
No reviewer from the other model family executed Java for this bug; the executed evidence rests on the finder, the independent reproduction, the intended-behaviour check, the adjudication, the follow-up re-runs, the repro minimisation and this doc.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | JUnit with a two-pass control fails on main (-ea, -da), 10.5.2, 9.12.3, 8.0.0, branch_10_6; passes on 7.7.3; fix keeps 51 upstream grouping tests green. |
| review (other model family) | real, high (reading only) | OpenAI GPT-5.6; mechanism agrees; intended case moderate; corrected the 7.7.3 story (the pass there came from includeScores). |
| Independent reproducer | real | Own 5-test JUnit fails 2 of 5 on main, 10.5.2, 9.12.3, 8.0.0; 3 controls pass; own one-line fix passes. |
| Refute lens (other model family) | not real, low, procedural only | OpenAI GPT-5.6 could not execute anything on this host, so "not reproduced" holds by construction; its reading agrees with the root cause; a separate run re-ran the finder's test for it (fails). |
| Intended-behaviour devil's advocate | real, moderate | Built and rejected the "accepted in Lucene 8" case; probes show the flag has no effect on the doc-block path. |
| adjudication | confirmed, medium | Re-ran the independent reproduction's JUnit with a fresh seed (-ea, -da): 5 run, 2 failed. |
| Follow-up re-run and credibility audit | keep | The finder's test fails 1 of 2 (-ea, -da) and passes with the fix (2026-10-28). |
| this doc | confirmed on 645d4fe14a20 | The one-method repro fails through gradle and the standalone runner (assertions on and off) and on 10.5.2 and 9.12.3; fix A validated. |
| Independent fact and diagram checks | substantively correct; all errors fixed | An independent fact check re-ran the doc's gradle command, fix A and the repro on the 10.6.0 release jars (fails); a fact check by another model family (OpenAI GPT-5.6, reading only) found the brief factually sound. Their errors (10.6.0 released, wording and precision fixes, two diagram details) are fixed in this version. |
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 661 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,327 words
- Registry title
- GroupingSearch ignores setIncludeMaxScore for doc-block grouping: with field-only group and within-group sorts, TopGroups.maxScore and every GroupDocs.maxScore are NaN, while two-pass grouping with the same settings reports the real max scores (since 8.0.0)
- Area
- grouping / GroupingSearch doc-block path (BlockGroupingCollectorManager score mode)
- Priority
- Low, rank 569: Doc-block GroupingSearch with field sorts ignores setIncludeMaxScore and reports the documented NaN, which is visible, has a workaround and leaves hits unchanged.
- Impact class
- minor
- Impact note
- Applications that group block-indexed documents through GroupingSearch(Query groupEndDocs) (doc-block grouping, an opt-in @lucene.experimental path that needs documents indexed as contiguous blocks) with a group sort and a within-group sort that are both field sorts get TopGroups.maxScore and every GroupDocs.maxScore = NaN ('scores were not computed') on every such search, in every release from 8.0.0 to 10.6.0 and on main, at any index size and with the default includeMaxScore=true; hits, group order, group membership and per-hit data are unchanged, nothing throws, two-pass grouping with the same settings reports the real values, and building BlockGroupingCollectorManager directly with needsScores=true is a workaround; classed minor, not silent wrong results, because NaN is the documented 'not computed' marker, so the defect is a documented option that is ignored rather than a wrong number.
- Summary
- You group documents indexed as blocks with new GroupingSearch(groupEndDocs), sort the groups and the documents inside each group by fields, and leave setIncludeMaxScore at its default true (or set it), expecting TopGroups.maxScore and each GroupDocs.maxScore to hold the best score, as the setIncludeMaxScore javadoc ('Whether to include the score of the most relevant document per group') says and as two-pass grouping with the same settings returns (0.22094907 in the repro). Instead both are NaN, because GroupingSearch.groupByDocBlock asks BlockGroupingCollectorManager to compute scores only when one of the two sorts needs them and never reads includeMaxScore. Adding includeMaxScore to that needsScores argument, the rule the two-pass path already follows, fixes it.
- Introduced
- 55bfadbce115a825a75686fe0bfe71406bc3ee44 (master, LUCENE-8412 'Remove trackDocScores from TopFieldCollector factory methods', patch by jpountz with +1 from romseygeek and mikemccand, migrated as apache/lucene#9459; JIRA patch, no GitHub PR): it removed GroupingSearch.includeScores / setIncludeScores (default true) and replaced the block path's includeScores argument with groupSort.needsScores() || sortWithinGroup.needsScores(), while passing includeMaxScore only to the two-pass TopGroupsCollector. includeMaxScore was never read on the block path in any version, so before 8.0.0 block max scores existed only because includeScores defaulted to true [exec: javac builds of the parent 34686c00dde pass and of 55bfadbce11 fail, -ea and -da; a one-line swap flips the result both ways (7.7.3 with the post-commit expression fails, 8.0.0 with the pre-commit value passes); read: git log -S and git log -L on GroupingSearch.java in the history clone; the expression was later kept by the LUCENE-9570 reformat 2cbf261032d and by #15936 (d27d080d101 / 2e00e95ace6, BlockGroupingCollectorManager, 10.5.0)] (2018-07-23 (author date 09:05:02 +0200, commit date 09:06:49 +0200, master); first released in 8.0.0)
- First affected version
- 8.0.0 [exec] with default settings; 7.7.3 is the last unaffected release with default settings (and 7.4.0 the last release before the commit date) [exec: 7.0.0, 7.4.0, 7.5.0, 7.7.3 pass]; but on 7.7.3 too setIncludeScores(false) + setIncludeMaxScore(true) gives NaN [exec], because includeMaxScore was never wired into the block path; GroupingSearch itself exists since 4.0.0-alpha (LUCENE-3778, 89b55566d83)
- Affected versions
- every release from 8.0.0 to 10.5.2, the branch_10_6 head 4ae946e5a9b (10.6.0, released 2026-10-29) and main a9779ef3c193 (11.0.0-SNAPSHOT). [exec] the finder's JUnit (testBlockGrouping fails with 'TopGroups.maxScore expected:<0.22094907> but was:<NaN>', the two-pass control passes) fails 1 of 2 on 8.0.0 (-ea and -da), 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 (-ea and -da), branch_10_6 4ae946e5a9b and main (-ea and -da), and on a source build of 55bfadbce11; it passes on 7.0.0, 7.4.0, 7.5.0, 7.7.3 (-ea and -da) and on the parent 34686c00dde; the independent reproduction's own JUnit fails 2 of 5 (3 controls pass) on main (-ea and -da), 10.5.2 (-ea and -da), 9.12.3 (-ea and -da) and 8.0.0, and passes 5 of 5 on 7.7.3; the intended-behaviour check and the adjudication re-ran on main (-ea and -da). [read] the releases between the executed ones, from code identity (the expression is unchanged since 55bfadbce11). Not fixed upstream: upstream main c5ab3e57b9b (2026-10-28) is unchanged at the fix site; the newest upstream commit on GroupingSearch.java is 3afffeef5a8 (author date 2026-07-08), contained in a9779ef. All runs JDK 25.0.3 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 could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: code reading
- Upstream and prior reports
- none found (the prior-report record verdict new, searched 2026-10-28, refreshed 2026-10-28, read-only): no issue, PR, review comment, JIRA entry, mailing-list message or web page reports GroupingSearch ignoring includeMaxScore on the doc-block path or doc-block max scores being NaN with field sorts. Searched: scripted prior-report searches (51 queries, 509 distinct hits, 0 high, 8 medium; plus the finder's 45 queries) over apache/lucene issues, PRs and commits, downstream repositories (Solr, Elasticsearch and OpenSearch issues and PRs: 0 hits for BlockGroupingCollector and for GroupingSearch maxScore; Solr and lucene-solr: 0 for includeMaxScore), the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web; manual GitHub searches for GroupingSearch, BlockGroupingCollector, includeMaxScore, setIncludeMaxScore, groupEndDocs, 'doc block' grouping, TopGroups.maxScore and LUCENE-8412; the changed files of all 410 open apache/lucene PRs (only #14996 touches GroupingSearch.java, in groupByFieldOrFunction); PRs updated since 2026-10-27 (33, none on grouping); upstream commit author dates (newest on GroupingSearch.java 3afffeef5a8, 2026-07-08; newest on lucene/grouping 3e4a7a02ce0, 2026-10-07). Context, not reports: #9459 (LUCENE-8412, the change that produced the behaviour; its discussion never mentions grouping max scores), #15936 (BlockGroupingCollectorManager, kept the expression), #16508 (renamed the package-info doc-block example to setIncludeMaxScore(needsScores)), #15942 (block max-score performance change), #15872/#15873 and #10039/LUCENE-8996 (other grouping NaN maxScore bugs with different mechanisms), #16569 (open, two-pass TopGroupsCollector). Not fixed upstream (upstream main c5ab3e57b9b unchanged at the fix site).
- Lucene commit tested
- main
645d4fe14a20(runs of 2026-10-30) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the 9.12.3 and 10.5.2 release jars. The fix was validated in the scratch worktree: the repro, the whole:lucene:grouping:testand:lucene:grouping:check -x test. The independent fact check re-ran the gradle command and the fix on645d4fe14a20and ran the repro on the 10.6.0 release jars (release commit4ae946e5a9b; fails, assertions on and off). Earlier hunt runs: maina9779ef3c193(the grouping module is identical), branch_10_6 at4ae946e5a9b, the finder's test on releases from 7.0.0 to 10.5.2, and javac builds of the introducing commit55bfadbce11and its parent.
- Finding record
lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:44 (@lucene.experimental), :56 (includeMaxScore = true), :197-205 (two-pass path passes includeMaxScore to TopGroupsCollectorManager), :221-237 (groupByDocBlock: needsScores = groupSort.needsScores() || sortWithinGroup.needsScores()), :329-338 (setIncludeMaxScore javadoc), :345 and :371 (setAllGroups / setAllGroupHeads document their doc-block exceptions)lucene/grouping/src/java/org/apache/lucene/search/grouping/BlockGroupingCollector.java:302 (groupMaxScore = needsScores ? -Inf : NaN), :505-507 (scoreMode COMPLETE_NO_SCORES when !needsScores)lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroups.java:58-59 ('Highest score across all hits, or Float.NaN if scores were not computed')lucene/grouping/src/java/org/apache/lucene/search/grouping/package-info.java:144-152 (doc-block GroupingSearch example with setIncludeMaxScore(needsScores))lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java:1632-1633 (block maxScore assertion commented out as a TODO)
- The finding in 30 seconds
lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:44, (at 645d4fe14a20) :51-52, :56, :101-107, :132-133, :197-205, :221-237 (:231), :329-338lucene/grouping/src/java/org/apache/lucene/search/grouping/BlockGroupingCollector.java:274, (at 645d4fe14a20) :302, :307-311, :339-341, :506; BlockGroupingCollectorManager.java:89-90, :142-143, :148-159lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroupsCollector.java:128; (at 645d4fe14a20) TopGroups.java:59, :106-110, :404, :413, :430-432
- Who is affected
lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:44, (at 645d4fe14a20) :51-52, :56, :329-338lucene/grouping/src/java/org/apache/lucene/search/grouping/BlockGroupingCollectorManager.java:36-43, (at 645d4fe14a20) :51-53, :91
- Why it looks unintended
lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:205, (at 645d4fe14a20) :329-338, :344-345, :371; package-info.java:143-151; BlockGroupingCollector.java:205-207lucene/CHANGES.txt:6676; (at 645d4fe14a20) lucene/grouping/src/test/org/apache/lucene/search/grouping/TestGrouping.java:1632-1633
- Suggested fix
lucene/grouping/src/java/org/apache/lucene/search/grouping/TopGroupsCollector.java:128; (at 645d4fe14a20) GroupingSearch.java:231; package-info.java:149; BlockGroupingCollector.java:205-207, :506
- TestBlockGroupingIncludeMaxScore.java (JUnit test):
repro/LFV-881/src/org/apache/lucene/search/grouping/TestBlockGroupingIncludeMaxScore.java(56 lines)
A diagram, made in parallel with this doc from an executed probe on main.
assets/LFV-881/bug.png(661 KB)
