diff --git a/diskann/src/flat/index.rs b/diskann/src/flat/index.rs new file mode 100644 index 000000000..f91a9ccac --- /dev/null +++ b/diskann/src/flat/index.rs @@ -0,0 +1,109 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +//! [`FlatIndex`] — the index wrapper for an on which we do flat search. + +use std::marker::PhantomData; +use std::num::NonZeroUsize; + +use diskann_utils::future::SendFuture; + +use crate::{ + ANNResult, + error::IntoANNResult, + flat::{DistancesUnordered, FlatPostProcess, FlatSearchStrategy}, + graph::{SearchOutputBuffer, index::SearchStats}, + neighbor::{Neighbor, NeighborPriorityQueue}, + provider::DataProvider, +}; + +/// A `'static` thin wrapper around a [`DataProvider`] used for flat search. +/// +/// The provider is owned by the index. The index is constructed once at process startup and +/// shared across requests; per-query state lives in the [`crate::flat::OnElementsUnordered`] +/// implementation that the [`crate::flat::FlatSearchStrategy`] produces. +#[derive(Debug)] +pub struct FlatIndex { + /// The backing provider. + provider: P, + _marker: PhantomData P>, +} + +impl FlatIndex

{ + /// Construct a new [`FlatIndex`] around `provider`. + pub fn new(provider: P) -> Self { + Self { + provider, + _marker: PhantomData, + } + } + + /// Borrow the underlying provider. + pub fn provider(&self) -> &P { + &self.provider + } + + /// Brute-force k-nearest-neighbor flat search. + /// + /// Streams every element produced by the strategy's iterator through the query + /// computer, keeps the best `k` candidates in a [`NeighborPriorityQueue`], and hands + /// the survivors to the post-processor. + /// + /// # Arguments + /// - `k`: number of nearest neighbors to return. + /// - `strategy`: produces the per-query iterator and the query computer. See [`FlatSearchStrategy`] + /// - `processor`: post-processes the survivor candidates into the output type. + /// - `context`: per-request context threaded through to the provider. + /// - `query`: the query. + /// - `output`: caller-owned output buffer. + pub fn knn_search( + &self, + k: NonZeroUsize, + strategy: &S, + processor: &PP, + context: &P::Context, + query: &T, + output: &mut OB, + ) -> impl SendFuture> + where + S: FlatSearchStrategy, + T: ?Sized + Sync, + O: Send, + OB: SearchOutputBuffer + Send + ?Sized, + PP: for<'a> FlatPostProcess, T, O> + Send + Sync, + { + async move { + let mut callback = strategy + .create_callback(&self.provider, context) + .into_ann_result()?; + + let computer = strategy.build_query_computer(query).into_ann_result()?; + + let k = k.get(); + let mut queue = NeighborPriorityQueue::new(k); + let mut cmps: u32 = 0; + + callback + .distances_unordered(&computer, |id, dist| { + cmps += 1; + queue.insert(Neighbor::new(id, dist)); + }) + .await + .into_ann_result()?; + + let result_count = processor + .post_process(&mut callback, query, queue.iter().take(k), output) + .await + .into_ann_result()? as u32; + + Ok(SearchStats { + cmps, + hops: 0, + result_count, + range_search_second_round: false, + }) + } + } +} diff --git a/diskann/src/flat/iterator.rs b/diskann/src/flat/iterator.rs new file mode 100644 index 000000000..fa07e92b4 --- /dev/null +++ b/diskann/src/flat/iterator.rs @@ -0,0 +1,147 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +//! [`OnElementsUnordered`] — the sequential access primitive for accessing a flat index. +//! +//! [`FlatIterator`] — a lending async iterator that can be bridged into +//! [`OnElementsUnordered`] via [`DefaultIteratedOperator`]. + +use diskann_utils::{Reborrow, future::SendFuture}; +use diskann_vector::PreprocessedDistanceFunction; + +use crate::{error::StandardError, provider::HasId}; + +/// Callback-driven sequential scan over the elements of a flat index. +/// +/// `OnElementsUnordered` is the streaming counterpart to [`crate::provider::Accessor`]. +/// Where an accessor exposes random retrieval by id, this trait exposes a *sequential* +/// walk that invokes a caller-supplied closure for every element. +/// +/// Algorithms see only `(Id, ElementRef)` pairs and treat the stream as opaque. +pub trait OnElementsUnordered: HasId + Send + Sync { + /// A reference to a yielded element with an unconstrained lifetime, suitable for + /// distance-function HRTB bounds. + type ElementRef<'a>; + + /// The error type yielded by [`Self::on_elements_unordered`]. + type Error: StandardError; + + /// Drive the entire scan, invoking `f` for each yielded element. + fn on_elements_unordered(&mut self, f: F) -> impl SendFuture> + where + F: Send + for<'a> FnMut(Self::Id, Self::ElementRef<'a>); +} + +/// Extension of [`OnElementsUnordered`] that drives the scan with a pre-built query +/// computer, invoking a callback with `(id, distance)` pairs instead of raw elements. +/// +/// The concrete computer is insantiated and supplied externally +/// by the [`FlatSearchStrategy`](crate::flat::FlatSearchStrategy). +/// +/// The default implementation delegates to [`OnElementsUnordered::on_elements_unordered`], +/// calling `computer.evaluate_similarity` on each element. +pub trait DistancesUnordered: OnElementsUnordered { + /// Drive the entire scan, scoring each element with `computer` and invoking `f` with + /// the resulting `(id, distance)` pair. + fn distances_unordered( + &mut self, + computer: &C, + mut f: F, + ) -> impl SendFuture> + where + C: for<'a> PreprocessedDistanceFunction, f32> + Send + Sync, + F: Send + FnMut(Self::Id, f32), + { + self.on_elements_unordered(move |id, element| { + let dist = computer.evaluate_similarity(element); + f(id, dist); + }) + } +} + +////////////// +// Iterator // +////////////// + +/// A lending, asynchronous iterator over the elements of a flat index. +/// +/// Implementations provide element-at-a-time access via [`Self::next`]. Providers that +/// only implement `FlatIterator` can be wrapped in [`DefaultIteratedOperator`] to obtain +/// an [`OnElementsUnordered`] implementation automatically. +pub trait FlatIterator: HasId + Send + Sync { + /// A reference to a yielded element with an unconstrained lifetime, suitable for + /// distance-function HRTB bounds. + type ElementRef<'a>; + + /// The concrete element returned by [`Self::next`]. Reborrows to [`Self::ElementRef`]. + type Element<'a>: for<'b> Reborrow<'b, Target = Self::ElementRef<'b>> + Send + Sync + where + Self: 'a; + + /// The error type yielded by [`Self::next`]. + type Error: StandardError; + + /// Advance the iterator and asynchronously yield the next `(id, element)` pair. + /// + /// Returns `Ok(None)` when the scan is exhausted. The yielded element borrows from + /// the iterator and is invalidated by the next call to `next`. + #[allow(clippy::type_complexity)] + fn next( + &mut self, + ) -> impl SendFuture)>, Self::Error>>; +} + +///////////// +// Default // +///////////// + +/// Bridges a [`FlatIterator`] into an [`OnElementsUnordered`] by looping over +/// [`FlatIterator::next`] and reborrowing each element into the closure. +/// +/// This is the default adapter for providers that implement element-at-a-time iteration. +/// Providers that can do better (prefetching, SIMD batching, bulk I/O) should implement +/// [`OnElementsUnordered`] directly. +pub struct DefaultIteratedOperator { + inner: I, +} + +impl DefaultIteratedOperator { + /// Wrap an iterator to produce an [`OnElementsUnordered`] implementation. + pub fn new(inner: I) -> Self { + Self { inner } + } + + /// Unwrap, returning the inner iterator. + pub fn into_inner(self) -> I { + self.inner + } +} + +impl HasId for DefaultIteratedOperator { + type Id = I::Id; +} + +impl OnElementsUnordered for DefaultIteratedOperator +where + I: FlatIterator + HasId + Send + Sync, +{ + type ElementRef<'a> = I::ElementRef<'a>; + type Error = I::Error; + + fn on_elements_unordered(&mut self, mut f: F) -> impl SendFuture> + where + F: Send + for<'a> FnMut(Self::Id, Self::ElementRef<'a>), + { + async move { + while let Some((id, element)) = self.inner.next().await? { + f(id, element.reborrow()); + } + Ok(()) + } + } +} + +impl DistancesUnordered for DefaultIteratedOperator where I: FlatIterator + HasId + Send + Sync +{} diff --git a/diskann/src/flat/mod.rs b/diskann/src/flat/mod.rs new file mode 100644 index 000000000..bf1290ed8 --- /dev/null +++ b/diskann/src/flat/mod.rs @@ -0,0 +1,45 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +//! Sequential ("flat") search infrastructure. +//! +//! This module is the streaming counterpart to the random-access [`crate::provider::Accessor`] +//! family. It is designed for backends whose natural access pattern is a one-pass scan over +//! their data — for example append-only buffered stores, on-disk shards streamed via I/O, +//! or any provider where random access is significantly more expensive than sequential. +//! +//! # Architecture +//! +//! The module mirrors the layering used by graph search: +//! +//! | Graph (random access) | Flat (sequential) | +//! | :------------------------------------ | :-------------------------------- | +//! | [`crate::provider::DataProvider`] | [`crate::provider::DataProvider`] | +//! | [`crate::graph::DiskANNIndex`] | [`FlatIndex`] | +//! | [`crate::provider::Accessor`] | [`FlatIterator`] | +//! | [`crate::graph::glue::SearchStrategy`] | [`FlatSearchStrategy`] | +//! | [`crate::graph::glue::SearchPostProcess`] | [`FlatPostProcess`] | +//! | [`crate::graph::Search`] | [`FlatIndex::knn_search`] | +//! +//! # Hot loop +//! +//! Algorithms drive the scan via [`FlatIterator::next`] (lending iterator) or override +//! [`FlatIterator::on_elements_unordered`] when batching/prefetching wins. The default +//! implementation of `on_elements_unordered` simply loops over `next`. +//! +//! See [`FlatIndex::knn_search`] for the canonical brute-force k-NN algorithm built on these +//! primitives. + +pub mod index; +pub mod iterator; +pub mod post_process; +pub mod strategy; + +pub use index::FlatIndex; +pub use iterator::{ + DefaultIteratedOperator, DistancesUnordered, FlatIterator, OnElementsUnordered, +}; +pub use post_process::{CopyIds, FlatPostProcess}; +pub use strategy::FlatSearchStrategy; diff --git a/diskann/src/flat/post_process.rs b/diskann/src/flat/post_process.rs new file mode 100644 index 000000000..2cab763dd --- /dev/null +++ b/diskann/src/flat/post_process.rs @@ -0,0 +1,72 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +//! [`FlatPostProcess`] — terminal stage of the flat search pipeline. + +use diskann_utils::future::SendFuture; + +use crate::{ + error::StandardError, flat::OnElementsUnordered, graph::SearchOutputBuffer, neighbor::Neighbor, + provider::HasId, +}; + +/// Post-process the survivor candidates produced by a flat search and +/// write them into an output buffer. +/// +/// This is the flat counterpart to [`crate::graph::glue::SearchPostProcess`]. Processors +/// receive `&mut S` so they can consult any iterator-owned lookup state (e.g., an +/// `Id -> rich-record` table built up during the scan) when assembling outputs. +/// +/// The `O` type parameter lets callers pick the output element type (raw `(Id, f32)` +/// pairs, fully hydrated hits etc.). +pub trait FlatPostProcess::Id> +where + S: OnElementsUnordered, + T: ?Sized, +{ + /// Errors yielded by [`Self::post_process`]. + type Error: StandardError; + + /// Consume `candidates` (in distance order) and write at most `k` results into + /// `output`. Returns the number of results written. + fn post_process( + &self, + iter: &mut S, + query: &T, + candidates: I, + output: &mut B, + ) -> impl SendFuture> + where + I: Iterator> + Send, + B: SearchOutputBuffer + Send + ?Sized; +} + +/// A trivial [`FlatPostProcess`] that copies each `(Id, distance)` pair straight into the +/// output buffer. +#[derive(Debug, Default, Clone, Copy)] +pub struct CopyIds; + +impl FlatPostProcess for CopyIds +where + S: OnElementsUnordered, + T: ?Sized, +{ + type Error = crate::error::Infallible; + + fn post_process( + &self, + _iter: &mut S, + _query: &T, + candidates: I, + output: &mut B, + ) -> impl SendFuture> + where + I: Iterator::Id>> + Send, + B: SearchOutputBuffer<::Id> + Send + ?Sized, + { + let count = output.extend(candidates.map(|n| (n.id, n.distance))); + std::future::ready(Ok(count)) + } +} diff --git a/diskann/src/flat/strategy.rs b/diskann/src/flat/strategy.rs new file mode 100644 index 000000000..5da3349ed --- /dev/null +++ b/diskann/src/flat/strategy.rs @@ -0,0 +1,77 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + */ + +//! [`FlatSearchStrategy`] — glue between [`DataProvider`] and per-query [`FlatIterator`]s. + +use diskann_vector::PreprocessedDistanceFunction; + +use crate::{ + error::StandardError, + flat::{DistancesUnordered, OnElementsUnordered}, + provider::DataProvider, +}; + +/// Per-call configuration that knows how to construct a [`DistancesUnordered`] for a provider +/// and how to pre-process queries of type `T` into a distance computer. +/// +/// `FlatSearchStrategy` is the flat counterpart to [`crate::graph::glue::SearchStrategy`]. +/// A strategy instance is stateless config — typically constructed at the call site, used +/// for one search, and dropped. +/// +/// # Why two methods? +/// +/// - [`Self::create_callback`] is query-independent and may be called multiple times per +/// request (e.g., once per parallel query in a batched search). +/// - [`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. +/// +/// # Type parameters +/// +/// - `Provider`: the [`DataProvider`] that backs the index. +/// - `T`: the query type. Often `[E]` for vector queries; can be any `?Sized` type. +pub trait FlatSearchStrategy: Send + Sync +where + P: DataProvider, + T: ?Sized, +{ + /// The iterator type produced by [`Self::create_callback`]. Borrows from `self` and the + /// provider. + type Callback<'a>: DistancesUnordered + where + Self: 'a, + P: 'a; + + /// The query computer produced by [`Self::build_query_computer`]. + /// + /// The HRTB on `ElementRef` ensures the same computer can score every element yielded + /// by every lifetime of `Iter`. Two lifetimes are needed: `'a` for the iterator + /// instance and `'b` for the reborrowed element. + type QueryComputer: for<'a, 'b> PreprocessedDistanceFunction< + as OnElementsUnordered>::ElementRef<'b>, + f32, + > + Send + + Sync + + 'static; + + /// The error type for both factory methods. + type Error: StandardError; + + /// Construct a fresh iterator over `provider` for the given request `context`. + /// + /// This is where lock acquisition, snapshot pinning, and any other per-query setup + /// should happen. The returned callback object owns whatever borrows / guards it needs to + /// remain valid until it is dropped. + fn create_callback<'a>( + &'a self, + provider: &'a P, + context: &'a P::Context, + ) -> Result, Self::Error>; + + /// Pre-process a query into a [`Self::QueryComputer`] usable for distance computation + /// against any iterator produced by [`Self::create_callback`]. + fn build_query_computer(&self, query: &T) -> Result; +} diff --git a/diskann/src/lib.rs b/diskann/src/lib.rs index 71cb3ed41..9c1f6ac76 100644 --- a/diskann/src/lib.rs +++ b/diskann/src/lib.rs @@ -13,6 +13,7 @@ pub mod utils; pub(crate) mod internal; // Index Implementations +pub mod flat; pub mod graph; // Top level exports. diff --git a/rfcs/00983-flat-search.md b/rfcs/00983-flat-search.md new file mode 100644 index 000000000..b79018a5d --- /dev/null +++ b/rfcs/00983-flat-search.md @@ -0,0 +1,228 @@ +# Flat Search + +| | | +|------------------|--------------------------------| +| **Authors** | Aditya Krishnan, Alex Razumov, Dongliang Wu | +| **Created** | 2026-04-24 | +| **Updated** | 2026-04-28 | + +## Motivation + +### Background + +DiskANN today exposes a single abstraction family centered on the +[`crate::provider::Accessor`] trait. Accessors are random access by design since the graph greedy search algorithm needs to decide which ids to fetch and the accessor materializes the corresponding elements (vectors, quantized vectors and neighbor lists) on demand. This is the right contract for graph search, where neighborhood expansion is inherently random-access against the [`crate::provider::DataProvider`]. + +A growing class of consumers diverge from our current pattern of use by accesssing their index **sequentially**. Some consumers build their index in an "append-only" fashion and require that they walk the index in a sequential, fixed order, relying on iteration position to enforce versioning / deduplication invariants. + +### Problem Statement + +The problem-statement here is simple: provide first-class support for sequential, one-pass scans over a data backend without +stuffing the algorithm or the backend through the `Accessor` trait surface. + +### Goals + +1. Define a streaming access primitive — `OnElementsUnordered` — that mirrors the role + `Accessor` plays for graph search but exposes a callback-driven scan instead of + random access. +2. Provide flat-search algorithm implementations (with `knn_search` as default and filtered and diverse variants to opt-into) built on the new + primitives, so consumers can use this against their own providers / backends. +3. Expose support for features and implementations native to the repo like quantized distance computers out-of-the-box. + +## Proposal + +The flat-search infrastructure is built on a small sequence of traits. The only required traits for the algorithm is `OnElementsUnordered` and its subtrait `DistancesUnordered`. A strategy - `FlatSearchStrategy` - instantiates these implementations for specific providers. An opt-in iterator trait `FlatIterator` and default implementations of the core traits - `DefaultIteratedOperator` - exist for convenience for backends that naturally expose element-at-a-time iteration. + +### `OnElementsUnordered` — the core scan + +```rust +pub trait OnElementsUnordered: HasId + Send + Sync { + type ElementRef<'a>; + type Error: StandardError; + + fn on_elements_unordered(&mut self, f: F) -> impl SendFuture> + where + F: Send + for<'a> FnMut(Self::Id, Self::ElementRef<'a>); +} +``` + +A single required method: drive the entire scan via a callback. Async to match +[`crate::provider::Accessor`]. Implementations choose iteration order, prefetching, and +any SIMD-friendly bulk reads if they want; algorithms see only `(Id, ElementRef)` pairs. + +### `DistancesUnordered` — the distance subtrait + +```rust +pub trait DistancesUnordered: OnElementsUnordered { + fn distances_unordered( + &mut self, computer: &C, mut f: F, + ) -> impl SendFuture> + where + C: for<'a> PreprocessedDistanceFunction, f32> + Send + Sync, + F: Send + FnMut(Self::Id, f32), + { + // default delegates to on_elements_unordered + evaluate_similarity + } +} +``` + +A subtrait that fuses scanning with scoring. The default implementation loops +`on_elements_unordered` and calls `computer.evaluate_similarity` on each element. + +The query computer is a generic parameter rather than an associated type, so the same +callback type can be driven by different computers. The `FlatSearchStrategy` is the +source of truth for which computer is used in any given search. + +### `FlatIterator` and `DefaultIteratedOperator` — convenience for element-at-a-time backends + +For backends that naturally expose element-at-a-time iteration, `FlatIterator` is a +lending async iterator: + +```rust +pub trait FlatIterator: HasId + Send + Sync { + type ElementRef<'a>; + // lifetime gymnastics to make lifetime of `Element<'_>` to play nice with HRTB + type Element<'a>: for<'b> Reborrow<'b, Target = Self::ElementRef<'b>> + Send + Sync + where Self: 'a; + type Error: StandardError; + + fn next( + &mut self, + ) -> impl SendFuture)>, Self::Error>>; +} +``` + +`DefaultIteratedOperator` wraps any `FlatIterator` and implements `OnElementsUnordered` +(and `DistancesUnordered` by inheritance) by looping over `next()` and reborrowing each +element. + + +### The glue: `FlatSearchStrategy` + +While `OnElementsUnordered` is the primary handle the algorithm uses to walk the index, +it is scoped to each query. We introduce a constructor — `FlatSearchStrategy` — similar +to `SearchStrategy` for `Accessor`, to instantiate the per-query callback object. +A strategy is per-call configuration that is stateless, cheap to construct and scoped to one +search. It produces both a per-query callback and a query computer. + +```rust +pub trait FlatSearchStrategy: Send + Sync +where + P: DataProvider, + T: ?Sized, +{ + /// The per-query callback type produced by [`Self::create_callback`]. Borrows from + /// `self` and the provider. + type Callback<'a>: DistancesUnordered + where + Self: 'a, + + /// The query computer produced by [`Self::build_query_computer`]. + type QueryComputer: for<'a, 'b> PreprocessedDistanceFunction< + as OnElementsUnordered>::ElementRef<'b>, + f32, + > + Send + + Sync + + 'static; + + /// The error type + type Error: StandardError; + + /// Construct a fresh callback over `provider` for the given request `context`. + fn create_callback<'a>( + &'a self, + provider: &'a P, + context: &'a P::Context, + ) -> Result, Self::Error>; + + /// Pre-process a query into a [`Self::QueryComputer`] usable for distance computation + /// against any callback produced by [`Self::create_callback`]. + fn build_query_computer(&self, query: &T) -> Result; +} +``` + +The `ElementRef<'b>` that the `QueryComputer` acts on is tied to the +`OnElementsUnordered::ElementRef` of the callback produced by `create_callback`. + +### `FlatIndex` + +`FlatIndex` is a thin `'static` wrapper around a `DataProvider`. The same `DataProvider` +trait used by graph search is reused here - flat and graph subsystems share a single +provider surface and the same `Context` / id-mapping / error machinery. + +```rust +pub struct FlatIndex { + provider: P, +} + +impl FlatIndex

{ + pub fn new(provider: P) -> Self; + pub fn provider(&self) -> &P; + + pub fn knn_search( + &self, + k: NonZeroUsize, + strategy: &S, + processor: &PP, + context: &P::Context, + query: &T, + output: &mut OB, + ) -> impl SendFuture> + where + S: FlatSearchStrategy, + T: ?Sized + Sync, + O: Send, + OB: SearchOutputBuffer + Send + ?Sized, + PP: for<'a> FlatPostProcess, T, O> + Send + Sync, +} +``` + +The `knn_search` method is the canonical brute-force search algorithm: + +1. Construct the per-query callback via `strategy.create_callback`. +2. Build the query computer via `strategy.build_query_computer`. +3. Drive the scan via `callback.distances_unordered(&computer, ...)`, inserting each + `(id, distance)` pair into a `NeighborPriorityQueue` of capacity `k`. +4. Hand the survivors (in distance order) to `processor.post_process`. +5. Return search stats. + +Other algorithms (filtered, range, diverse) can be added later as additional methods on +`FlatIndex`. + +## Trade-offs + +### Reusing `DataProvider` + +This design leans into using the `DataProvider` trait which requires implementations to implement `InternalId` and `ExternalId` conversions (via the context). Arguably, this requirement is too restrictive for some consumers of a flat-index. Reasons for sticking with `DataProvider`: + +- Every concrete provider already implements `DataProvider`, so a separate trait adds + an abstraction that existing consumers will have to implement if they want to opt-in to the flat-index path. +- Sharing `DataProvider` means the `Context`, id-mapping (`to_internal_id` / + `to_external_id`), and error machinery are identical across graph and flat search, + reducing the learning surface for new contributors. + +### Async vs sync scan API + +`on_elements_unordered` and `distances_unordered` return a future, making the scan +surface async. This is the right default for disk-backed and network-backed backends +where advancing the scan involves real I/O. It also matches the `Accessor` surface, +keeping the two subsystems shaped the same way. + +The cost is paid by in-memory consumers: the scan goes through the future machinery +even when results are immediately available. In a tight brute-force loop this overhead — +poll scaffolding, pinning etc — could be measurable. + +We chose async because the wider audience of consumers (disk, network, mixed) benefits +more than in-memory consumers lose. + +### Expand `Element` to support batched distance computation? + +The current design yields one element per `next()` call, and the query computer scores +elements one at a time via `PreprocessedDistanceFunction::evaluate_similarity`. This could leave some optimization and performance on the table; especially with the upcoming effort around batched distance kernels. Of course, a consumer can choose to implement their own optimized implementation of `distances_unordered` that uses batching. + +An alternative is to make `next()` yield a *batch* instead of a single vector representation like `Element<'_>`. Some work will need to be done to define the right interaction between the batch type, the element type in the batch, the interaction with `QueryComputer`'s types and way IDs and distances are collected in the queue. + +## Future Work +- Support for other flat-search algorithms like - filtered, range and diverse flat algorithms as additional methods on `FlatIndex`. +- Index build -- this is just one part of the picture; more work needs to be done around how this fits in with any traits / interface we need for index build. +