pgvector
Reference for the pgvector adapter, which wraps a pg Pool or Client and compiles filters to JSONB predicates.
The pgvector adapter wraps any client with a query(text, params) method that resolves to { rows }. A pg Pool or Client works as is. An index is a table.
import { Pool } from "pg";
import { createPgvectorStore } from "vecstore-sdk/pgvector";
const store = createPgvectorStore({
client: new Pool({ connectionString }),
schema: "public",
});Install the client alongside the SDK:
bun add vecstore-sdk pgFor postgres.js, wrap sql.unsafe so it resolves to { rows }, and pass { query: async (text, params) => ({ rows: await sql.unsafe(text, params) }) } as the client. The test suite does not run against postgres.js.
Table layout
createIndex runs CREATE EXTENSION IF NOT EXISTS vector, creates the table below, and adds an HNSW (Hierarchical Navigable Small World) index for the metric and a GIN (Generalized Inverted Index) index on metadata:
CREATE TABLE "docs" (
id text NOT NULL,
namespace text NOT NULL DEFAULT '',
embedding vector(1536) NOT NULL,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
PRIMARY KEY (namespace, id)
);CREATE EXTENSION needs a role that can create extensions. If your application role cannot, create the extension yourself before you call createIndex.
pgvector's HNSW index holds at most 2,000 dimensions. createIndex with a larger dimension returns { kind: "provider" } with the message column cannot have more than 2000 dimensions for hnsw index and creates nothing. A 3,072-dimension model such as OpenAI's text-embedding-3-large needs its output shortened to 2,000 dimensions or fewer for this adapter.
An index name can be at most 49 bytes, so that the _embedding_idx and _metadata_idx index names stay under the 63-byte Postgres identifier limit. createIndex returns invalid_argument for a longer name.
When the database has a role named anon, as every Supabase project does, createIndex also turns on row level security for the new table and adds no policy. The role that ran createIndex owns the table and keeps full access. anon and authenticated see and change nothing through the Supabase REST API until you write a policy, the same as for a table the Supabase adapter creates. On a database without an anon role the table is created without row level security. Tables created by version 0.1.4 or earlier have it off; turn it on with alter table "docs" enable row level security;.
Metric detection
Queries read the metric from the HNSW or IVFFlat index definition in pg_indexes. When no index exists, they default to cosine. This also works for tables the SDK did not create.
The store caches the metric per table after the first query and forgets it when createIndex or deleteIndex on the same store touches that table, or when a query on that table fails. If another process recreates a table with a different metric, create a new store.
listIndexes returns the tables in the schema that have an embedding vector column.
Filtered and large queries
pgvector applies the namespace and the filter after it scans the HNSW index, and a scan stops at hnsw.ef_search candidates, 40 by default. Without an iterative scan, a query with a topK above 40, a selective filter, or a namespace that holds part of the table returns fewer than topK records. On pgvector 0.8.0 or later the adapter turns on an iterative scan for each query, so the scan continues until topK records pass. It sets hnsw.iterative_scan to strict_order for the current transaction, which under autocommit is the query's own statement, and keeps a value you set yourself, such as relaxed_order on the database. If you pass a client inside an explicit transaction, the setting stays on for the rest of that transaction.
On an older pgvector there is no iterative scan and the adapter sends the plain statement. Upgrade the extension, or raise hnsw.ef_search for the role or the database. A filter that matches few records can still return fewer than topK records on any version, because pgvector stops an iterative scan after hnsw.max_scan_tuples tuples, 20,000 by default. The adapter also sends the plain statement to a table with an IVFFlat index, which the SDK never creates.
What the adapter stores
| Concern | pgvector |
|---|---|
| Index | Table |
| Namespace | namespace column, part of the primary key |
| Id | id text |
| Metadata | metadata jsonb |
| Metric | Read from the HNSW index operator class |