UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

96 lines (95 loc) • 4.6 kB
/** * pgvector Vector Store Adapter * * Client-injection `VectorStore` implementation backed by Postgres + * the `pgvector` extension. Callers construct and own the Postgres client * (e.g. a `pg.Pool`, or an in-process `@electric-sql/pglite` instance) and * pass it in — this module adds zero vendor SDKs as runtime dependencies. * * Storage model: one table per `indexName`, created lazily on first * `upsert()` (`CREATE TABLE IF NOT EXISTS`, so it is safe to call * repeatedly). Schema: * id text PRIMARY KEY * embedding vector(n) -- n fixed to the dimension of the first upsert * metadata jsonb NOT NULL DEFAULT '{}'::jsonb * * Scoring: matches `InMemoryVectorStore`'s cosine-similarity convention * (higher is better, 1.0 = identical direction) by converting pgvector's * `<=>` cosine *distance* operator (0 = identical) via `score = 1 - distance`. * * `MetadataFilter` -> SQL: every value that flows into a query is a bound * parameter (`$1`, `$2`, ...), including metadata field *names* — nothing * from the filter is ever string-concatenated into SQL text. The one thing * that IS textually embedded is the derived table name, and only after it * passes a strict identifier allow-list (see `IDENTIFIER_RE`), because * Postgres has no way to bind an identifier as a query parameter. * * Operators not expressible without ambiguity are rejected loudly instead of * silently no-op'ing: * - `$nor` (top-level logical operator) and `$size` (field-level operator) * are declared on `MetadataFilter` but (per the reference * `InMemoryVectorStore.matchesFilter`) are not actually implemented * there either — rather than guess at semantics that would silently * diverge from any caller's expectations, this adapter throws. * - Any other unrecognized `$foo` key, top-level or field-level, throws * for the same reason. * * Known, documented behavioral differences from `InMemoryVectorStore`: * - `$eq` / bare-value equality compares via Postgres jsonb structural * equality, whereas the in-memory reference uses JS `!==` (so it can * never match on array/object metadata values — two distinct array * instances are never `===`). This adapter is strictly more capable * for those value types. * - `$regex` runs Postgres POSIX regex (`~`) rather than a JS `RegExp`. * For non-string or missing fields the in-memory store tests the * pattern against `""` (so e.g. `^$` or `.*` can still match); this * adapter instead requires the field to be a JSON string, so those * edge-case patterns will not match a missing/non-string field here. */ import type { MetadataFilter, PgClientLike, PgVectorStoreOptions, VectorQueryResult, VectorStore } from "../../types/index.js"; export declare class PgVectorStore implements VectorStore { private readonly client; private readonly tablePrefix; /** Tables we've already run `CREATE TABLE IF NOT EXISTS` for, this process. */ private readonly ensuredTables; constructor(client: PgClientLike, options?: PgVectorStoreOptions); /** * Derive and validate the backing table name for an index. Throws on * anything that isn't a safe bare SQL identifier — this is the injection * guard for `indexName`. */ private tableName; /** * Ensure the `vector` extension and the index's table exist. Safe to call * repeatedly (`IF NOT EXISTS` throughout). `dimension` is fixed by the * first upsert into a fresh index; it is a validated positive integer * embedded directly in DDL (Postgres cannot bind type modifiers either). */ private ensureTable; /** * Upsert vectors into an index, creating its table on first use. * Mirrors `InMemoryVectorStore.upsert` (metadata defaults to `{}`). */ upsert(indexName: string, items: Array<{ id: string; vector: number[]; metadata?: Record<string, unknown>; }>): Promise<void>; /** * Query by cosine similarity. Returns `[]` if the index's table doesn't * exist yet (mirrors `InMemoryVectorStore.query` on an unknown index) * rather than throwing. */ query(params: { indexName: string; queryVector: number[]; topK?: number; filter?: MetadataFilter; includeVectors?: boolean; }): Promise<VectorQueryResult[]>; /** * Delete vectors by id. No-op if the index's table doesn't exist yet * (mirrors `InMemoryVectorStore.delete`). */ delete(indexName: string, ids: string[]): Promise<void>; }