Compare

SkimmerDB vs pgvector

If your documents already live in Postgres, pgvector is often enough: one database, one transaction, no copy to keep in step. SkimmerDB is for when search outgrows that: text, regex and vectors on the same documents, a namespace per tenant with keys that reach only theirs, and nothing to size or vacuum.

Side by side

What each one is, and where they overlap.

Postgres + pgvectorSkimmerDB
What it isAn open-source extension: vectors as a column type in your PostgresA hosted search database over HTTP and MCP
Where the data isBeside your other tables, in one transaction with themA copy in object storage that you write to, or sync from your bucket or DuckLake
Vector indexHNSW or IVFFlat, up to 2,000 dimensions for vector (4,000 halfvec); exact search with no indexIVF + RaBitQ per segment, up to 2,048 dimensions; recall@10 about 0.97 by default
Filters with vectorsApproximate indexes filter after the scan; iterative index scans (0.8+), partial indexes or partitions fill the gapFilters and text narrow the candidates before ranking; a top-k answer is complete or says partial
Exact text, regexLIKE / ~ scan rows unless you add pg_trgm indexesA trigram index answers substrings and RE2 regex, with exact counts
Full texttsvector / ts_rank_cd; you fuse with vectors yourself (RRF or a cross-encoder)BM25 per namespace; rank: "bm25" with a vector fuses both server-side
Tenantstenant_id column plus row-level security, or a schema or database eachA namespace per tenant and an API key limited to it
Who runs itYou (or your Postgres host): sizing, memory for HNSW, vacuum, upgradesWe do; idle namespaces cost their stored bytes
AgentsYou write the tool and its database roleA remote MCP server with OAuth, scoped to the user’s namespaces

Stay on pgvector when

  • The data is already there. Embeddings next to the rows they describe, updated in the same transaction.
  • It fits. The HNSW index sits comfortably in memory and search load doesn’t slow your writes.
  • Queries are vectors plus WHERE. No substring, regex or ranked keyword search on the same documents.
  • One tenant, or RLS you trust. Every path connects as a role the policy applies to.

Add SkimmerDB when

  • Search competes with your app. Index memory and query CPU belong somewhere other than the primary.
  • Agents need exact text too. Error codes, ids and regex, counted exactly, next to vectors and BM25.
  • Hybrid in one call. How BM25 and vector rankings fuse: hybrid search, explained.
  • Each customer gets their own. A namespace and a key per tenant; an agent holding Birch’s key can’t name Maple’s data.
  • Most tenants are idle. An idle namespace runs nothing and costs its bytes.

The tenant boundary, both ways

Three documents for two customers, Birch and Maple. In Postgres, row-level security adds the tenant condition, for roles it applies to. In SkimmerDB, the key decides which namespaces exist for the caller. Both were run on October 23, 2026; the comments are what they printed.

Postgres 18 + pgvector 0.8.1
CREATE TABLE docs (
  tenant    text NOT NULL,
  id        text NOT NULL,
  body      text NOT NULL,
  embedding vector(4) NOT NULL,
  PRIMARY KEY (tenant, id)
);
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
INSERT INTO docs VALUES
  ('birch', 'birch-1', 'Refunds within 14 days',        '[0.9,0.1,0,0]'),
  ('birch', 'birch-2', 'Shipping takes 3 days',         '[0.1,0.9,0,0]'),
  ('maple', 'maple-1', 'Store credit only, no refunds', '[0.95,0,0.1,0]');

-- the database adds the tenant condition itself
ALTER TABLE docs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_only ON docs
  USING (tenant = current_setting('app.tenant', true));
CREATE ROLE app_user LOGIN PASSWORD '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON docs TO app_user;

-- connected as app_user, for Birch
SET app.tenant = 'birch';
SELECT id FROM docs ORDER BY embedding <=> '[1,0,0,0]' LIMIT 2;
-- birch-1, birch-2
RESET app.tenant;
SELECT count(*) FROM docs;
-- 0

