Skip to content

feat(bindings): build and inspect covered indexes from Java and Python - #8959

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

feat(bindings): build and inspect covered indexes from Java and Python#8959
vivek-bharathan wants to merge 7 commits into
lance-format:mainfrom
vivek-bharathan:vb/covering-bindings

Conversation

@vivek-bharathan

Copy link
Copy Markdown
Contributor

Stacked on #8958 — only the last commit is new here.

Builds and inspects a covered index from Python (covering_columns= on create_index) and Java
(VectorIndexParams.Builder.setCoveringColumns), and reports the covering columns on index
metadata in both.

Carries the spec commit from #8856 (under PMC vote) via the stack, hence the format-change label;
it drops out on rebase once #8856 merges.

@github-actions github-actions Bot added A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-format On-disk format: protos and format spec docs format-change A change to the format spec, which requires a vote. Remove if minor (e.g. fixing typo). labels Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Important

Format specification vote

This PR modifies the Lance format specification, so it requires 3 binding +1 votes from PMC members (excluding the proposer) and a minimum 72-hour voting period, weekends excluded, before it can merge. Vote by approving this PR (+1) or requesting changes (−1, a veto). See the voting process.

Status: ❌ Blocked — 0 of 3 required approvals

Approvals (this commit) none (0/3)
Vetoes none
Voting period ends Tue 2026-09-08 00:44 UTC (Mon 17:44 PDT)

Updated automatically by the format-spec vote gate, which re-checks every 15 minutes — just voted? Re-check now (press Run workflow; leave the input blank to re-check every open format PR). A PMC member may apply the format-waived label to waive the vote for a trivial edit (typo, wording, formatting).

@lance-gatekeeper lance-gatekeeper Bot 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.

Gate recommendation: request changes.

This head still transitively lands the unmerged carried-column storage and query-wire contract from #8856 together with its implementation. Lance’s stable-format process requires that spec/proto change to land as its own PMC-approved PR, so #8959 cannot safely be accepted first.

Land #8856, then land the complete implementation represented by #8958 as one merge unit—including source-field stamping and per-segment capability verification—and rebase this bindings-only commit on that result. This preserves fallback safety and avoids exposing incomplete intermediate stack states.

Please mark this PR with the breaking-change label.

@lance-gatekeeper lance-gatekeeper Bot added the K-changes Latest Gatekeeper recommendation requests changes. label Sep 3, 2026
@vivek-bharathan
vivek-bharathan marked this pull request as draft September 3, 2026 00:53
@lance-gatekeeper lance-gatekeeper Bot removed the K-changes Latest Gatekeeper recommendation requests changes. label Sep 3, 2026
@vivek-bharathan
vivek-bharathan force-pushed the vb/covering-bindings branch 2 times, most recently from 65d44c5 to 5a638b8 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.
…ed column

lance-format#8535 declared `IndexMetadata.covering_fields` but left the physical side unspecified;
this specifies it -- carried values are extra columns in `auxiliary.idx`, discovered by
exclusion against the quantizer's internal columns and bound to their source fields by a
new `covering_field_ids` metadata key, with no `index_version` bump.

It also permits an index to carry a column it is also keyed on, the only case where an id
repeats in `fields`, so readers must take the carried set from `covering_fields` rather
than subtract the keyed prefix.

The "Current state" note becomes a rule about verifying each segment rather than a snapshot
of which writers exist, so it stays accurate as implementations land instead of needing an
edit to this spec each time one does.

Adds `VectorQueryProto.covering_projection` (field 15) to reserve the tag, with the one
initializer the new field forces on `query_to_proto`; no writer emits carried values yet,
so the implementation follows separately.
A covered index materialised every column it carries on every query and loaded
those columns with every partition it probed, caching them beside the
quantization codes -- so a query touching none of them still paid for all of
them twice, and a wide covering column evicted the codes it shared a cache entry
with. This narrows both reads: the scanner declares which covering columns a
query actually reads, and partition loads now fetch only the storage's own
columns, with covering values read by position for the rows that survive scoring
and not cached.

BREAKING CHANGE: `lance_index::vector::Query` gains a `covering_projection`
field. Construct it with `..Default::default()`, or pass `None` for the previous
behaviour.

BREAKING CHANGE: `IvfPq::load_partition_storage` gains a required `columns:
PartitionColumns` parameter, between `partition_id` and `io_stats`. Partition
loads now read only the storage's own internal columns by default, so a caller
must say which set it wants: `PartitionColumns::Internal` reproduces the new
default and `PartitionColumns::All` reproduces the previous behaviour of loading
the covering columns too.
Covering was reachable only from Rust: both bindings could read an index's
`covering_fields` back, but neither could create a covered index, because both
create paths passed an empty list.

Java gains `VectorIndexParams.coveringColumns`, read through JNI into the Rust
params. Python gains an `covering_columns` argument on `create_index`,
`create_index_uncommitted` and the shared implementation they both funnel
through -- all three, since threading only some of them leaves a path that
silently builds an uncovered index. Passing it where it cannot be honoured now
raises instead of being dropped.

Python also surfaces covering when reading an index back: the ids an index
carries, their names resolved against the current schema, and both in the
description's repr, so a covered and an uncovered index no longer print
identically.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-format On-disk format: protos and format spec docs A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-python Python bindings enhancement New feature or request format-change A change to the format spec, which requires a vote. Remove if minor (e.g. fixing typo).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant