Skip to content

[flat index] Flat Search Interface#983

Open
arkrishn94 wants to merge 12 commits intomainfrom
u/adkrishnan/flat-index
Open

[flat index] Flat Search Interface#983
arkrishn94 wants to merge 12 commits intomainfrom
u/adkrishnan/flat-index

Conversation

@arkrishn94
Copy link
Copy Markdown
Contributor

@arkrishn94 arkrishn94 commented Apr 28, 2026

This PR introduces a trait interface and a light index to support brute-force search for providers that can be used as/are a flat-index. There is an associated RFC that walks through the interface and associated implementation in diskann as a new flat module.

Rendered RFC link.

Motivation

The repo has no first-class surface for brute-force search today.

This PR introduces a small trait hierarchy that gives flat search the same provider-agnostic shape that Accessor / SearchStrategy give graph search, so any backend (in-memory full-precision, quantized, disk, remote) can plug in once and reuse a shared algorithm.

This allows implementations of algorithms - diverse search, knn search, range search, pre-filtered search - to live in the repo and let consumers only worry about defining the provider; just like we do for graph-search.

The core traits

The following is a light-introduction to the trait surface in order of importance - reviewers are recommended to review in this order.

OnElementsUnordered - core scan

Implemented in flat\iterator.rs

pub trait OnElementsUnordered: HasId + Send + Sync {
    type ElementRef<'a>;
    type Error: StandardError;

    fn on_elements_unordered<F>(&mut self, f: F) -> impl SendFuture<Result<(), Self::Error>>
    where F: Send + for<'a> FnMut(Self::Id, Self::ElementRef<'a>);
}

A single (async) method that drives the entire scan via callback. Implementations choose iteration order, prefetching, and/or bulk reads if they want. Brute-force algorithm and computer sees only (Id, ElementRef) pairs.

DistancesUnordered - distance subtrait

pub trait DistancesUnordered: OnElementsUnordered {
    fn distances_unordered<C, F>(&mut self, computer: &C, f: F) -> impl SendFuture<...>
    where C: for<'a> PreprocessedDistanceFunction<Self::ElementRef<'a>, f32> + Send + Sync,
          F: Send + FnMut(Self::Id, f32);
}

Fuses scanning with scoring. The default implementation loops on_elements_unordered and calls evaluate_similarity on each element. Backends that can fuse retrieval and scoring can override this method.

FlatSearchStrategy - the glue

Implemented in flat\strategy.rs.

FlatSearchStrategy<P, T> is the per-call configuration that mirrors graph's diskann::glue::SearchStrategy:

  • create_callback(&provider, &context) produces a per-query Callback: DistancesUnordered.
  • build_query_computer(&query) produces the QueryComputer that scores each element.

The strategy is the glue that creates the computer that is used in a given search, and is what links a provider to a particular flat-search execution.

FlatIndex

Implemented in flat\index.rs. FlatIndex<P> is a thin wrapper around a DataProvider. Its knn_search method:

  1. asks the strategy for a callback and a query computer,
  2. drives callback.distances_unordered(&computer, ...) through the priority queue,
  3. hands the survivors to a FlatPostProcess to write into the output buffer.

FlatIterator - opt-in convenience

For backends that naturally expose element-at-a-time iteration, FlatIterator is a lending async iterator with a single next() method. DefaultIteratedOperator<I> wraps any FlatIterator and implements OnElementsUnordered (and DistancesUnordered by inheritance) by looping over next() and reborrowing each element.

This means a backend can opt in at exactly the right layer: bulk-friendly backends implement OnElementsUnordered directly; element-at-a-time backends implement FlatIterator and get the rest for free.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 0% with 93 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.38%. Comparing base (f458cf6) to head (8af5e00).

Files with missing lines Patch % Lines
diskann/src/flat/index.rs 0.00% 51 Missing ⚠️
diskann/src/flat/iterator.rs 0.00% 29 Missing ⚠️
diskann/src/flat/post_process.rs 0.00% 13 Missing ⚠️

❌ Your patch status has failed because the patch coverage (0.00%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #983      +/-   ##
==========================================
- Coverage   89.48%   89.38%   -0.11%     
==========================================
  Files         448      451       +3     
  Lines       84081    84174      +93     
==========================================
- Hits        75239    75238       -1     
- Misses       8842     8936      +94     
Flag Coverage Δ
miri 89.38% <0.00%> (-0.11%) ⬇️
unittests 89.22% <0.00%> (-0.10%) ⬇️

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

Files with missing lines Coverage Δ
diskann/src/flat/post_process.rs 0.00% <0.00%> (ø)
diskann/src/flat/iterator.rs 0.00% <0.00%> (ø)
diskann/src/flat/index.rs 0.00% <0.00%> (ø)

... and 2 files with indirect coverage changes

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

@arkrishn94 arkrishn94 marked this pull request as ready for review April 28, 2026 16:02
@arkrishn94 arkrishn94 requested review from a team and Copilot April 28, 2026 16:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an RFC plus an initial “flat” (sequential scan) search surface in diskann, analogous to the existing graph/random-access search pipeline built around DataProvider/Accessor.

Changes:

  • Added an RFC describing the flat iterator/strategy/index abstraction and trade-offs.
  • Added a new diskann::flat module with FlatIterator, FlatSearchStrategy, FlatIndex::knn_search, and FlatPostProcess (+ CopyFlatIds).
  • Exported the new flat module from the crate root.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
rfcs/00983-flat-search.md RFC describing the design for sequential (“flat”) index search APIs.
diskann/src/lib.rs Exposes the new flat module publicly.
diskann/src/flat/mod.rs New module root + re-exports for the flat search surface.
diskann/src/flat/iterator.rs Defines the async lending iterator primitive FlatIterator.
diskann/src/flat/strategy.rs Defines FlatSearchStrategy to create per-query iterators and query computers.
diskann/src/flat/index.rs Implements FlatIndex and the brute-force knn_search scan algorithm.
diskann/src/flat/post_process.rs Defines FlatPostProcess and a basic CopyFlatIds post-processor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread diskann/src/flat/index.rs
* Licensed under the MIT license.
*/

//! [`FlatIndex`] — the index wrapper for an on which we do flat search.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Doc comment grammar issue: "the index wrapper for an on which we do flat search" is missing a noun (e.g., "for an index" / "around a provider"). Please fix to avoid confusing rustdoc output.

Suggested change
//! [`FlatIndex`] — the index wrapper for an on which we do flat search.
//! [`FlatIndex`] — the index wrapper around a [`DataProvider`] on which we do flat search.

Copilot uses AI. Check for mistakes.
S: FlatIterator,
T: ?Sized,
{
type Error = crate::error::Infallible;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

CopyFlatIds uses crate::error::Infallible, but the analogous graph::glue::CopyIds uses std::convert::Infallible (graph/glue.rs:417). Using the std type here too would improve consistency and reduce cognitive overhead for readers comparing the two pipelines.

Suggested change
type Error = crate::error::Infallible;
type Error = std::convert::Infallible;

Copilot uses AI. Check for mistakes.
Comment thread rfcs/00983-flat-search.md
Comment on lines +139 to +163
```rust
pub struct FlatIndex<P: DataProvider> {
provider: P,
/* private */
}

impl<P: DataProvider> FlatIndex<P> {
pub fn new(provider: P) -> Self;
pub fn provider(&self) -> &P;

pub fn knn_search<S, T, O, OB>(
&self,
k: NonZeroUsize,
strategy: &S,
processor: &PP,
context: &P::Context,
query: &T,
output: &mut OB,
) -> impl SendFuture<ANNResult<SearchStats>>
where
S: FlatSearchStrategy<P, T>,
T: ?Sized + Sync,
O: Send,
OB: SearchOutputBuffer<O> + Send + ?Sized,
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The RFC FlatIndex::knn_search signature uses processor: &PP but PP is not declared in the generic parameter list, and the struct shows provider as private while the implementation has it as a public field. Please align the RFC snippet with the actual API so the rendered RFC stays accurate.

Copilot uses AI. Check for mistakes.
/// - [`Self::build_query_computer`] is iterator-independent — the same query can be
/// pre-processed once and used against multiple iterators.
///
/// Both methods may borrow from the strategy itself.
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

The doc comment says "Both methods may borrow from the strategy itself", but QueryComputer is bounded by + 'static, so build_query_computer cannot return a computer that borrows from self. Consider rewording to clarify that create_iter may borrow, while the query computer must own its state (or is otherwise 'static).

Suggested change
/// Both methods may borrow from the strategy itself.
/// [`Self::create_iter`] may return an iterator that borrows from the strategy itself
/// and the provider. [`Self::build_query_computer`] may use the strategy while
/// constructing the query computer, but the returned [`Self::QueryComputer`] must own
/// its state or otherwise satisfy `'static`.

Copilot uses AI. Check for mistakes.
Comment thread diskann/src/flat/index.rs Outdated

iter.on_elements_unordered(|id, element| {
let dist = computer.evaluate_similarity(element);
cmps += 1;
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

cmps is counted with a u32 and incremented once per scanned element. In flat search a full scan could exceed u32::MAX, which will panic on overflow in debug builds and wrap in release, producing invalid stats. Consider using saturating arithmetic (cap at u32::MAX) or switching the counter to a wider type before converting to SearchStats.

Suggested change
cmps += 1;
cmps = cmps.saturating_add(1);

Copilot uses AI. Check for mistakes.
Comment thread diskann/src/flat/index.rs
})
}
}
}
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

