Skip to content

feat(index)!: support covering columns in distributed vector index builds - #8957

Draft
vivek-bharathan wants to merge 4 commits into
lance-format:mainfrom
vivek-bharathan:vb/covering-distributed
Draft

feat(index)!: support covering columns in distributed vector index builds#8957
vivek-bharathan wants to merge 4 commits into
lance-format:mainfrom
vivek-bharathan:vb/covering-distributed

Conversation

@vivek-bharathan

Copy link
Copy Markdown
Contributor

Stacked on #8811 — only the last commit is new here. The first three are under review there.

Carries covering columns through sharded index builds, so a distributed build produces the same
covered index a single-node build does.

BREAKING CHANGE: the four public init_writer_for_{flat,pq,sq,rq} functions in lance-index each
take a new covering_fields argument. Pass an empty slice for the previous behaviour.
VectorStore also gains a required INTERNAL_COLUMNS associated const naming the storage's own
non-covering columns.

@github-actions github-actions Bot added A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI labels Sep 3, 2026
@github-actions github-actions Bot added enhancement New feature or request breaking-change labels Sep 3, 2026
@vivek-bharathan
vivek-bharathan marked this pull request as draft September 3, 2026 00:53
@vivek-bharathan
vivek-bharathan force-pushed the vb/covering-distributed branch 2 times, most recently from 8452481 to d098711 Compare September 3, 2026 14:25
An IVF_PQ index can store the values of chosen extra columns next to its
compressed vectors, and a search returns them directly from the index. A query
whose projection those columns satisfy no longer reads the base table at all.

Each storage format names its own internal columns, so covering detection is a
per-storage filter rather than a per-type special case. Nested fields, blob
columns, duplicates, reserved storage names and non-IVF_PQ index types are
rejected at creation. `fields` is built as `[keyed_id] ++ covering_fields`
wherever an index is created, so every covered index satisfies the suffix rule
`IndexMetadata::validate_covering_fields` enforces at commit, and segments of
one logical index are rejected if they disagree on `covering_fields` -- the read
path derives its output schema from the first segment, so a disagreement yields
a plan no segment can satisfy.

BREAKING CHANGE: `VectorIndexParams` gains a public `covering_columns:
Vec<String>` field. The struct is not `#[non_exhaustive]`, so code constructing
it with an exhaustive struct literal needs one added line.
Covering columns worked only on IVF_PQ. Each storage format now declares which
of its own columns are internal, so anything else is treated as covered payload,
and the creation-time restriction to IVF_PQ is gone. IVF_SQ, RQ, FLAT and the
HNSW variants each get end-to-end coverage.
A covered index now survives the operations that change its data: schema
evolution, overlays, remap, compaction, and concurrent commits. Guards reject
the alterations that would desync covering data -- casting an indexed key
column, and any rename, cast or nullability change of a covering column --
because the read path resolves covering columns from the live schema while
index storage still emits the old name and type.

A partial `merge_insert` that updates a covered column is rewritten as a
row-move on the indexed-scan path, so only the rows it touched leave the
covering index instead of the whole fragment. The move is an optimisation and
never fails the operation: stable row ids, sources carrying inserts, legacy v1
blob columns and partial struct subschemas each fall back to the in-place path,
which is correct for all of them.

Four public surfaces reported a covered index wrongly -- one answering "no index
on this column" for an indexed column, another returning an unrelated column's
centroids. All are the same mistake: `fields` answers "what invalidates this
index", not "what it can serve". The keyed prefix answers the second.
…ilds

A vector index built as separate shards and merged afterwards now carries its
covering columns through instead of rejecting them. Each shard stores its
covered values and the merge step classifies them from the shard schema, using
each storage format's own list of internal column names so a new internal
column is excluded everywhere at once. Both distributed commit styles are
covered by tests that check query results against the base table.

BREAKING CHANGE: the four public `init_writer_for_{flat,pq,sq,rq}` functions in
lance-index each take a new `covering_fields` argument. Pass an empty slice for
the previous behaviour. `VectorStore` also gains a required `INTERNAL_COLUMNS`
associated const naming the storage's own non-covering columns. No compatibility
overloads were added: an empty slice is trivially expressible at the call site,
and carrying a second name for each function would outlive the reason for it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-python Python bindings breaking-change enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant