Integration · pgvector (Postgres)
Add semantic search to your existing Postgres in 50 lines — parsr chunks + pgvector + HNSW.
pgvector is the simplest path to RAG: a Postgres extension that adds a `vector` column type and HNSW / IVFFlat indices. ~50K GitHub stars, ships with every Postgres on Neon / Supabase / Crunchy Bridge / RDS Aurora out of the box. The wedge for parsr customers: if your application already runs on Postgres (most fintechs and AI bookkeeping agents do), pgvector means RAG without operating a second datastore. parsr's chunks ingest into a single `chunks` table; metadata filtering is just SQL `WHERE` clauses you already know how to write. Joins between parsr-extracted entities (transactions, invoices) and chunk vectors become trivial — semantic search composed with relational filters in one query.
Install
One command
pip install parsr-sdk psycopg[binary] pgvector openaiCode
Working sample
from parsr_sdk import AsyncParsr
import psycopg, pgvector.psycopg
parsr = AsyncParsr(api_key="sk_eu_live_...")
conn = psycopg.connect("postgresql://...")
pgvector.psycopg.register_vector(conn)What you get
Highlights
- No second datastore — RAG inside the Postgres you already operate
- SQL metadata filters compose with vector search in one query
- Joins between parsr-extracted entities and chunk vectors are native
- HNSW indices give sub-10ms p95 at million-vector scale
- Neon EU + parsr EU = end-to-end EU residency, no extra vendor
Architecture
How the pieces fit
Single Postgres database with two tables: a relational `documents` table (parsr structured extraction landed here) and a `chunks` table with `(id, doc_id, text, embedding vector(1536), metadata jsonb)`. HNSW index on the embedding column for sub-10ms semantic retrieval. parsr.parse(*, include_chunks=true) produces both the structured payload (insert into `documents`) and the chunks (insert into `chunks` after embedding). Joins between the two are native SQL — semantic retrieval composed with relational filters in a single query.
Quickstart
End-to-end example
Parse a document with `include_chunks=true`, embed each chunk, upsert into pgvector (Postgres), query.
import os
import asyncio
from parsr_sdk import AsyncParsr
import psycopg
from pgvector.psycopg import register_vector
from openai import AsyncOpenAI
parsr = AsyncParsr(api_key=os.environ["PARSR_API_KEY"])
openai = AsyncOpenAI()
conn = psycopg.connect(os.environ["DATABASE_URL"], autocommit=True)
register_vector(conn)
# 1. Schema (run once).
conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
conn.execute("""
CREATE TABLE IF NOT EXISTS chunks (
id text PRIMARY KEY,
org_id text NOT NULL,
doc_type text NOT NULL,
text text NOT NULL,
page_numbers int[] NOT NULL,
metadata jsonb NOT NULL,
embedding vector(1536) NOT NULL
);
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS chunks_embedding_idx
ON chunks USING hnsw (embedding vector_cosine_ops)
""")
conn.execute("CREATE INDEX IF NOT EXISTS chunks_org_idx ON chunks(org_id)")
# 2. Parse + chunks.
result = await parsr.parse_invoice(
document_url="https://files.example.com/invoice.pdf",
include_chunks=True,
chunking={"strategy": "block"},
)
# 3. Embed and insert.
texts = [c.text for c in result.chunks]
embeds = await openai.embeddings.create(model="text-embedding-3-small", input=texts)
with conn.cursor() as cur:
for c, e in zip(result.chunks, embeds.data):
cur.execute(
"""
INSERT INTO chunks (id, org_id, doc_type, text, page_numbers, metadata, embedding)
VALUES (%s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO UPDATE
SET text = EXCLUDED.text,
embedding = EXCLUDED.embedding,
metadata = EXCLUDED.metadata
""",
(
c.id, "org_acme", "invoice", c.text,
c.page_numbers, c.metadata, e.embedding,
),
)
# 4. Semantic query — composed with SQL metadata filter.
question = "What was the largest line item?"
qe = await openai.embeddings.create(
model="text-embedding-3-small", input=[question]
)
rows = conn.execute(
"""
SELECT text, page_numbers, 1 - (embedding <=> %s) AS similarity
FROM chunks
WHERE org_id = %s AND doc_type = 'invoice'
ORDER BY embedding <=> %s
LIMIT 3
""",
(qe.data[0].embedding, "org_acme", qe.data[0].embedding),
).fetchall()
for r in rows:
print(r["similarity"], r["text"][:80], r["page_numbers"])Cost
What you'll actually pay
pgvector itself is free — you pay only for the underlying Postgres. On Neon EU at 5GB / 1M vectors the cost is ~€20/mo. On a self-hosted CAX22 with persistent storage it's ~€8/mo. Add OpenAI embedding cost (~€0.02 per 1K chunks at text-embedding-3-small) and parsr's per-page rate. The total RAG-on-Postgres pipeline at 10K invoices/mo is typically under €60 — cheaper than every dedicated vector DB short of running them yourself.
Performance
Tuning tips
- HNSW (hnsw vector_cosine_ops) over IVFFlat at >1M rows — recall is materially better at the same query cost
- Add a partial index on (org_id) WHERE org_id IS NOT NULL for multi-tenant filtering
- Use `EXPLAIN ANALYZE` to confirm the planner uses your HNSW index — common pitfall is forgetting to set ef_search higher than the LIMIT
- Don't store embeddings on the same row as large text content — keep `chunks` narrow + an `chunk_text` table if your chunks are >2KB; tighter row size means more vectors fit per page
Three lines and you're calling parsr from pgvector (Postgres).
Start building