All findings · Lucene finding · LFV-881 · block-groupingsearch-ignores-include-max-score

GroupingSearch ignores setIncludeMaxScore when grouping by doc block: with field sorts every maxScore is NaN (since 8.0.0)

With doc-block grouping (GroupingSearch(groupEndQuery)) and field sorts for groups and documents, every maxScore is NaN, although setIncludeMaxScore is on by default. Two-pass grouping with the same settings reports the real scores (0.22094907 in the example); hits are unaffected, and it has been so since 8.0.0.

Summary

Priority
Low, rank 569Doc-block GroupingSearch with field sorts ignores setIncludeMaxScore and reports the documented NaN, which is visible, has a workaround and leaves hits unchanged.
Impact
minor
Since
8.0.0 (LUCENE-8412, 2018)introduced 2018-07-23 · about 8 years
Affected
8.0.0 to 10.6.0 and main; 7.x has the values with default settings
Status
documented
Found by
a bytecode inventory of no-score search sites, then code reading

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

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.

Background: two ways to group

  1. 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.
  2. 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.
  3. Both return TopGroups: for each group a page of documents and maxScore, and TopGroups.maxScore over all hits. NaN there is documented as "scores were not computed".
Diagram of the bug: two groups indexed as document blocks, query body:a with scores 0.19659248 and 0.15317328 in group 0 and 0.22094907 in group 1, both sorts by the field id. Expected group max scores 0.19659248 and 0.22094907; doc-block GroupingSearch reports NaN for both and for TopGroups.maxScore, because groupByDocBlock passes needsScores = groupSort.needsScores() || sortWithinGroup.needsScores(), which is false, and never reads includeMaxScore. Two-pass grouping with the same settings reports the real values.
The two-block example: the scores a plain search gives, how the one line in 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.

The finding, step by step

  1. Index two groups as document blocks; a query body:a scores their matching documents 0.19659248 and 0.15317328 (group 0) and 0.22094907 (group 1). Search with new GroupingSearch(groupEndQuery) and sort the groups and the documents inside them by a numeric field id.
  2. Leave setIncludeMaxScore at its default, true: "include the score of the most relevant document per group". You expect group maxima 0.19659248 and 0.22094907 and TopGroups.maxScore 0.22094907. Two-pass grouping, new GroupingSearch("group"), returns exactly that.
  3. Instead doc-block grouping returns NaN for both groups and for TopGroups.maxScore, which TopGroups documents as "scores were not computed". setIncludeMaxScore(true) or (false) makes no difference.
  4. 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 stays NaN. The two-pass path passes the flag on.
  5. 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.

Who is affected

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.

Terms

  • Doc-block grouping: each group is indexed with one IndexWriter.addDocuments call and a query (groupEndDocs) matches the last document of each block. new GroupingSearch(Query groupEndDocs) (GroupingSearch.java:106) dispatches to groupByDocBlock (:132-133).
  • Two-pass grouping: new GroupingSearch(String groupField) (or a function or group selector); its second pass is TopGroupsCollectorManager.
  • setIncludeMaxScore: "Whether to include the score of the most relevant document per group" (GroupingSearch.java:329-338), default true (:56). TopGroups.maxScore: "Highest score across all hits, or Float.NaN if scores were not computed" (TopGroups.java:59).

Mechanism (main 645d4fe14a20)

  1. groupByDocBlock (GroupingSearch.java:221-237) builds new BlockGroupingCollectorManager<>(groupSort, groupOffset, groupLimit, groupSort.needsScores() || sortWithinGroup.needsScores(), groupEndDocs, ...); the fourth argument (:231) is needsScores. The field includeMaxScore is read only at :205 (two-pass); its setter (:335-336) writes it.
  2. With two field sorts needsScores is false. BlockGroupingCollector.scoreMode() then returns COMPLETE_NO_SCORES (BlockGroupingCollector.java:506), so no scores are computed.
  3. In getTopGroups, groupMaxScore = needsScores ? Float.NEGATIVE_INFINITY : Float.NaN (:302) and the maximum is taken only if (needsScores) (:307-311): every group keeps NaN, and TopGroups.maxScore, a Math.max over them (:339-341), is NaN too.
  4. BlockGroupingCollectorManager.reduce (:148-159) passes the collectors' results to TopGroups.mergeBlockGroups, which keeps their GroupDocs (TopGroups.java:430-432) and merges the overall maximum from NaN with nonNANmax, which stays NaN when every input is NaN (:106-110, :404, :413).
  5. The two-pass path passes includeMaxScore to TopGroupsCollectorManager (GroupingSearch.java:197-205), and TopGroupsCollector scores when getMaxScores || withinGroupSort.needsScores() (TopGroupsCollector.java:128).

The numbers (executed on main, assertions on and off)

