verso-db
v0.8.1
Published
Embedded TypeScript vector database for Bun, Node.js, and browsers, with portable HNSW search and OPFS persistence.
Maintainers
Readme
Verso
Verso is an embedded TypeScript vector database for Bun, Node.js, and browser applications. Its stable API is intentionally small:
VectorDB -> Collection<TMetadata> -> records and searchHNSW, storage backends, workers, WAL, and quantization are advanced extension surfaces. They are not part of the root package contract.
Install
bun add verso-db
# or
npm install verso-dbOpen, write, search
import { VectorDB, type CollectionConfig } from 'verso-db';
type Document = {
title: string;
category: 'tech' | 'science';
score: number;
tags: string[];
};
const db = await VectorDB.open({ path: './vectors' });
const config: CollectionConfig<Document> = {
vector: { dimensions: 768, metric: 'cosine' },
index: { type: 'hnsw', profile: 'balanced' },
};
const documents = await db.createCollection<Document>('documents', config);
await documents.insert([
{
id: 'doc-1',
vector: embedding,
metadata: {
title: 'HNSW explained',
category: 'tech',
score: 0.94,
tags: ['vector', 'search'],
},
},
]);
const result = await documents.search({
vector: queryEmbedding,
limit: 10,
filter: {
$and: [
{ category: { $in: ['tech', 'science'] } },
{ score: { $gte: 0.8 } },
],
},
});
for (const match of result.matches) {
console.log(match.id, match.score, match.metadata?.title);
}
await db.close();distance is metric-native and lower is always better. score is the stable
application-facing value and higher is always better: cosine uses similarity,
while Euclidean and dot-product scores are the negated metric distance.
Collection operations
await documents.upsert(records);
await documents.insertPacked({ ids, vectors: packedFloat32, metadata });
await documents.import(recordsAsyncIterable, { mode: 'upsert', batchSize: 2_000 });
const record = await documents.get('doc-1', {
include: { vector: true },
});
const records = await documents.getMany(['doc-1', 'doc-2']);
const page = await documents.list({ limit: 100, cursor: pageCursor });
for await (const record of documents.scan({ filter })) {
// export or inspect records without a large array allocation
}
await documents.update('doc-1', {
metadata: { score: 0.97 }, // shallow merge
});
await documents.delete({ ids: ['doc-2'] });
await documents.delete({ filter: { category: 'science' } });insert rejects duplicate IDs. upsert replaces the supplied vector and
metadata. update merges metadata and optionally replaces the vector. Deletes
are tombstones until compact() physically rebuilds the index.
Filters
Filters are strict and support equality, comparisons, membership, existence, array membership, prefixes, ranges, nested dotted paths, and Boolean composition:
import { where } from 'verso-db';
const filter = where.and(
where.in('category', ['tech', 'science']),
where.gte('score', 0.8),
where.contains('tags', 'vector'),
);Supported operators are $eq, $ne, $gt, $gte, $lt, $lte, $in,
$nin, $exists, $contains, $containsAny, $containsAll, $startsWith,
$between, $and, $or, and $not. Comparisons never coerce strings and
numbers. Missing fields differ from explicit null; $ne and $nin match a
missing field, while positive comparisons do not. Metadata must be plain,
finite, JSON-compatible values.
Search strategies and diagnostics
The ordinary API is independent of HNSW tuning:
const exact = await documents.search({
vector: queryEmbedding,
limit: 10,
strategy: 'exact',
});
const highRecall = await documents.search({
vector: queryEmbedding,
limit: 10,
accuracy: 'high',
tuning: { efSearch: 256, quantization: 'auto', oversampling: 4 },
explain: true,
});
const many = await documents.searchMany([
{ vector: queryA, limit: 10 },
{ vector: queryB, limit: 10 },
], { concurrency: 'auto' });Verso intentionally stops at vector retrieval. Full-text indexing, hybrid
fusion, query expansion, and model reranking belong in the application search
layer. Exact-vs-approximate recall comparison is available from the explicit
verso-db/advanced subpath for tuning and regression tests.
Lifecycle, persistence, and operations
VectorDB.open() is asynchronous and discovers corruption or unavailable
persistence before returning. Node and Bun use filesystem storage; browsers
use OPFS when available. A requested persistent backend fails closed by
default. Use { storage: { type: 'opfs', fallback: 'memory' } } only when an
explicit in-memory fallback is acceptable.
const info = await documents.describe();
const stats = await documents.stats();
const report = await documents.verify();
const compacted = await documents.compact({ onProgress: console.log });
await db.setAlias('documents-current', 'documents');
const current = await db.collection('documents-current');
const snapshot = await db.snapshot();
await db.restore(snapshot, { overwrite: true });
const collectionSnapshot = await documents.exportSnapshot();
await db.importCollectionSnapshot(collectionSnapshot, { name: 'documents-copy' });
await db.backupTo('./backups/documents');Use durability: 'manual' for bulk ingestion and call flush() explicitly;
the default is immediate persistence. write()/batch() provide a named
mutation session for application workflows, while insertMany() and
import()/export() support async iterables. namespace(value) creates a
scoped handle over an indexed metadata field.
Package boundaries
The root export contains VectorDB, the generic Collection type, typed
record/filter/search types, where, and structured errors. Advanced APIs are
explicit:
import { HNSWIndex, ScalarQuantizer } from 'verso-db/hnsw';
import { MemoryStorage, defineStorageAdapter } from 'verso-db/storage';
import { WorkerPool, WriteAheadLog } from 'verso-db/advanced';The root does not export concrete storage implementations, raw HNSW classes, WAL/worker protocols, or quantizer internals.
Development
bun install
bun test
bun run build
bun run verify:dist
bun run test:browserPerformance measurements are workload-specific. Use the bundled benchmark commands when making recall or latency claims.
