WeaveDocs
Basis

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
FieldRole
vector_strandCanonical write log of VectorEntry blocks
index_strandReserved for HNSW snapshots (stub today)
hnswIn-memory graph used by search
id_to_internal_indexLookup from caller-supplied UUID to HNSW node index
did_to_internal_indexLookup 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:

  1. Construct a fresh BasisHnsw with a new RNG.
  2. Allocate a fresh DashMap<Uuid, usize>.
  3. Read the vector strand from sequence 0 to len() - 1.
  4. For each VectorEntry::Add, record (id, vector) in a DashMap.
  5. For each VectorEntry::Remove, drop the id from the map.
  6. Insert every surviving (id, vector) into the new HNSW.
  7. 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

OperationLocks heldLock kind
addvector strand, then HNSW, then id mapwrite × 3, never simultaneous
searchHNSW onlyread
removevector strand, id map (write), HNSW (write during rebuild)write
rebuild_indexvector 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.append to simulate a slow disk; verify that search continues to serve.
  • Corrupt a single block in the vector strand; rebuild_index must skip malformed entries (if let Ok(entry) = serde_json::from_slice ...).
  • Trigger remove for a present id under sustained add load and confirm searches still return.

Where to extend

  • HNSW snapshot serialization (currently a TODO in update_index_strand).
  • Configurable distance function (cosine, inner-product) — today L2 is hard-coded.
  • SIMD-accelerated MetricPoint::distance — straightforward via wide or std::simd.
  • Dimensionality enforcement at the Basis level rather than at the caller.