request (both sorts by id unless stated)TopGroups.maxScoregroup maxima
doc-block, setIncludeMaxScore(true) or defaultNaNNaN, NaN
doc-block, setIncludeMaxScore(false)NaNNaN, NaN
two-pass, setIncludeMaxScore(true)0.220949070.19659248, 0.22094907
two-pass, setIncludeMaxScore(false)NaNNaN, NaN
doc-block, relevance within groups0.220949070.19659248, 0.22094907
BlockGroupingCollectorManager, needsScores=true0.220949070.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 reads TopGroups.maxScore or GroupDocs.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). GroupingSearch is @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 gets NaN arithmetic.
  • 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 NaN is right there anyway.
  • Workaround. Build BlockGroupingCollectorManager directly with needsScores=true (executed: real values). It is not a drop-in replacement: the caller builds the end-of-block Weight itself (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 BlockGroupingCollector in Solr, Elasticsearch or OpenSearch, and their issue and PR trackers have nothing on it; private applications are not visible to such searches.

Minimal repro

doc-block GroupingSearch ignores setIncludeMaxScore(true) when the group sort and the within-group sort are both field sorts, so TopGroups.maxScore is NaN.

TestBlockGroupingIncludeMaxScore.java (JUnit test)56 lines
/*
 * 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);
      }
    }
  }
}
Command
# 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 -ea and -da.

All files of this repro: repro/LFV-881/

More files, environment, full output34 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# 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)

Why it looks unintended

  • The setIncludeMaxScore javadoc names no exception, while its neighbours setAllGroups and setAllGroupHeads document theirs for doc-block grouping.
  • The package docs' doc-block GroupingSearch example calls setIncludeMaxScore(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 TopFieldCollector cleanup whose records never mention GroupingSearch or 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 the GroupingSearch convenience utility" with new GroupingSearch(groupEndDocs) and groupingSearch.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: needsScores is 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.maxScore but added an explicit group maximum to BlockGroupingCollector and a MaxScoreCollector to 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 about TopFieldCollector's trackDocScores; its CHANGES entry (lucene/CHANGES.txt:6676) and MIGRATE entry mention neither GroupingSearch nor the removal of setIncludeScores. Its discussion touches grouping only once: Steven Rowe reported a Solr TestDistributedGrouping failure (.grouped[a_i1].doclist.maxScore:1.0!=null) bisected to 55bfadb, and the follow-up d010cdd87ee changed only Solr's TopGroupsFieldCommand. Nothing there mentions GroupingSearch, doc-block grouping or includeMaxScore.
  • 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-1633 has the per-group maxScore check as a commented-out TODO since 2011; TestBlockGrouping and TestGroupingSearch use 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).

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.

A. Honour includeMaxScore on the doc-block path (recommended, validated)

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.COMPLETE instead of COMPLETE_NO_SCORES and computes scores; two-pass grouping already pays this by default. Not measured. Callers who do not need max scores get the old behaviour with setIncludeMaxScore(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 645d4fe14a20 with 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:test passes (15 suites, 57 tests, 0 failed, with -Ptests.asserts=true and false), 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.

B. Document the limitation (not built)

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.

C. Wire the flag but keep today's default cost (not built, not recommended)

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.

Downstream note

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.

How it was found

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_SCORES or TOP_DOCS to 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 (the COMPLETE_NO_SCORES weight for the end-of-block query) and BlockGroupingCollector.scoreMode:506 (COMPLETE_NO_SCORES when needsScores is false), led to reading groupByDocBlock, whose needsScores argument (:231) ignores includeMaxScore, 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.

Prior reports, history and checks

Prior reports

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.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene); downstream repositories2026-10-28a 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-solrno report; the related items above; 0 downstream hits
dev@, java-user@, issues@ and commits@ lists; JIRA archive; web2026-10-2820 list, 4 JIRA and 6 web queries through the tool, plus direct list and JIRA searchesnothing relevant (two Stack Overflow questions about NaN scores in plain sorted search and in Solr)
GitHub (recheck for this doc)2026-10-30upstream 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-28still unfixed; nothing new
GitHub, Maven Central (independent fact check)2026-10-30read-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 #16569no report; 10.6.0 released with the expression unchanged

History

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.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20, a9779ef3c193 affectedNaN (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) affectedNaN (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 affectedNaN (also this doc's test)
10.0.0, 9.0.0, 8.11.4, 8.0.0 affectedNaN (-ea; 8.0.0 also -da)
55bfadbce11 vs its parent 34686c00dde bisectthe commit fails, the parent passes
7.7.3, 7.5.0, 7.4.0, 7.0.0 defaults onlyreal max scores with default settings; NaN on 7.7.3 with setIncludeScores(false)
fix A on main not affected1.0 (the repro passes)

Independent checks

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.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateJUnit 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 realOwn 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 onlyOpenAI 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, moderateBuilt and rejected the "accepted in Lucene 8" case; probes show the flag has no effect on the doc-block path.
adjudication confirmed, mediumRe-ran the independent reproduction's JUnit with a fresh seed (-ea, -da): 5 run, 2 failed.
Follow-up re-run and credibility audit keepThe finder's test fails 1 of 2 (-ea, -da) and passes with the fix (2026-10-28).
this doc confirmed on 645d4fe14a20The 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 fixedAn 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.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,327 words

Finding record

LFV-881, snapshot of 2026-11-06

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:test and :lucene:grouping:check -x test. The independent fact check re-ran the gradle command and the fix on 645d4fe14a20 and ran the repro on the 10.6.0 release jars (release commit 4ae946e5a9b; fails, assertions on and off). Earlier hunt runs: main a9779ef3c193 (the grouping module is identical), branch_10_6 at 4ae946e5a9b, the finder's test on releases from 7.0.0 to 10.5.2, and javac builds of the introducing commit 55bfadbce11 and its parent.

Lucene and upstream sources

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-338
  • lucene/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-159
  • lucene/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-338
  • lucene/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-207
  • lucene/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

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

A diagram, made in parallel with this doc from an executed probe on main.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.