Crates.io | hannoy |
lib.rs | hannoy |
version | 0.0.8 |
created_at | 2025-07-29 18:51:57.110495+00 |
updated_at | 2025-09-18 09:19:39.657428+00 |
description | HNSW Approximate Nearest Neighbors in Rust, based on LMDB and optimized for memory usage |
homepage | |
repository | https://github.com/nnethercott/hannoy |
max_upload_size | |
id | 1772685 |
size | 1,943,602 |
hannoy is a key-value backed HNSW implementation based on arroy.
Many popular HNSW libraries are built in memory, meaning you need enough RAM to store all the vectors you're indexing. Instead, hannoy
uses LMDB — a memory-mapped KV store — as a storage backend. This is more well-suited for machines running multiple programs, or cases where the dataset you're indexing won't fit in memory. LMDB also supports non-blocking concurrent reads by design, meaning its safe to query the index in multi-threaded environments.
use hannoy::{distances::Cosine, Database, Reader, Result, Writer};
use heed::EnvOpenOptions;
use rand::{rngs::StdRng, SeedableRng};
fn main() -> Result<()> {
const DIM: usize = 3;
let vecs: Vec<[f32; DIM]> = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let env = unsafe {
EnvOpenOptions::new()
.map_size(1024 * 1024 * 1024 * 1) // 1GiB
.open("./")
}
.unwrap();
let mut wtxn = env.write_txn().unwrap();
let db: Database<Cosine> = env.create_database(&mut wtxn, None)?;
let writer: Writer<Cosine> = Writer::new(db, 0, DIM);
// insert into lmdb
writer.add_item(&mut wtxn, 0, &vecs[0])?;
writer.add_item(&mut wtxn, 1, &vecs[1])?;
writer.add_item(&mut wtxn, 2, &vecs[2])?;
// ...and build hnsw
let mut rng = StdRng::seed_from_u64(42);
let mut builder = writer.builder(&mut rng);
builder.ef_construction(100).build::<16,32>(&mut wtxn)?;
wtxn.commit()?;
// search hnsw using a new lmdb read transaction
let rtxn = env.read_txn()?;
let reader = Reader::<Cosine>::open(&rtxn, 0, db)?;
let query = vec![0.0, 1.0, 0.0];
let nns = reader.nns(1).ef_search(10).by_vector(&rtxn, &query)?;
dbg!("{:?}", &nns);
Ok(())
}
import hannoy
from hannoy import Metric
import tempfile
tmp_dir = tempfile.gettempdir()
db = hannoy.Database(tmp_dir, Metric.COSINE)
with db.writer(3, m=4, ef=10) as writer:
writer.add_item(0, [1.0, 0.0, 0.0])
writer.add_item(1, [0.0, 1.0, 0.0])
writer.add_item(2, [0.0, 0.0, 1.0])
reader = db.reader()
nns = reader.by_vec([0.0, 1.0, 0.0], n=2)
(closest, dist) = nns[0]
Search in an hnsw always traverses from the top to bottom layers of the graph, so we know a priori some vectors will be needed. We can hint to the kernel that these vectors (and their neighbours) should be loaded into RAM using madvise
to speed up search.
Doing so can reduce cold-start latencies by several milliseconds, and is configured through the HANNOY_READER_PREFETCH_MEMORY
environment variable.
E.g. prefetching 10MiB of vectors into RAM.
export HANNOY_READER_PREFETCH_MEMORY=10485760