Skip to content

Backport incremental doc-values updates to 10.x (#16418) - #16554

Open
jimczi wants to merge 3 commits into
apache:branch_10xfrom
jimczi:backport_16418_branch_10x
Open

Backport incremental doc-values updates to 10.x (#16418)#16554
jimczi wants to merge 3 commits into
apache:branch_10xfrom
jimczi:backport_16418_branch_10x

Conversation

@jimczi

@jimczi jimczi commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Backport of #16418 (incremental doc-values updates) to branch_10x.

Opening this mostly to confirm we're ok backporting it to 10.x, since it touches the segments file format.

What it does

A set-only doc-values update (NUMERIC or BINARY) is written as a sparse "delta" overlay holding just the updated docs, layered over the base column at read time instead of rewriting the whole column. Turns per-update write amplification from O(column) into O(updated docs).

Unlike on main, it's opt-in on 10.x (DEFAULT_MAX_DOC_VALUES_OVERLAYS = 0), since it's a new feature landing in a minor. Enable with IndexWriterConfig#setMaxDocValuesOverlays(n).

The thing worth confirming for 10.x

The segments file format gets a new version, SegmentInfos.VERSION_10_6, written unconditionally on every commit (same as main). So any index written by 10.6+ is rejected by older 10.x readers with IndexFormatTooNewException, whether or not it uses the feature.

I kept the bump unconditional to match main rather than gating it on overlay presence. It follows the forward-only policy (older Lucene never promises to read a newer index) and matches the VERSION_86 precedent, which was itself introduced in 8.6. Flagging it since a format bump for every index in a minor is the part most worth a second opinion.

Differences from the main commit

  • Feature is opt-in (DEFAULT_MAX_DOC_VALUES_OVERLAYS = 0 instead of 16).
  • VERSION_11_0 renamed to VERSION_10_6 (same value, named for the release it lands in).
  • SegmentDocValuesProducer#getSkipper keeps throws IOException here, since DocValuesProducer#getSkipper still declares it on 10.x (main dropped it in an unrelated change).
  • The standalone benchmark is not backported (it will be removed from main separately).

Otherwise a straight cherry-pick of 75eddfa, plus the follow-up coverage from #16550 (soft deletes over the overlay, addIndexes(Directory) carry-over, and an explicit checkIndex on the deep fold path).

Plan is to let it bake on main a bit longer before this merges, so no rush here, just want the backport signed off.

@ChrisHegarty ChrisHegarty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM


static final int VERSION_CURRENT = VERSION_86;
/** The version that records per-field incremental doc-values overlay generations. */
public static final int VERSION_10_6 = 11;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issue here, but for the main branch, maybe we can rename the constant field, from VERSION_11 to VERSION_10_6 - after this PR is merged?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came here to say exactly that, +1

jimczi added 3 commits August 27, 2026 11:01
When you update a doc-values field today, Lucene rewrites the whole column for that field, even if you only touched a few docs. So a tiny change writes a lot, and it gets worse as the segment grows.

The idea is simple: when an update only sets values (no unset), write just the changed docs as a sparse "delta"
generation, and stack the deltas on top of the base column at read time, newest wins. Updating a field becomes
`O(changed docs)` instead of `O(column)`.

Deltas would pile up, so there's a small lifecycle per field:

- every flush writes a delta with only the changed docs
- too many deltas -> fold them into one sparse generation
- deltas end up covering the whole column -> fold back to a single dense column
- a merge flattens everything back to a normal column

Numeric and binary only, set-only (a value unset falls back to the current dense rewrite). No doc-values format
change, the deltas use the codec's existing sparse encoding.

An old Lucene can't read the overlay, so this rolls the segments format version (`VERSION_11_0`), the same way past
segments-file changes did (like the SegmentCommitInfo id in 8.6): every commit written by this version uses it, and an older reader rejects the index up front with `IndexFormatTooNewException` instead of reading a delta as if it was the whole column. Reading a newer index with an older Lucene isn't in the contract anyway (Lucene is forward-only), so nothing is lost. On `main` the feature is on by default; pass `IndexWriterConfig#setMaxDocValuesOverlays(0)` for the classic full-column rewrite (0 = off, N = keep up to N overlays before a fold). A 10.x backport would be opt-in via
that default.

(cherry picked from commit 75eddfa)
Cover the soft-deletes field riding the overlay fold lifecycle, the
addIndexes(Directory) copy-as-is carry-over, and an explicit checkIndex
on the deep sparse-fold-over-dense-base path.

(cherry picked from commit c026d00)
updateBinaryDocValue lacked the index-sort guard that updateNumericDocValue and the varargs updateDocValues already have. An index sort field is never binary, so the check goes before the doc-values-type validation, which would otherwise mask it with a less clear type-mismatch error.
@jimczi
jimczi force-pushed the backport_16418_branch_10x branch from de5413e to 960181a Compare August 27, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants