After the update, the segment reads m through OverlaySortedNumericDocValues: the update layer over the multi-valued base column. Its advanceExact moves only layers that are behind the target, so a second advanceExact on the current document leaves the base column's value cursor where the first read stopped.
- A SORTED_NUMERIC field stores a sorted list of numbers per document. You read it with
advanceExact(doc), thennextValue()up todocValueCount()times. CallingadvanceExactagain on the same document is allowed, and Lucene's own sorting relies on it starting the values over. - With overlays on (main's default), a doc-values update is not a rewrite of the column: it is a small layer holding only the updated documents, merged with the old column at read time until the segment is merged (or the updates are folded into one full column).
SortedNumericSortFieldkeys each document by its minimum (or maximum). Once the top-n queue is full,TopFieldCollectorreads every competitive document twice,compareBottom(doc)thencopy(slot, doc), and each read starts withadvanceExact(doc).
- Index three documents with a multi-valued
SortedNumericDocValuesFieldm: doc 0 = {50, 60}, doc 1 = {10, 90}, doc 2 = {30, 40}. Then callupdateSortedNumericDocValue(new Term("id", "2"), "m", 70)with the defaultIndexWriterConfigon main: the update is stored as a small overlay layer on top of the old column. - Search all documents for the top 1 by
new SortedNumericSortField("m", LONG), which sorts by each document's minimum. You expect doc 1: 10 is smaller than 50 and 70. With overlays off, that is what you get. - The collector reads a competitive document twice:
compareBottomreads doc 1 as 10, thencopycallsadvanceExact(1)again. The overlay view does not touch a layer that already sits on doc 1, so the old column's value cursor is not reset and the second read returns 90. - Doc 1 is stored with sort key 90, doc 2 (70) beats it, and you get doc 2, with no error. With a MAX sort the stale cursor can also run past the end of the column and throw.
Applications on main (default) or on 10.6.0 with setMaxDocValuesOverlays(n > 0) that call updateSortedNumericDocValue on a field with multi-valued documents and sort on it. Such sorts can be wrong until the segment is merged, and the documents you did not update get the wrong keys. Single-valued fields and NUMERIC/BINARY updates are unaffected; data on disk is correct.
Mechanism, terms and numbers814 words
With doc-values overlays (IndexWriterConfig#setMaxDocValuesOverlays, @lucene.experimental; on by default on main with 8 overlays, opt-in in 10.6.0), a set-only doc-values update is written as a sparse layer and merged with the old column at read time. After IndexWriter#updateSortedNumericDocValue (new in 10.6.0, #16581) on a segment whose SORTED_NUMERIC field is multi-valued in some document, the segment reads the field through OverlaySortedNumericDocValues. Its advanceExact delegates to DocValuesOverlayMerger#advanceExact, which moves only layers whose docID() < target; on advanceExact(docID()) nothing moves, so nextValue() continues on the base column where the previous read stopped instead of restarting the document's values. DocValuesIterator#advanceExact allows target == docID() on purpose (LUCENE-7462: comparators read a value more than once), and TopFieldCollector does exactly that for every competitive hit (compareBottom, then copy). So SortedNumericSortField sorts can return wrong top hits and wrong sort values, silently and the same way every time for the same index and search, until a merge (or a fold to one dense column) rewrites the segment; a MAX sort can instead throw IndexOutOfBoundsException or RuntimeException(EOFException). Three documents and one update suffice: the top hit by minimum is doc 2 instead of doc 1. The documents that go wrong are the ones that were not updated; data on disk is correct. Fields that are single-valued everywhere and NUMERIC/BINARY overlays are not affected. A one-method fix in OverlaySortedNumericDocValues#advanceExact (re-position the winning layer when the target is the current document) passes the test and the doc-values-update, overlay and sort suites.
- The view.
SegmentDocValuesProducer#getSortedNumericreturns the base iterator when a field has one producer, elseOverlaySortedNumericDocValues.from(...)(SegmentDocValuesProducer.java:218-224).fromtakes the singletonOverlayNumericDocValuespath only when every layer is single-valued (OverlaySortedNumericDocValues.java:79-89), else buildsOverlaySortedNumericDocValues(:91), which "works whether the base column is single- or multi-valued" (:23-28). - Delegation.
nextValue()anddocValueCount()readlayers[merger.valueLayer()](:41-49);advanceExact(target)ismerger.advanceExact(target)(:51-54). - The merger (
DocValuesOverlayMerger.java:72-80):
while (layers[heap[1]].docID() < target) {
layers[heap[1]].advance(target);
siftDown();
}
docID = target;
winner = layers[heap[1]].docID() == target ? heap[1] : -1;A layer already on target is not touched. The class javadoc says layers "are advanced with advance only (never advanceExact) so iteration and random access can be interleaved" (:28-29); it says nothing about re-reading the current document.
- The base column's cursor. In the default codec's dense multi-valued iterator,
advanceExactsetsstart/endfrom the address table andnextValue()isvalues.get(start++)with no end check (Lucene90DocValuesProducer.java:2061-2072). Not re-positioned, a second read continues with the document's next value, then the next document's values (a neighbour's, or the pre-update values of an updated document), or runs past the end of the column. - The consumer.
LongLeafComparator#getValueForDoccallsadvanceExact(doc)(LongComparator.java:79-85) forcompareBottom,compareTopandcopy(:93-107), throughSortedNumericSelector.MinValue(SortedNumericSelector.java:135-142: onenextValue()) orMaxValue(:169-174,:194-201:docValueCount()values). Once the queue is full,TopFieldCollectorcallscompareBottom(doc)(TopFieldCollector.java:93) and, for a competitive hit,copy(bottom.slot, doc)(:116). The first n documents only getcopy(:122-126), one read.
A traced run of the three documents (main, assertions off): copy(doc 0) reads 50, the bottom is 50. compareBottom(doc 1) reads 10: competitive. copy(doc 1) calls advanceExact(1) again; the base layer stays on doc 1 with its cursor after 10, so nextValue() is 90 and doc 1 is stored with key 90. compareBottom(doc 2) reads 70 from the update layer; 70 < 90, so doc 2 replaces doc 1. Top hit: doc 2, key 70. With setMaxDocValuesOverlays(0): doc 1, key 10.
- Direct re-read:
advanceExact(1),nextValue()= 10,advanceExact(1),nextValue()= 90. - A MAX sort (reverse) on the same three documents returns doc 2 (70) instead of doc 1 (90).
- Two documents
{5}(updated to 1) and{10, 90}, MAX descending, top 1:IndexOutOfBoundsExceptionon aByteBuffersDirectory,RuntimeException(EOFException: read past EOF)on anFSDirectory. Correct with overlays off. - Wrong sort keys: with doc 0 updated to 70 instead, the MIN top hit is doc 2 with key 40 (its true minimum is 30); with doc 0 updated to 1, a MAX sort returns the right hit, doc 1, but with key 40, doc 2's value.
searchAfter: page 2 after (doc 1, key 10) returns doc 1 again, already on page 1, with key 30 (doc 2's pre-update value); with overlays off it returns doc 0, key 50.- Updated documents read correctly: their winning layer is the single-valued update, whose
nextValue()returns the same value every time. The wrong keys go to documents that were not updated. - Not affected: NUMERIC and BINARY overlays (they have no per-document value cursor) and SORTED_NUMERIC fields that are single-valued in every document of the segment (singleton path): 12,846 re-read checks over 200 random indexes, 0 mismatches.
Exposure and likelihood439 words
- Who is exposed. Users of the new
IndexWriter#updateSortedNumericDocValue(orupdateDocValueswith aSortedNumericDocValuesField) on a field that is multi-valued in at least one document of the segment; the javadoc and CHANGES both allow a multi-valued field ("The field itself may be single- or multi-valued"). On main this is the default configuration; in 10.6.0 it needs the experimentalsetMaxDocValuesOverlays(n > 0). - Effect. Silent wrong top hits and wrong sort values (
FieldDoc.fields) forSortedNumericSortFieldwith MIN or MAX. It shows when the collector re-reads a competitive document that was not updated, so that its values come from the multi-valued base column; that needs more matches than the requested top n (orsearchAfter); whether the hits or keys then come out wrong depends on the values, and the same index and search give the same wrong result every time. Sometimes loud: with MAX the stale cursor can run past the column and fail the query (IndexOutOfBoundsExceptionorRuntimeException(EOFException)). - How long. Until the segment is merged, or until a fold rewrites the field as one dense column, for readers opened after that (a reader that is already open keeps its view). A fold happens while writing the next set-only update once the field already has
maxDocValuesOverlaysdelta generations; it is a dense rewrite, one producer that reads correctly, when the prior deltas' summed docs-with-value counts reach half the segment'smaxDoc. Otherwise a sparse fold keeps the base plus one folded generation, still read through the overlay view. - Scale. Three documents and one update suffice. In a random probe with 300 documents of 1 to 3 values each (2 to 11 segments) and 400 single-valued updates (6 seeds, overlay limits 1, 2 and 8), a second read was wrong for 71 to 227 of the 300 documents in each run, while single reads were right in all 73,200 checks and
CheckIndexwas clean. - Not affected. Fields that are single-valued in every document of the segment (the motivating case of #16580), NUMERIC and BINARY overlays, overlays off, and persisted data: in the same probe every single read was right, after the updates and after reopening the writer with overlays off. Merges ran in only 3 of its 18 runs, so for merges the evidence is mainly upstream tests such as
TestSortedNumericDocValuesUpdates#testUpdateKeepsOtherDocsMultiValuedAcrossMerge, which pass with the fix. - Also executed.
searchAfterpaging, which reads each hit withcompareTopand thencopy(withcompareBottomfirst once its queue is full): page 2 can return a hit from page 1 again. Application code that callsadvanceExacttwice on a document to read its values again gets the direct re-read above.
One JUnit test in lucene/core, public API, default IndexWriterConfig and codec: three documents, one update, a top-1 SortedNumericSortField search. It fails on main with assertions on and off; on 10.6.0, add .setMaxDocValuesOverlays(8) to see it.
/*
* 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.index;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StringField;
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.SortedNumericSortField;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
public class TestOverlaySortedNumericSort extends LuceneTestCase {
public void testMinSortAfterUpdate() throws Exception {
try (Directory dir = newDirectory()) {
try (IndexWriter w =
new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()))) {
w.addDocument(doc("0", 50, 60));
w.addDocument(doc("1", 10, 90));
w.addDocument(doc("2", 30, 40));
w.commit();
w.updateSortedNumericDocValue(new Term("id", "2"), "m", 70);
}
try (DirectoryReader r = DirectoryReader.open(dir)) {
Sort sort = new Sort(new SortedNumericSortField("m", SortField.Type.LONG));
TopDocs top = new IndexSearcher(r).search(MatchAllDocsQuery.INSTANCE, 1, sort);
// expected: doc 1 (min 10) on top; actual on main: doc 2 (doc 1 is re-read as 90)
assertEquals(1, top.scoreDocs[0].doc);
}
}
}
private static Document doc(String id, long... values) {
Document doc = new Document();
doc.add(new StringField("id", id, Field.Store.NO));
for (long v : values) {
doc.add(new SortedNumericDocValuesField("m", v));
}
return doc;
}
}
cp $REPRO/TestOverlaySortedNumericSort.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestOverlaySortedNumericSort -Ptests.seed=DEADBEEF -Ptests.asserts=true
# also with -Ptests.asserts=false
- Expected
- Top hit doc 1: its minimum, 10, is smaller than doc 0's 50 and doc 2's 70.
- Observed
- Top hit doc 2:
AssertionError: expected:<1> but was:<2>, with assertions on and off.
More files, environment, full output219 words
The test sits in org.apache.lucene.index like the upstream doc-values update tests but uses only public classes and methods. It pins the default codec (Lucene104, whose doc values use the Lucene90 format; the only codec run); newDirectory() picks the Directory by seed, and every seed tried fails (DEADBEEF through gradle and the standalone runner, assertions on and off; 1, C0FFEE and 5EED42 on the standalone runner, assertions off). With setMaxDocValuesOverlays(0) the same scenario passes.
Suggested regression test: this same test, for example as a method of TestSortedNumericDocValuesUpdates.
On 10.6.0 overlays are off by default (DEFAULT_MAX_DOC_VALUES_OVERLAYS = 0), so the test passes there as written; with .setMaxDocValuesOverlays(8) added to the IndexWriterConfig it fails with the same message. On 10.5.2 it does not compile (updateSortedNumericDocValue does not exist).
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also 10.6.0 with setMaxDocValuesOverlays(8) |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; the code path is pure Java) |
TestOverlaySortedNumericSort > testMinSortAfterUpdate FAILED
java.lang.AssertionError: expected:<1> but was:<2>
at __randomizedtesting.SeedInfo.seed([DEADBEEF:75239423F2B9FA40]:0)
at org.junit.Assert.fail(Assert.java:89)
at org.junit.Assert.failNotEquals(Assert.java:835)
at org.junit.Assert.assertEquals(Assert.java:647)
at org.junit.Assert.assertEquals(Assert.java:633)
at org.apache.lucene.index.TestOverlaySortedNumericSort.testMinSortAfterUpdate(TestOverlaySortedNumericSort.java:49)
:lucene:core:test (FAILURE): 1 test, 1 failure
# the same with -Ptests.asserts=false, and on the standalone runner with -ea and -da
# 10.6.0 + setMaxDocValuesOverlays(8): the same failure; 10.6.0 default: passes
# with the recommended fix: 1 test, 0 failuresadvanceExactallowstarget == docID()on purpose: LUCENE-7462 chose>=so comparators can read a value more than once, andTopFieldCollectordoes.- The default codec, the asserting test wrapper and MemoryIndex (fixed for this in LUCENE-7890) restart the values; with overlays off the index sorts correctly.
- The update API allows multi-valued fields, and overlays are only a storage choice.
The full argument308 words
- Contract.
DocValuesIterator#advanceExact: "targetmust be greater than or equal to the current doc ID" (DocValuesIterator.java:24-28). Adrien Grand on LUCENE-7462 (#8514, 2016-10-20): "Initially I wanted to require that the target is strictly greater than the current doc id but this caused issues with comparators that may need to get the value multiple times [...] So the current patch only requires that the target is greater than or equal to the current document." - The documented way to re-read. In the 2018 java-user thread "Read DocValue twice", the same maintainer answered: "If you want to read the values again, you need to call setDocument (Lucene < 7.0) or advanceExact (Lucene >= 7.0) before calling nextValue()".
- Same defect class, fixed before. LUCENE-7890 (#8941, 2017): "The
SortedSetDocValuesandSortedNumericDocValuesinstances returned by the MemoryIndex should support subsequentadvanceExact(0)invocations"; its testTestMemoryIndex#testDocValues_resetIteratoris still on main and callsadvanceExact(0)three times, expecting all five values each time (TestMemoryIndex.java:327-366). LUCENE-8117 (#9165, 2018) fixedadvanceExactreturning invalid SORTED_NUMERIC values in the Lucene54 producer. - The other implementations checked restart. The default codec recomputes
starton eachadvanceExact, in the dense (Lucene90DocValuesProducer.java:2061-2067) and the sparse multi-valued iterator (:2184-2198,:2263-2271); MemoryIndex does since LUCENE-7890;AssertingSortedNumericDocValues#advanceExactacceptstarget >= in.docID()and resets its value budget,valueUpto = 0(AssertingLeafReader.java:1176-1187). No implementation that continues the cursor was found besides the overlay view (not an exhaustive audit). - Overlays are a storage choice.
setMaxDocValuesOverlaystrades read cost for write amplification (IndexWriterConfig.java:335-342); #16581's description says supporting a multi-valued base is intended ("really it's not hard to support multi-valuedSortedNumericDocValues"). With overlays off the same data sorts correctly. - Why tests missed it.
TestSortedNumericDocValuesUpdatesnever callsadvanceExactand usesSortedNumericSortFieldonly to check that an index-sort field is rejected; the overlay and update tests read each document once.
The devil's-advocate check145 words
Strongest case (the intended-lens reviewers and the adjudication): the merger documents an advance-only design; the SortedNumericDocValues javadoc only says "Do not call this more than docValueCount times for the document" and never says that a repeated advanceExact restarts the values; overlays are @lucene.experimental. So continuing the cursor could be called allowed.
Why it fails: the advance-only rule is about moving layers ("so iteration and random access can be interleaved"), not about re-reading a document; the >= contract exists for comparators that read a value more than once, and core TopFieldCollector is that caller, so the view breaks Lucene's own sorting, not an outside misuse; @lucene.experimental marks API instability, and the feature is on by default on main. A maintainer could also document the restart explicitly, but documentation alone would not repair TopFieldCollector on overlays: a code change is still needed, preferably the overlay-local one.
Recommended Restart the winning layer in OverlaySortedNumericDocValues#advanceExact
In OverlaySortedNumericDocValues.advanceExact, when the target is the current document, call advanceExact(target) on the winning layer so its values start over (+8/-1, one file; the layer already sits on the target, so nothing moves). The test and 10 doc-values-update, overlay and sort suites (138 tests) pass, with assertions on and off.
@Override
public boolean advanceExact(int target) throws IOException {
final boolean sameDoc = target == merger.docID();
final boolean found = merger.advanceExact(target);
if (found && sameDoc) {
// advanceExact(docID()) must restart the document's values, but the merger does not move a
// layer that already sits on target: re-position the winning layer (target == its docID()).
layers[merger.valueLayer()].advanceExact(target);
}
return found;
}All options and trade-offs4 options ยท 510 words
Two forms of the same restart were built and run in a scratch worktree of 645d4fe14a20: in the view (recommended) and in the shared merger. A value-buffering design and a documentation-only change are listed for completeness.
Restart the winning layer in OverlaySortedNumericDocValues#advanceExact (validated by the checks) (recommended)
When the target is the current document and a layer has it, call advanceExact(target) on the winning layer, so its values start over (OverlaySortedNumericDocValues.java, +8/-1). The idea is the first finder's.
@Override
public boolean advanceExact(int target) throws IOException {
final boolean sameDoc = target == merger.docID();
final boolean found = merger.advanceExact(target);
if (found && sameDoc) {
// advanceExact(docID()) must restart the document's values, but the merger does not move a
// layer that already sits on target: re-position the winning layer (target == its docID()).
layers[merger.valueLayer()].advanceExact(target);
}
return found;
}Trade-offs
- Safe by construction: the winning layer sits on
targetwith a value, soadvanceExact(target)is legal, returns true and leaves it there; the merger's heap and its advance-only rule for moving layers are unchanged. NUMERIC and BINARY overlays are untouched. - Cost: one extra
advanceExacton one layer, only when the same document is positioned again. No benchmark was run.
What the checks validated
- Stock + test: fails through gradle with assertions on and off (
expected:<1> but was:<2>); + fix: passes. - + fix: 10 suites (the test,
TestSortedNumericDocValuesUpdates,TestIncrementalDocValuesUpdates,TestMixedDocValuesUpdates,TestNumericDocValuesUpdates,TestBinaryDocValuesUpdates,TestOverlayNumericDocValues,TestOverlayBinaryDocValues,TestSortedNumericSortField,TestTopFieldCollector): 138 tests, 2 skipped, 0 failures, with assertions on and off;:lucene:core:check -x testpasses. - In the hunt: the bounded-exhaustive check finds 0 counterexamples with this fix, and the independent verifiers' and reviewers' own tests pass with it.
- The independent fact check repeated these gradle runs in its own scratch worktree with the same results.
- Not run: the full Lucene test suite, the nightly tests, any benchmark.
When target == docID and a layer wins, call layers[winner].advanceExact(target); reword the class javadoc, which says "never advanceExact" (+10/-2).
Trade-offs
Covers any future multi-valued overlay built on the merger. Adds the extra call to NUMERIC and BINARY re-reads, which do not need it, and changes a class shared by all overlay types; a committer may still prefer it as the single place that owns positioning.
What the checks validated
- + this fix instead: the same 10 suites, 138 tests, 2 skipped, 0 failures, with assertions on (not run with assertions off or through
check).
Copy the winning layer's docValueCount() values into an array on every positioning and serve nextValue() from it.
Trade-offs
Independent of how layers behave, but adds a copy for every document positioned, whether it is read once or twice.
Change the javadoc instead of the code.
Trade-offs
Contradicts the LUCENE-7462 contract and leaves TopFieldCollector broken on overlays: documentation alone would not repair the collector, so a code change is still needed.
A returned sort value can come from a neighbouring document of the same field and segment, which matters only to an application that shows sort values to users and hides that neighbour through its own authorization.
A code-reading hypothesis about the overlay merger's value cursor, confirmed by a bounded-exhaustive check: every short sequence of moves on tiny segments, run on the real merger and view, disagreed with an independent reference exactly when advanceExact was repeated on the current document. An Alloy model found it independently; both led to a public-API JUnit.
The method in full154 words
Method: semi-formal (a code-reading hypothesis witnessed by bounded-exhaustive translation validation), a hunt unit, 2026-10-25. The harness drives the real DocValuesOverlayMerger and OverlaySortedNumericDocValues through every legal move sequence (nextDoc, advance, advanceExact) over up to 3 layers and 5 documents, compares every read with an independently written union reference, and repeats with advanceExact(docID()) allowed and forbidden. Re-run on 645d4fe14a20: 38,874 layer assignments, 386,132 reachable merger states; SORTED_NUMERIC counterexamples only when the repeated advanceExact is allowed, NUMERIC 0 in both modes, 0 everywhere with the fix.
Independently, a hunt unit built an Alloy 6 model of the overlay read protocol, got a 4-step counterexample and confirmed it with a differential test on the real jar. Later units (a hunt unit, a hunt unit with the MAX-sort exception, and round-17 probes) saw it again; the adjudication collapsed all of them into this row and the no-noise gate reduced the repro to one test.
No prior report found in the searches listed: GitHub issues, PRs and commits (Lucene and three downstream projects), the Lucene mailing lists, the JIRA archive and the web, searched 2026-10-29 and refreshed 2026-10-30. The same defect class was fixed long ago in MemoryIndex (LUCENE-7890) and the Lucene54 producer (LUCENE-8117).
Searches4 searches
Context, not reports: #16581 / #16580 introduced it (one review comment raised five numbered points and a second reviewer left one inline comment, none about re-reading the current document); #16418 / #16554 / #16550 added overlays and the merger; #16644, #16645, #16648, #16714, #16566, #16872 and #16923 are other overlay or doc-values-update items. No open PR touches the two overlay classes. Search limits: keyword search only; GitHub search does not index PR diffs (open PRs were checked by changed files); the mailing-list index misses dotted stack-trace tokens.
| Where | When | How | Found |
|---|---|---|---|
| GitHub (apache/lucene issues, PRs, commits; Elasticsearch, OpenSearch, Solr) | 2026-10-29 | two scripted prior-report searches (each 16 issue/PR, 3 commit, 2 downstream, 20 mailing-list and 4 JIRA-archive queries, and 11 and 12 web queries; class and method terms; symptom terms such as advanceExact twice, wrong sort, IndexOutOfBoundsException) and 50 manual queries; full discussions of #16418, #16581, #16580, #16554, #16550 and later overlay items | no report; only the introducing PRs and other overlay items |
| Open apache/lucene PRs and commit history | 2026-10-29 | changed files of all 409 open PRs; commits touching both fix-site files on main, branch_10x and branch_10_6 | no open PR touches the overlay classes; only #16418 / #16554 and #16581 touched the files |
| Mailing lists, JIRA archive, web | 2026-10-29 | dev@, java-user@, issues@, commits@, builds@; LUCENE-7462, LUCENE-7890, LUCENE-8117, LUCENE-8138 read | no report (the overlay code postdates the JIRA era); the 2018 java-user thread states the re-read contract |
| Refresh | 2026-10-30 | upstream heads; commits to both files on main and branch_10_6; issue and PR search for the class names, updateSortedNumericDocValue, "overlay advanceExact", "doc values overlays sort" | no new item (an earlier search for overlay advanceExact, 2026-10-29, returned only #16418, the change that added overlays); both files unchanged since #16581 and #16418 / #16554 |
Introduced by #16581 (169cbffd12f, merged 2026-09-21), which added updateSortedNumericDocValue and the multi-valued overlay view on top of #16418's merger. Affects main by default and 10.6.0 with overlays opted in; 10.5.2 and older lack the API.
Versions tested7 rows
#16581 "Allow single-valued updates for SortedNumericDocValues" (HoustonPutman, for their issue #16580) is 169cbffd12f on main and eaa3a7a9edb on branch_10_6, both 2026-09-21. It added the update API and OverlaySortedNumericDocValues, whose nextValue() reads the winning layer's cursor. The merger's advance-only advanceExact is older: #16418 (75eddfaff22, jimczi, 2026-08-13; 10.x backport #16554 3a6cd003aaf). It was harmless there, because the NUMERIC and BINARY views read the current value without a cursor. Both files are identical in 10.6.0 (4ae946e5a9b) and 645d4fe14a20. 10.6.0 ships overlays off by default (DEFAULT_MAX_DOC_VALUES_OVERLAYS = 0); main turned them on (8).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 (11.0.0-SNAPSHOT), default config | affected | the test fails, assertions on and off (gradle and standalone, 4 seeds) |
upstream main 716d6f216f52, branch_10_6 head | read | same code: OverlaySortedNumericDocValues last changed by #16581 on both; DocValuesOverlayMerger by #16418 on main and its backport #16554 on branch_10_6 (2026-10-30) |
10.6.0 (4ae946e5a9b) with setMaxDocValuesOverlays(8) | opt-in | the test fails: expected:<1> but was:<2> (release jars, assertions off) |
10.6.0 (4ae946e5a9b), default config | overlays off | passes |
169cbffd12f (#16581) vs its parent 2165146d01c | bisect | #16581 fails (second read 90, MIN top hit doc 2); its parent has no update API (compile error) |
| 10.5.2, 9.12.3 | API absent | the test does not compile: updateSortedNumericDocValue does not exist |
with the fix (main 645d4fe14a20) | not affected | the test and 10 doc-values-update, overlay and sort suites pass |
The finder, cross-model reviews (OpenAI GPT-5.6, with code execution), more verifiers and an adjudication rate it real; the adjudication re-ran it on main. A credibility audit lowered the impact score and kept the claim. This doc's runs reproduced it and validated the fix.
Checks and verdicts430 words
Every check re-ran a repro on stock jars. The cross-model reviewers wrote and ran their own tests (one outside Lucene's packages, public API only); their checkout ea57360912ed has identical sources for every class on the read and sort path (SortedNumericSortField differs only in a deserialization branch, #16919, not on the sort path), and both reviewers' tests fail the same way on 645d4fe14a20 (the intended-lens test in the verification, the refute-lens tests in the fact check).
| Check | Verdict | What it found |
|---|---|---|
| finder (a hunt unit, first find; the adjudication) | candidate | Bounded-exhaustive check: SORTED_NUMERIC counterexamples only when advanceExact(docID()) is allowed, 0 with the fix; the repro fails on main with -ea and -da, passes with overlays 0 and with the fix. |
| cross-model review, refute lens (OpenAI GPT-5.6) | real, high | Two own tests reproduce the wrong re-read and the wrong MIN top hit with -ea, -da and 4 seeds; overlays-0 controls pass; the fix passes. |
| Independent reproducer | real | Own public-API tests: MIN top hit doc 2 instead of doc 1, re-read 90 instead of 10, MAX test throws; 10.6.0 fails only with overlays opted in; the fix passes all three. |
| Cross-model review, intended lens (OpenAI GPT-5.6, effort high) | real, high | The works-as-designed case is weak; its own test fails 2 of 3 on main and on 10.6.0 with overlays 8. |
| Devil's advocate | real, high | No intended-behaviour argument survives the contract, LUCENE-7462, LUCENE-7890, the asserting wrapper and core sorting. |
| adjudication | confirmed, high | Re-ran the finder's repro (-ea, -da) and the independent reproduction's test (-da) on main: all fail. |
| credibility audit and no-noise gate | claim; impact 80 -> 70 | The sort-only form fails with -da and passes with the fix; the score was recomputed per scenario (default only on unreleased main, opt-in in 10.6.0). |
| this doc's runs | reproduced, fix validated | The one-method test on main (gradle and standalone, assertions on and off, 4 seeds), on 10.6.0 (default and opt-in) and 10.5.2; both fix forms with the suites. |
| Independent fact check | real (2026-10-30) | The doc's gradle command fails in its own scratch worktree with assertions on and off, with the same output; both fixes pass; 10.6.0 fails only with the opt-in; its own probe executed the wrong-key and searchAfter variants. Five small text errors, fixed in this doc. |
| Second fact check (OpenAI GPT-5.6, read-only) | sound; wording fixed | Checked the brief against saved sources and run records (no execution): substantively sound; two medium precision errors, three overclaims and seven missing caveats, all addressed. |
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 606 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,606 words
- Registry title
- With doc-values overlays (on by default on main), a second advanceExact on the same document does not restart a multi-valued SORTED_NUMERIC column's values, so SortedNumericSortField sorts return the wrong top hits (or throw)
- Area
- index / doc-values overlays (OverlaySortedNumericDocValues over DocValuesOverlayMerger), seen through search / field sorting (SortedNumericSelector, LongComparator, TopFieldCollector)
- Priority
- Medium, rank 95: Deterministic silent wrong top hits for SortedNumericSortField, on by default only on unreleased main and opt-in behind the experimental overlay in 10.6.0, and only after a single-valued update on a field that is really multi-valued and only until a merge.
- Impact class
- silent wrong results
- Impact note
- Applications that call updateSortedNumericDocValue (or updateDocValues with a SortedNumericDocValuesField) on a SORTED_NUMERIC field whose segment holds at least one multi-valued document, and then sort on that field with SortedNumericSortField (MIN or MAX) or read a document twice through advanceExact, get wrong top hits and wrong sort keys with no error (with MAX sometimes an IndexOutOfBoundsException or EOFException instead) until a merge rewrites the segment; this is the default configuration on main (11.0.0-SNAPSHOT, overlays on by default) and needs the @lucene.experimental setMaxDocValuesOverlays(n > 0) in the released 10.6.0; three documents and one update suffice, while all-single-valued SORTED_NUMERIC fields (the motivating case of #16580) and NUMERIC/BINARY overlays are not affected.
- Summary
- You index three documents with a multi-valued SortedNumericDocValuesField m = {50,60}, {10,90}, {30,40}, set doc 2 to the single value 70 with IndexWriter.updateSortedNumericDocValue (default IndexWriterConfig on main), and sort all documents by new SortedNumericSortField("m", LONG), which keys each document by its minimum value. You expect doc 1 first (minimum 10), which is what you get with overlays disabled. You get doc 2: the overlay view ignores a second advanceExact on the document it already sits on, so when the collector reads doc 1 again its value cursor continues at 90 instead of restarting at 10, and with a MAX sort the cursor can run past the end of the column and throw.
- Introduced
- 169cbffd12f4579ad8bd2906a4b70e2e70130711 (apache/lucene#16581 'Allow single-valued updates for SortedNumericDocValues', by HoustonPutman for their issue #16580; merged on main 2026-09-21; branch_10_6 cherry-pick eaa3a7a9edb the same day) [exec: commit-level bracket with core jars built from each commit: the parent 2165146d01c cannot express the trigger (updateSortedNumericDocValue does not exist: compile error), 169cbffd12f fails (second read of doc 1 = 90, MIN top hit doc 2)]. The defective step is older: DocValuesOverlayMerger.advanceExact, which moves only layers behind the target, comes from #16418 (75eddfaff22, jimczi, 2026-08-13; 10.x backport #16554 = 3a6cd003aaf) and was harmless there because the NUMERIC and BINARY overlay views have no per-document value cursor [read; NUMERIC/BINARY re-reads checked on main: 0 mismatches]. #16581 added OverlaySortedNumericDocValues, whose nextValue() reads the winning layer's cursor [read]. (2026-09-21 (#16581, author and merge date on main and branch_10_6; the merger logic dates from #16418, 2026-08-13))
- First affected version
- 10.6.0 [exec] (only with the @lucene.experimental opt-in IndexWriterConfig#setMaxDocValuesOverlays(n > 0); the 10.6.0 default of 0 overlays is not affected). On main (11.0.0-SNAPSHOT, unreleased) overlays are on by default (DEFAULT_MAX_DOC_VALUES_OVERLAYS = 8).
- Affected versions
- [exec] main 645d4fe14a20 with the default IndexWriterConfig (-ea and -da; finder seeds DEADBEEF, 1, C0FFEE and 5EED42, seed 5EED7A11), the introducing commit 169cbffd12f, and 10.6.0 with setMaxDocValuesOverlays(8) (published jars, SHA-1 verified: the repro fails with -ea, the independent reproduction's two tests fail 2 of 2 with -da, the GPT intended-lens test 2 of 3; also a core jar built from 4ae946e5a9b). Released in 10.6.0: the tag releases/lucene/10.6.0 exists (tag object e29d290eeb05, peels to 4ae946e5a9b; git ls-remote 2026-10-29). [read] upstream main head 716d6f216f5 and branch_10_6 head 7e5aec4370e: DocValuesOverlayMerger.java and OverlaySortedNumericDocValues.java unchanged since #16581 (GitHub commit history re-checked 2026-10-29). Not affected [exec]: 10.6.0 at its default (0 overlays, passes); 10.5.2, 9.12.3 and main before 169cbffd12f (the update API does not exist: compile error). Not affected by this mechanism on any version [exec on main]: NUMERIC and BINARY overlays and all-single-valued SORTED_NUMERIC columns (singleton path), 12,846 re-read checks, 0 mismatches.
- 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: code reading; also reached by Alloy, exhaustive check
- Upstream and prior reports
- none found (2026-10-29, read-only; the prior-report record verdict new): two scripted prior-report searches (each 16 GitHub issue/PR, 3 commit, 2 downstream-repo, 20 mailing-list over dev@, java-user@, issues@ and commits@, 4 JIRA-archive queries and web), 50 manual GitHub search-API queries (apache/lucene, Elasticsearch, OpenSearch, Solr) plus code search for setMaxDocValuesOverlays downstream (0 hits), the changed files of all 409 open apache/lucene PRs and of every PR updated since 2026-10-26 (none touches the overlay classes), the commit history of both fix-site files on main, branch_10x and branch_10_6 (author and committer dates: only #16418 and #16581), the full discussions of #16418, #16581, #16580, #16554, #16550 and the overlay follow-ups, lists.apache.org (dev@, java-user@, issues@, builds@), the JIRA archive and web search; the candidate's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches agree; refresh 2026-10-29: no new item. Context, not reports: #16581 and #16580 (the introducing change; jimczi's review raised five points, none about re-reading the current document on the read-side overlay), #16418 / #16554 / #16550 (overlays and the merger), #16644, #16645, #16648, #16714, #16566, #16872, #16923 (other overlay or doc-values-update items). Precedents of the same defect class in other code, fixed long ago: LUCENE-7890 (#8941, MemoryIndex), LUCENE-8117 (Lucene54 producer); contract sources LUCENE-7462 (#8514), LUCENE-8138 (#9186) and the 2018 java-user thread 'Read DocValue twice'. Tracking issue #16937 and its cross-references do not mention overlays.
- Lucene commit tested
- main
645d4fe14a20(11.0.0-SNAPSHOT, commit of 2026-10-28): the repro test through gradle in a scratch worktree (assertions on and off), stock and with each fix, plus 10 doc-values-update, overlay and sort suites with the fixes; the standalone runner with-ea(seed DEADBEEF) and-da(seeds DEADBEEF, 1, C0FFEE, 5EED42) and with the recommended fix shadowed. 10.6.0 release jars (4ae946e5a9b, SHA-1 checked) at the default and withsetMaxDocValuesOverlays(8); 10.5.2 release jars (compile error: no update API). Upstream main716d6f216f52and branch_10_6 were read on 2026-10-30: the last change toOverlaySortedNumericDocValuesis #16581 on both, toDocValuesOverlayMerger#16418 on main and its backport #16554 on branch_10_6. An independent fact check (2026-10-30) repeated the gradle runs in its own scratch worktree with the same results. Only the default codec (Lucene104), Linux aarch64 and OpenJDK 25.0.3 were run.
- Finding record
lucene/core/src/java/org/apache/lucene/index/DocValuesOverlayMerger.java:22-30 (at 645d4fe14a20) (class javadoc: layers are advanced with advance only, never advanceExact, 'so iteration and random access can be interleaved'), :72-80 (advanceExact: advances only layers whose docID() < target, then picks the winner; a layer already on target keeps its partly consumed value cursor)lucene/core/src/java/org/apache/lucene/index/OverlaySortedNumericDocValues.java:23-28 (at 645d4fe14a20) (works whether the base column is single- or multi-valued), :41-54 (nextValue/docValueCount delegate to the winning layer; advanceExact delegates to the merger), :76-93 (all-single-valued layers take the singleton OverlayNumericDocValues path, else OverlaySortedNumericDocValues)lucene/core/src/java/org/apache/lucene/index/DocValuesIterator.java:24-33 (at 645d4fe14a20) (advanceExact: target 'must be greater than or equal to the current doc ID')lucene/core/src/java/org/apache/lucene/index/SortedNumericDocValues.java:28-38 (at 645d4fe14a20) (nextValue/docValueCount are about 'the current document')lucene/core/src/java/org/apache/lucene/index/SegmentDocValuesProducer.java:224 (at 645d4fe14a20) and ReadersAndUpdates.java:658 (reads of an updated SORTED_NUMERIC field go through OverlaySortedNumericDocValues.from)lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:115 (at 645d4fe14a20) (DEFAULT_MAX_DOC_VALUES_OVERLAYS = 8), :335-348 (setMaxDocValuesOverlays: 'Enabled by default', @lucene.experimental, a read-cost versus write-amplification trade-off)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:2020-2037 (at 645d4fe14a20) (updateSortedNumericDocValue: 'The field itself may be single- or multi-valued')lucene/core/src/java/org/apache/lucene/search/SortedNumericSelector.java:135-142 (at 645d4fe14a20) (MinValue.advanceExact: in.advanceExact + nextValue), :169-174 and :194-201 (MaxValue reads docValueCount values)lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:79-106 (at 645d4fe14a20) (getValueForDoc calls advanceExact(doc); compareBottom, compareTop and copy each call it)lucene/core/src/java/org/apache/lucene/search/TopFieldCollector.java:93, (at 645d4fe14a20) :116 (a competitive hit is read by compareBottom and then copy)lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:2061-2071 (at 645d4fe14a20) (dense SORTED_NUMERIC advanceExact recomputes start/end; nextValue is values.get(start++) with no end check)lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java:1176-1186 (at 645d4fe14a20) (AssertingSortedNumericDocValues.advanceExact accepts target >= in.docID() and resets valueUpto = 0)lucene/memory/src/test/org/apache/lucene/index/memory/TestMemoryIndex.java:328-366 (at 645d4fe14a20) (testDocValues_resetIterator, LUCENE-7890: repeated advanceExact(0) re-reads all SORTED_NUMERIC values)https://github.com/apache/lucene/pull/16581; https://github.com/apache/lucene/issues/16580; https://github.com/apache/lucene/pull/16418https://github.com/apache/lucene/issues/8514 (LUCENE-7462); https://github.com/apache/lucene/issues/8941 (LUCENE-7890); https://lists.apache.org/thread/tm3tvnpfo130zbbdm7roj4m0gvncj6sx (java-user 'Read DocValue twice', 2018)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/SegmentDocValuesProducer.java:218-224a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/OverlaySortedNumericDocValues.java:23-28, :41-54, :76-93a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/DocValuesOverlayMerger.java:22-30, :72-80a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90DocValuesProducer.java:2061-2072a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/comparators/LongComparator.java:79-107; search/SortedNumericSelector.java:135-142, :169-174, :194-201a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/TopFieldCollector.java:93, :116, :122-126
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:2020-2037 (updateSortedNumericDocValue javadoc)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:110-115, :334-348 (default 8, 'Enabled by default', @lucene.experimental)a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:523-527 (GITHUB#16580: 'the field may be single- or multi-valued')a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/ReadersAndUpdates.java:60, :363-368, :370-407, :600-613 (fold once prior delta generations >= maxDocValuesOverlays; a sparse fold keeps the base plus one folded generation; fold-to-dense once the prior deltas' docs-with-value reach 50% of maxDoc clears the overlay)a Lucene clone 4ae946e5a9b (10.6.0) lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:116 (DEFAULT_MAX_DOC_VALUES_OVERLAYS = 0), :343 ('Disabled by default')
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/DocValuesIterator.java:24-33a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/SortedNumericDocValues.java:28-38https://github.com/apache/lucene/issues/8941 (LUCENE-7890); https://github.com/apache/lucene/issues/9165 (LUCENE-8117)a Lucene main checkout (645d4fe14a20) lucene/memory/src/test/org/apache/lucene/index/memory/TestMemoryIndex.java:327-366a Lucene main checkout (645d4fe14a20) lucene/test-framework/src/java/org/apache/lucene/tests/index/AssertingLeafReader.java:1176-1187a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:334-348https://github.com/apache/lucene/pull/16581 (description)a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/index/TestSortedNumericDocValuesUpdates.java (0 advanceExact calls; SortedNumericSortField only in testRejectIndexSortField, :328-331)
- History
https://github.com/apache/lucene/pull/16581; https://github.com/apache/lucene/issues/16580; https://github.com/apache/lucene/pull/16418a Lucene clone: git diff --stat 4ae946e5a9b 645d4fe14a20 -- OverlaySortedNumericDocValues.java DocValuesOverlayMerger.java (empty)
- TestOverlaySortedNumericSort.java (lucene/core test, one method):
repro/LFV-1051/TestOverlaySortedNumericSort.java(62 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and a traced run of the repro's three documents.
assets/LFV-1051/bug.png(606 KB)
