Skip to content

[WIP] IVF Index: Insert and Search - #1187

Draft
Aditya Krishnan (arkrishn94) wants to merge 47 commits into
mainfrom
u/adkrishnan/ivf-index
Draft

Aditya Krishnan (arkrishn94) wants to merge 47 commits into
mainfrom
u/adkrishnan/ivf-index

Conversation

@arkrishn94

@arkrishn94 Aditya Krishnan (arkrishn94) commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

I am still working on adding documentation, testing and cleaning up some parts of this. If you're interested, please take a look at traits.rs and index.rs to see the entry points for the algorithm.

Introduces diskann::ivf with the IvfIndex wrapper, five core glue traits (ListAccessor, SearchAccessor, SearchStrategy, InsertAccessor, IvfInsertStrategy), an example list-owned IVF-Flat provider, an exact oracle harness, and a test suite covering correctness, concurrency, inserts (including concurrent inserts), and error escalation.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.97727% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.51%. Comparing base (d9ce362) to head (b70b235).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
diskann/src/ivf/test/provider.rs 88.95% 39 Missing ⚠️
diskann/src/ivf/test/cases/insert.rs 96.69% 4 Missing ⚠️
diskann/src/ivf/test/cases/correctness.rs 94.91% 3 Missing ⚠️
diskann/src/ivf/index.rs 97.75% 2 Missing ⚠️
diskann/src/ivf/test/cases/errors.rs 96.66% 2 Missing ⚠️
diskann/src/ivf/test/harness.rs 97.95% 2 Missing ⚠️
diskann/src/ivf/test/cases/concurrency.rs 98.73% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1187      +/-   ##
==========================================
+ Coverage   89.47%   89.51%   +0.03%     
==========================================
  Files         486      495       +9     
  Lines       92161    93050     +889     
==========================================
+ Hits        82458    83290     +832     
- Misses       9703     9760      +57     
Flag Coverage Δ
miri 89.51% <93.97%> (+0.03%) ⬆️
unittests 89.16% <93.97%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
diskann/src/ivf/test/cases/mod.rs 100.00% <100.00%> (ø)
diskann/src/ivf/test/cases/concurrency.rs 98.73% <98.73%> (ø)
diskann/src/ivf/index.rs 97.75% <97.75%> (ø)
diskann/src/ivf/test/cases/errors.rs 96.66% <96.66%> (ø)
diskann/src/ivf/test/harness.rs 97.95% <97.95%> (ø)
diskann/src/ivf/test/cases/correctness.rs 94.91% <94.91%> (ø)
diskann/src/ivf/test/cases/insert.rs 96.69% <96.69%> (ø)
diskann/src/ivf/test/provider.rs 88.95% <88.95%> (ø)

... and 18 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread diskann/src/ivf/index.rs Outdated
}

/// Insert a vector under external id `id`.
pub fn insert<'a, S, T>(

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.

Do we also want delete in this interface?

Comment thread diskann/src/ivf/index.rs Outdated
context: &'a P::Context,
id: &P::ExternalId,
vector: T,
) -> impl SendFuture<ANNResult<()>>

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.

Should we have the concept of an index that is read-only? I am thinking of designs where the index is loaded into memory for maintenance and then queries are served from a slower provider. In that case, we don't necessarily want to support insert/delete in the slower provider.

Comment thread diskann/src/ivf/index.rs Outdated
pub fn knn_search<'a, S, T, OB>(
&'a self,
k: NonZeroUsize,
nprobe: usize,

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.

Should nprobe be a NonZeroUsize that we enforce is greater than or equal to k?

Also, how will this contract deal with parameters used for searching the centroid index (i.e. L_search)? Maybe it should take in a type CentroidSearchParams or something similar instead

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.

Well, >= k maybe doesn't make sense, but maybe n_probe * min_size >= k? That would, however, mean we needed to enforce the minimum size strictly.

Comment thread diskann/src/ivf/index.rs Outdated

let k = k.get();
let mut queue = NeighborPriorityQueue::new(k);
let mut cmps: u32 = 0;

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.

We should also count distance comparisons done to select centroids, right?

Comment thread diskann/src/ivf/glue.rs Outdated
}

/// Per-call factory for IVF insert.
pub trait InsertStrategy<'a, Provider, T>: Send + Sync

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.

Trying to understand the vision for the dynamic case here--is the idea that we will later extend this to deletes, at which point we call it something like DynamicStrategy, or that there will be a separate DeleteStrategy object?

Comment thread rfcs/01187-incremental-ivf.md Outdated

After a maintenance accessor returns `Ok(())` from `apply`:

1. Every visible point is assigned to exactly one live list.

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.

This seems like something we do not want to deeply commit to in our architecture. Replication of points is a pretty standard technique in IVF.

Comment thread rfcs/01187-incremental-ivf.md Outdated
2. `assignment[point] == list` if and only if `point` is a member of `list`.
3. Every live list has exactly one live centroid with the same logical id.
4. Every list member has an available canonical vector and scan payload.
5. Retired list ids are never reused for a different centroid.

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.

Why can't we reuse list ids? This seems like something we might want to do.

Comment thread rfcs/01187-incremental-ivf.md Outdated
5. Retired list ids are never reused for a different centroid.
6. Point identity mappings agree with point visibility in the partition.
7. Inserts may trigger splits but not dissolves.
8. Deletes may trigger dissolves but not splits.

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.