New behavior (FlatIndex::knn_search) is introduced without tests. Given the repo has unit tests for graph search and output buffers, it would be good to add at least one test covering: (1) correct top-k ordering, (2) that CopyFlatIds writes expected (id, distance) pairs, and (3) that SearchStats { cmps, result_count } are consistent for a tiny in-memory iterator.

Suggested change
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run_tiny_flat_scan(
k: usize,
items: &[(u32, f32)],
) -> (Vec<(u32, f32)>, SearchStats) {
let mut queue = NeighborPriorityQueue::new(k);
let mut cmps = 0u32;
for (id, distance) in items.iter().copied() {
cmps += 1;
queue.insert(Neighbor::new(id, distance));
}
let copied: Vec<(u32, f32)> = queue
.iter()
.take(k)
.map(|neighbor| (neighbor.id, neighbor.distance))
.collect();
let stats = SearchStats {
cmps,
hops: 0,
result_count: copied.len() as u32,
range_search_second_round: false,
};
(copied, stats)
}
#[test]
fn knn_search_keeps_top_k_in_distance_order() {
let (copied, stats) = run_tiny_flat_scan(
3,
&[(10, 4.0), (11, 1.5), (12, 3.0), (13, 0.5), (14, 2.0)],
);
assert_eq!(copied, vec![(13, 0.5), (11, 1.5), (14, 2.0)]);
assert_eq!(stats.result_count, 3);
}
#[test]
fn copied_flat_ids_match_expected_id_distance_pairs() {
let (copied, _) = run_tiny_flat_scan(2, &[(21, 9.0), (22, 1.25), (23, 4.5)]);
assert_eq!(copied, vec![(22, 1.25), (23, 4.5)]);
}
#[test]
fn search_stats_are_consistent_for_tiny_in_memory_scan() {
let items = &[(31, 7.0), (32, 2.0), (33, 5.0), (34, 1.0)];
let (copied, stats) = run_tiny_flat_scan(2, items);
assert_eq!(stats.cmps, items.len() as u32);
assert_eq!(stats.hops, 0);
assert_eq!(stats.result_count, copied.len() as u32);
assert!(!stats.range_search_second_round);
}
}

Copilot uses AI. Check for mistakes.
Comment thread rfcs/00983-flat-search.md
Comment thread rfcs/00983-flat-search.md Outdated
Comment thread rfcs/00983-flat-search.md
@arkrishn94 arkrishn94 changed the title [RFC] Flat Index Search [flat index] Flat Search Interface Apr 29, 2026
Comment thread diskann/src/flat/index.rs
callback
.distances_unordered(&computer, |id, dist| {
cmps += 1;
queue.insert(Neighbor::new(id, dist));
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’s our extension path for dropping candidates whose distance exceeds a given threshold?
Would this logic live inside callback.distance_unordered()? If so, the cmps wouldn’t include the distance computations for candidates that get discarded - it is an issue.

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.

This is a good point, let me think about this.

) -> impl SendFuture<Result<(), Self::Error>>
where
C: for<'a> PreprocessedDistanceFunction<Self::ElementRef<'a>, f32> + Send + Sync,
F: Send + FnMut(Self::Id, f32),
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.

FnMut is not friendly to parallel iterators. If we ever want to parallelize the distance calculations for flat index, what is our extension path?

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.

How so? The mutability bound on F is restricted to the scope of the search for a single query. This doesn't prevent us from parallelizing each scan across multiple queries. It just restricts you from parallelizing within the search scope for a query.

This is an identical pattern to the trait constraints for graph search.

Copy link
Copy Markdown
Contributor

@arrayka arrayka Apr 29, 2026

Choose a reason for hiding this comment

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

Yep, I'm talking about an option to parallelize distance computations between a single query vector and the indexed vectors - trading off CPU for lower search latency.

Comment thread diskann/src/lib.rs
pub(crate) mod internal;

// Index Implementations
pub mod flat;
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 rationale behind adding flat index to diskann crate if compared with creating diskann-flat crate?

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.

The provider is defined in diskann which is a first-class object for both graph and flat index.

That said, I'm open to creating a more intuitive separation between graph and flat indexes in diskann. Although I think as of now, the interface for the flat index is too small to justify any major separation. But I'm open to ideas here.

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.

4 participants