Internals
How Basis stores, indexes, and rebuilds vectors under the hood.
What this page covers
A walk through the private state and helper paths so you can audit, extend, or debug the implementation.
State
pub struct Basis {
vector_strand: Arc<RwLock<Strand>>,
index_strand: Arc<RwLock<Strand>>,
hnsw: Arc<RwLock<BasisHnsw>>,
id_to_internal_index: Arc<DashMap<Uuid, usize>>,
did_to_internal_index: Arc<DashMap<String, usize>>,
}
type BasisHnsw = Hnsw<Point, StdRng, M, M0>; // M=12, M0=24| Field | Role |
|---|---|
vector_strand | Canonical write log of VectorEntry blocks |
index_strand | Reserved for HNSW snapshots (stub today) |
hnsw | In-memory graph used by search |
id_to_internal_index | Lookup from caller-supplied UUID to HNSW node index |
did_to_internal_index | Lookup from DID string to HNSW node index, used by membership checks |
VectorEntry encoding
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum VectorEntry {
Add { id: Uuid, vector: Vec<f32> },
Remove { id: Uuid },
}Entries are encoded with serde_json::to_vec. JSON keeps the encoding human-inspectable at the cost of size. A future revision will likely switch to a binary frame; the schema is stable and forward-compatible.
Insert path
add(id, vector)
│
├─► VectorEntry::Add → serde_json::to_vec → strand.append
│ (await on vector_strand.write())
│
├─► Point { id, vec } → hnsw.insert(point, &mut searcher)
│ returns the HNSW internal index
│
├─► id_to_internal_index.insert(id, internal_index)
│
└─► update_index_strand() // stub: returns Ok(())Two locks are taken in sequence: the vector strand write lock, then the HNSW write lock. They are never held simultaneously, so a long Strand append does not block in-flight searches.
Search path
search(query, k)
│
├─► Build Point { id: nil, vec: query.to_vec() }
│
├─► ef = max(k, 24)
│
├─► dest = vec![Neighbor { index: 0, distance: u32::MAX }; k]
│
├─► hnsw.nearest(&probe, ef, &mut searcher, &mut dest)
│ returns filled subslice of dest
│
└─► For each neighbor:
point = hnsw.feature(neighbor.index)
push (point.id, f32::from_bits(neighbor.distance))Searcher is allocated per call. For high-throughput query loads, pool searchers to cut allocation pressure (not yet exposed).
Rebuild path
rebuild_index is invoked when a remove finds an entry in id_to_internal_index:
- Construct a fresh
BasisHnswwith a new RNG. - Allocate a fresh
DashMap<Uuid, usize>. - Read the vector strand from sequence 0 to
len() - 1. - For each
VectorEntry::Add, record(id, vector)in aDashMap. - For each
VectorEntry::Remove, drop the id from the map. - Insert every surviving
(id, vector)into the new HNSW. - Swap the new graph and id map into place under write locks.
Cost: O(N · log N) time, O(N) memory transient overhead.
Concurrency model
| Operation | Locks held | Lock kind |
|---|---|---|
add | vector strand, then HNSW, then id map | write × 3, never simultaneous |
search | HNSW only | read |
remove | vector strand, id map (write), HNSW (write during rebuild) | write |
rebuild_index | vector strand (read), then HNSW (write), then id map (write) | mixed |
DashMap is internally sharded; the id map locks are fine-grained.
Failure injection points
If you are stress-testing the implementation, the most useful synthetic faults are:
- Block
strand.appendto simulate a slow disk; verify thatsearchcontinues to serve. - Corrupt a single block in the vector strand;
rebuild_indexmust skip malformed entries (if let Ok(entry) = serde_json::from_slice ...). - Trigger
removefor a present id under sustainedaddload and confirm searches still return.
Where to extend
- HNSW snapshot serialization (currently a
TODOinupdate_index_strand). - Configurable distance function (cosine, inner-product) — today L2 is hard-coded.
- SIMD-accelerated
MetricPoint::distance— straightforward viawideorstd::simd. - Dimensionality enforcement at the
Basislevel rather than at the caller.