DocIdSetBuilder gathers one segment's matching doc ids and build() sorts them. Before sorting, it computes the sort width as PackedInts.bitsRequired(maxDoc - 1). With maxDoc 0 that is bitsRequired(-1), which throws, although the buffer is empty and the sorter would skip it.
- A reader is a list of leaves (segments).
FacetsCollectorvisits each leaf, records its matching doc ids in aDocIdSetBuildersized by the leaf'smaxDoc, and callsbuild()when the leaf is done. - A leaf can have
maxDoc0 when the application composes a reader in memory, for examplenew ParallelLeafReader()with no sub-readers inside aMultiReader.IndexWriternever writes such a segment. A fully deleted segment is different: it hasnumDocs0 butmaxDoc> 0, and is not affected.
build() asks bitsRequired(maxDoc - 1) = bitsRequired(-1) for the sort width and throws before the sorter can skip the empty buffer. Select the still to open it at full size. Generated with AI, checked against the code.- Create
new DocIdSetBuilder(0), add nothing and callbuild().FacetsCollectordoes exactly this for a leaf withmaxDoc0, for examplenew ParallelLeafReader()with no sub-readers. - You expect the empty set: its iterator's first
nextDoc()isNO_MORE_DOCS, as fornew DocIdSetBuilder(1)(or 2, 128, 1000), and as Lucene 6.0 returned for 0. - Instead
build()throwsIllegalArgumentException: maxValue must be non-negative (got: -1), with assertions on and off. - Why: before sorting its buffer, which is empty,
build()computes the sort width asPackedInts.bitsRequired(maxDoc - 1). WithmaxDoc0 that isbitsRequired(-1), which rejects negative values. - Effect: on 9.12.3 a faceted search over
MultiReader(2-document index, new ParallelLeafReader())throws instead of returning the 2 hits of the other leaf. On 10.x and mainIndexSearcherrejects that reader earlier (LFV-202), and fixing that brings this failure back.
Applications that facet over a reader they compose in memory with a leaf with maxDoc 0, such as an empty ParallelLeafReader, and direct callers of new DocIdSetBuilder(0). On 9.12.3 a plain faceted search fails; on 10.x and main IndexSearcher rejects the leaf first (LFV-202), and fixing that exposes this. Never a wrong result.
Mechanism, terms and numbers411 words
DocIdSetBuilder collects the matching doc ids of one segment, sized by the segment's maxDoc. With maxDoc 0 and nothing added, build() throws IllegalArgumentException: maxValue must be non-negative (got: -1) instead of returning the empty set, because its sparse branch always computes the radix-sort width as PackedInts.bitsRequired(maxDoc - 1). A public caller, and the demonstrated route, is FacetsCollector, which builds one per leaf, so it fails on a leaf with maxDoc 0 (for example an empty ParallelLeafReader inside a MultiReader) instead of counting the other leaves. A plain faceted search reaches it on 9.12.3; on 10.x and main IndexSearcher rejects such a reader first (LFV-202), so only code that drives FacetsCollector directly does. It is a regression from LUCENE-7261 (6.1.0); 5.3.0 to 6.0.1 returned the empty set. IndexWriter never writes a 0-document segment, so only readers composed in memory are affected, and the failure is loud, never a wrong result.
- No lower bound.
DocIdSetBuilder(int maxDoc): "Create a builder that can contain doc IDs between {@code 0} and {@code maxDoc}." (:148-151), with no check. The class is@lucene.internal(:35). - With
maxDoc0 the sparse path is always empty.threshold = maxDoc >>> 7is 0 (:187).grow(n)for anyn > 0exceeds it and switches to aFixedBitSet(maxDoc)(:220-232,:296), which builds an empty set fine. With nothing added, orgrow(0), the builder stays on the sparse path with an empty buffer. - The fix site.
build()(:313), sparse branch:sorter.sort(PackedInts.bitsRequired(maxDoc - 1), concatenated.array, concatenated.length)(:322). - Why an empty buffer still throws.
LSBRadixSorter.sortreturns early for fewer than 30 values (LSBRadixSorter.java:89-93), but Java evaluates the argument first, andPackedInts.bitsRequired(int)throws "maxValue must be non-negative (got: -1)" for any negative value (PackedInts.java:803-806). - A public caller, and the demonstrated route.
FacetsCollector.doSetNextReadercreatesnew DocIdSetBuilder(context.reader().maxDoc())(FacetsCollector.java:108) andfinish()callsbuild()(:120). An emptyParallelLeafReadersetsmaxDoc = numDocs = 0(ParallelLeafReader.java:96-98).FacetsCollectoris one of 21new DocIdSetBuilder(sites in non-test code; the others were not probed.
| call | result |
|---|---|
new DocIdSetBuilder(0).build() (nothing added, or grow(0), or add(DocIdSetIterator.empty())) | throws |
new DocIdSetBuilder(0), grow(1), build() (dense path) | empty BitDocIdSet (cost estimate 1, iterator empty) |
new DocIdSetBuilder(1 / 2 / 128 / 1000).build() | empty IntArrayDocIdSet |
FacetsCollector.getLeafCollector(new ParallelLeafReader().getContext()), then finish() | throws |
Exposure and likelihood217 words
- Who. Applications that facet (
FacetsCollectororFacetsCollectorManager) over a reader they compose in memory that contains a leaf withmaxDoc0, such as an emptyParallelLeafReaderinside aMultiReaderor a customLeafReader, and direct callers of the@lucene.internalnew DocIdSetBuilder(0). A fully deleted segment (numDocs0,maxDoc> 0) does not trigger it. - Through search. On 9.12.3,
new IndexSearcher(new MultiReader(twoDocIndex, new ParallelLeafReader())).search(new MatchAllDocsQuery(), new FacetsCollectorManager())throws this exception fromFacetsCollector.finish. On 10.x and mainIndexSearcherrejects that reader earlier with LFV-202's "minDocId is greater than than maxDoc: [0] > [0]", so today only code that drivesFacetsCollectordirectly reaches it. - Coupled with LFV-202. With LFV-202's recommended fix (range-check only true partitions) applied on main,
count(MatchAllDocsQuery)over the same reader returns 2 and the faceted search then throws this bug fromFacetsCollector.finish<-IndexSearcher.searchLeaf; with both fixes it returns 2 hits. LFV-202's alternative "skip empty leaves when slicing" would keep this one hidden. - Default vs opt-in. No setting is involved, but
IndexWriternever writes a 0-document segment, so readers over on-disk indexes are not affected. - Effect and scale. One clean
IllegalArgumentException; no wrong or missing hits, no index change. No scale needed. - Likelihood: rare. It needs an in-memory empty leaf, and no public report was found since 6.1.0 (2016).
One JUnit test method, no index: new DocIdSetBuilder(0).build().iterator().nextDoc() must be NO_MORE_DOCS. Deterministic and single-threaded; it fails with assertions on and off.
/*
* 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.util;
import java.io.IOException;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestDocIdSetBuilderZeroMaxDocBuild extends LuceneTestCase {
// Expected: an empty set; actual: IllegalArgumentException "maxValue must be non-negative (got: -1)".
public void testEmptyWithZeroMaxDoc() throws IOException {
DocIdSetBuilder builder = new DocIdSetBuilder(0);
assertEquals(DocIdSetIterator.NO_MORE_DOCS, builder.build().iterator().nextDoc());
}
}
cp $REPRO/TestDocIdSetBuilderZeroMaxDocBuild.java $LUCENE/lucene/core/src/test/org/apache/lucene/util/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.util.TestDocIdSetBuilderZeroMaxDocBuild -Ptests.seed=DEADBEEF
# and with -Ptests.asserts=false: the same failure
- Expected
build()returns the empty set: the firstnextDoc()isNO_MORE_DOCS.- Observed
IllegalArgumentException: maxValue must be non-negative (got: -1)atPackedInts.bitsRequired(PackedInts.java:805)<-DocIdSetBuilder.build(DocIdSetBuilder.java:322).
More files, environment, full output118 words
TestDocIdSetBuilderZeroMaxDocBuild (one method, extends LuceneTestCase, package org.apache.lucene.util of the lucene/core tests). It draws no random values and builds no index. Under a second.
As a regression test the same method fits in TestDocIdSetBuilder; its 104-character comment line is the only thing checkGoogleJavaFormat flags.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): fails; 9.12.3, 10.0.0, 10.5.2, 10.6.0: the same or an equivalent test fails; 6.1.0 to 9.0.0 (6 releases): a main() form fails; 5.3.0, 5.5.5, 6.0.0, 6.0.1: clean |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64 (nothing platform-specific) |
TestDocIdSetBuilderZeroMaxDocBuild > testEmptyWithZeroMaxDoc FAILED
java.lang.IllegalArgumentException: maxValue must be non-negative (got: -1)
at __randomizedtesting.SeedInfo.seed([DEADBEEF:3FE013F3FB3FB23E]:0)
at org.apache.lucene.util.packed.PackedInts.bitsRequired(PackedInts.java:805)
at org.apache.lucene.util.DocIdSetBuilder.build(DocIdSetBuilder.java:322)
at org.apache.lucene.util.TestDocIdSetBuilderZeroMaxDocBuild.testEmptyWithZeroMaxDoc(TestDocIdSetBuilderZeroMaxDocBuild.java:28)
1 test completed, 1 failed
# the same trace with -Ptests.asserts=false- The constructor allows "doc IDs between 0 and maxDoc" with no lower bound; the class returns the empty set for
maxDoc0 on its dense path and for everymaxDocof 1 or more. - 5.3.0 to 6.0.1 returned the empty set; the throw came with a 2016 sort speed-up (LUCENE-7261).
- The public
FacetsCollectorreaches it on an emptyParallelLeafReader.
The full argument225 words
- Contract.
DocIdSetBuilder(int maxDoc): "Create a builder that can contain doc IDs between {@code 0} and {@code maxDoc}." (DocIdSetBuilder.java:148);build(): "Build a {@link DocIdSet} from the accumulated doc IDs." (:312). No javadoc, assert, CHANGES or MIGRATE entry saysmaxDocmust be at least 1. That is strong evidence, not proof: the class is@lucene.internal, and "between 0 and maxDoc" is loose about the usual exclusive upper bound. With the points below it makesmaxDoc0 a plausible unsupported-boundary defect rather than a documented limit. - The class handles the case elsewhere.
maxDoc0 on the dense path (grow(1), thenbuild()) returns an emptyBitDocIdSet(cost estimate 1 from thegrow(1)reservation, iterator empty). - Tests. Upstream
TestDocIdSetBuilder.testEmptyasserts that an empty builder builds an empty set, formaxDoc=1 + random().nextInt(1000)(TestDocIdSetBuilder.java:30-32). That form predates the regression (first in8f96faf3d33, 2014); it never exercises 0, and no rationale for leaving 0 out is recorded.TestParallelReaderEmptyIndexbuildsnew ParallelLeafReader()on purpose (:50). - History. LUCENE-7261 "Speed up LSBRadixSorter" (#8316, 2016) replaced
sorter.sort(buffer, 0, bufferSize)withsorter.sort(PackedInts.bitsRequired(maxDoc - 1), buffer, bufferSize)to skip radix passes. Its discussion is about radix-sort speed only; 6.0.x returned the empty set formaxDoc0. - Precedent. LUCENE-5045 (#6109): DrillSideways threw on an empty reader and was fixed as a bug.
The devil's-advocate check169 words
The strongest case for "intended": Lucene never writes 0-document segments, and maintainers have said such segments must not exist (LUCENE-5173 #6237, opened partly over a Solr split-shard error with this very message, and the related LUCENE-5116 #6180, both 2013; later #12872 and #15803). The builder is @lucene.internal, and on 10.x and main IndexSearcher blocks the search route anyway (LFV-202).
Why it mostly fails: those statements concern segments that IndexWriter writes and codecs read, while an empty ParallelLeafReader is public API with an explicit zero-reader branch and an upstream test. @lucene.internal is a compatibility tag, not a precondition, though it does leave maintainers free to define the builder's domain; the failure is reached through the public FacetsCollector. On 9.12.3 a plain faceted search hits it, and LFV-202's recommended fix brings it back on main. Verdict: not intended, a Low robustness defect. A maintainer may still call it too minor to fix, or prefer LFV-202's skip-empty-leaves option or fix C; that is a fair outcome for a Low bug.
Recommended Clamp the bitsRequired argument to 0 (A, recommended, validated)
Clamp the sort width: PackedInts.bitsRequired(Math.max(maxDoc - 1, 0)) at DocIdSetBuilder.java:322. The repro passes with assertions on and off, and 37 tests of the closest core and facet suites pass.
sorter.sort(
PackedInts.bitsRequired(Math.max(maxDoc - 1, 0)),
concatenated.array,
concatenated.length);All options and trade-offs3 options ยท 322 words
Option A was built and checked through gradle on 5adff78bb168; B and C were not run.
In DocIdSetBuilder.build(), sparse branch (:322), pass Math.max(maxDoc - 1, 0) to PackedInts.bitsRequired, wrapped as google-java-format wants it.
sorter.sort(
PackedInts.bitsRequired(Math.max(maxDoc - 1, 0)),
concatenated.array,
concatenated.length);Trade-offs
One Math.max per sparse build(), nothing per document (expected negligible, not benchmarked); every maxDoc of 1 or more sorts exactly as before. Caveat: it also stops most negative maxDoc values (invalid, but the constructor does not reject them) from throwing here, and they would then sort with a 1-bit width; option B is narrower, or reject a negative maxDoc in the constructor. Open PR #12800 rewrites the same line (and keeps bitsRequired(maxDoc - 1)).
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168, seedDEADBEEF: the repro fails without the fix (assertions on and off) and passes with it (assertions off). - With the fix and assertions on: the repro (1),
TestDocIdSetBuilder(9),TestLSBRadixSorter(6),TestIntArrayDocIdSet(7) and, in:lucene:facet,TestDrillSideways(14): 37 tests, 0 failures.checkGoogleJavaFormatflags only the repro's long comment line, not the fix. - Earlier the finder, the independent reproducer and the cross-model reviewer each checked the clamp by shadowing the class: their tests passed, and
TestDocIdSetBuilderpassed 9 of 9 in the finder's and the reviewer's runs.
Guard the sorter.sort(...) call with if (concatenated.length > 0).
Trade-offs
Equivalent for this bug, since a maxDoc 0 builder's sparse buffer is always empty; it also skips work for every empty builder, at the cost of one more branch.
In FacetsCollector, record DocIdSet.EMPTY for a leaf with maxDoc 0 instead of building one.
Trade-offs
Fixes the faceting route only and leaves the builder's boundary in place for other callers.
Downstream products are affected only if they facet over readers they compose with an empty leaf; end-user queries and documents cannot create such a leaf.
A proof-obligation audit of DocIdSetBuilder's contract: build() must return a set for every documented maxDoc, including the empty domain 0. The sparse branch fails that obligation at bitsRequired(maxDoc - 1); a one-method JUnit test then witnessed the exception.
The method in full121 words
Method: semi-formal (a proof-obligation audit of a class's contract, witnessed by an executable repro).
- The finder checked
build()against the constructor's domain ("doc IDs between 0 and maxDoc") on both paths, dense and sparse, and found that only the sparse path evaluates an expression that is undefined atmaxDoc0. - The finder then wrote a public-API JUnit test, probes for the
FacetsCollectorandIndexSearcherroutes, and the one-line clamp that makes the test pass. - An independent reproducer and an executed cross-model review reproduced it with their own tests; a devil's-advocate verifier and an adjudication re-ran the finder's test (the adjudication with a new seed), and the verifier's own faceting test showed it once LFV-202 is fixed ("Prior reports, history and checks").
No matching prior report in apache/lucene issues, PRs and commits (incl. the JIRA archive), the mailing lists, downstream trackers or the web (searched 2026-11-03). Nearest: #8316 (the introducing change) and #12800 (open, keeps the same expression).
Searches3 searches
No matching public report was found in the documented searches. Nearest items, none a report of this bug: #8316 (LUCENE-7261, the introducing change; its discussion never mentions maxDoc 0), #8324 (LUCENE-7269, another zero case in another constructor, fixed in 2016), #12800 (open, rewrites the line but keeps bitsRequired(maxDoc - 1)), #16847 (open; in FacetsCollector it only changes the expression passed to new DocIdSetBuilder(...), from context.reader().maxDoc() to context.maxLeafDoc(), with no DocIdSetBuilder.java change, so not a fix), #6237 (LUCENE-5173: the same message, from 0-document segments on disk, 2013), #6180 (LUCENE-5116, the related 2013 addIndexes fix for deleted segments) and #6109 (LUCENE-5045, a precedent). Limits: keyword search; GitHub search does not index every review thread; private reports are not visible.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive) | 2026-11-03; re-checked the same day | two scripted prior-report searches (the builder and sorter terms; the faceting route) and about 25 manual searches: DocIdSetBuilder, LSBRadixSorter, the exception message, bitsRequired(-1), zero-doc leaf, empty ParallelLeafReader, FacetsCollector | no report; #8316 and #12800 are the nearest |
| Open and recent PRs | 2026-11-03 | the changed files of open PRs updated since 2026-10-01 and PRs closed since 2026-11-01; diffs and comments of every PR touching DocIdSetBuilder.java or LSBRadixSorter.java (#709, #12800, #15383, #16089) | none fixes it; #12800 keeps the expression |
| Mailing lists, downstream, web | 2026-11-03 | dev@, java-user@, issues@ and commits@ (the scripted searches' queries plus the DocIdSetBuilder subject lists), Elasticsearch, OpenSearch and Solr trackers, 7 web queries | only the 2013 on-disk 0-document-segment threads, before this bug existed |
Introduced by LUCENE-7261 (8ca6f6651ed on branch_6x, ef45d4b2e1f on master, 2016-04-28), first released in 6.1.0; 5.3.0 to 6.0.1 pass. Every executed release since fails, through 10.6.0 and main. Not fixed upstream.
Versions tested6 rows
Introduced by LUCENE-7261 "Speed up LSBRadixSorter" (Adrien Grand, 2016-04-28; #8316, JIRA era, no PR): ef45d4b2e1f on master and 8ca6f6651ed on branch_6x, which shipped first, in 6.1.0. It replaced sorter.sort(buffer, 0, bufferSize) with sorter.sort(PackedInts.bitsRequired(maxDoc - 1), buffer, bufferSize). git blame on main gives the line to 052fb97f828 (LUCENE-7258 "Speed up DocIdSetBuilder allocation", 2016-05-23), a wider refactor of the sparse storage that changed only this line's arguments; the bitsRequired(maxDoc - 1) expression comes from ef45d4b2e1f.
A commit-level A/B isolates it: the 6.0.0 jars with DocIdSetBuilder and LSBRadixSorter from 8ca6f6651ed fail at the changed line, and with the two classes from its parent they pass. 9.0.0 and older ran a main() form of the same steps; 9.12.3 and later ran the JUnit test. Upstream main still has the line at DocIdSetBuilder.java:322 (checked 2026-11-03).
| Version or commit | Verdict | Result |
|---|---|---|
| 5.3.0, 5.5.5, 6.0.0, 6.0.1 | main() form | pass: empty IntArrayDocIdSet |
| 6.0.0 with the parent's two classes | A/B, parent | pass |
| 6.0.0 with the two classes from 8ca6f6651ed | A/B, commit | fails at the changed line |
| 6.1.0, 6.6.6, 7.0.0, 7.7.3, 8.11.4, 9.0.0 | main() form | fails, same exception |
| 9.12.3, 10.0.0, 10.5.2, 10.6.0 | JUnit | fails, same exception |
| main 5adff78bb168 | JUnit, gradle | fails, assertions on and off |
A finder, an independent reproducer, an executed cross-model review (OpenAI GPT-5.6, real, high), a devil's-advocate verifier, an adjudication, a credibility audit and a final repro check reproduced it. This doc's gradle runs reproduced it on main and validated fix A.
Checks and verdicts224 words
The cross-model review ran on the older main ea57360912ed; its tests were re-run on 5adff78bb168 and fail the same way. Every other check ran on 5adff78bb168 or release jars.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Own test fails on main (assertions on and off), 10.6.0 and 9.12.3; probes show the FacetsCollector and 9.12.3 search routes; the clamp makes the test pass. |
| independent reproducer | real | Own test through add(DocIdSetIterator.empty()) fails on main (on, off), 10.6.0, 10.5.2 and 9.12.3; 6.0.0 passes, 6.1.0 and 8.11.4 fail; the clamp makes it pass. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Own core test (trigger plus two controls) and own FacetsCollector test fail on the trigger only; the clamp passes both. Strongest counter-point: reach (internal builder, masked by LFV-202 on main). |
| devil's-advocate verifier | real (moderate-high) | Rebutted every intent argument; with LFV-202's recommended fix applied, faceted search on main throws this bug; with both fixes it returns 2 hits. |
| adjudication | confirmed, Low (high) | Re-ran the test on main with seed 1CEB00DA, assertions on and off: fails as stated. A final repro check kept it as a Low. |
| this doc's gradle check | reproduced; fix A validated | Fails on main with assertions on and off; with the clamp, 37 tests of the repro and closest suites pass. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 547 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,431 words
- Registry title
- DocIdSetBuilder.build() on an empty builder with maxDoc 0 throws IllegalArgumentException 'maxValue must be non-negative (got: -1)' instead of returning the empty set, because the sparse path always evaluates PackedInts.bitsRequired(maxDoc - 1), so FacetsCollector fails on a zero-document leaf (regression in 6.1.0)
- Area
- core / util (DocIdSetBuilder sparse build; reached from facet / FacetsCollector)
- Priority
- Low, rank 786: Faceting over a reader with an empty in-memory leaf throws instead of counting the other leaves, because DocIdSetBuilder.build() mishandles maxDoc 0, in every release since 6.1.0: on 9.x through a plain faceted search, on 10.x only when FacetsCollector is driven directly. The failure is loud with no wrong counts and needs a reader that IndexWriter never produces, so it is rare.
- Impact class
- minor
- Impact note
- Hits applications that facet (FacetsCollector or FacetsCollectorManager) over a reader they assemble in memory with a zero-document leaf (for example new ParallelLeafReader() inside a MultiReader, or a custom LeafReader with maxDoc() 0), and direct callers of the @lucene.internal DocIdSetBuilder(0): build() throws a clean IllegalArgumentException with -ea and -da instead of returning the empty set; on 9.12.3 a plain IndexSearcher.search(MatchAllDocsQuery, FacetsCollectorManager) over such a reader throws it, while on 10.x and main IndexSearcher rejects the leaf earlier (LFV-202) and fixing LFV-202 the recommended way exposes this failure again; readers over indexes that IndexWriter wrote never contain such a leaf, no scale or setting is needed, and no wrong result is ever returned; affected since 6.1.0.
- Summary
- You create a DocIdSetBuilder for an empty doc-ID range (new DocIdSetBuilder(0), which FacetsCollector does for every leaf as new DocIdSetBuilder(context.reader().maxDoc())), add nothing and call build(), and expect the empty set, as the same class already returns for maxDoc 0 on its dense path and for every maxDoc >= 1. Instead build() throws IllegalArgumentException 'maxValue must be non-negative (got: -1)', because its sparse branch always computes the radix sort width as PackedInts.bitsRequired(maxDoc - 1) = bitsRequired(-1), even though there is nothing to sort. So faceting over a reader that contains a zero-document leaf (for example an empty ParallelLeafReader) fails instead of counting the other leaves; Lucene 6.0.x returned the empty set.
- Introduced
- ef45d4b2e1f9c967b62340acb027f50888a00ba2 (master, Adrien Grand, LUCENE-7261 'Speed up LSBRadixSorter', migrated as apache/lucene#8316; pre-GitHub-PR era, patch on JIRA) and its branch_6x copy 8ca6f6651ede19bfaee9051e9b87927685cb9be0 (same author date, the copy that shipped first, in 6.1.0): it replaced sorter.sort(buffer, 0, bufferSize) with sorter.sort(PackedInts.bitsRequired(maxDoc - 1), buffer, bufferSize) in DocIdSetBuilder.build() [read: git log -S 'bitsRequired(maxDoc - 1)' finds only ef45d4b2e1f; git blame on 5adff78bb168 gives 052fb97f828 (LUCENE-7258, 2016-05-23), which only renamed the arguments to concatenated.array / concatenated.length, and blame on 052fb97f828^ gives ef45d4b2e1f] [exec: 6.0.0 jars with DocIdSetBuilder and LSBRadixSorter shadowed from 8ca6f6651ed fail at the changed line, shadowed from its parent pass] (2016-04-28 (author date 2016-04-28T18:20:41+02:00 on both master and branch_6x))
- First affected version
- 6.1.0 [exec] (tagged 2016-06-12; contains 8ca6f6651ed; CHANGES.txt lists LUCENE-7261 under 6.1.0 Optimizations); on the master line the first release is 7.0.0. Not a new API: DocIdSetBuilder(int) and build() exist since 5.3.0 (LUCENE-6645) and returned the empty set for maxDoc 0 in 5.3.0 through 6.0.1 [exec].
- Affected versions
- Every release from 6.1.0 through 10.6.0 (4ae946e5a9b, released 2026-10-29) and main 5adff78bb168 (12.0.0-SNAPSHOT); not fixed upstream (upstream main still has the line at DocIdSetBuilder.java:322, checked 2026-11-03). [exec] fail: 6.1.0 (-ea, -da), 6.6.6, 7.0.0, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, 10.6.0 (-ea, -da) and main (-ea, -da) (the history sweep ran a main() form of the same steps on the older releases and the JUnit repro on 9.12.3, 10.0.0, 10.6.0 and main; 10.5.2 by the independent reproduction's own test); [read] all release tags from 6.1.0 on contain the commit. Not affected [exec]: 5.3.0, 5.5.5, 6.0.0, 6.0.1 (empty set); no release before 5.3.0 has DocIdSetBuilder. Public IndexSearcher route [exec]: 9.12.3 fails through IndexSearcher.search(MatchAllDocsQuery, FacetsCollectorManager) over MultiReader(2-doc index, new ParallelLeafReader()); 10.6.0 and main stop earlier with LFV-202's IllegalArgumentException. Not new in 10.6 (10.5.2 and older releases fail). All runs used 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 agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal: proof-obligation audit
- Upstream and prior reports
- none found (2026-11-03, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches; rechecked the same day): two scripted prior-report searches (the builder/sorter spec and the faceting-route spec: 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list queries over dev@, java-user@, issues@ and commits@ and 4 JIRA-archive queries each; 280 and 3109 distinct hits, 1 high by term overlap (#8313, LUCENE-7258 allocation tuning, read: not this bug)), the finder's own run, about 25 manual GitHub searches on apache/lucene (DocIdSetBuilder issues and PRs, all titles read; LSBRadixSorter; 'maxValue must be non-negative'; 'bitsRequired(-1)'; zero-doc leaf; empty ParallelLeafReader; FacetsCollector IllegalArgumentException) and on Elasticsearch, OpenSearch and Solr, the changed files of the open apache/lucene PRs updated since 2026-10-01 plus those closed since 2026-11-01, the diffs and comments of every PR that touches DocIdSetBuilder.java or LSBRadixSorter.java (#12800, #709, #15383, #16089), lists.apache.org (192 dev@ and 1 java-user@ DocIdSetBuilder mails; the two java-user@ threads with the same message read), JIRA (SOLR-9235, SOLR-9125) and 7 web queries: no issue, PR, commit, JIRA entry, list thread, downstream issue or web page reports that DocIdSetBuilder.build() throws for maxDoc 0 or that FacetsCollector fails on a zero-document leaf. Nearest items, not this bug: #8316 (LUCENE-7261, the introducing change; its discussion never mentions maxDoc 0), #8324 (LUCENE-7269, docCount 0 in the stats constructor, other code), #12800 (open, rewrites the same line but keeps bitsRequired(maxDoc - 1)), #16847 (open, renames FacetsCollector's constructor argument only), #6237 and #6180 (LUCENE-5173/5116, the same message from 0-document segments on disk in 2013, codec code), #6109 (LUCENE-5045, a faceting IllegalArgumentException on an empty reader fixed as a bug, other code). Same-day recheck: GitHub search on apache/lucene for DocIdSetBuilder items updated since 2026-11-01 (0), 'DocIdSetBuilder build maxDoc' (0), '"maxValue must be non-negative"' (#6237 only), 'FacetsCollector maxDoc 0' (0); the newest upstream main commit on DocIdSetBuilder.java is still 1169cd2007d (#16089, author and committer date 2026-10-14) and on FacetsCollector.java aa86c2b8a04 (#13774, 2024-09-12); upstream main still has the bitsRequired(maxDoc - 1) line at DocIdSetBuilder.java:322.
- Lucene commit tested
- main
5adff78bb168(12.0.0-SNAPSHOT) through gradle on 2026-11-03 in a scratch worktree, seedDEADBEEF: the repro without the fix (assertions on and off: fails) and with the clamp fix (assertions off: passes; assertions on, with 4 closest core suites andTestDrillSideways: 37 tests, 0 failures), pluscheckGoogleJavaFormat. The hunt ran the repro or an equivalent test on main (assertions on and off), 10.6.0, 10.5.2, 10.0.0 and 9.12.3 (fail), amain()form on 6.1.0, 6.6.6, 7.0.0, 7.7.3, 8.11.4 and 9.0.0 (fail) and on 5.3.0, 5.5.5, 6.0.0 and 6.0.1 (pass), and a commit-level A/B of LUCENE-7261 on the 6.0.0 jars. Only Linux aarch64 with OpenJDK 25.0.3 was run.
- Finding record
lucene/core/src/java/org/apache/lucene/util/DocIdSetBuilder.java:35 (at 5adff78bb168) (@lucene.internal), :148-151 (DocIdSetBuilder(int maxDoc): 'Create a builder that can contain doc IDs between 0 and maxDoc', no range check), :187 (threshold = maxDoc >>> 7), :296 (dense path new FixedBitSet(maxDoc)), :312-322 (build(): the sparse branch calls sorter.sort(PackedInts.bitsRequired(maxDoc - 1), concatenated.array, concatenated.length) at :322, fix site)lucene/core/src/java/org/apache/lucene/util/packed/PackedInts.java:803-808 (at 5adff78bb168) (bitsRequired throws IllegalArgumentException 'maxValue must be non-negative (got: ' + maxValue + ')' for maxValue < 0)lucene/core/src/java/org/apache/lucene/util/LSBRadixSorter.java:89-93 (at 5adff78bb168) (sort(numBits, array, len): the len < 30 insertion-sort shortcut runs only after the numBits argument has been evaluated)lucene/facet/src/java/org/apache/lucene/facet/FacetsCollector.java:105-120 (at 5adff78bb168) (doSetNextReader: new DocIdSetBuilder(context.reader().maxDoc()) at :108; finish(): docsBuilder.build() at :120)lucene/core/src/java/org/apache/lucene/index/ParallelLeafReader.java:93-97 (at 5adff78bb168) (the zero-reader branch sets maxDoc = numDocs = 0)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/util/DocIdSetBuilder.java:35, (at 5adff78bb168) :148-151, :187, :220-232, :296, :312-322lucene/core/src/java/org/apache/lucene/util/LSBRadixSorter.java:89-93 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/util/packed/PackedInts.java:803-806 (at 5adff78bb168)lucene/facet/src/java/org/apache/lucene/facet/FacetsCollector.java:106-120 (at 5adff78bb168)lucene/core/src/java/org/apache/lucene/index/ParallelLeafReader.java:91-98 (at 5adff78bb168)Lucene main 5adff78bb168: grep of 'new DocIdSetBuilder(' in non-test code (21 sites)
- Who is affected
lucene/facet/src/java/org/apache/lucene/facet/FacetsCollector.java:108, (at 5adff78bb168) :120
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/util/DocIdSetBuilder.java:148, (at 5adff78bb168) :312lucene/core/src/test/org/apache/lucene/util/TestDocIdSetBuilder.java:30-32; (at 5adff78bb168) lucene/core/src/test/org/apache/lucene/index/TestParallelReaderEmptyIndex.java:50
- Suggested fix
this finding's record, Upstream and prior reports (open PR #12800 keeps the expression)
- History
a Lucene clone: git tag --contains 8ca6f6651ed
- Diagrams
lucene/core/src/java/org/apache/lucene/util/DocIdSetBuilder.java:313-322; (at 5adff78bb168) lucene/core/src/java/org/apache/lucene/util/packed/PackedInts.java:803-806; lucene/facet/src/java/org/apache/lucene/facet/FacetsCollector.java:108, :120
- TestDocIdSetBuilderZeroMaxDocBuild.java (one JUnit test, lucene/core tests):
repro/LFV-1215/TestDocIdSetBuilderZeroMaxDocBuild.java(30 lines)
The diagram follows the repro and the code at 5adff78bb168 (DocIdSetBuilder.java:322, PackedInts.java:803-806, FacetsCollector.java:108, :120).
assets/LFV-1215/bug.png(547 KB)
