@anvia/pgvector

Postgres pgvector vector store adapter for Anvia.

@anvia/pgvector connects Anvia's vector store interface to PostgreSQL with the pgvector extension. Use it when you already have Postgres and want to add vector search without a separate database.

Install

pnpm add @anvia/pgvector

The pg and pgvector packages are transitive dependencies. Your Postgres instance must have the vector extension available.

Quick Start

import { PgVectorStore } from "@anvia/pgvector";
import { createFastEmbedEmbeddingModel } from "@anvia/fastembed";

const model = await createFastEmbedEmbeddingModel();

// Connect (creates table and extension if missing)
const store = await PgVectorStore.connect({
  connectionString: "postgresql://user:pass@localhost:5432/mydb",
  tableName: "documents",
  vectorSize: 384,  // must match your embedding model's dimensions
});

// Upsert embedded documents
await store.upsertDocuments([
  {
    id: "doc-1",
    document: "Anvia is a TypeScript AI agent framework.",
    embeddings: await model.embedTexts(["Anvia is a TypeScript AI agent framework."]),
  },
]);

// Search
const index = store.index(model);
const results = await index.search({ query: "What is Anvia?", topK: 5 });
console.log(results[0].document);

Connection Options

type PgVectorStoreConnectOptions = {
  client?: PgClientLike;      // pre-configured pg client (Pool, Client, etc.)
  connectionString?: string;   // Postgres connection string
  tableName: string;           // required
  vectorSize: number;          // required: embedding dimensions
  createIfMissing?: boolean;   // default: true
  distance?: PgVectorDistance; // default: "cosine"
};
// With connection string
const store = await PgVectorStore.connect({
  connectionString: "postgresql://user:pass@localhost:5432/mydb",
  tableName: "documents",
  vectorSize: 384,
});

// With existing pg client
import pg from "pg";
const pool = new pg.Pool({ connectionString: "..." });

const store = await PgVectorStore.connect({
  client: pool,
  tableName: "documents",
  vectorSize: 384,
});

// Schema-qualified table name
const store = await PgVectorStore.connect({
  connectionString: "...",
  tableName: "public.documents",
  vectorSize: 384,
});

When createIfMissing is true (default), the adapter:

  1. Runs CREATE EXTENSION IF NOT EXISTS vector
  2. Creates the table with the correct schema if it does not exist
  3. Validates that the existing table's vector dimension matches vectorSize

Table Schema

The auto-created table has this structure:

CREATE TABLE IF NOT EXISTS "documents" (
  id text PRIMARY KEY,
  document_id text NOT NULL,
  document jsonb NOT NULL,
  metadata jsonb,
  embedding vector(384) NOT NULL
)
  • id is a deterministic hash of the logical document ID
  • document_id is the original document ID
  • document is the JSON-serialized document content
  • metadata is optional JSONB metadata
  • embedding is the pgvector column

Upserting Documents

await store.upsertDocuments([
  {
    id: "doc-1",
    document: { title: "Getting Started", content: "..." },
    metadata: { category: "guide", version: 2 },
    embeddings: await model.embedTexts(["Getting Started content..."]),
  },
]);

Uses INSERT ... ON CONFLICT ... DO UPDATE so documents are upserted atomically.

Searching

const index = store.index(model);

// Basic search
const results = await index.search({ query: "agent tools", topK: 5 });

// With threshold
const results = await index.search({
  query: "agent tools",
  topK: 10,
  threshold: 0.7,
});

// With metadata filter
const results = await index.search({
  query: "agent tools",
  topK: 5,
  filter: { type: "eq", key: "category", value: "guide" },
});

Metadata Filters

// Equality
{ type: "eq", key: "category", value: "guide" }

// Greater than / less than (numeric values only)
{ type: "gt", key: "version", value: 2 }
{ type: "lt", key: "version", value: 5 }

// Logical combinators
{ type: "and", filters: [...] }
{ type: "or", filters: [...] }

Filters are translated to Postgres JSONB queries on the metadata column. The gt and lt filters cast the metadata value to numeric, so they require numeric metadata values.

Distance Metrics

MetricValueSQL Operator
Cosine (default)"cosine"<=>
L2 (Euclidean)"l2"<->
Inner product"innerProduct"<#>

Agent Tool

Expose the vector index as an agent tool:

const searchTool = index.asTool({
  name: "search_docs",
  description: "Search the documentation.",
});

const agent = new AgentBuilder("support", model)
  .tool(searchTool)
  .build();

Error Handling

  • connect() throws if Postgres is unreachable or the vector extension is unavailable
  • connect() throws if the table exists but the vector dimension does not match vectorSize
  • upsertDocuments() throws if a document has zero embeddings
  • Metadata keys starting with __anvia_ are reserved and rejected