Vecal

API Reference

Vecal 1.0 methods, types, states, and stable errors.

API reference

All storage and search methods are asynchronous and execute through a Dedicated Worker. Vectors are Float32Array values with exactly the configured dimension.

VectorDB.open(options)

const db = await VectorDB.open<MyMetadata>({
    name: 'documents',
    dimension: 384,
    metric: 'cosine',
    // Optional; use one override, not both:
    workerUrl: customWorkerUrl,
    // workerFactory: () => new Worker(appWorkerUrl, { type: 'module' }),
});

name, dimension, and metric are required. The metric is fixed for the database and all Exact/HNSW searches. A conflicting reopen throws SchemaMismatchError.

Records

add(input) => Promise<string>

Inserts a record. Omit id to generate one with crypto.randomUUID(). An existing ID throws RecordConflictError.

addMany(inputs) => Promise<string[]>

Inserts all inputs in one transaction. The operation is all-or-nothing, including conflicts within the input batch.

upsert(inputWithId) => Promise<string>

Inserts or completely overwrites the record at an explicit ID.

get(id) => Promise<VectorRecord | undefined>

Returns one record. With cosine metric, the returned stored vector is normalized.

getMany(ids) => Promise<Array<VectorRecord | undefined>>

Returns records in the same order as the requested IDs.

update(id, patch) => Promise<void>

Updates vector, metadata, or both. If metadata is present, it replaces the entire metadata object. A missing ID throws RecordNotFoundError.

delete(id) => Promise<boolean>

Returns whether an existing record was deleted.

deleteMany(ids) => Promise<number>

Deletes unique IDs in one transaction and returns the number found.

count() => Promise<number>

Returns the current entry count.

clear() => Promise<void>

Atomically removes entries and snapshots, increments the revision, and returns the index state to absent.

search(query, options?) => Promise<SearchResult[]>

const results = await db.search(query, {
    k: 10,
    strategy: 'auto',       // auto | exact | hnsw
    efSearch: 64,
    where: {
        tenant: { $eq: 'acme' },
        rating: { $gte: 4 },
    },
    minScore: 0.3,
    signal: controller.signal,
});
  • k defaults to 10 and must be a positive integer.
  • strategy defaults to auto.
  • efSearch defaults to 64 and applies to HNSW.
  • where supports top-level AND conditions with $eq, $in, $gt, $gte, $lt, and $lte.
  • minScore is inclusive.
  • signal cancels Worker computation with OperationCancelledError.

Scores are higher-is-better: cosine similarity, dot product, or negative Euclidean distance. Equal scores sort by ID.

auto uses HNSW only in ready state. Explicit hnsw throws IndexNotReadyError if the index is not ready.

Index lifecycle

ensureIndex(options) => Promise<void>

await db.ensureIndex({
    type: 'hnsw',
    m: 16,
    efConstruction: 200,
    seed: 0x5eed1234,
    onProgress: ({ completed, total, ratio }) => {},
    signal: controller.signal,
});

Defaults are m: 16, efConstruction: 200, and a stable built-in seed. A revision collision retries once; a second collision throws IndexBuildConflictError.

indexStatus() => IndexStatus

Returns the most recently reported Worker status synchronously:

interface IndexStatus {
    state: 'absent' | 'building' | 'ready' | 'stale' | 'closed';
    revision: number;
    nodeCount: number;
    tombstoneRatio: number;
    config?: { m: number; efConstruction: number; seed: number };
}

Each subsequent public operation reads the authoritative IndexedDB revision and refreshes this status if another instance committed a change.

close() => Promise<void>

Waits for a ready index checkpoint, closes IndexedDB and BroadcastChannel, terminates the Worker, and moves the instance to closed. It is idempotent. Later operations throw DatabaseClosedError.

Core types

type Metric = 'cosine' | 'l2' | 'dot';
type SearchStrategy = 'auto' | 'exact' | 'hnsw';

type JSONValue =
    | string
    | number
    | boolean
    | null
    | JSONValue[]
    | { [key: string]: JSONValue };

type Metadata = Record<string, JSONValue>;

interface VectorInput<M extends Metadata> {
    id?: string;
    vector: Float32Array;
    metadata?: M;
}

interface VectorRecord<M extends Metadata> {
    id: string;
    vector: Float32Array;
    metadata?: M;
}

interface SearchResult<M extends Metadata> {
    id: string;
    score: number;
    metadata?: M;
}

Metadata must be a finite, acyclic JSON object that structured clone can transfer. undefined, Date, class instances, functions, symbols, bigint, and non-finite numbers are rejected.

Stable errors

All public errors extend VecalError and carry a stable code.

ClassCodeMeaning
UnsupportedEnvironmentErrorUNSUPPORTED_ENVIRONMENTWorker or IndexedDB unavailable/blocked
SchemaMismatchErrorSCHEMA_MISMATCHDurable dimension, metric, or schema differs
RecordConflictErrorRECORD_CONFLICTadd/addMany ID already exists
RecordNotFoundErrorRECORD_NOT_FOUNDupdate target does not exist
IndexNotReadyErrorINDEX_NOT_READYExplicit HNSW requested without a ready graph
IndexBuildConflictErrorINDEX_BUILD_CONFLICTData changed during both build attempts
OperationCancelledErrorOPERATION_CANCELLEDAbortSignal cancelled the request
DatabaseClosedErrorDATABASE_CLOSEDOperation used after close
ValidationErrorVALIDATION_ERRORInvalid option, vector, metadata, or filter