Insert and Search
Append vectors to a Basis instance and run k-nearest-neighbor queries.
What this page covers
The two operations a developer reaches for first: add to insert a vector under a UUID, and search to return the k nearest matches for a query vector.
HNSW is an approximate index. Recall depends on ef_search and graph topology — for high-precision workloads, retrieve a larger k and re-rank with an exact distance computation or a cross-encoder.
Construction
Basis::new(vector_strand, index_strand) takes two Arc<RwLock<Strand>> handles. The vector strand stores the canonical write log; the index strand reserves space for HNSW snapshots.
use std::sync::Arc;
use tokio::sync::RwLock;
use strand::{Strand, StrandConfig};
use basis::Basis;
use uuid::Uuid;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let vec_cfg = StrandConfig::new().with_storage(dir.path().join("vectors"));
let idx_cfg = StrandConfig::new().with_storage(dir.path().join("index"));
let vector_strand = Arc::new(RwLock::new(Strand::new(vec_cfg).await?));
let index_strand = Arc::new(RwLock::new(Strand::new(idx_cfg).await?));
let basis = Basis::new(vector_strand, index_strand);
// Insert three 4-dimensional vectors.
let red = Uuid::new_v4();
let green = Uuid::new_v4();
let blue = Uuid::new_v4();
basis.add(red, &[1.0, 0.0, 0.0, 0.0]).await?;
basis.add(green, &[0.0, 1.0, 0.0, 0.0]).await?;
basis.add(blue, &[0.0, 0.0, 1.0, 0.0]).await?;
// Query for the 2 nearest to [0.9, 0.1, 0.0, 0.0] — should be red, then green.
let hits = basis.search(&[0.9, 0.1, 0.0, 0.0], 2).await?;
assert_eq!(hits[0].0, red);
println!("nearest: {} at L2 distance {}", hits[0].0, hits[0].1);
Ok(())
}What add does
- Serializes
VectorEntry::Add { id, vector }withserde_json. - Appends the bytes to the vector strand. The Strand returns the new sequence number.
- Inserts the vector into the in-memory HNSW graph and records the internal index in a
DashMap<Uuid, usize>. - Calls
update_index_strand()(currently a no-op stub — see Snapshot and Recovery).
The add call is O(log N) on average for the HNSW insertion plus one Strand append.
What search does
- Builds a
Point { id: Uuid::nil(), vec: query }as the search probe. - Walks the HNSW graph with
ef = max(k, 24)to gather candidates. - Returns up to
k(Uuid, f32)pairs, sorted by ascending L2 distance.
| Parameter | Meaning |
|---|---|
query | Query vector. Must have the same dimensionality as inserted vectors. |
k | Maximum number of results. |
ef (internal) | Search width. Always ≥ 24 to keep recall stable on small k. |
Errors
| Variant | Cause | Recovery |
|---|---|---|
BasisError::Strand(String) | Underlying Strand append/read failed | Retry; check disk and quorum |
BasisError::Serialization(serde_json::Error) | VectorEntry could not be encoded | Verify vector length matches the rest of the corpus |
BasisError::VectorNotFound(Uuid) | Removal target absent | Check id_to_internal_index map |
Practical sizing
The HNSW graph is held entirely in memory. With M=12, M0=24 and 32-bit floats, a 768-dimensional embedding consumes ≈ 3.1 KB per vector (vector + neighbor lists + struct overhead). 1 M vectors fits in ≈ 3 GB of RAM. Larger corpora require sharding across multiple Basis instances.