Pluggable vector search
By default the engine scores vectors in JavaScript. That’s fine for thousands of facts per
entity. For larger corpora, plug in an approximate-nearest-neighbour index through
VectorRanker. The engine still handles embedding validation, hybrid blending, and row hydration.
interface VectorRanker { rankBySimilarity(args: { entityId: string; queryVec: Float32Array | number[]; // treat as read-only candidateIds?: string[]; // set when a keyword pre-filter is active limit: number; }): Promise<Array<{ id: string; semanticScore: number }>>; // higher = more similar
onEmbeddingPersisted?(e: { entityId: string; factId: string; vector: Float32Array | null }): void | Promise<void>;}Example: sqlite-vec
Section titled “Example: sqlite-vec”const sqliteVecRanker: VectorRanker = { async rankBySimilarity({ entityId, queryVec, candidateIds, limit }) { let sql = `SELECT id, (1.0 - distance) AS semanticScore FROM vec_facts WHERE entity_id = ?`; const params: unknown[] = [entityId]; if (candidateIds) { sql += ` AND id IN (${candidateIds.map(() => '?').join(',')})`; params.push(...candidateIds); } sql += ` ORDER BY vec MATCH vec_neighbor(?) LIMIT ?`; // adjust to your sqlite-vec version params.push(queryVec, limit); return db.getAllAsync(sql, params); },
async onEmbeddingPersisted({ entityId, factId, vector }) { if (vector) { await db.runAsync(`INSERT OR REPLACE INTO vec_facts (id, entity_id, vec) VALUES (?, ?, ?)`, [factId, entityId, vector]); } else { await db.runAsync(`DELETE FROM vec_facts WHERE id = ?`, [factId]); } },};
createWiki(db, { llmProvider, vectorRanker: sqliteVecRanker });The SQLite embedding_blob column stays the source of truth. It’s written before the hook
is called.
When the ranker fails
Section titled “When the ranker fails”createWiki(db, { llmProvider, vectorRanker, vectorRankerFallback: 'js-cosine', // default onVectorRankerFallback: ({ error, policy }) => log.warn('ranker failed', policy, error),});| Policy | Behavior | Choose it for |
|---|---|---|
'js-cosine' (default) |
Score in-process, the same as having no ranker | Availability |
'keyword' |
Keyword results only | Fast degradation |
'empty' |
No facts for this read (tasks and events still load) | Strict consistency |
'throw' |
Reject the read() |
Failing fast |
Rules for adapter authors
Section titled “Rules for adapter authors”- Parameterize everything. Never concatenate
entityId,factId, orcandidateIdsinto SQL. - Filter by
entityIdin every query, to prevent cross-tenant leaks. - Scrub credentials from errors. The engine also sanitizes them by default (
sanitizeRankerErrors: true). - Cap
limitandcandidateIds.length, and don’t keep references tovectorafter the call returns. - If
onEmbeddingPersistedis eventually consistent (for example a remote index), document the lag. Reads may miss new facts until the index catches up. - If
onEmbeddingPersistedthrows while a fact is written or imported, the engine reportshook_failedwith the fact id and carries on, so your index can miss that fact. Reconcile from the diagnostic. (Removals are different: a failed removal stays owed and is retried.)
Hybrid blending still applies on top of ranker scores when hybridWeight is set.