Summary
Replace the brute-force cosine scan in store.ts's loadVectorMatrix() with an Approximate Nearest Neighbor (ANN) index (usearch or hnswlib) behind the existing Embedder-style interface. The current O(N) scan is correct and fast for small workspaces but hits memory and latency walls at scale.
Current behavior
```typescript
// store.ts — loadVectorMatrix()
// Loads ALL embeddings for a workspace into a Float32Array matrix
// engine.ts — recall()
const { ids, vectors } = this.#store.loadVectorMatrix(q.workspaceId);
for (let i = 0; i < vectors.length; i++) {
// brute-force cosine against every stored vector
for (let j = 0; j < queryVector.length; j++) sum += v[j]! * queryVector[j]!;
scored.push({ id: ids[i]!, score: sum });
}
```
The comment in store.ts is already honest about the scaling ceiling:
"Fine up to ~100k episodes on a laptop; swap to a real ANN index behind the same interface if we outgrow it."
Scale analysis
| Episodes |
Matrix size (384-dim float32) |
Scan time (est.) |
| 10K |
15 MB |
<5ms |
| 50K |
77 MB |
~20ms |
| 200K |
307 MB |
~80ms |
| 500K |
768 MB |
OOM risk |
At 50K+ episodes (a power user with months of sessions), the matrix load itself becomes the bottleneck, and at 200K+ it's a memory problem.
Proposed approach
VectorIndex interface
Extract the vector search responsibility into a pluggable interface alongside Embedder:
```typescript
// src/daemon/memory/vector-index.ts
export interface VectorIndex {
readonly size: number;
/** Add/update a vector for an episode id. /
upsert(id: string, vector: Float32Array): void;
/* Remove by id. /
remove(id: string): void;
/* Find top-K nearest neighbors to queryVector. Returns [id, score] pairs. /
search(queryVector: Float32Array, k: number): Array<{ id: string; score: number }>;
/* Persist index to disk (if backend supports it). /
save(path: string): Promise;
/* Load from disk. */
load(path: string): Promise;
}
```
Implementations
BruteForceVectorIndex (current behavior, default):
- In-memory Float32Array matrix
- Exact cosine, O(N) per query
- Zero deps — keep as default + fallback
UsearchVectorIndex (opt-in, production scale):
usearch npm package — WASM-first, Bun-compatible, no native rebuild
- HNSW graph index, O(log N) search
- Persists to
~/.codeoid/vectors/<workspaceId>.usearch
- Fall back to BruteForce if usearch init fails
```typescript
import { Index } from 'usearch';
export class UsearchVectorIndex implements VectorIndex {
#index: Index;
constructor(dimensions: number) {
this.#index = new Index({ metric: 'cos', dimensions });
}
upsert(id: string, vector: Float32Array) {
this.#index.add(hashId(id), vector);
}
search(queryVector: Float32Array, k: number) {
const result = this.#index.search(queryVector, k);
return result.keys.map((key, i) => ({ id: unhashId(key), score: result.distances[i] }));
}
// ...
}
```
Config
```json
{
"memory": {
"vectorBackend": "bruteforce" // "bruteforce" (default) | "usearch"
}
}
```
Migration
On first startup with vectorBackend: "usearch", the engine reads all existing embeddings from SQLite and populates the usearch index. Progress logged. Subsequent startups load the persisted index file (~5× smaller than loading all raw Float32Arrays).
Why usearch over hnswlib
usearch has a pure WASM npm build — no native compilation, works with Bun out of the box
hnswlib-node requires native rebuild (problematic with Bun's Node compat layer)
- usearch supports cosine metric natively (BGE vectors are L2-normalized, cosine = dot product)
- usearch actively maintained; MIT licensed
Files
- New
src/daemon/memory/vector-index.ts — VectorIndex interface + BruteForceVectorIndex + UsearchVectorIndex
src/daemon/memory/store.ts — remove loadVectorMatrix(), replace with VectorIndex integration
src/daemon/memory/engine.ts — accept VectorIndex in MemoryEngineOptions, use in recall()
src/daemon/memory/index.ts — factory: pick backend from config, init index on startup
src/config.ts — memory.vectorBackend config option
Acceptance criteria
Related
Summary
Replace the brute-force cosine scan in
store.ts'sloadVectorMatrix()with an Approximate Nearest Neighbor (ANN) index (usearch or hnswlib) behind the existing Embedder-style interface. The current O(N) scan is correct and fast for small workspaces but hits memory and latency walls at scale.Current behavior
```typescript
// store.ts — loadVectorMatrix()
// Loads ALL embeddings for a workspace into a Float32Array matrix
// engine.ts — recall()
const { ids, vectors } = this.#store.loadVectorMatrix(q.workspaceId);
for (let i = 0; i < vectors.length; i++) {
// brute-force cosine against every stored vector
for (let j = 0; j < queryVector.length; j++) sum += v[j]! * queryVector[j]!;
scored.push({ id: ids[i]!, score: sum });
}
```
The comment in
store.tsis already honest about the scaling ceiling:Scale analysis
At 50K+ episodes (a power user with months of sessions), the matrix load itself becomes the bottleneck, and at 200K+ it's a memory problem.
Proposed approach
VectorIndex interface
Extract the vector search responsibility into a pluggable interface alongside Embedder:
```typescript
// src/daemon/memory/vector-index.ts
export interface VectorIndex {
readonly size: number;
/** Add/update a vector for an episode id. /
upsert(id: string, vector: Float32Array): void;
/* Remove by id. /
remove(id: string): void;
/* Find top-K nearest neighbors to queryVector. Returns [id, score] pairs. /
search(queryVector: Float32Array, k: number): Array<{ id: string; score: number }>;
/* Persist index to disk (if backend supports it). /
save(path: string): Promise;
/* Load from disk. */
load(path: string): Promise;
}
```
Implementations
BruteForceVectorIndex (current behavior, default):
UsearchVectorIndex (opt-in, production scale):
usearchnpm package — WASM-first, Bun-compatible, no native rebuild~/.codeoid/vectors/<workspaceId>.usearch```typescript
import { Index } from 'usearch';
export class UsearchVectorIndex implements VectorIndex {
#index: Index;
constructor(dimensions: number) {
this.#index = new Index({ metric: 'cos', dimensions });
}
upsert(id: string, vector: Float32Array) {
this.#index.add(hashId(id), vector);
}
search(queryVector: Float32Array, k: number) {
const result = this.#index.search(queryVector, k);
return result.keys.map((key, i) => ({ id: unhashId(key), score: result.distances[i] }));
}
// ...
}
```
Config
```json
{
"memory": {
"vectorBackend": "bruteforce" // "bruteforce" (default) | "usearch"
}
}
```
Migration
On first startup with
vectorBackend: "usearch", the engine reads all existing embeddings from SQLite and populates the usearch index. Progress logged. Subsequent startups load the persisted index file (~5× smaller than loading all raw Float32Arrays).Why usearch over hnswlib
usearchhas a pure WASM npm build — no native compilation, works with Bun out of the boxhnswlib-noderequires native rebuild (problematic with Bun's Node compat layer)Files
src/daemon/memory/vector-index.ts—VectorIndexinterface +BruteForceVectorIndex+UsearchVectorIndexsrc/daemon/memory/store.ts— removeloadVectorMatrix(), replace withVectorIndexintegrationsrc/daemon/memory/engine.ts— acceptVectorIndexinMemoryEngineOptions, use inrecall()src/daemon/memory/index.ts— factory: pick backend from config, init index on startupsrc/config.ts—memory.vectorBackendconfig optionAcceptance criteria
VectorIndexinterface extracted and documentedBruteForceVectorIndexpasses all existing recall tests (behavior-identical to current)UsearchVectorIndeximplemented, behindmemory.vectorBackend: "usearch"configusearchlisted as optional dep — not required if brute force is usedRelated
src/daemon/memory/store.ts—loadVectorMatrix()src/daemon/memory/engine.ts—recall()vector branchperf: cache MemoryEngine vector matrix instead of reloading per recall— this supersedes that issue once usearch is in place