feat: implement vector index details#6099
feat: implement vector index details#6099wjones127 wants to merge 28 commits intolance-format:mainfrom
Conversation
PR Review: feat: implement vector index detailsP0:
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Previously, vector indices returned index_type "Unknown" and empty details in describe_indices(). This populates VectorIndexDetails at creation time from build params, derives a human-readable index type string (e.g. "IVF_PQ"), serializes details as JSON, and infers details from index files on disk as a fallback for legacy indices. Also changes proto num_bits from Bits enum to uint32 to support RQ's default of 1 bit, and adds rotation_type to RabitQuantization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace imperative serde_json::Map construction with #[derive(Serialize)] structs for clearer, more maintainable JSON serialization. This also adds the missing rotation_type field to RQ compression output. Add snapshot-style unit tests that assert exact JSON strings to guard backwards compatibility of the describe_indices() output format. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, vector index details for legacy indices were only inferred lazily in describe_indices(). This moves inference to load_indices() and migrate_indices(), so details are populated before caching and persisted into new manifest versions. Inference runs once per index name, concurrently. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Also handles the case where index_details is None (very old indices) by checking if the indexed field is a vector type. Moves inference outside the cache-miss branch in load_indices so it also runs on indices that were opportunistically cached during Dataset::open. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix compression oneof field numbers (5,6,7 -> 4,5,6) to avoid gap - Add comment that target_partition_size = 0 means unset - Extract infer_missing_vector_details helper to deduplicate logic between load_indices and migrate_indices Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…etails # Conflicts: # rust/lance/src/index/append.rs # rust/lance/src/index/create.rs # rust/lance/src/index/vector.rs
| // Details for vector indexes. | ||
| message VectorIndexDetails { | ||
| enum VectorMetricType { | ||
| L2 = 0; | ||
| COSINE = 1; | ||
| DOT = 2; | ||
| HAMMING = 3; | ||
| } | ||
|
|
||
| VectorMetricType metric_type = 1; | ||
|
|
||
| // 0 means unset (unknown or not applicable). | ||
| uint64 target_partition_size = 2; | ||
|
|
||
| optional HnswIndexDetails hnsw_index_config = 3; | ||
|
|
||
| message ProductQuantization { | ||
| uint32 num_bits = 1; | ||
| uint32 num_sub_vectors = 2; | ||
| } | ||
| message ScalarQuantization { | ||
| uint32 num_bits = 1; | ||
| } | ||
| message RabitQuantization { | ||
| enum RotationType { | ||
| FAST = 0; | ||
| MATRIX = 1; | ||
| } | ||
| uint32 num_bits = 1; | ||
| RotationType rotation_type = 2; | ||
| } | ||
|
|
||
| // An unset compression oneof means flat / no quantization. | ||
| oneof compression { | ||
| ProductQuantization pq = 4; | ||
| ScalarQuantization sq = 5; | ||
| RabitQuantization rq = 6; | ||
| } | ||
| } | ||
|
|
||
| // Hierarchical Navigable Small World (HNSW) index details, used as an optional configuration for IVF indexes. | ||
| message HnswIndexDetails { | ||
| // The maximum number of outgoing edges per node in the HNSW graph. Higher values | ||
| // means more connections, better recall, but more memory and slower builds. | ||
| // Referred to as "M" in the HNSW literature. | ||
| uint32 max_connections = 1; | ||
| // "construction exploration factor": The size of the dynamic list used during | ||
| // index construction. | ||
| uint32 construction_ef = 2; | ||
| } |
There was a problem hiding this comment.
This representation works for the existing set of vector indices, but wondering if this is good for future plans. The current internal design has the concept of "stages", so you could have something like ivf-ivf-pq or hnsw-pq (no IVF). Some sequence of stages just doesn't make sense, like pq-pq. So I'm not sure the stages representation makes sense.
I was thinking it could be a tree-like system, where IVF and HNSW could have children, but PQ and SQ can't.
What do you think? @BubbleCal @eddyxu
There was a problem hiding this comment.
I've always seen it as three layers. The first is partitioning (IVF is really the only choice here). The second is searching within partition (flat vs hnsw) and the third is quantization (pq, rq, sq, etc.) So I don't think the concept of stages makes sense. For example, I don't see ivf-ivf-pq. I see ivf (layers=2) - pq.
…x.proto These messages belong with other index-related protos. After the move, VectorIndexDetails reuses the existing top-level VectorMetricType enum instead of defining its own nested copy. Rust imports updated from lance_table::format::pb to lance_index::pb throughout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rd, type_url - Fix inverted comment on hnsw_index_config field in index.proto - Use tracing::warn! instead of log::warn! in details.rs - Prefer non-empty index_details when carrying forward in append - Revert describe_indices to original chunk_by pattern - Update Python test type_url to match new proto package Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
westonpace
left a comment
There was a problem hiding this comment.
I'm not 100% sure we can get away with changing the protobuf type URL. Can you create a compatibility test to ensure that old versions can read new indexes created with these new details?
| } | ||
|
|
||
| // Details for vector indexes, stored in the manifest's index_details field. | ||
| message VectorIndexDetails { |
There was a problem hiding this comment.
Where are the IVF details? was it hierarchical? How many partitions in each stage?.
There was a problem hiding this comment.
I'm using VectorIndexDetails here for the IVF stage. I suppose I could make it a substruct to make it clearer. The only parameter right now is target_partition_size.
How many partitions in each stage?
What do you mean by this?
| } | ||
|
|
||
| // Hierarchical Navigable Small World (HNSW) index details, used as an optional configuration for IVF indexes. | ||
| message HnswIndexDetails { |
There was a problem hiding this comment.
Minor nit: I think of "index details" as the top-level message describing a type of index. This is a nested message that cannot stand on its own so maybe just HnswParameters?
| assert info.type_url == "/lance.table.VectorIndexDetails" | ||
| # This is currently Unknown because vector indices are not yet handled by plugins | ||
| assert info.index_type == "Unknown" | ||
| assert info.type_url == "/lance.index.pb.VectorIndexDetails" |
There was a problem hiding this comment.
Regrettably, I do not think we can change the type URL for backwards compatibility reasons.
There was a problem hiding this comment.
I can move that back if it's necessary.
| // Carry forward existing index details, preferring the first segment | ||
| // that has populated (non-empty) details. |
There was a problem hiding this comment.
I think this is a safe assumption but we are assuming all segments have the same details right?
There was a problem hiding this comment.
Should be. Ideally, we would create some top-level index configuration that is deduplicated across segments, but that's a complex format change for another day.
| use vector::details::{ | ||
| derive_vector_index_type, infer_missing_vector_details, vector_details_as_json, | ||
| }; | ||
| pub(crate) use vector::details::{vector_index_details, vector_index_details_default}; |
There was a problem hiding this comment.
Should these be at the top of the file?
There was a problem hiding this comment.
Yeah I should move those.
We have one here that goes back to 0.29.1.beta2: lance/python/python/tests/compat/test_vector_indices.py Lines 25 to 32 in db07e77 I wonder if we need to check earlier than that to see incompatabilities. |
|
Should be able to implement this TODO: lance/rust/lance/src/dataset/scanner.rs Lines 3422 to 3433 in de393a2 |
|
@westonpace I had claude go through and see if this breaks backwards compat as is. Other than the type_url, it claims that the indexes don't have different compatibility from what's on |
Previously, `scanner.rs:3425` called `open_vector_index` just to get the metric type for metric compatibility checks. This required expensive deserialization of the index file. Now we read the metric type directly from `IndexMetadata.index_details` (a `VectorIndexDetails` proto) added on the feat/vector-index-details branch. This provides a fast path for newer indices without I/O. For legacy indices without populated details (empty proto value bytes), we fall back to the original expensive path. Adds `metric_type_from_index_metadata` helper in `details.rs` that: - Returns `None` for missing or empty details (legacy indices) - Converts `VectorIndexDetails.metric_type` to `DistanceType` for populated details - Uses the existing `From<VectorMetricType> for DistanceType` impl Changes `matching_index` tuple from `(index, idx, index_metric)` to `(index, index_metric)` since `idx` is only used in the fallback path. Fixes lance-format#5231 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
| } | ||
|
|
||
| // An unset compression oneof means flat / no quantization. | ||
| oneof compression { |
There was a problem hiding this comment.
maybe worth adding index_version for each one.
Found it's complicated for vector index to maintain compatibility when introducing breaking changes because it's hard to get the index version.
The other scalar index has index_version already today.
- Rename HnswIndexDetails -> HnswParameters (westonpace) - Move imports to top of index.rs (westonpace) - Add index_version field to VectorIndexDetails proto (BubbleCal) - Add explicit FlatCompression message instead of using unset oneof (westonpace) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e into feat/vector-index-details
…etails # Conflicts: # rust/lance/src/index/append.rs # rust/lance/src/index/vector/ivf.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VectorIndexDetails moved from table.proto to index.proto, so the Python binding needs to reference lance_index::pb instead of lance_table::format::pb. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a `map<string, string> runtime_hints` field to `VectorIndexDetails` for storing optional build preferences that don't affect index structure (e.g., KMeans iterations, shuffle concurrency, GPU accelerator). These are needed so a background index rebuild process can reproduce the original build configuration. Keys use reverse-DNS namespacing: `lance.*` for core Lance hints, `lancedb.*` for LanceDB-specific hints (e.g., `lancedb.accelerator`). Runtimes that don't recognize a key must silently ignore it. Only non-default values are written to keep the map minimal. Also adds `apply_runtime_hints()` to read hints back into build params. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uild Adds `vector_params_from_details()` to reconstruct a full `VectorIndexParams` from stored `VectorIndexDetails` (core spec fields + runtime hints). This enables future index rebuild logic to reproduce the original build config from the manifest without re-opening index files. Also wires the `lance.skip_transpose` hint into `optimize_vector_indices_v2` so incremental rebuilds honour the original skip_transpose preference rather than silently reverting to false on each append. Adds Python tests validating that non-default build params appear as `runtime_hints` in `describe_indices()` output, and that default values are omitted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- `python/src/dataset.rs`: `max_iters` kwarg was not forwarded to `IvfBuildParams`/`PQBuildParams` in `prepare_vector_index_params`, so it was silently ignored and never stored as a runtime hint - `python/src/indices.rs`: wrong proto path for `VectorIndexDetails` (`lance_table::format::pb` → `lance_index::pb`) - `java/lance-jni/src/utils.rs`: missing `runtime_hints` field in `VectorIndexParams` struct literal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@westonpace @BubbleCal do you want to take another look? I changed up the format a little bit to handle other index parameters. |
|
|
||
| #[derive(Serialize)] | ||
| #[serde(tag = "type", rename_all = "lowercase")] | ||
| enum CompressionDetailsJson { |
| StageParams::Hnsw(hnsw) => { | ||
| if let Some(raw) = hints.get("lance.hnsw.prefetch_distance") { | ||
| hnsw.prefetch_distance = if raw == "none" { | ||
| None |
There was a problem hiding this comment.
IIRC the default is Some(2)
There was a problem hiding this comment.
nvm, just noticed this is setting it to None
Cache vector index configuration within the index metadata, such as the distance type and build parameters.
Previously, to determine things like the distance type or index type of a vector index, the index file itself had to be opened. This PR stores that information in
VectorIndexDetailswithin the manifest'sindex_detailsfield, which is fetched and cached eagerly when loading the manifest.Old indexes have this field left blank. When blank, the details are extracted from the index files and cached. This migration happens on the first write with a new library version.
What's stored in VectorIndexDetails
Core build parameters (typed fields — required for any runtime to build the index):
metric_typetarget_partition_size(IVF)hnsw_index_config—max_connections,construction_ef,max_level(HNSW)compression— PQ/SQ/RQ/flat, includingnum_bits,num_sub_vectors,rotation_typeindex_versionRuntime hints (
map<string, string> runtime_hints):Optional build preferences that don't affect index structure. Stored so a background rebuild process can reproduce the original configuration. Runtimes that don't recognize a key must silently ignore it. Only non-default values are written.
Keys use reverse-DNS namespacing:
lance.*for core Lance hints, other prefixes for runtime-specific hints (e.g.,lancedb.acceleratorfor GPU acceleration in LanceDB Enterprise).Current
lance.*hints:lance.ivf.max_iters,lance.ivf.sample_rate,lance.ivf.shuffle_partition_batches,lance.ivf.shuffle_partition_concurrency,lance.pq.max_iters,lance.pq.sample_rate,lance.pq.kmeans_redos,lance.sq.sample_rate,lance.hnsw.prefetch_distance,lance.skip_transpose.Also adds
apply_runtime_hints()to read hints back into build params for future rebuild logic.Closes #5963