Introduction: Why RAG with PostgreSQL in 2026?
By mid-2026, the hype around large language models (LLMs) has settled into pragmatic engineering. We no longer ask "Can AI answer this?" but rather "How do we make AI answer this correctly, using our data, without hallucinating?" The answer, for most production systems, remains Retrieval-Augmented Generation (RAG).
In 2026, the landscape of vector databases has matured, but one surprising champion has emerged from the relational world: PostgreSQL with the pgvector extension. Why? Because developers building RAG systems in 2026 demand a single source of truth. They don't want to manage synchronization between a transactional database (like MySQL or vanilla PostgreSQL) and a separate vector store (like Pinecone or Weaviate). They want ACID compliance, JSON support, full-text search, and vector similarity search — all within one SQL engine.
This guide is for engineers who know SQL but are new to building AI pipelines. We will walk through a complete, production-ready RAG system using PostgreSQL (pgvector), OpenAI embeddings, and FastAPI. We will use 2026 best practices: batched embeddings, partitioning for scale, and hybrid search (combining vector similarity with keyword-based BM25). By the end, you will have a blueprint for a system that can ingest thousands of documents and answer questions with cited sources.
What You Will Build
- A PostgreSQL database storing documents and their vector embeddings.
- A Python pipeline using OpenAI's
text-embedding-3-largemodel (the 2026 standard) to create embeddings. - A FastAPI server with two endpoints:
/ingest(to add documents) and/query(to ask questions). - A hybrid search mechanism that combines semantic similarity with keyword matching using PostgreSQL's
tsvector.
Q&A: Building Your RAG System Step-by-Step
Q1: Why choose PostgreSQL over a dedicated vector database like Pinecone or Weaviate in 2026?
Short answer: operational simplicity and transactional guarantees.
In 2026, the trend is toward "converged databases." Running a separate vector store introduces a new infrastructure component: you must manage data replication, consistency, and backups across two systems. If your document metadata changes (e.g., you update a document title), you must update both the relational store and the vector index. This creates a distributed transaction problem.
PostgreSQL with pgvector solves this. You can store your document text, metadata (author, date, tags), and the vector embedding all in one row. You can use standard SQL transactions (BEGIN, COMMIT, ROLLBACK) to ensure atomic updates. The pgvector extension (now at version 0.8 in 2026) supports IVFFlat and HNSW indexes, which are competitive with specialized vector databases for recall up to 99% at reasonable latency.
| Feature | PostgreSQL + pgvector | Dedicated Vector DB (e.g., Pinecone) |
|---|---|---|
| ACID compliance | Full | Limited (eventual consistency) |
| Hybrid search (vector + text) | Native (tsvector + vector) | Often requires separate index |
| Operational overhead | Single database | Two systems to manage |
| Cost at scale (10M vectors) | Moderate (SSD storage) | High (per-vector pricing) |
| Maturity of SQL tooling | Excellent (pgAdmin, DBeaver) | Proprietary APIs |
If you need sub-5ms latency for 100M+ vectors, a dedicated vector store may still win. But for 90% of applications (document Q&A, customer support bots, internal knowledge bases), PostgreSQL is the pragmatic choice.
Q2: How do I set up PostgreSQL with pgvector and generate embeddings using OpenAI?
First, ensure your PostgreSQL instance (version 16 or 17 in 2026) has the pgvector extension. On Ubuntu 24.04:
sudo apt install postgresql-16-pgvector
Then enable the extension:
CREATE EXTENSION vector;
Now, create a table to store documents and their embeddings. We'll also add a tsvector column for full-text search:
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
content_tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
embedding vector(3072), -- dimension for text-embedding-3-large
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_documents_embedding ON documents USING hnsw (embedding vector_cosine_ops);
CREATE INDEX idx_documents_tsv ON documents USING gin (content_tsv);
Key decision: embedding dimension. In 2026, OpenAI's text-embedding-3-large model outputs 3072 dimensions by default. You can reduce this to 256 or 512 (via the dimensions parameter) to save storage and speed up queries, at a slight cost to recall. For most RAG systems, 1024 dimensions is a sweet spot.
Now, generate embeddings in Python. We'll use the openai library (version 1.50+):
import openai
from pgvector.psycopg2 import register_vector
import psycopg2
client = openai.OpenAI(api_key="sk-...")
conn = psycopg2.connect("dbname=rag user=postgres password=secret")
register_vector(conn)
cur = conn.cursor()
def ingest_document(title: str, content: str):
# Generate embedding with reduced dimensions for efficiency
response = client.embeddings.create(
model="text-embedding-3-large",
input=content,
dimensions=1024 # trade-off: smaller dimension, less storage
)
embedding = response.data[0].embedding
cur.execute(
"INSERT INTO documents (title, content, embedding) VALUES (%s, %s, %s)",
(title, content, embedding)
)
conn.commit()
Notice we use dimensions=1024. This reduces storage by 66% compared to 3072, with minimal impact on retrieval quality for most use cases.
Q3: How do I perform hybrid search — combining vector similarity with keyword matching?
Pure vector search is great for capturing semantic meaning, but it can miss exact keyword matches that matter. For example, if a user searches for "Python 3.12 deprecation", you want to boost documents that literally contain those keywords. This is where hybrid search shines.
PostgreSQL makes this trivial. We combine cosine distance (for vectors) with ts_rank (for text):
def hybrid_search(query: str, alpha: float = 0.7, top_k: int = 10):
# Generate query embedding
response = client.embeddings.create(
model="text-embedding-3-large",
input=query,
dimensions=1024
)
query_embedding = response.data[0].embedding
# Create tsquery from user input
cur.execute(
"""
SELECT id, title, content,
(1 - (embedding <=> %s::vector)) AS vector_score,
ts_rank(content_tsv, to_tsquery('english', %s)) AS keyword_score
FROM documents
WHERE (1 - (embedding <=> %s::vector)) > 0.5 -- semantic threshold
OR content_tsv @@ to_tsquery('english', %s) -- keyword match
ORDER BY (%s * (1 - (embedding <=> %s::vector)) + (1 - %s) * ts_rank(content_tsv, to_tsquery('english', %s))) DESC
LIMIT %s;
""",
(query_embedding, query, query_embedding, query,
alpha, query_embedding, alpha, query, top_k)
)
return cur.fetchall()
The parameter alpha controls the balance: alpha=1.0 is pure vector search, alpha=0.0 is pure keyword search. In practice, 0.7 works well — semantic meaning dominates, but exact keyword matches get a boost.
2026 best practice: Normalize scores. Vector similarity ranges from 0 to 1 (for cosine), but ts_rank can be arbitrarily large. Consider scaling keyword scores to [0,1] using min-max normalization for consistent blending.
Q4: How do I handle large-scale ingestion (millions of documents) efficiently?
Ingesting 1 million documents one-by-one is slow. In 2026, we batch operations and use parallel processing.
Batching embeddings: OpenAI's API supports batch inputs. Send multiple texts in one call:
def ingest_batch(documents: list[tuple[str, str]], batch_size: int = 100):
for i in range(0, len(documents), batch_size):
batch = documents[i:i+batch_size]
texts = [doc[1] for doc in batch]
response = client.embeddings.create(
model="text-embedding-3-large",
input=texts,
dimensions=1024
)
values = []
for j, doc in enumerate(batch):
embedding = response.data[j].embedding
values.append((doc[0], doc[1], embedding))
# Use executemany for fast inserts
cur.executemany(
"INSERT INTO documents (title, content, embedding) VALUES (%s, %s, %s)",
values
)
conn.commit()
Parallelism: Use Python's concurrent.futures to parallelize across multiple database connections. On a 16-core machine, you can achieve 500-1000 documents per second.
Partitioning by date: If your documents are time-series (e.g., support tickets), partition the table by created_at using PostgreSQL's declarative partitioning. This speeds up queries that filter by date range and simplifies archival.
CREATE TABLE documents (
id SERIAL,
title TEXT,
content TEXT,
embedding vector(1024),
created_at TIMESTAMPTZ DEFAULT NOW()
) PARTITION BY RANGE (created_at);
CREATE TABLE documents_2026_q1 PARTITION OF documents
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE documents_2026_q2 PARTITION OF documents
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
Q5: How do I build the FastAPI server with proper error handling and streaming?
Here is a minimal but production-ready FastAPI application:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import psycopg2
from pgvector.psycopg2 import register_vector
import openai
app = FastAPI()
# Database connection pool (use psycopg2.pool for production)
conn = psycopg2.connect("dbname=rag user=postgres password=secret")
register_vector(conn)
cur = conn.cursor()
client = openai.OpenAI()
class QueryRequest(BaseModel):
question: str
top_k: int = 5
alpha: float = 0.7
class QueryResponse(BaseModel):
answer: str
sources: list[dict]
@app.post("/query", response_model=QueryResponse)
async def query_rag(req: QueryRequest):
try:
# 1. Generate embedding for question
response = client.embeddings.create(
model="text-embedding-3-large",
input=req.question,
dimensions=1024
)
q_emb = response.data[0].embedding
# 2. Hybrid search (simplified; use the function from Q3)
cur.execute(
"""
SELECT title, content, 1 - (embedding <=> %s::vector) AS score
FROM documents
ORDER BY score DESC
LIMIT %s
""",
(q_emb, req.top_k)
)
results = cur.fetchall()
# 3. Build context for LLM
context = "\n\n".join([f"Title: {r[0]}\nContent: {r[1]}" for r in results])
sources = [{"title": r[0], "content": r[1][:200] + "..."} for r in results]
# 4. Call OpenAI GPT-4.1 (2026 model) with context
completion = client.chat.completions.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": "Answer using the provided context. Cite sources."},
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {req.question}"}
]
)
return QueryResponse(answer=completion.choices[0].message.content, sources=sources)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Streaming for UX: In 2026, users expect token-by-token streaming. Use FastAPI's StreamingResponse with openai.stream() to send tokens as they arrive.
Q6: How do I ensure the RAG system is secure and handles sensitive data?
Three critical considerations:
-
Data leakage prevention. Never send raw documents to OpenAI if they contain PII or trade secrets. Instead, use a local LLM (e.g., Llama 3.2 70B) or an Azure OpenAI instance with data residency guarantees. In 2026, many organizations run PostgreSQL + pgvector on-premises and use a local embedding model like
BAAI/bge-large-en-v1.5(1024 dimensions) to keep data entirely within their infrastructure. -
Row-level security (RLS). If your system serves multiple tenants (e.g., different companies using the same RAG instance), use PostgreSQL's Row-Level Security to ensure user A cannot retrieve documents belonging to user B.
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.current_tenant_id')::INT);
- Rate limiting and cost control. Embedding generation costs money. Implement rate limiting per user (e.g., 100 queries/hour) using Redis or a PostgreSQL advisory lock. Also, cache frequent queries: store the question and its embedding in a separate
query_cachetable with a TTL.
Q7: What are the biggest mistakes developers make when building RAG with PostgreSQL in 2026?
Mistake 1: Not normalizing embeddings. Vector databases expect unit-length vectors for cosine similarity. If you don't normalize, your results will be erratic. OpenAI embeddings are already normalized by default, but if you use a different model (e.g., from HuggingFace), normalize explicitly.
Mistake 2: Choosing the wrong index type. IVFFlat builds quickly but has slower query time and lower recall at high dimensions. HNSW (Hierarchical Navigable Small World) is the default recommendation in 2026 for any dataset over 100K rows. It builds slower but queries are 10x faster with >99% recall.
| Index Type | Build Time (1M rows) | Query Latency (p99) | Recall @ 10 |
|---|---|---|---|
| IVFFlat (lists=100) | 2 minutes | 50 ms | 92% |
| HNSW (m=16, ef_construction=200) | 15 minutes | 5 ms | 99% |
Mistake 3: Ignoring chunking strategies. Sending an entire book as one document leads to poor retrieval. Chunk your documents into 500-1000 token segments with overlap (100 tokens). Use a library like langchain-text-splitters or unstructured for intelligent chunking that respects paragraph and sentence boundaries.
Mistake 4: Not monitoring retrieval quality. Set up a simple evaluation: take a holdout set of 100 question-answer pairs. Measure recall@k (the fraction of relevant documents retrieved in the top-k). If recall drops below 90%, tune your chunk size, embedding model, or hybrid search weight (alpha).
Conclusion: The RAG Stack for 2026 and Beyond
Building a RAG system in 2026 is no longer about choosing between SQL and vector databases — you can have both. PostgreSQL with pgvector offers a compelling foundation: ACID transactions, mature tooling, hybrid search, and the ability to scale to tens of millions of vectors with HNSW indexing. Combined with OpenAI's embeddings and a FastAPI server, you can build a production-grade Q&A system in a single weekend.
Your next steps:
1. Set up a PostgreSQL 17 instance with pgvector.
2. Install the openai and fastapi Python libraries.
3. Ingest 1000 documents from your domain (e.g., technical documentation, product manuals).
4. Build the /query endpoint and test with real questions.
5. Iterate on chunking and hybrid search parameters until retrieval quality meets your bar.
If you want to master not just RAG but the underlying SQL skills — complex joins, window functions, query optimization, and schema design — our course "SQL and Databases: From Beginner to Confident User" covers exactly this. You'll learn to design normalized schemas that support both OLTP and vector workloads, write optimized queries that run in milliseconds, and understand the internals of PostgreSQL and MySQL. Start building AI applications on a solid data foundation.
The future of AI applications is not just about smarter models — it's about cleaner data pipelines. And SQL is the language of data. Master it.
Comments