Do we want to commit that deletes can't trigger splits? I think this is a bit contrary to some of our discussions on a batching API, and iirc we did at least one experiment that shows that this policy would degrade recall.

Comment thread diskann/src/ivf/dynamic.rs Outdated
Comment on lines +181 to +189
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PointMove<Id, ListId> {
/// Internal point id being inserted, deleted, or reassigned.
pub id: Id,
/// Previous list, or `None` for a newly inserted point.
pub from: Option<ListId>,
/// New list, or `None` for a deleted point.
pub to: Option<ListId>,
}

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.

It might make debugging/reading code easier if we made this an enum over delete, insert, and reassign, but just a thought

Comment thread rfcs/01187-incremental-ivf.md Outdated
### Split Semantics

A split retires one parent and installs two children, for a net increase of one
live cluster. Parent ids are never reused. Candidate centroids for a region are

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.

Why can't parent ids be reused?

Comment thread rfcs/01187-incremental-ivf.md Outdated
current assignment.
2. Stage deletion of each point id.
3. Group deletes by list and compute projected post-delete sizes.
4. Admit underfull victims without crossing `min_clusters`.

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.

Is a victim a centroid that is being deleted, or the points in the deleted centroid's posting list? What does it mean to "admit" them?

Comment thread rfcs/01187-incremental-ivf.md Outdated
centroid cannot make a point already assigned to another surviving centroid
prefer the removed centroid. Deletes never trigger a split in the same
operation. A survivor above the split threshold remains eligible for the next
insert-driven split.

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.

We should add a section for a combined insert/delete batch API since that will likely be necessary. Also, I don't think we should allow deletes to over-fill a posting list. In fact, we should maintain an invariant that posting list updates passed via the PartitionUpdate can never exceed the maximum number of points. We can be flexible on the minimum number of points, but by being inflexible on the max we maintain openness to providers that do a fixed allocation for each posting list.

Comment thread rfcs/01187-incremental-ivf.md Outdated
- Design blob coordination separately rather than forcing disk and blob through
one generic publication protocol.

## Testing Strategy

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.

We should add some integration tests with exact baselines using the same framework as is used for DiskANN.

Comment thread diskann/src/lib.rs
// Index Implementations
pub mod flat;
pub mod graph;
pub mod ivf;

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.

Meta-question, do we want a separate diskann-ivf crate or do we want inclusion within diskann?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great question. Not quite sure. If diskann is going to be the home for the algorithms, then maybe it should live there?

Comment thread diskann/src/ivf/dynamic.rs Outdated
/// Errors from list selection or scanning.
type Error: ToRanked + Debug + Send + Sync + 'static;

/// Select exactly the available requested number of lists for the bound query.

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.

This should probably also return some kind of stats object

Comment thread diskann/src/ivf/dynamic.rs Outdated
///
/// The centroid catalog is authoritative. Approximate implementations, such as
/// a graph navigator, must retain enough catalog information to reject retired
/// ids and recover with exact selection when navigation cannot produce a usable

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.

We expect a graph index to handle fallback to flat scan? Why is that? That's not typical for DiskANN in general, is there a reason it would be needed for an IVF centroid index?

Co-authored-by: Magdalen Manohar <magdalen@magdalen.localdomain>
Comment thread diskann/src/ivf/online/batch.rs Outdated
/// Staged points, their canonical vectors, and their routes.
///
/// Batch position `i` refers to `ids[i]`, row `i` of `vectors`, and `routes[i]`.
pub(in crate::ivf) struct StagedBatch<Id, L> {

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.

What is the difference between pub(in crate::ivf) and pub(crate) here?

@arkrishn94 Aditya Krishnan (arkrishn94) changed the title [WIP] IVF index interface [WIP] IVF Index: Insert and Search Oct 27, 2026
Comment thread diskann/src/ivf/index.rs
///
/// - [`ReassignCount::None`] corresponds to no neighborhood gathering.
/// - [`ReassignCount::Count`] coresponds to a non-zero neighborhood size to be gathered.
pub reassign_count: ReassignCount,

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.

Why should we allow this to be zero?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In case we want to do pure splits, without any neighborhood re-assignment.

Comment thread diskann/src/ivf/index.rs
/// - [`ReassignCount::Count`] coresponds to a non-zero neighborhood size to be gathered.
pub reassign_count: ReassignCount,
/// Positive number of Lloyd steps per parent.
pub split_iterations: NonZeroUsize,

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.

Is the number of Lloyd steps something that we really want to configure? Are there like meaningful performance tradeoffs that the user might want to have a say in here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm fair enough. There is a sort of tradeoff here, could probably set it between 4-5 iterations. Below that quality degrades but is pretty fast, more than that gives minimal gains but is much slower.

Comment thread diskann/src/ivf/index.rs
pub reassign_count: ReassignCount,
/// Positive number of Lloyd steps per parent.
pub split_iterations: NonZeroUsize,
pub seed: u64,

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.

Same question here I guess, is it really what we want to pass in a seed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see what you're saying. We want the Config to be only what the user passes in. I guess the seed should move to some kind of set of default values.

Comment thread diskann/src/ivf/index.rs

async move {
let mut accessor = strategy.build_accessor(context).into_ann_result()?;

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.

Would be helpful to have some comments on this part

Comment thread diskann/src/ivf/index.rs
async move {
let mut accessor = strategy.build_accessor(context).into_ann_result()?;

let mut parents = BTreeSet::new();

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.

Should this be a set?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement the accessor and the insert path

3 participants