-- connected as the table's owner (a migration user, say): the policy doesn't apply
SELECT id FROM docs ORDER BY embedding <=> '[1,0,0,0]' LIMIT 2;
-- maple-1, birch-1

The last query is the one to plan for: the table’s owner, a superuser or a BYPASSRLS role sees every tenant, so the application must never connect as one, and pooled connections must set app.tenant on every checkout.

SkimmerDB
B=https://api.skimmerdb.com/v1
H=(-H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json")

curl -fsS "$B/namespaces/kb-birch/docs" "${H[@]}" -d '{"wait":true,"docs":[
  {"id":"birch-1","text":"Refunds within 14 days","vector":[0.9,0.1,0,0]},
  {"id":"birch-2","text":"Shipping takes 3 days","vector":[0.1,0.9,0,0]}]}' >/dev/null
curl -fsS "$B/namespaces/kb-maple/docs" "${H[@]}" -d '{"wait":true,"docs":[
  {"id":"maple-1","text":"Store credit only, no refunds","vector":[0.95,0,0.1,0]}]}' >/dev/null

# the key your backend hands to Birch's requests
BIRCH_KEY=$(curl -fsS "$B/keys" "${H[@]}" \
  -d '{"name":"birch","scope":"read","namespaces":["kb-birch"]}' | jq -r .key)
K=(-H "Authorization: Bearer $BIRCH_KEY" -H "Content-Type: application/json")

curl -fsS "$B/namespaces/kb-birch/search" "${K[@]}" -d '{"vector":[1,0,0,0],"limit":2}' | jq -c '[.hits[].id]'
# ["birch-1","birch-2"]
curl -sS -o /dev/null -w '%{http_code}\n' "$B/namespaces/kb-maple/search" "${K[@]}" -d '{"vector":[1,0,0,0]}'
# 404

Here the boundary is in the credential. The admin key still reaches everything, so it stays on your backend; the agent or request gets the narrow key. See API keys and namespaces vs metadata filters.

What the docs say

Read October 23, 2026.

SourceWhat it says
pgvectorExact search by default; HNSW and IVFFlat indexes up to 2,000 dimensions for vector (halfvec 4,000). “With approximate indexes, filtering is applied after the index is scanned”; iterative index scans, partial indexes and partitioning are the remedies. Hybrid search: “Use together with Postgres full-text search”, combining results with Reciprocal Rank Fusion or a cross-encoder. Version 0.8.7 at the time of reading.
PostgreSQL row security“Superusers and roles with the BYPASSRLS attribute always bypass the row security system”; table owners do too unless the table is set to FORCE ROW LEVEL SECURITY. PostgreSQL 18 documentation.

Questions

Should I use pgvector or a separate vector database?

Start with pgvector if your data already lives in Postgres, fits comfortably in its memory and disk, and your queries are vector search plus ordinary WHERE clauses. A separate retrieval layer earns its place when search load or index memory starts to compete with your transactional workload, when you need exact substring, regex or BM25 search alongside vectors, or when many tenants need isolated search without growing one table.

Is pgvector slower than SkimmerDB?

We haven’t run the two on one workload, so this page makes no speed claim. Both are approximate by default once indexed; speed depends on dataset, filters, hardware and settings. Measure on your own data.

How do I keep tenants apart in pgvector?

A tenant column with row-level security makes Postgres add the tenant condition to every query of a role the policy applies to. Connect as a role without BYPASSRLS that doesn’t own the table (or set FORCE ROW LEVEL SECURITY), and set the tenant from the session on every connection, including pooled ones.

Can I use both?

Yes. Keep Postgres as the system of record and write searchable copies to SkimmerDB from the same job, one namespace per tenant. Documents are written by id, so writing again is safe, and deleting a tenant there is deleting its namespace.

What does SkimmerDB cost next to a Postgres I already pay for?

Free up to 1 GB stored and 100k queries a month. After that, $0.10 per GB-month stored, $0.50 per million queries, with a $10 monthly minimum. pgvector adds no licence cost; it costs the Postgres capacity it uses.

Try it with your own documents

Free up to 1 GB stored and 100k queries a month. No card to start.