Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ byteorder = "1.4.3"
serde_json = "1.0.64"
num-traits = "0.2.14"
bitarray = { version = "0.9.1", default-features = false, features = ["space"] }
memmap2 = "0.9"

[profile.dev]
opt-level = 3
Expand Down
2 changes: 1 addition & 1 deletion benches/neighbors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fn bench_neighbors(c: &mut Criterion) {
let all_sizes = (space_mags).map(|n| 2usize.pow(n));
let max_linear_size = 2usize.pow(14);
let filepath = "data/akaze";
let total_descriptors = all_sizes.clone().rev().next().unwrap();
let total_descriptors = all_sizes.clone().next_back().unwrap();
let descriptor_size_bytes = 61;
let total_query_strings = 10000;

Expand Down
6 changes: 3 additions & 3 deletions examples/recall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ fn process<const M: usize, const M0: usize>(opt: &Opt) -> (Vec<f64>, Vec<f64>) {
}
}
// Get the worst distance
v.into_iter().take(opt.k).last().unwrap()
v.into_iter().take(opt.k).next_back().unwrap()
})
.collect();
eprintln!("Done.");
Expand Down Expand Up @@ -202,13 +202,13 @@ fn process<const M: usize, const M0: usize>(opt: &Opt) -> (Vec<f64>, Vec<f64>) {
};
opt.k
];
let stats = easybench::bench_env(dest, |mut dest| {
let stats = easybench::bench_env(dest, |dest| {
let mut refmut = state.borrow_mut();
let (searcher, query) = &mut *refmut;
let (ix, query_feature) = query.next().unwrap();
let correct_worst_distance = correct_worst_distances[ix];
// Go through all the features.
for &mut neighbor in hnsw.nearest(&query_feature, ef, searcher, &mut dest) {
for &mut neighbor in hnsw.nearest(&query_feature, ef, searcher, dest) {
// Any feature that is less than or equal to the worst real nearest neighbor distance is correct.
if Euclidean.distance(&search_space[neighbor.index], &query_feature)
<= correct_worst_distance
Expand Down
3 changes: 1 addition & 2 deletions examples/recall_discrete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,9 @@ where
);
let correct_worst_distances: Vec<_> = query_strings
.iter()
.cloned()
.map(|feature| {
let mut v = vec![];
for distance in search_space.iter().map(|n| Hamming.distance(n, &feature)) {
for distance in search_space.iter().map(|n| Hamming.distance(n, feature)) {
let pos = v.binary_search(&distance).unwrap_or_else(|e| e);
v.insert(pos, distance);
if v.len() > opt.k {
Expand Down
2 changes: 2 additions & 0 deletions src/hnsw.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod feature_store;
mod hnsw_const;
mod hnsw_runtime;
mod nodes;
#[cfg(feature = "serde")]
mod serde_impl;

pub use feature_store::FeatureStore;
pub use hnsw_const::*;
pub use hnsw_runtime::HnswRuntime;
74 changes: 74 additions & 0 deletions src/hnsw/feature_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Pluggable storage for the feature vectors an index is built over.

use alloc::vec::Vec;

/// Backing storage for an index's feature vectors.
///
/// The default is [`Vec<T>`], which keeps every feature on the heap. Supplying a
/// different implementation lets the features live somewhere else — an `mmap`ed
/// file, an arena, or any other region the caller manages — while the graph
/// itself stays in memory.
///
/// # Contract
///
/// A reference returned by [`get_feature`](FeatureStore::get_feature) must
/// remain valid, unmoved, and unmutated for as long as it is held, regardless of
/// any later calls to `get_feature`. Equivalently: each index must denote its own
/// storage location with a stable address, and reads must not disturb one
/// another.
///
/// This is load-bearing rather than a formality. The neighbor-selection
/// heuristic keeps up to three feature references live simultaneously while
/// pruning a saturated neighbor list — the target node, the candidate being
/// considered, and each already-kept neighbor the candidate is compared
/// against. An implementation that decoded features into a shared scratch
/// buffer would alias all three onto one address; every comparison would
/// degenerate toward `distance(x, x)`, the diversity heuristic would silently
/// collapse into nearest-M truncation, and dense clusters would close
/// themselves off from the rest of the graph.
///
/// Suitable backings therefore include `Vec<T>`, a fixed `mmap` region, an
/// arena, or an append-only cache that never evicts. Backings that reuse a
/// decode buffer, evict entries, or synthesize values per read cannot uphold
/// this contract and are not supported.
///
/// # Naming
///
/// The methods are deliberately named `get_feature` / `push_feature` /
/// `feature_count` rather than `get` / `push` / `len`. A trait method named
/// `get` on `Vec<T>` shadows `slice::get` at every call site where this trait is
/// in scope, because trait methods on the receiver type are considered before
/// inherent methods reached through `Deref`. The verbose names keep `Vec`'s own
/// API usable in the same module.
///
/// # Panics
///
/// Implementations are expected to panic on an out-of-bounds index, matching
/// `Vec`'s behavior.
pub trait FeatureStore<T> {
/// Returns the feature stored at `index`.
fn get_feature(&self, index: usize) -> &T;

/// Appends a feature, giving it the next index.
fn push_feature(&mut self, feature: T);

/// The number of features stored.
fn feature_count(&self) -> usize;
}

impl<T> FeatureStore<T> for Vec<T> {
#[inline]
fn get_feature(&self, index: usize) -> &T {
&self[index]
}

#[inline]
fn push_feature(&mut self, feature: T) {
Vec::push(self, feature)
}

#[inline]
fn feature_count(&self) -> usize {
Vec::len(self)
}
}
Loading