A range group selector can bucket hits by their score; it reads the score from the collector's scorer. But only each pass's sorts (and the max-score setting) decide whether the query computes scores, so with a field group sort the first pass reads 0.
[0, 1); the second pass scores them, finds none in [0, 1) and returns one empty group. Select the still to open it at full size. Generated with AI, checked against the code.- Index five documents and search
a OR b OR cmade of constant-score clauses: they score 1, 2, 3, 1, 3. Group them withGroupingSearchandnew DoubleRangeGroupSelector(DoubleValuesSource.SCORES, new DoubleRangeFactory(0, 1, 10)). - With the defaults plus
setGroupDocsLimit(10)(the default returns one document per group) the ranges are right:[1,2)= docs 0, 3;[2,3)= doc 1;[3,4)= docs 2, 4. Now sort the groups by theidfield. You expect the same three groups: a document's score does not depend on how groups are ordered. - You get one group,
[0, 1), with no documents: 5 hits, 0 grouped. No error on main, with assertions on or off. - Why: each pass asks the query for scores only when its own sort or max-score setting needs them, and a group selector cannot ask. Pass 1 runs without scores: every hit reads 0 and lands in
[0, 1). Pass 2 scores (for the max score), and every real score falls outside[0, 1), so every hit is skipped. - Same cause, other outputs:
setIncludeMaxScore(false)with a field sort inside groups gives the three right ranges, all empty;AllGroupsCollectorManageralone reports 1 range instead of 3, andAllGroupHeadsCollectorManageralone with a field sort 1 head instead of 3.
Applications that group with DoubleRangeGroupSelector or LongRangeGroupSelector over a source that needs scores, and either sort groups by a field, turn off includeMaxScore with a field sort inside groups, or run AllGroupsCollectorManager (or AllGroupHeadsCollectorManager with a field sort) alone. Opt-in and rare; the GroupingSearch defaults are not affected; any index size; queries that compute scores only when asked (term, boolean).
Mechanism, terms and numbers759 words
DoubleRangeGroupSelector and LongRangeGroupSelector group hits by ranges of a values source, and they hand that source the collector's scorer, so a source that reads the score (DoubleValuesSource.SCORES) can group hits into score bands. But a GroupSelector cannot say that it needs scores, and the four grouping collectors that use one decide whether the query computes scores from their sorts or reducer alone. In a pass where none of them needs scores, the query runs in COMPLETE_NO_SCORES, the selector reads a placeholder (0 for the term and boolean queries of every repro) for every hit, and the groups come out silently wrong: one empty group with a field group sort, the right groups without their documents with includeMaxScore=false, and 1 group or head instead of several from AllGroupsCollectorManager or AllGroupHeadsCollectorManager run alone. It has been there since the selectors were added in 8.6.0; the GroupingSearch defaults (relevance group sort, includeMaxScore=true) are not affected.
- Group selector (
GroupSelector): decides the group of each hit.DoubleRangeGroupSelectorandLongRangeGroupSelectorread a value per hit from a values source and put the hit into a fixed-width range (DoubleRangeFactory(0, 1, 10): ranges of width 1 from 0 to 10). - Score-dependent source: a
DoubleValuesSourcewhoseneedsScores()is true, such asDoubleValuesSource.SCORES(the hit's score) or itstoLongValuesSource(). The range selectors feed it the collector's scorer throughDoubleValuesSource.fromScorer(DoubleRangeGroupSelector.java:60-63,LongRangeGroupSelector.java:61-64). - Score mode: each collector tells the query whether it needs scores (
Collector.scoreMode()). InCOMPLETE_NO_SCORES, "scores won't be available" (ScoreMode.java:25-26); a non-scoring term weight, for example, uses a dummy scorer that returns 0 (TermQuery.java:77-91). - Two-pass grouping:
GroupingSearchruns a first pass (FirstPassGroupingCollector) to find the top groups, then a second pass (TopGroupsCollector, aSecondPassGroupingCollector) to collect each group's documents;AllGroupsCollectorandAllGroupHeadsCollectorcan run alongside or on their own.
GroupSelectorhassetScorerbut noneedsScores()(GroupSelector.java:34-72), so a selector cannot ask for scores.- The four collectors that use a group selector take their score mode from their sorts or reducer only, each pass on its own: the first pass from the group sort (
FirstPassGroupingCollector.java:105, 124-127), the second pass from the reducer, which needs scores only for the max score or a within-group sort that needs them (SecondPassGroupingCollector.java:76-79,TopGroupsCollector.java:127-128),AllGroupHeadsCollectorfrom its sort (:119-122), andAllGroupsCollectornever (:91-94, "the result is unaffected by relevancy"). - So with a field group sort the first pass runs without scores. The selector reads 0 for all five hits and keeps one group,
[0, 1). - The second pass scores, because
includeMaxScoredefaults to true, and the selector now reads the real scores 1, 2, 3, 1, 3. None falls into[0, 1), the only group, soadvanceToreturnsSKIPfor every hit (DoubleRangeGroupSelector.java:75,SecondPassGroupingCollector.java:90): one empty group. - With the relevance group sort but
includeMaxScore=falseand a field within-group sort, the first pass is right and the second pass reads 0: the three right ranges come back empty.AllGroupsCollectorandAllGroupHeadsCollectoralone read 0 for every hit: 1 range, 1 head.
DoubleValuesSource.SCORES.needsScores() is true (DoubleValuesSource.java:349-368); the information exists, but no grouping collector asks the selector. When a sort happens to need scores, the selector is right by accident.
Not every query is hit. Without scores the value a scorer returns is unspecified (ScoreMode only says scores "won't be available"). Term and boolean queries and ConstantScoreQuery clauses return 0 (TermQuery.java:77-91, ConstantScoreQuery.java:141-146, 202-203), so they are grouped wrongly; queries whose score is a constant boost in every mode, such as MatchAllDocsQuery or a point range query (ConstantScoreWeight), still return their real score and are grouped right (executed).
Five documents with tags {a}, {a,b}, {a,b,c}, {a}, {a,b,c}; a OR b OR c of ConstantScoreQuery(TermQuery) clauses scores them 1, 2, 3, 1, 3 (a plain search, asserted by the control test). Expected for every configuration (with setGroupDocsLimit(10)): {[1,2)=[0, 3], [2,3)=[1], [3,4)=[2, 4]}. Group sort by id: {DoubleRange(0.0, 1.0)=[]} (totalHitCount 5, totalGroupedHitCount 0; with setAllGroups(true) also totalGroupCount 1). LongRangeGroupSelector over SCORES.toLongValuesSource(): {LongRange(0, 1)=[]}. includeMaxScore=false with a within-group sort by id: {[1,2)=[], [2,3)=[], [3,4)=[]}. AllGroupsCollectorManager alone: [DoubleRange(0.0, 1.0)]. AllGroupHeadsCollectorManager with sort id alone: heads [0] instead of [0, 1, 2]. An independent reproducer with 12 documents whose scores are 10.8 to 16.8 recorded what the selector reads under an id sort: 0.0 for all 12.
Exposure and likelihood340 words
- Who. Grouping with
DoubleRangeGroupSelectororLongRangeGroupSelectorover a values source that needs scores:DoubleValuesSource.SCORES, itstoLongValuesSource(), or (by reading, not run) an expression that binds_scoreto it, the pattern the expressions module documents (SimpleBindings.java:35), plus any one of: (a) a group sort that does not need scores; (b)includeMaxScore=falseand a within-group sort that does not need scores, even with the default relevance group sort; (c)AllGroupsCollectorManager, orAllGroupHeadsCollectorManagerwith a sort that does not need scores, run on its own. - Where (c) shows. On 10.6.0 (the 10.6 branch) and main. On 8.6.0-10.5.2 those stand-alone collectors did not forward the scorer, and the same search throws a
NullPointerExceptioninstead (a related bug in the same selectors); #16247 (released in 10.6.0) turned that exception into these wrong groups. - Default vs opt-in. Grouping by score ranges is opt-in; the
GroupingSearchdefaults (relevance group sort,includeMaxScore=true) request scores in both passes and are right. Once configured with a trigger, a search is mis-grouped whenever the placeholder falls into a different range from the real scores, as for the repro's boolean query; constant-score queries such asMatchAllDocsQueryare grouped right. - Scale. None: five documents in one segment show it, deterministically, with assertions on and off.
- Effect. Silent wrong groups: one empty group, the right groups without documents, documents missing from their groups, or 1 group or head instead of several. Nothing fails on main with a plain
IndexSearcher;totalHitCountstays right whiletotalGroupedHitCountdrops to 0. - How likely. Rare: grouping by the score is unusual, the defaults hide it, no use of the range selectors was found in Elasticsearch, OpenSearch or Solr, and no public report was found in the searched sources.
- Workaround. Add a score tie-break to the group sort,
new Sort(new SortField("id", SortField.Type.INT), SortField.FIELD_SCORE)(executed), and keepincludeMaxScore=trueor a within-group sort that needs scores. For the stand-alone collectors, run them in aMultiCollectorManagernext to a collector that needs scores (executed by the fact check: 3 right ranges, heads[0, 1, 2]).
The test shows that grouping by DoubleRangeGroupSelector(DoubleValuesSource.SCORES, ...) with a field group sort runs the first pass without scores, so every hit falls in the range of score 0.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.search.grouping;
import java.util.ArrayList;
import java.util.List;
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.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
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 TestScoreRangeGroupingFieldGroupSort extends LuceneTestCase {
public void testGroupByScoreRangeSortedByField() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
try (IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc0 = new Document();
doc0.add(new StringField("tag", "a", Field.Store.NO));
doc0.add(new NumericDocValuesField("id", 0));
w.addDocument(doc0);
Document doc1 = new Document();
doc1.add(new StringField("tag", "a", Field.Store.NO));
doc1.add(new StringField("tag", "b", Field.Store.NO));
doc1.add(new NumericDocValuesField("id", 1));
w.addDocument(doc1);
}
try (DirectoryReader reader = DirectoryReader.open(dir)) {
Query q =
new BooleanQuery.Builder()
.add(new ConstantScoreQuery(new TermQuery(new Term("tag", "a"))), Occur.SHOULD)
.add(new ConstantScoreQuery(new TermQuery(new Term("tag", "b"))), Occur.SHOULD)
.build();
GroupingSearch grouping =
new GroupingSearch(
() ->
new DoubleRangeGroupSelector(
DoubleValuesSource.SCORES, new DoubleRangeFactory(0, 1, 10)));
grouping.setGroupSort(new Sort(new SortField("id", SortField.Type.INT)));
TopGroups<DoubleRange> groups = grouping.search(new IndexSearcher(reader), q, 0, 10);
List<DoubleRange> ranges = new ArrayList<>();
for (GroupDocs<DoubleRange> group : groups.groups) {
ranges.add(group.groupValue());
}
// doc0 scores 1 and doc1 scores 2: expected [1,2) and [2,3); actual one group [0,1)
assertEquals(List.of(new DoubleRange(1, 2), new DoubleRange(2, 3)), ranges);
}
}
}
}
# in a copy of this finding's repro folder (repro/LFV-884/, 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.TestScoreRangeGroupingFieldGroupSort
- Expected vs actual
- Expected groups
[DoubleRange(1.0, 2.0), DoubleRange(2.0, 3.0)]for two hits that score 1 and 2; actual[DoubleRange(0.0, 1.0)], with-eaand-da.
More files, environment, full output38 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:<[DoubleRange(1.0, 2.0), DoubleRange(2.0, 3.0)]> but was:<[DoubleRange(0.0, 1.0)]>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:120)
org.junit.Assert.assertEquals(Assert.java:146)
org.apache.lucene.search.grouping.TestScoreRangeGroupingFieldGroupSort.testGroupByScoreRangeSortedByField(TestScoreRangeGroupingFieldGroupSort.java:75)- The selectors take any values source and pass it the collector's scorer on purpose, which only matters for sources that need scores.
- Lucene's value-source sort passes
needsScores()on; a missing pass-through was fixed as a bug (#14654). - Group membership changes with the group sort. The "unaffected by relevancy" comment predates score-reading selectors.
The full argument296 words
- Selector contract.
DoubleRangeGroupSelectortakes "a DoubleValuesSource to retrieve double values per document" (DoubleRangeGroupSelector.java:46;LongRangeGroupSelector.java:47the same for longs), with no restriction; the package javadoc lists it as grouping "based on the value of a DoubleValuesSource" (package-info.java:67-70). - The bridge serves this case.
setScorerbindssource.getValues(context, DoubleValuesSource.fromScorer(scorer)).DoubleValuesSource.getValuessays callers "may safely pass null" for the scores whenneedsScores()is false (DoubleValuesSource.java:60-66), so the scorer, which the selectors pass to every source, is needed only by score-dependent sources; the class javadoc documents scores as a value source throughfromScorer(:43-46). - Score-mode contract.
COMPLETE_NO_SCORES: "Produced scorers will allow visiting all matches but scores won't be available" (ScoreMode.java:25-26);Collector.scoreMode(): "Indicates what features are required from the scorer" (Collector.java:55-56). - Unrelated settings change membership.
setIncludeMaxScoreis documented as "Whether to include the score of the most relevant document per group" (GroupingSearch.java:329-338); turning it off empties every group. A group sort by a field merges every hit into[0, 1). - Convention and precedent.
DoubleValuesSortField.needsScores()returnsproducer.needsScores()(DoubleValuesSource.java:637-640), which is why a sort by a score-dependent source works in grouping today. #14654 (CHANGES 10.3.0, Bug Fixes) fixed a missing pass-through of the same kind: "would throw errors when used if the DoubleValuesSource needed scores". - Tests. No upstream grouping test uses a score-dependent source:
TestDoubleRangeGroupSelectorandTestLongRangeGroupSelectoruse field sources, and nothing inlucene/grouping/src/testusesSCORESorfromScorer. The case is untested, not decided. - History. LUCENE-6912 (2015) wrote the sort-based score modes and the "unaffected by relevancy" comment, when no selector could read a scorer. LUCENE-7889 (2020) added
setScorerand the range selectors with the score bridge and changed no score mode; its PR has no review comments and its discussion never mentions scores.
The devil's-advocate check254 words
The strongest case for "intended" (built by the devil's-advocate checker and the other-family reviews; the reviews rate it moderate, the devil's advocate gives no rating): AllGroupsCollector states "the result is unaffected by relevancy"; GroupSelector never had a way to ask for scores, its javadoc says nothing about score availability (GroupSelector.java:25-33), and ValueSourceGroupSelector.setScorer is empty, so the module may never have meant group keys to depend on scores; the defaults work; a score tie-break is a workaround; the tests use field sources only; the collectors and GroupingSearch are marked @lucene.experimental, which leaves maintainers room to narrow the contract; and LUCENE-7889's commit message gives numeric-valued fields as the motivation.
Why it fails: the comment predates score-reading selectors by 4.5 years and was not revisited when they were added; the fromScorer bridge is needed only by score-dependent sources, so wiring it and never requesting scores looks like an oversight; no javadoc restricts the source, SCORES is public and documented, and the numeric-field motivation says why the feature was added, not what it excludes; and the groups change with unrelated settings, which no contract can intend. Verdict: most likely not intended, a defect born with the feature; a javadoc restriction remains a legitimate maintainer alternative. The fact checks read it the same way. The expected behaviour is inferred from the API's shape and the needsScores convention, not quoted from a grouping javadoc, so a maintainer may prefer to document a restriction. Not security: wrong groups through correct use of public APIs, chosen by the application.
Recommended Let a GroupSelector declare that it needs scores
Add GroupSelector#needsScores() (default false), return source.needsScores() from the two range selectors, and OR it into the score modes of the first-pass, second-pass, all-groups and all-group-heads collectors (+26/-4 in 7 files). The whole grouping module passes, the repros included, with assertions on and off, and its static checks pass.
All options and trade-offs3 options · 486 words
One code fix was built and run in a scratch worktree at a9779ef3c193; the javadoc alternative was not run. Both are legitimate.
The finder's fix, formatted with gradlew tidy: a non-abstract GroupSelector#needsScores() returning false; DoubleRangeGroupSelector and LongRangeGroupSelector override it with source.needsScores(); FirstPassGroupingCollector, SecondPassGroupingCollector (so also TopGroupsCollector and DistinctValuesCollector), AllGroupsCollector and AllGroupHeadsCollector OR it into their score modes. +26/-4 in 7 files.
// GroupSelector
public boolean needsScores() {
return false;
}
// DoubleRangeGroupSelector, LongRangeGroupSelector
@Override
public boolean needsScores() {
return source.needsScores();
}
// FirstPassGroupingCollector
this.needsScores = groupSort.needsScores() || groupSelector.needsScores();
// SecondPassGroupingCollector (AllGroupHeadsCollector: sort.needsScores(); AllGroupsCollector: the selector alone)
return groupReducer.needsScores() || groupSelector.needsScores()
? ScoreMode.COMPLETE
: ScoreMode.COMPLETE_NO_SCORES;Trade-offs
- A new public, non-abstract method on a public abstract class: additive and source-compatible. A custom
GroupSelectorthat reads scores must override it; say so in the javadoc and in CHANGES. - A score-dependent selector now makes the query compute scores: a cost only for those users, and what they need for a correct result. Score-independent selectors are unchanged.
- It follows the existing convention (
DoubleValuesSortField.needsScores()returnsproducer.needsScores()). - It drops the "the result is unaffected by relevancy" comment, which could stay as "unless the selector reads scores".
- Out of scope (a related bug in the same selectors): the score-less cache replay (
setCaching(n, false)) still cannot serve a score-dependent selector, andDistinctValuesCollector's value selector never getssetScorer.
What the checks validated
Gradle in a scratch worktree at a9779ef3c193, seed DEADBEEF: the whole :lucene:grouping:test with the 5 repro classes added, 19 classes, 82 tests, 0 failures, with assertions on and off; all 19 repro tests that fail on stock pass. :lucene:grouping:check -x test with the fix and the primary test (google-java-format, ecj lint, forbidden APIs): BUILD SUCCESSFUL. The repro runner's re-runs pass with the fix shadowed too (the finder's test 4 of 4, the reproducer's 13 of 13, the probe on main); those used the finder's untidied copy of the same change, which differs only in line wrapping. An independent fact check repeated the primary test with the fix and the static checks in a fresh scratch worktree: 7 of 7 pass, BUILD SUCCESSFUL. The brief's own scratch worktree was deleted and its HEAD was not logged. Not run: the full Lucene test suite, benchmarks.
Say in the javadoc of both range selectors that the source must not need scores unless a sort that needs scores is used, and correct the AllGroupsCollector comment. Not run.
Trade-offs
The smallest change and a legitimate maintainer choice, but a documented public input keeps giving a silent wrong answer, and the selectors' fromScorer bridge would then serve only passes whose sort or reducer already requests scores.
Throw IllegalArgumentException when source.needsScores() is true. Not run.
Trade-offs
Breaks configurations that work today (relevance sorts); not recommended.
Only grouping output changes (no exception, nothing extra returned or exposed), so a downstream product is affected only if it offers grouping by score ranges together with a field sort or the stand-alone group collectors.
A bytecode scan of 20 Lucene jars listed every non-scoring score mode that reaches a consumer and every bridge that feeds scores into a values source (561 rows). The range selectors' bridges sat next to grouping collectors that never ask for scores; reading them gave the lead and a public-API test confirmed it.
The method in full183 words
Method: static bytecode inventory plus code reading, then a public-API test (the hunt's "semi-formal" tier; the lead came from reading rows of a mechanical inventory, which a strict reading could put in the non-formal tier).
- The hunt unit looked for weights or collectors that run without scores while something reads scores. It scanned the class files of 20 non-test jars with the JDK 25 class-file API: 561 rows, among them every site where
COMPLETE_NO_SCORESor aneedsScores()-conditional mode reaches a consumer and everyDoubleValuesSource.fromScorerbridge. Each non-test row was triaged by reading. - The bridge rows
DoubleRangeGroupSelector.setScorerandLongRangeGroupSelector.setScorersat next to grouping collectors whosescoreMode()isCOMPLETE_NO_SCORESunless a sort or reducer needs scores. The finder wrote the public-API JUnit test, which failed 3 of 4 with assertions on and off. - A secondary dynamic sweep ran 5,696 module compositions under the test framework's asserting searcher; its only new scorer-protocol sites were this bug's first-pass and second-pass collectors, including the second-pass-only trigger. A planted non-scoring collector that reads scores fired as a positive control.
- Independent checks then reproduced it (see the checks below).
No prior report found: 127 queries over GitHub issues, PRs and commits, the mailing lists, JIRA, downstream trackers and the web, plus the file lists of all 410 open PRs (2026-10-28). Every search for the two selector names returns nothing; two open PRs rewrite the first-pass score mode and would keep the bug.
Searches3 searches
Registry verdict new: no report or discussion of this bug by anyone else was found. Nearest items, none a report: #8940 / apache/lucene-solr#1484 (LUCENE-7889, the feature; no discussion of scores), LUCENE-6912 (#7970, the 2015 sort-only score modes and the comment), #16247 (merged 2026-07-08; scorer forwarding for the related null-values bug, score modes unchanged), open PRs #15210 and #15494 (rewrite FirstPassGroupingCollector.scoreMode(), still from the group sort only), #16569, #16548 and #16847 (touch the same collectors without consulting the selector), #14654 and SOLR-9494 (the same class of defect fixed elsewhere). Limits: GitHub search returns 50 results per query, which cut two generic one-word queries (every class-name query came back complete); loose mailing-list matches were read by title; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; dev@, java-user@, issues@, commits@; JIRA archive; downstream trackers; web | 2026-10-28 | a scripted prior-report search, 70 queries (DoubleRangeGroupSelector, LongRangeGroupSelector, GroupSelector needsScores / scoreMode, grouping by score range, AllGroupsCollector scoreMode); 1,958 distinct hits, 0 high, 24 medium; every medium hit and every narrow-query hit read | no report |
| GitHub (apache/lucene, apache/solr, all of GitHub), JIRA, lists.apache.org, web | 2026-10-28 | 57 manual searches and reads, refreshed; the finder's 45 queries | 0 issues or PRs naming either selector anywhere; #240 (unrelated) for AllGroupsCollector scoreMode |
| apache/lucene open PRs and recent activity | 2026-10-28 | the changed-file lists of all 410 open PRs (the grouping diffs of five read); issues and PRs created since 2026-10-26; grouping commits since 2026-09-01 | 13 PRs touch grouping, ScoreMode or values sources; none changes a collector's score need for the selector |
Born with the feature: LUCENE-7889 (7c350d22c70, backport 5e62c42c5f3, 2020-05-11) added the range selectors and their score bridge but no way to ask for scores; first released in 8.6.0. All 49 releases from 8.6.0 to 10.5.2, 10.6.0 (the 10.6 branch at 4ae946e5a9b) and main fail the same probe.
Versions tested7 rows
Introduced by LUCENE-7889 "Allow grouping on Double/LongValuesSource" (apache/lucene-solr#1484, JIRA mirror #8940), by @romseygeek, author date 2020-05-11: 7c350d22c7070acf362fb4f477e6797f2711c910 on the master line (first release 9.0.0) and 5e62c42c5f304eee7b5695f97f02a2aea7d00366 on branch_8x (first released in 8.6.0). It added GroupSelector.setScorer and the two range selectors with the fromScorer bridge, and changed no collector's scoreMode(); those lines come from LUCENE-6912 (2015), LUCENE-4100 (2017) and LUCENE-8412 (2018) and were complete when written, because no selector read a scorer before. Not a regression. #16247 (2026-07-08, released in 10.6.0) made the stand-alone AllGroupsCollector and AllGroupHeadsCollector forward setScorer: there the related bug's NullPointerException (8.6.0-10.5.2) became these silent wrong groups (10.6.0, main). Upstream main c5ab3e57b9b (2026-10-28) has no change at the fix site.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | 5 repro classes through gradle: 19 of 26 tests fail, assertions on and off |
branch_10_6 at 4ae946e5a9b (2026-10-26) | affected | the finder's test fails 3 of 4, assertions on and off; AllGroups alone silently returns [DoubleRange(0.0, 1.0)] |
| 10.5.2, 10.0.0, 9.12.3, 9.0.0, 8.11.4 | affected | the finder's API-adapted test (assertions on; also off on 10.5.2, 9.12.3, 8.11.4): field sort gives {DoubleRange(0.0, 1.0)=[]}; AllGroups alone throws the related NullPointerException |
| 8.6.0 | first release | the same, assertions on and off |
| all 49 releases 8.6.0-10.5.2 | affected | a version-neutral main() probe fails 4 of 4 checks with identical result lines (assertions on; also off on 10.5.2) |
| 8.5.2 | no API | the range selectors do not exist yet (the repros do not compile) |
fix on a9779ef3c193 | not affected | the whole grouping module, repros included: 82 tests, 0 failures, assertions on and off |
An independent reproducer (its own 12-document test), the test written by the step that drove a reading-only review from another model family, an intended-behaviour devil's advocate and an adjudication all reproduced it on main with assertions on and off; the fix makes every repro pass. No other-family reviewer executed code.
Checks and verdicts575 words
Each check read or ran on its own; the registration check and an audit re-ran the finder's test with fresh seeds, and this doc re-ran all five repro classes through gradle on a9779ef3c193.
| Check | Verdict | What it found |
|---|---|---|
| finder (bytecode inventory, code reading, JUnit, module sweep) | candidate | JUnit fails 3 of 4 on main (-ea, -da) and branch_10_6; wrong groups on 10.5.2, 9.12.3, 8.11.4 and 8.6.0; the fix passes 4 of 4 and keeps the upstream grouping tests green. |
| cross-model review (reading only) | real, high | An independent reviewer from another model family (OpenAI GPT-5.6) read the code and the executed outputs: mechanism agrees, fix fixes it, intended-behaviour case moderate. An earlier refute-lens attempt could not start any command and returned a procedural "not real". |
| independent reproducer | real | Own test from the claim: 12 documents with real BM25-based scores 10.8 to 16.8; the selector reads 0.0 for all 12 under an id sort; 7 of 9 fail on main (-ea, -da), its GroupingSearch test 3 of 4; reproduces on 10.5.2 and 9.12.3; 13 of 13 pass with the fix. |
| cross-model review, intended lens | real, high (reading only) | The other-family reviewer could only read code: a first attempt returned real, low because no command could start; the reading-only second attempt returned real, high. A separate run ran its own probe test (boosts 1, 2, 4: fails 3 of 5 on main) and re-ran the finder's test (3 of 4 fail, -ea and -da). |
| intended-behaviour devil's advocate | real, high | Built the strongest "intended" case (the 2015 comment, no hook, defaults work) and rejected it; found the includeMaxScore=false trigger (three right ranges, all empty), fails 1 of 2 on main (-ea, -da); re-ran the finder's 10.5.2 test. |
| adjudication | confirmed, high | Re-ran the reproducer's test with a new seed: 7 of 9 fail with -ea and -da. Duplicate of none; related to the null-values bug in the same selectors. |
| registry re-runs and audit | keep | The finder's test fails 3 of 4 on main with -ea and -da (three fresh seeds), passes 4 of 4 with the fix shadowed. |
| this doc's gradle runs | stock fails, fix passes | Stock main: 19 of 26 repro tests fail with assertions on and off, every control passes. Fix: the whole grouping module, 82 tests, 0 failures, on and off; static checks pass. |
| independent fact check (executed) | claim stands; wording fixed | Re-ran the primary test through gradle in a fresh scratch worktree at a9779ef3c193 (5 of 7 fail, assertions on and off; 7 of 7 pass with the fix; static checks pass) and ran its own public-API probe: every trigger and number confirmed; constant-score queries such as MatchAllDocsQuery are grouped right; the MultiCollectorManager workaround works. Its four wording fixes are applied. |
| fact check by the other-family reviewer (reading only) | yes, with fixes | An independent reviewer from another model family (OpenAI GPT-5.6) read the code, the logs and this doc: the defect, the repros, the collector paths, the introducing change and the fix hold up. Its wording fixes (per-pass score needs, where no exception is thrown, release coverage, prior-art scope) are applied; it judges the evidence to favour a defect, with a javadoc restriction as a legitimate alternative. |
| diagram check | pass | Every code panel, value and claim on screen matches the code at a9779ef3c193 and the probe logs; the title was later reworded (placeholder score, per pass) and re-rendered. |
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 629 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,462 words
- Registry title
- Grouping collectors never ask for scores on behalf of their group selector, so DoubleRangeGroupSelector or LongRangeGroupSelector over a score-dependent source (e.g. DoubleValuesSource.SCORES) groups every hit by the placeholder score of a non-scoring search (0 in every repro) when no sort or reducer needs scores, or when AllGroups/AllGroupHeads runs alone (since 8.6.0)
- Area
- grouping / GroupSelector score needs (FirstPass, SecondPass/TopGroups, AllGroups, AllGroupHeads collector score modes)
- Priority
- Low, rank 327: Needs uncommon score-range grouping with a non-scoring sort, and GroupingSearch defaults are unaffected.
- Impact class
- silent wrong results
- Impact note
- Applications that group by DoubleRangeGroupSelector or LongRangeGroupSelector over a values source that needs scores (for example DoubleValuesSource.SCORES) get silently wrong groups, with no exception under -ea or -da, at any index size (5 documents suffice), in every release from 8.6.0 to 10.6.0 and on main, whenever (a) the group sort does not need scores, (b) includeMaxScore=false and the within-group sort does not need scores (even with the default relevance group sort), or (c) AllGroupsCollectorManager, or AllGroupHeadsCollectorManager with a sort that does not need scores, runs on its own (on 10.6/main; on 8.6.0-10.5.2 that path throws LFV-414's NullPointerException instead); GroupingSearch's defaults (relevance group sort, includeMaxScore=true) are not affected, and a sort that needs scores (for example a score tie-break) is a workaround.
- Summary
- You group hits into score ranges, for example with new DoubleRangeGroupSelector(DoubleValuesSource.SCORES, new DoubleRangeFactory(0, 1, 10)), and sort the groups by a field, expecting each hit in the range of its own score, which is what the same search returns with the default relevance group sort (three ranges, [1,2) = {0, 3}, [2,3) = {1}, [3,4) = {2, 4}, in the repro). Instead GroupingSearch returns one empty group [0,1) (LongRangeGroupSelector: LongRange(0, 1)), and AllGroupsCollectorManager on its own reports one range instead of three, because the grouping collectors take their ScoreMode only from their sorts or reducer, so the query runs without scores and the selector, which reads the collector's Scorable through DoubleValuesSource.fromScorer, sees the placeholder score 0 for every hit. GroupSelector has no way to say that it needs scores; a GroupSelector#needsScores() hook (true for the range selectors when their source needs scores), ORed into the four collectors' score modes, fixes it.
- Introduced
- 7c350d22c7070acf362fb4f477e6797f2711c910 (master; branch_8x backport 5e62c42c5f304eee7b5695f97f02a2aea7d00366), LUCENE-7889 'Allow grouping on Double/LongValuesSource' (apache/lucene-solr#1484 by romseygeek, merged 2020-05-11 with no review comments; JIRA mirror apache/lucene#8940) [read: git blame -w and git log -L]: it added the abstract GroupSelector.setScorer and the two range selectors, whose setScorer binds source.getValues(context, DoubleValuesSource.fromScorer(scorer)), but gave GroupSelector no way to request scores and changed no collector's scoreMode(). The score-mode lines are older and were complete when written, because no selector read a scorer before (LUCENE-6912 31b6cd6851c, 2015; LUCENE-4100 4fc5a872ded, 2017; LUCENE-8412 55bfadbce11, 2018). Not a regression: the bug was born with the feature. (2020-05-11 (author date 17:34:01+01:00 for both the master and the branch_8x commit); first released in 8.6.0)
- First affected version
- 8.6.0 [exec] (the first release with the range selectors; git tag --contains on the branch_8x backport starts at 8.6.0, on the master commit at 9.0.0); 8.5.2 has no range selectors (both repros fail to compile there [exec]), so no earlier release works correctly with this API
- Affected versions
- every release that has the range selectors: all 49 releases 8.6.0-8.11.4, 9.0.0-9.12.3 and 10.0.0-10.5.2 [exec: a version-neutral public-API probe, main() without the test framework, fails 4 of 4 checks with identical output on all of them], plus branch_10_6 4ae946e5a9b (10.6.0, released 2026-10-29) and main a9779ef3c193 [exec]. The finder's JUnit fails with wrong groups on 8.6.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 (-ea; -da as well on 8.6.0 and 10.5.2) and 3 of 4 on branch_10_6 and main (-ea and -da). The standalone AllGroups/AllGroupHeads path shows this bug only since apache/lucene#16247 (10.6.0, released 2026-10-29, and main); on 8.6.0-10.5.2 it throws LFV-414's NullPointerException instead. With the candidate fix shadowed over main the probe and the JUnit pass. Not fixed on upstream main c5ab3e57b9b (2026-10-28).
- 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, commit message, JIRA entry, mailing-list message, downstream report or web page reports grouping collectors ignoring a group selector's score needs or score-range grouping putting every hit into the score-0 range. Searched: 127 queries (70 scripted prior-report search: 30 GitHub issue/PR, 6 commit, 4 downstream-repository, 20 over the dev@, java-user@, issues@ and commits@ lists, 4 JIRA, plus web; 1,958 distinct hits, 0 high, 24 medium, all read; 57 manual searches and reads), the finder's 45 queries, and the changed files of all 410 open apache/lucene PRs (13 touch lucene/grouping, ScoreMode.java or the values sources; none changes a collector's score need for the selector). Every query naming DoubleRangeGroupSelector or LongRangeGroupSelector returns 0 on GitHub (apache/lucene, apache/solr, all of GitHub), JIRA and the lists. Refresh for this row: GitHub issue/PR search for DoubleRangeGroupSelector, LongRangeGroupSelector, 'GroupSelector needsScores', 'GroupSelector scoreMode', 'grouping score range selector' (0 each) and 'AllGroupsCollector scoreMode' (1, #240, unrelated); the 22 issues and PRs created since 2026-10-26 (none on grouping); PRs updated since 2026-10-26 that mention grouping (only #16731, HNSW); upstream lucene/grouping commits since 2026-09-01 by author date: d148795b182 (#16292, 2026-10-06) and 3e4a7a02ce0 (#16779, 2026-10-07), both facet collectors. Context, not reports: #8940 / apache/lucene-solr#1484 (LUCENE-7889, the feature; no discussion of scores), LUCENE-6912 (#7970, origin of the sort-only score modes and of 'the result is unaffected by relevancy', 2015), #16247 (merged 2026-07-08; added setScorer forwarding to AllGroups and AllGroupHeads for the LFV-414 mechanism, score modes unchanged), open PRs #15210 and #15494 (rewrite FirstPassGroupingCollector.scoreMode() but still derive it from the group sort only, so they would keep the bug), #16569 and #16548 (TopGroupsCollector, selector not consulted), #14654 and SOLR-9494 (the same class of defect fixed in other consumers). Not fixed upstream: upstream main c5ab3e57b9b (2026-10-28) has no grouping change at the fix site.
- Lucene commit tested
- main
a9779ef3c193(gradle in a scratch worktree: the 5 repro classes on stock with assertions on and off; the fix on the whole grouping module with assertions on and off, and the module's static checks; the repro runner: the finder's test, the independent reproducer's, the test written and run by the step that drove the cross-model review (the other-family model ran no code), the devil's-advocate probe, the adjudication's, the registry and audit re-runs, several seeds, assertions on and off; an independent fact check re-ran the primary test and the fix through gradle in a fresh scratch worktree, plus its own probe); branch_10_6 at4ae946e5a9b(2026-10-26); a version-neutral public-API probe on the release jars of all 49 releases 8.6.0-10.5.2 (assertions on; also off on 10.5.2), and the finder's API-adapted test on 8.6.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 (assertions on; also off on 8.6.0, 8.11.4, 9.12.3 and 10.5.2); 8.5.2 (API absent). JDK 25.0.3, Linux aarch64.
- Finding record
lucene/grouping/src/java/org/apache/lucene/search/grouping/DoubleRangeGroupSelector.java:44-49 (constructor javadoc: 'a DoubleValuesSource to retrieve double values per document'), :61-63 (setScorer binds source.getValues(context, DoubleValuesSource.fromScorer(scorer)))lucene/grouping/src/java/org/apache/lucene/search/grouping/LongRangeGroupSelector.java:63 (same bridge)lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupSelector.java:45-46 ('Set the current Scorer'; the class has no needsScores())lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollector.java:105 (needsScores = groupSort.needsScores()), :126 (COMPLETE_NO_SCORES otherwise)lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupsCollector.java:93 (always COMPLETE_NO_SCORES, comment 'the result is unaffected by relevancy')lucene/grouping/src/java/org/apache/lucene/search/grouping/AllGroupHeadsCollector.java:121 (sort.needsScores() only)lucene/grouping/src/java/org/apache/lucene/search/grouping/SecondPassGroupingCollector.java:78 (groupReducer.needsScores() only) and TopGroupsCollector.java:128 (getMaxScores || withinGroupSort.needsScores())lucene/core/src/java/org/apache/lucene/search/ScoreMode.java:25-26 (COMPLETE_NO_SCORES: 'scores won't be available')lucene/core/src/java/org/apache/lucene/search/TermQuery.java:77-91 (non-scoring TermWeight uses a dummy SimScorer that returns 0)lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:44 (scores through fromScorer), :356 (SCORES), :479 (fromScorer)
- The finding in 30 seconds
a Lucene main checkout lucene/grouping/src/java/org/apache/lucene/search/grouping/DoubleRangeGroupSelector.java:60-75; LongRangeGroupSelector.java:61-64; GroupSelector.java:34-72a Lucene main checkout lucene/grouping/src/java/org/apache/lucene/search/grouping/FirstPassGroupingCollector.java:105, 124-127; SecondPassGroupingCollector.java:76-90; TopGroupsCollector.java:127-128; AllGroupsCollector.java:72-94; AllGroupHeadsCollector.java:119-140a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/ScoreMode.java:25-26; TermQuery.java:77-91; ConstantScoreQuery.java:141-146, 202-203; MatchAllDocsQuery.java:40, 48; PointRangeQuery.java:132; DoubleValuesSource.java:349-368, 479-491
- Who is affected
a Lucene main checkout lucene/expressions/src/java/org/apache/lucene/expressions/SimpleBindings.java:35; ExpressionValueSource.java:43-53a Lucene main checkout lucene/grouping/src/java/org/apache/lucene/search/grouping/GroupingSearch.java:51-56 (defaults)
- Why it looks unintended
a Lucene main checkout lucene/grouping/src/java/org/apache/lucene/search/grouping/DoubleRangeGroupSelector.java:46, 60-63; LongRangeGroupSelector.java:47; package-info.java:67-70; GroupingSearch.java:44, 329-338; AllGroupsCollector.java:32, 91-94; GroupSelector.java:25-33; FirstPassGroupingCollector.java:41; SecondPassGroupingCollector.java:33; AllGroupHeadsCollector.java:39a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:43-46, 60-66, 637-640; ScoreMode.java:25-26; Collector.java:55-56a Lucene main checkout lucene/CHANGES.txt:1743 (#14654), :5710 (LUCENE-7889), :9732 (LUCENE-6912)
- Suggested fix
a Lucene main checkout lucene/core/src/java/org/apache/lucene/search/DoubleValuesSource.java:637-640
- TestScoreRangeGroupingFieldGroupSort.java (JUnit test):
repro/LFV-884/src/org/apache/lucene/search/grouping/TestScoreRangeGroupingFieldGroupSort.java(79 lines)
A step-by-step picture of the repro's example, made in parallel with this doc from an executed probe on main (the probe also sorts within groups by id and sets setAllGroups(true), hence its totalGroupCount). The PNG is the final summary frame.
assets/LFV-884/bug.png(629 KB)
