feat(index)!: covering ("included") columns for IVF vector indexes - #7566
feat(index)!: covering ("included") columns for IVF vector indexes#7566vivek-bharathan wants to merge 7 commits into
Conversation
|
Important This PR touches the Lance format specification. Substantive changes to the format specification — the If this is a meaningful format change:
|
700c05d to
1a4e0bd
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
1a4e0bd to
52234c9
Compare
52234c9 to
6be3150
Compare
📝 WalkthroughWalkthroughThe PR adds ChangesCovering-column support
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Dataset
participant IvfIndexBuilder
participant ProductQuantizationStorage
participant ANNIvfSubIndexExec
Dataset->>IvfIndexBuilder: configure include_columns
IvfIndexBuilder->>ProductQuantizationStorage: persist row_id, codes, covering columns
ANNIvfSubIndexExec->>ProductQuantizationStorage: search partitions
ProductQuantizationStorage-->>ANNIvfSubIndexExec: covering batch
ANNIvfSubIndexExec-->>Dataset: distance, row_id, covering columns
Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rust/lance/src/dataset/scanner.rs`:
- Around line 4012-4027: Add a regression test near the existing IVF_PQ scanner
tests that configures a non-empty include_columns payload field, creates indexed
data, appends additional unindexed rows, and runs the combined ANN/flat scan.
Assert the result schema includes the covering column and verify its values for
both indexed and unindexed rows, exercising the columns logic before
topk_appended projection.
In `@rust/lance/src/dataset/schema_evolution.rs`:
- Around line 3247-3255: Update the negative tests
test_drop_columns_fails_on_covered_column and
test_alter_columns_rename_nullable_fail_on_covered_column to assert the returned
error matches Error::InvalidInput { .. } before checking its message contents.
Preserve the existing assertions for the column, index, and remediation text,
covering each failure case rather than relying solely on err.to_string().
In `@rust/lance/src/index/vector.rs`:
- Around line 1901-1910: Update the source-field resolution in the index-copy
logic around included_columns to fail when any source_index.included_fields ID
is absent from source_dataset.schema(), rather than silently dropping it with
filter_map. Mirror the target-side resolution and error handling used in the
corresponding target-field block around the existing target resolution logic,
preserving the resolved field names only when all advertised fields are valid.
In `@rust/lance/src/index/vector/ivf.rs`:
- Line 577: Restrict included_columns propagation in the IVF rebuild paths
around the with_include_columns calls to only (SubIndexType::Flat,
QuantizationType::Product), passing an empty list or rejecting validation for
all other index formats, including binary IVF_FLAT, IVF_SQ, IVF_RQ, IVF_HNSW_SQ,
and IVF_HNSW_PQ. Add a regression test covering that non-IVF_PQ rebuilds do not
retain covering columns.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 66b72a0d-503d-4280-9d5b-e80f0194841d
📒 Files selected for processing (32)
java/lance-jni/src/transaction.rsjava/lance-jni/src/utils.rsprotos/table.protopython/src/indices.rspython/src/transaction.rsrust/lance-index/src/vector/pq/storage.rsrust/lance-index/src/vector/storage.rsrust/lance-namespace-impls/src/dir/manifest.rsrust/lance-table/src/format/index.rsrust/lance/src/dataset/cleanup.rsrust/lance/src/dataset/mem_wal/index.rsrust/lance/src/dataset/mem_wal/memtable/flush.rsrust/lance/src/dataset/optimize.rsrust/lance/src/dataset/optimize/remapping.rsrust/lance/src/dataset/scanner.rsrust/lance/src/dataset/schema_evolution.rsrust/lance/src/dataset/transaction.rsrust/lance/src/dataset/write/merge_insert.rsrust/lance/src/index.rsrust/lance/src/index/append.rsrust/lance/src/index/create.rsrust/lance/src/index/frag_reuse.rsrust/lance/src/index/mem_wal.rsrust/lance/src/index/scalar.rsrust/lance/src/index/scalar/btree.rsrust/lance/src/index/vector.rsrust/lance/src/index/vector/builder.rsrust/lance/src/index/vector/details.rsrust/lance/src/index/vector/ivf.rsrust/lance/src/index/vector/ivf/v2.rsrust/lance/src/io/commit/conflict_resolver.rsrust/lance/src/io/exec/knn.rs
6be3150 to
4fa3e60
Compare
96dc6d0 to
33db682
Compare
310fde1 to
45f060c
Compare
45f060c to
8c27317
Compare
…a contract An index can now declare extra "covering" columns whose values it will store alongside the indexed data. This commit adds only the metadata surface: the new field on index metadata, the matching option on index creation parameters, and round-trip support in the Python and Java bindings. A new writer feature flag makes older versions of Lance refuse to modify datasets that use covering columns instead of silently losing the declaration. Nothing builds or reads covering columns yet. BREAKING CHANGE: index metadata and vector index creation parameters each gain a new required field, so code that constructs them directly needs a one-line addition. `apply_feature_flags` now takes a `FeatureFlagsConfig` instead of two booleans: whether a commit needs the covering fence depends on the index list accompanying it, which the old signature had no way to see.
IVF_PQ indexes can now store the values of chosen extra columns next to the compressed vectors, and searches return those values directly from the index. A query that only needs covered columns no longer reads the base table at all. Invalid choices such as nested fields, blob columns, duplicates, and reserved names are rejected at creation time, and other index types reject covering for now. Rows that match a filter but have no index entry are still returned correctly.
Covering columns now work on every IVF vector index type, not just IVF_PQ. Each storage format declares which of its columns are internal, so anything else can be treated as covered payload. The creation-time restriction to IVF_PQ is removed, and each type gets its own end-to-end tests.
When data in a covered column changes, the index must stop serving its stored copy of that value. This commit wires that rule through every path that can change data: updates, merges, in-place replacements, overlays, schema changes, and concurrent commits. Changing a single subfield of a covered struct counts as changing the struct. Commits that would drop or reshape a covered column, or leave an index's declaration out of sync with its stored data, are rejected outright.
…ng row-moves Updating a covered column through a partial merge used to patch the value in place, leaving the index holding the old copy until the next optimize. Such updates now move the affected rows instead: the old row is deleted and the updated one re-inserted, which automatically hides the stale copy, matching how the regular update path behaves. The move is streamed in small batches so large updates do not need to hold everything in memory. Cases that cannot be moved safely yet are rejected with a clear error.
…ilds
Distributed index builds, where shards are built separately and then merged,
previously rejected covering columns. Each shard now stores the covered values
and the merge step carries them into the combined index instead of silently
dropping them. Both distributed commit styles are covered by tests that check
query results against the base table. Merging RaBitQ shards remains broken for
an unrelated pre-existing reason.
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. No compatibility overloads were added: an empty-slice
default is trivially expressible at the call site, and carrying a second name
for each function would outlive the reason for it.
… APIs Python and Java could already read back an index's covering declaration but could not create a covered index. Both create APIs now accept a list of columns to cover and pass it through to the Rust core, which performs all validation. Tests confirm an index created from each language reports its covered column.
8c27317 to
75c5b1e
Compare
There was a problem hiding this comment.
The change closes the base-table take at the right boundary, and its metadata fencing plus field-aware lifecycle handling preserve covered values across updates, remaps, rebuilds, and distributed merges.
ANN execution currently loads and materializes every declared included field on every query, even when the projection does not use it. Wide string or struct payloads can therefore regress I/O and memory for unrelated searches. Keep include_columns limited to small, frequently returned fields and benchmark both used and unused projections; if broader payloads are expected, pass the requested field subset into partition loading.
Description
Lets a vector index materialize extra columns alongside its quantization codes, so a query whose projection is fully covered is answered from the index with no take against the base table.
The index metadata gains
included_fields(table.protofield 11), and a covered query drops the base-table take from the plan.Scope
Covering works on all seven IVF variants —
IVF_PQ,IVF_SQ,IVF_RQ,IVF_FLAT,IVF_HNSW_PQ,IVF_HNSW_SQ,IVF_HNSW_FLAT. Each quantizer storage carries its owncovering_field_indices()override so its internal code/factor columns are never mistaken for payload.Creation is available from Rust, Python, and Java; validation stays centralized in the Rust core.
Supported across the index lifecycle: append/optimize (merge, append, retrain), compaction and row-id remap, partition split/join, distributed sharded builds (per-shard commit and cross-shard merge), schema evolution, and concurrent-commit conflict resolution.
Rejected up front, with a clear error
_rowid,_distance,__ivf_part_id, quantizer code/factor columns), duplicates, blob columnsmerge_insertof a covered column on a stable-row-id dataset, combined with inserts, or on legacy v1 blob columns — provide the full target schemainclude_columnstogether with precomputed shuffle buffers (those buffers do not carry the payload)Format change
New optional proto field
IndexMetadata.included_fields(field 11) plus a writer feature flag,FLAG_COVERED_INDEX_METADATA(bit 128).The flag exists because
prostsilently drops unknown fields: a writer predating this change would re-serialize the index section withoutincluded_fieldswhile the index files still physically hold the payload columns, leaving storage the manifest no longer describes. The flag is writer-only on purpose — a reader that ignoresincluded_fieldssimply falls back to a base-table take, which is correct, so fencing readers would turn a read-side optimization into a fleet-wide break.Breaking changes
IndexMetadatagains a requiredincluded_fieldsfieldVectorIndexParamsgains a requiredinclude_columnsfieldapply_feature_flagskeeps its name but takes(&mut Manifest, &FeatureFlagsConfig)instead of(&mut Manifest, bool, bool)init_writer_for_{flat,pq,sq,rq}inlance-indexgain a trailingcovering_fields: &[FieldRef]parameter (pass&[]for the previous behavior)