Üretimde RAG: PostgreSQL ile Ölçeklenebilir Mimari
Dizüstü bilgisayarınızda çalışan bir RAG prototipi oluşturmak nispeten basittir. Getir onu saniyeden kısa gecikmelerle yüzlerce eşzamanlı kullanıcıya hizmet verecek üretim kontrollü kullanılabilirlik ve maliyetler ve tamamen farklı bir mühendislik sorunu.
Serinin bu son makalesinde üretim RAG mimarisini şu şekilde inşa ediyoruz: PostgreSQL: bağlantı havuzu PgBouncer ile, yanıtları oku vektör aramaya adanmış, bölümleme milyar vektörlü veri kümeleri için, önbelleğe alma Redis ile uygulama düzeyinde, Prometheus ve Grafana ile izleme, ve sıfır kesinti süreli güncellemeler için Kubernetes dağıtım stratejileri.
Felsefeyi korurken "Sadece Postgres'i Kullanın" 2026: vektör yok ayrı veritabanı, Çam Kozalağı yok, Qdrant yok. Pgvector ile PostgreSQL, iyi yapılandırılmış, Gerçek vakaların %90'ında maliyet/performans açısından uzman rakiplerini geride bırakıyor.
Seriye Genel Bakış
| # | Öğe | Odak |
|---|---|---|
| 1 | pgvektör | Kurulum, operatörler, indeksleme |
| 2 | Derinlikteki Gömmeler | Modeller, mesafeler, nesil |
| 3 | PostgreSQL ile RAG | Uçtan uca RAG boru hattı |
| 4 | Benzerlik Arama | Algoritmalar ve optimizasyon |
| 5 | HNSW ve IVFFlat | Gelişmiş indeksleme stratejileri |
| 6 | Buradasınız - Üretimde RAG | Ölçeklenebilirlik ve performans |
Ne Öğreneceksiniz
- PostgreSQL'de RAG kuruluşu için referans mimarisi
- PgBouncer ile bağlantı havuzu oluşturma: neden ve nasıl yapılandırılır
- Vektör aramaya ayrılmış replikaları okuyun
- Milyon/milyar vektör veri kümesi için bölümleme
- Tekrarlanan sorgular için Redis ile uygulama düzeyinde önbelleğe alma
- İzleme: temel ölçümler, Grafana kontrol paneli, uyarı
- Kubernetes'te dağıtım: durum bilgisi kümesi, PVC, sürekli güncellemeler
- Çoklu kiracılık: Farklı müşterilerden gelen verileri yalıtın
- Üretim için PostgreSQL yapılandırma ayarı
- Autovacuum ve vektör indeks bakımı
Üretimde RAG için Referans Mimarisi
## Architettura RAG Produzione su PostgreSQL
┌─────────────────────────────────────┐
│ Load Balancer │
│ (nginx / AWS ALB) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ RAG API Service │
│ (FastAPI, K8s Deployment) │
│ Replicas: 3-10 pod │
└──┬─────────────────────┬────────────┘
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ Redis Cache │ │ Embedding API │
│ (query cache, │ │ (OpenAI / local │
│ emb cache) │ │ Sentence Tr.) │
└─────────────────┘ └─────────────────┘
│
┌─────────────▼───────────────────────────┐
│ PgBouncer │
│ Connection Pool │
│ (transaction mode, pool_size=100) │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ PostgreSQL Primary │
│ (write: ingestion, updates) │
│ shared_buffers=32GB, m=16 │
└──────────────┬──────────────────────────┘
│ Streaming Replication
┌──────────────▼──────────────────────────┐
│ PostgreSQL Read Replica │
│ (read: vector search, RAG queries) │
│ Dedicated for ANN queries │
│ ef_search tuned per use case │
└─────────────────────────────────────────┘
PgBouncer ile Bağlantı Havuzu Oluşturma
PostgreSQL her bağlantı için bir süreç (çatal) oluşturur. 200 eş zamanlı kullanıcı çalışıyor sorgu vektörü, her bağlantı yalnızca işlem için ~5-10MB RAM kullanır. PgBouncer veritabanına gerçek bağlantılardan oluşan bir havuz sağlar ve uygulama isteklerini sıraya koyar, PostgreSQL bağlantılarının sayısını yüzlerceden birkaç düzineye düşürmek.
PgBouncer Kurulumu ve Yapılandırması
# pgbouncer.ini - Configurazione per RAG workload
[databases]
# Database principale per ingestion (write)
ragdb_write = host=postgres-primary port=5432 dbname=ragdb user=raguser
# Read replica per vector search (read)
ragdb_read = host=postgres-replica port=5432 dbname=ragdb user=raguser
[pgbouncer]
# Pool mode: transaction e il più efficiente per query brevi
# session: la connessione e riservata per tutta la sessione
# transaction: la connessione e rilasciata dopo ogni transazione (OTTIMALE per RAG)
# statement: rilascio dopo ogni statement (non compatibile con transazioni multi-statement)
pool_mode = transaction
# Numero massimo di connessioni per database verso PostgreSQL
max_db_connections = 50
# Connessioni per pool (per user+database combination)
default_pool_size = 25
# Limite totale connessioni client verso PgBouncer
max_client_conn = 1000
# Timeout e resilienza
query_wait_timeout = 30 # secondi prima di errore "query too long"
server_idle_timeout = 600 # chiudi connessioni idle dopo 10 minuti
client_idle_timeout = 0 # no timeout per i client (gestito dall'app)
# Autenticazione
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# Monitoring
stats_period = 60
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
listen_addr = 0.0.0.0
listen_port = 5432
Python'da Bağlantı Havuzu Oluşturma ile Kullanma
import psycopg2.pool
from contextlib import contextmanager
import threading
class DatabasePool:
"""
Thread-safe connection pool verso PgBouncer.
Usa connessioni separate per write (primary) e read (replica).
"""
def __init__(self, write_dsn: str, read_dsn: str,
min_conn: int = 2, max_conn: int = 20):
self._write_pool = psycopg2.pool.ThreadedConnectionPool(
minconn=min_conn, maxconn=max_conn, dsn=write_dsn
)
self._read_pool = psycopg2.pool.ThreadedConnectionPool(
minconn=min_conn, maxconn=max_conn, dsn=read_dsn
)
self._lock = threading.Lock()
@contextmanager
def get_write_conn(self):
"""Connessione per operazioni di scrittura (primary)."""
conn = self._write_pool.getconn()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
self._write_pool.putconn(conn)
@contextmanager
def get_read_conn(self):
"""Connessione per operazioni di lettura (read replica)."""
conn = self._read_pool.getconn()
try:
# Imposta ef_search ottimale per vector queries
with conn.cursor() as cur:
cur.execute("SET hnsw.ef_search = 60")
yield conn
finally:
conn.rollback() # rollback per rilasciare lock
self._read_pool.putconn(conn)
# Inizializzazione del pool (una volta all'avvio dell'applicazione)
db_pool = DatabasePool(
write_dsn="host=pgbouncer port=5432 dbname=ragdb_write user=raguser password=xxx",
read_dsn="host=pgbouncer port=5432 dbname=ragdb_read user=raguser password=xxx"
)
# Utilizzo nelle API:
async def search_endpoint(query: str):
with db_pool.get_read_conn() as conn:
results = semantic_search(conn, query)
return results
async def ingest_endpoint(document_path: str):
with db_pool.get_write_conn() as conn:
ingest_document(conn, document_path)
PgBouncer İzleme
-- Connettiti a PgBouncer sulla porta di admin (default: 6432)
-- psql -h localhost -p 6432 -U pgbouncer pgbouncer
-- Statistiche pool in tempo reale
SHOW POOLS;
-- Output:
-- database | user | cl_active | cl_waiting | sv_active | sv_idle | maxwait
-- ragdb_read | raguser | 23 | 0 | 23 | 2 | 0
-- ragdb_write| raguser | 5 | 0 | 5 | 0 | 0
-- Statistiche aggregate per database
SHOW STATS;
-- total_query_count | total_query_time | avg_query | avg_wait
-- 1248532 | 1832743.2 | 1.46 | 0.02
-- Lista connessioni client attuali
SHOW CLIENTS;
-- Lista connessioni server attuali verso PostgreSQL
SHOW SERVERS;
-- Alert: se cl_waiting > 0 per più di 5 secondi, il pool e saturo
-- Soluzione: aumenta default_pool_size o max_db_connections
Vektör Arama için Kopyaları Okuyun
Vektör sorguları (YSA araması) CPU ve bellek yoğundur ancak yeni veri gerektirmez. Onları bir yerde izole edin özel yanıtı oku birincil olanı işlemler için serbest bırakır alımı (INSERT) sağlar ve her iki iş yükü için gecikmeyi düşük tutar.
Akış Çoğaltma ile Okuma Çoğaltma Kurulumu
# Sul PRIMARY server
# postgresql.conf
wal_level = replica # abilita WAL per replication
max_wal_senders = 5 # max 5 replica connesse
wal_keep_size = '1GB' # mantieni almeno 1GB di WAL per le replica
# pg_hba.conf - permetti replica connection
host replication repuser replica-host/32 md5
# Crea utente di replication
CREATE USER repuser REPLICATION LOGIN PASSWORD 'strongpassword';
# ===========================
# Sul REPLICA server
# ===========================
# Clona il primary:
pg_basebackup -h primary-host -U repuser -D /var/lib/postgresql/16/main -P -Xs -R
# Il flag -R crea automaticamente standby.signal e i parametri di recovery
# postgresql.conf sulla replica - ottimizzato per vector search:
hot_standby = on # permette letture sulla replica
shared_buffers = '32GB' # abbondante per l'indice HNSW
effective_cache_size = '96GB' # hint al planner sulla memoria totale
max_parallel_workers_per_gather = 4 # query vector parallele
hnsw.ef_search = 60 # parametro globale per la replica
random_page_cost = 1.1 # SSD: costo basso per random I/O
effective_io_concurrency = 200 # per SSD NVMe
Çoğaltma İzleme
-- Sul primary: stato delle repliche connesse
SELECT
client_addr AS "Indirizzo replica",
application_name AS "Nome replica",
state AS "Stato",
sent_lsn AS "WAL inviato",
write_lsn AS "WAL scritto",
flush_lsn AS "WAL flushed",
replay_lsn AS "WAL replicato",
-- Lag in bytes:
sent_lsn - replay_lsn AS "Lag bytes",
-- Lag in tempo (approssimato):
now() - write_lag AS "Write lag",
now() - replay_lag AS "Replay lag"
FROM pg_stat_replication;
-- Sulla replica: verifica che sta replicando
SELECT now() - pg_last_xact_replay_timestamp() AS "Replica lag seconds";
-- Alert: se replica lag > 30 secondi, possibile problema di replication
-- Verifica pg_stat_wal_receiver sulla replica:
SELECT status, received_lsn, last_msg_receipt_time, latest_end_lsn
FROM pg_stat_wal_receiver;
Büyük Veri Kümeleri için Tablo Bölümleme
On milyonlarca vektörün bulunduğu tek bir tablonun yönetilmesi zorlaşır. bölümleme tabloyu fiziksel olarak ayrı parçalara böler, daha hızlı sorgulara (bölüm budama) ve ayrıntılı bakıma olanak tanır.
Verilere Göre Bölümleme (Zaman Serisi RAG)
-- Partitioning per mese: ottimo per documenti con timestamp (news, email, log)
CREATE TABLE documents_partitioned (
id BIGSERIAL,
source_path TEXT NOT NULL,
source_type TEXT,
content TEXT NOT NULL,
embedding vector(1536),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (id, created_at) -- la colonna di partizione deve essere nella PK
) PARTITION BY RANGE (created_at);
-- Crea partizioni per gli ultimi 12 mesi
DO $
DECLARE
d DATE;
BEGIN
FOR i IN 0..11 LOOP
d := DATE_TRUNC('month', NOW()) - (i || ' months')::INTERVAL;
EXECUTE format(
'CREATE TABLE documents_%s PARTITION OF documents_partitioned
FOR VALUES FROM (%L) TO (%L)',
TO_CHAR(d, 'YYYY_MM'),
d,
d + INTERVAL '1 month'
);
-- Indice HNSW su ogni partizione
EXECUTE format(
'CREATE INDEX ON documents_%s USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64)',
TO_CHAR(d, 'YYYY_MM')
);
RAISE NOTICE 'Partizione creata: documents_%', TO_CHAR(d, 'YYYY_MM');
END LOOP;
END $;
-- Partition pruning automatico (PostgreSQL usa solo le partizioni rilevanti):
EXPLAIN (ANALYZE)
SELECT id, content, embedding <=> query_vec AS dist
FROM documents_partitioned
WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01' -- solo partizione Gennaio 2026
ORDER BY embedding <=> query_vec
LIMIT 10;
-- Output: scansiona solo documents_2026_01, ignora le altre 11 partizioni
Kiracıya Göre Bölümlendirme (Çoklu kiracılık)
-- Partitioning per tenant_id: isola i dati di ogni cliente
CREATE TABLE documents_multitenant (
id BIGSERIAL,
tenant_id TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536),
metadata JSONB DEFAULT '{}',
PRIMARY KEY (id, tenant_id)
) PARTITION BY LIST (tenant_id);
-- Crea partizione per ogni tenant
CREATE TABLE docs_tenant_acme PARTITION OF documents_multitenant FOR VALUES IN ('acme');
CREATE TABLE docs_tenant_globex PARTITION OF documents_multitenant FOR VALUES IN ('globex');
CREATE TABLE docs_tenant_initech PARTITION OF documents_multitenant FOR VALUES IN ('initech');
-- Default partition per nuovi tenant
CREATE TABLE docs_tenant_default PARTITION OF documents_multitenant DEFAULT;
-- Indice HNSW per ogni tenant
CREATE INDEX ON docs_tenant_acme USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64);
CREATE INDEX ON docs_tenant_globex USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64);
CREATE INDEX ON docs_tenant_initech USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64);
-- Query con tenant isolation automatica (partition pruning):
SELECT id, content, embedding <=> query_vec AS dist
FROM documents_multitenant
WHERE tenant_id = 'acme' -- cerca SOLO nella partizione di acme
ORDER BY embedding <=> query_vec
LIMIT 5;
-- Row Level Security (RLS) come secondo livello di sicurezza:
ALTER TABLE documents_multitenant ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON documents_multitenant
USING (tenant_id = current_setting('app.current_tenant'));
-- In Python, impostare il tenant context:
with conn.cursor() as cur:
cur.execute("SET app.current_tenant = %s", (tenant_id,))
# Tutte le query successive vedono solo i documenti del tenant corrente
Redis ile önbelleğe alma
Birçok RAG sorgusu tekrarlanıyor: aynı soru, aynı bağlam. İki seviyeli bir önbellek uygulayın - önbellek yerleştirme (aynı sorgunun yerleştirilmesinin yeniden hesaplanmasını önler) e sonuç önbelleği (aynı sorgular için vektör aramasını yeniden çalıştırmaktan kaçının) - Gecikmeyi %90 ve OpenAI maliyetlerini %70 oranında azaltabilir.
import redis
import json
import hashlib
from typing import Optional
class RAGCache:
"""
Cache a due livelli:
1. Embedding cache: per i vettori delle query
2. Result cache: per i risultati completi RAG
"""
def __init__(self, redis_url: str = "redis://localhost:6379"):
self.r = redis.from_url(redis_url, decode_responses=True)
# TTL per diversi tipi di cache
self.embedding_ttl = 3600 * 24 * 7 # 7 giorni (embedding stabili)
self.result_ttl = 3600 # 1 ora (risultati possono invecchiare)
def _cache_key(self, prefix: str, *args) -> str:
"""Genera una chiave cache deterministica."""
content = json.dumps(args, sort_keys=True)
hash_val = hashlib.sha256(content.encode()).hexdigest()[:16]
return f"rag:{prefix}:{hash_val}"
# === EMBEDDING CACHE ===
def get_embedding(self, text: str) -> Optional[list[float]]:
key = self._cache_key("emb", text)
cached = self.r.get(key)
if cached:
return json.loads(cached)
return None
def set_embedding(self, text: str, embedding: list[float]) -> None:
key = self._cache_key("emb", text)
self.r.setex(key, self.embedding_ttl, json.dumps(embedding))
# === RESULT CACHE ===
def get_rag_result(self, query: str, filters: dict = None) -> Optional[dict]:
key = self._cache_key("result", query, filters or {})
cached = self.r.get(key)
if cached:
result = json.loads(cached)
result["from_cache"] = True
return result
return None
def set_rag_result(self, query: str, result: dict, filters: dict = None) -> None:
key = self._cache_key("result", query, filters or {})
# Non cachare risposte con fonti vuote o errori
if result.get("sources") and result.get("answer"):
self.r.setex(key, self.result_ttl, json.dumps(result))
# === CACHE STATISTICS ===
def get_stats(self) -> dict:
info = self.r.info("stats")
total = info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0)
hit_rate = info["keyspace_hits"] / total if total > 0 else 0
return {
"hit_rate": round(hit_rate, 3),
"total_queries": total,
"memory_used": self.r.info("memory")["used_memory_human"],
"rag_keys": self.r.dbsize()
}
# Integrazione nel RAG System
cache = RAGCache("redis://redis:6379")
def cached_embed(text: str, client) -> list[float]:
"""Genera o recupera dal cache l'embedding di un testo."""
cached = cache.get_embedding(text)
if cached:
return cached
embedding = client.embeddings.create(
input=[text], model="text-embedding-3-small"
).data[0].embedding
cache.set_embedding(text, embedding)
return embedding
def cached_rag_query(rag_system, question: str, filters: dict = None) -> dict:
"""Esegui una query RAG con caching del risultato."""
# Controlla result cache
cached = cache.get_rag_result(question, filters)
if cached:
return cached
# Esegui la query completa
result = rag_system.ask(question, filters=filters)
# Salva in cache
cache.set_rag_result(question, result, filters)
return result
FastAPI: RAG'a Async ile Hizmet Verme
from fastapi import FastAPI, HTTPException, Depends, BackgroundTasks
from pydantic import BaseModel
import asyncio
from concurrent.futures import ThreadPoolExecutor
app = FastAPI(title="RAG API", version="1.0.0")
executor = ThreadPoolExecutor(max_workers=10)
class QueryRequest(BaseModel):
question: str
top_k: int = 5
source_type: str = None
use_hybrid: bool = True
tenant_id: str = None
class QueryResponse(BaseModel):
answer: str
sources: list[dict]
from_cache: bool = False
retrieval_ms: float
generation_ms: float
@app.post("/api/query", response_model=QueryResponse)
async def query_rag(request: QueryRequest):
"""
Query endpoint con async processing e caching.
"""
import time
# 1. Controlla cache
cached = cache.get_rag_result(request.question, {"source_type": request.source_type})
if cached:
return QueryResponse(
answer=cached["answer"],
sources=cached["sources"],
from_cache=True,
retrieval_ms=0,
generation_ms=0
)
# 2. Embedding della query (con cache)
t0 = time.time()
# Esegui in thread per non bloccare l'event loop
query_vec = await asyncio.get_event_loop().run_in_executor(
executor,
lambda: cached_embed(request.question, openai_client)
)
# 3. Vector search su PostgreSQL
async with db_pool.get_read_conn() as conn:
if request.tenant_id:
# Imposta tenant context per RLS
async with conn.cursor() as cur:
await cur.execute("SET app.current_tenant = %s", (request.tenant_id,))
chunks = await run_hybrid_search(conn, query_vec, request.top_k)
retrieval_ms = (time.time() - t0) * 1000
# 4. Generazione risposta
t1 = time.time()
response = await asyncio.get_event_loop().run_in_executor(
executor,
lambda: rag_generator.generate(request.question, chunks)
)
generation_ms = (time.time() - t1) * 1000
result = {
"answer": response.answer,
"sources": response.sources
}
cache.set_rag_result(request.question, result, {"source_type": request.source_type})
return QueryResponse(
answer=response.answer,
sources=response.sources,
from_cache=False,
retrieval_ms=round(retrieval_ms, 1),
generation_ms=round(generation_ms, 1)
)
@app.get("/api/health")
async def health():
return {"status": "ok", "cache": cache.get_stats()}
İzleme: Temel Metrikler
Python ile Prometheus Metrikleri
from prometheus_client import Counter, Histogram, Gauge, start_http_server
# Metriche applicative
rag_queries_total = Counter(
"rag_queries_total",
"Total number of RAG queries",
["status", "from_cache"]
)
rag_query_duration_seconds = Histogram(
"rag_query_duration_seconds",
"RAG query duration in seconds",
["phase"], # "retrieval", "generation", "total"
buckets=[0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)
rag_retrieval_chunks_count = Histogram(
"rag_retrieval_chunks_count",
"Number of chunks retrieved per query",
buckets=[1, 2, 3, 5, 10, 20]
)
rag_retrieval_top_similarity = Gauge(
"rag_retrieval_top_similarity",
"Top similarity score of last retrieval"
)
# Metriche PostgreSQL (via pg_stat_activity)
db_active_connections = Gauge(
"db_active_connections",
"Active database connections",
["pool"] # "write", "read"
)
def track_rag_query(func):
"""Decorator per tracciare le metriche delle query RAG."""
def wrapper(*args, **kwargs):
import time
start = time.time()
try:
result = func(*args, **kwargs)
rag_queries_total.labels(
status="success",
from_cache=str(result.get("from_cache", False))
).inc()
return result
except Exception as e:
rag_queries_total.labels(status="error", from_cache="false").inc()
raise
finally:
rag_query_duration_seconds.labels("total").observe(time.time() - start)
return wrapper
# Avvia server Prometheus (porta 9090)
start_http_server(9090)
Grafana Kontrol Paneli: PostgreSQL Metrikleri için SQL Sorguları
-- Query per Grafana dashboard: metriche PostgreSQL vettoriali
-- 1. Latenza query per percentile (ultimi 5 minuti)
SELECT
percentile_cont(0.5) WITHIN GROUP (ORDER BY total_time) AS p50_ms,
percentile_cont(0.95) WITHIN GROUP (ORDER BY total_time) AS p95_ms,
percentile_cont(0.99) WITHIN GROUP (ORDER BY total_time) AS p99_ms,
COUNT(*) AS n_queries
FROM pg_stat_statements
WHERE query LIKE '%embedding%<=%>%' -- query che usano vector search
AND calls > 0;
-- 2. Cache hit ratio degli indici vettoriali
SELECT
indexrelname AS index_name,
idx_blks_read AS disk_reads,
idx_blks_hit AS cache_hits,
ROUND(idx_blks_hit::numeric / NULLIF(idx_blks_read + idx_blks_hit, 0) * 100, 2)
AS cache_hit_pct
FROM pg_statio_user_indexes
WHERE indexrelname LIKE '%hnsw%' OR indexrelname LIKE '%ivfflat%';
-- 3. Connessioni attive per stato
SELECT
state,
COUNT(*) AS connections,
MAX(now() - state_change) AS max_wait_time
FROM pg_stat_activity
WHERE datname = 'ragdb'
GROUP BY state;
-- 4. Tuple inserite/lette per secondo (indicatore di carico)
SELECT
relname,
n_tup_ins + n_tup_upd + n_tup_del AS writes_per_snapshot,
seq_tup_read + idx_tup_fetch AS reads_per_snapshot
FROM pg_stat_user_tables
WHERE relname LIKE '%document%';
RAG Üretimi için PostgreSQL Yapılandırması
Varsayılan PostgreSQL yapılandırması iş yükü vektörleri için optimize edilmemiştir. İşte
değiştirilmesi gereken kritik parametreler postgresql.conf üretimdeki bir RAG sistemi için
32GB+ RAM ile:
# postgresql.conf - Configurazione produzione per RAG su PostgreSQL 16
# Sistema: 64GB RAM, 16 CPU, SSD NVMe
# === MEMORIA ===
shared_buffers = '16GB' # 25% della RAM totale
effective_cache_size = '48GB' # 75% della RAM (hint al planner, non memoria allocata)
work_mem = '64MB' # per sort/hash nelle query (per connessione!)
maintenance_work_mem = '2GB' # per CREATE INDEX HNSW, VACUUM, pg_dump
max_worker_processes = 16
max_parallel_workers = 8
max_parallel_workers_per_gather = 4
# === STORAGE / I/O ===
random_page_cost = 1.1 # SSD: abbassa da 4.0 a 1.1 per favorire index scans
effective_io_concurrency = 200 # NVMe: può gestire molte richieste parallele
checkpoint_completion_target = 0.9 # distribuisce i checkpoint nel tempo
wal_buffers = '64MB' # default -1 (auto) è di solito sufficiente
# === AUTOVACUUM (CRITICO per tabelle vector con molti INSERT/DELETE) ===
autovacuum_vacuum_cost_delay = '2ms' # più aggressivo del default 20ms
autovacuum_vacuum_scale_factor = 0.05 # trigger a 5% di righe morte (default 20%)
autovacuum_analyze_scale_factor = 0.02 # trigger analyze a 2% di righe nuove
autovacuum_vacuum_cost_limit = 400 # più operazioni per autovacuum worker
# === LOGGING ===
log_min_duration_statement = 1000 # logga query > 1 secondo
log_lock_waits = on
track_io_timing = on # necessario per EXPLAIN (BUFFERS) accurato
# === ESTENSIONI ===
shared_preload_libraries = 'pg_stat_statements,vector'
# === pgvector SPECIFIC ===
# hnsw.ef_search: impostalo a livello di sessione nel codice applicativo
# Non impostarlo globalmente qui: ogni use case ha il suo valore ottimale
Otovakum ve Vektör İndekslerinin Bakımı
Gömülü tablolarda RAG ardışık düzeni sırasında sık sık GÜNCELLEME ve SİLME yaşanır (belgelerin güncellenmesi, yeniden parçalanması, eski belgelerin silinmesi). Otomatik vakum olmadan doğru yapılandırıldığında "tablo şişkinliği" vektör performansını düşürür.
-- Verifica bloat della tabella documenti
SELECT
schemaname,
tablename,
n_live_tup AS righe_vive,
n_dead_tup AS righe_morte,
ROUND(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) AS pct_morte,
last_vacuum,
last_autovacuum,
last_analyze
FROM pg_stat_user_tables
WHERE tablename LIKE '%document%';
-- Allarme: pct_morte > 5% significa che serve un VACUUM manuale
-- VACUUM manuale per recuperare spazio (non blocca letture):
VACUUM (VERBOSE, ANALYZE) documents;
-- VACUUM FULL per recuperare spazio fisico (BLOCCA scritture - fare in finestra manutenzione):
VACUUM FULL documents;
-- Imposta autovacuum aggressivo per la tabella documenti
ALTER TABLE documents SET (
autovacuum_vacuum_scale_factor = 0.02, -- 2% righe morte = trigger vacuum
autovacuum_analyze_scale_factor = 0.01, -- 1% nuove righe = trigger analyze
autovacuum_vacuum_cost_delay = '2ms'
);
-- Verifica stato degli indici HNSW dopo molti INSERT/DELETE
-- L'indice HNSW non si degrada con insert, ma lo spazio non viene recuperato dopo DELETE
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
pg_size_pretty(pg_total_relation_size(indexrelid)) AS total_size
FROM pg_stat_user_indexes
WHERE tablename = 'documents'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Ricostruzione indice senza downtime (per recuperare spazio dopo molti DELETE):
REINDEX INDEX CONCURRENTLY documents_hnsw_idx;
Kubernetes'te Dağıtım
PVC'li PostgreSQL için StatefulSet
## kubernetes/postgres-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-vector
namespace: rag-production
spec:
serviceName: postgres-vector
replicas: 2 # primary + 1 replica
selector:
matchLabels:
app: postgres-vector
template:
metadata:
labels:
app: postgres-vector
spec:
containers:
- name: postgres
image: pgvector/pgvector:pg16
env:
- name: POSTGRES_DB
value: ragdb
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: POSTGRES_SHARED_BUFFERS
value: "8GB"
- name: POSTGRES_MAINTENANCE_WORK_MEM
value: "2GB"
resources:
requests:
memory: "32Gi"
cpu: "8"
limits:
memory: "64Gi"
cpu: "16"
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
- name: postgres-config
mountPath: /etc/postgresql/custom.conf
subPath: custom.conf
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd # NVMe SSD per performance ottimale
resources:
requests:
storage: 500Gi # 500GB per indici HNSW grandi
RAG API'sinin Devamlı Güncellemelerle Dağıtımı
## kubernetes/rag-api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: rag-api
namespace: rag-production
spec:
replicas: 5 # 5 pod in parallelo
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 2 # crea 2 pod nuovi prima di eliminare quelli vecchi
maxUnavailable: 0 # zero downtime update
selector:
matchLabels:
app: rag-api
template:
spec:
containers:
- name: rag-api
image: your-registry/rag-api:latest
ports:
- containerPort: 8000
env:
- name: DB_WRITE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: write-url
- name: DB_READ_URL
valueFrom:
secretKeyRef:
name: db-secret
key: read-url
- name: REDIS_URL
value: "redis://redis-service:6379"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: openai-secret
key: api-key
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
livenessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
Maliyetler ve Özel Çözümlerle Karşılaştırma
| Çözüm | 1 milyon operatör/ay | 10 milyon sorgu/ay | Gecikme s.95 | Notlar |
|---|---|---|---|---|
| PostgreSQL + pgvector (kendi kendine barındırılan) | 200-400 ABD Doları (EC2/GKE) | dahil | 20-80ms | Sabit maliyet, toplam kontrol |
| Çam kozalağı (yönetilen) | 70-700$ | 0,04-0,08 USD/1000 sorgu | 10-40ms | Değişken maliyet, daha az kontrol |
| Süper taban vektör | 25-200$ | dahil | 30-100ms | Yönetilen PostgreSQL + pgvector |
| Qdrant (kendi kendine barındırılan) | 150-300$ | dahil | 5-20ms | Daha hızlı ama ek altyapı |
| pgvektör (kıyaslama 2025) | Çam kozalaklarından 28 kata kadar daha hızlı | 16 kat daha az maliyetle | doğrudan karşılaştırma | Benchmark.vector.dev |
Vektör Arama için PostgreSQL Ne Zaman Kullanılmamalı?
- Milyarlarca operatör: 500M-1B'nin üzerinde vektör, Qdrant veya Weaviate, yerel vektör nicemleme sayesinde bellek açısından daha verimli hale gelir.
- Milisaniyenin altında gecikme: <5 ms p99'a ihtiyacınız varsa, özel bir bellek içi vektör veritabanı (Qdrant bulutu, Pinecone sunucusuz) daha uygundur.
- DBA'sı olmayan takımlar: PostgreSQL becerileriniz yoksa Pinecone gibi yönetilen bir hizmet operasyonel karmaşıklığı azaltır.
- Çok modlu arama: Farklı gömme boyutlarına sahip resimler + metin + ses, PostgreSQL'in yerel olarak işlemediği mimarileri gerektirir.
RAG Bilgi Bankası için Yedekleme ve Olağanüstü Durum Kurtarma
RAG bilgi tabanı hem orijinal belgeleri hem de bunların vektör eklemelerini içerir. Yedekleme her ikisini de içermelidir ancak en uygun strateji veri kümesinin boyutuna bağlıdır ve güncellemelerin sıklığı:
# Strategia di backup per knowledge base RAG su PostgreSQL
# 1. Backup logico completo (pg_dump):
# Adatto per database fino a ~50GB. Include tutti i dati, indici, schema.
pg_dump -h postgres-primary \
-U raguser \
-d ragdb \
--format=custom \ # formato binario compresso
--compress=9 \ # massima compressione
--no-privileges \ # escludi permessi utente
--file=ragdb_backup_$(date +%Y%m%d_%H%M%S).dump
# Restore:
pg_restore -h postgres-new \
-U raguser \
-d ragdb \
--jobs=8 \ # restore parallelo (usa 8 CPU)
ragdb_backup.dump
# 2. Backup fisico (pg_basebackup) per DB grandi:
# Più veloce ma richiede più spazio. Usa WAL per point-in-time recovery.
pg_basebackup -h postgres-primary \
-U repuser \
-D /backup/ragdb_base_$(date +%Y%m%d) \
--format=tar \
--compress=9 \
--wal-method=stream \ # include WAL durante il backup
--checkpoint=fast
# 3. Solo embedding (se i documenti originali sono altrove):
# Esporta solo la tabella dei documenti con embedding
psql -h postgres-primary -U raguser ragdb -c \
"COPY (SELECT id, source_path, chunk_index, content, embedding::text, metadata, ingested_at
FROM rag_documents) TO STDOUT CSV HEADER" \
| gzip > rag_embeddings_$(date +%Y%m%d).csv.gz
# Stima dimensione backup:
# 1M vettori 1536-dim in float32 = 6GB solo embedding
# + contenuto testo: ~0.5GB per 1M chunk da 800 char
# Totale atteso: ~7GB per 1M documenti chunked
Uyarı: Bir şeyler ters gittiğinde
-- Alert queries per Prometheus/Grafana alerting
-- Esegui ogni 60 secondi come Prometheus exporter
-- 1. Alert: latenza p99 vettori > 500ms (ultime 5 minuti)
SELECT
CASE
WHEN percentile_cont(0.99) WITHIN GROUP (ORDER BY total_time) > 500
THEN 'ALERT'
ELSE 'OK'
END AS status,
percentile_cont(0.99) WITHIN GROUP (ORDER BY total_time) AS p99_ms
FROM pg_stat_statements
WHERE query LIKE '%embedding%<=%>%'
AND calls > 0;
-- 2. Alert: replica lag > 30 secondi
SELECT
CASE
WHEN EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) > 30
THEN 'ALERT'
ELSE 'OK'
END AS replica_status,
EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS lag_seconds;
-- 3. Alert: troppi dead tuples (bloat > 10%)
SELECT
tablename,
CASE
WHEN n_dead_tup::float / NULLIF(n_live_tup + n_dead_tup, 0) > 0.1
THEN 'ALERT'
ELSE 'OK'
END AS bloat_status,
ROUND(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) AS dead_pct
FROM pg_stat_user_tables
WHERE tablename LIKE '%document%';
-- 4. Alert: cache hit ratio indice vettoriale < 90%
SELECT
indexrelname,
CASE
WHEN idx_blks_hit::float / NULLIF(idx_blks_read + idx_blks_hit, 0) < 0.9
THEN 'ALERT'
ELSE 'OK'
END AS cache_status,
ROUND(idx_blks_hit::numeric / NULLIF(idx_blks_read + idx_blks_hit, 0) * 100, 2) AS hit_pct
FROM pg_statio_user_indexes
WHERE indexrelname LIKE '%hnsw%';
-- 5. Alert: connessioni vicine al limite
SELECT
CASE
WHEN COUNT(*) > (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') * 0.85
THEN 'ALERT'
ELSE 'OK'
END AS conn_status,
COUNT(*) AS active_connections
FROM pg_stat_activity;
Üretim Kontrol Listesi
-- CHECKLIST PRE-DEPLOY per RAG su PostgreSQL
-- 1. Verifica estensioni installate
SELECT extname, extversion FROM pg_extension WHERE extname IN ('vector', 'pg_stat_statements');
-- 2. Verifica indici vettoriali presenti e dimensioni
SELECT
tablename, indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
WHERE indexname LIKE '%hnsw%' OR indexname LIKE '%ivfflat%';
-- 3. Verifica configurazione memoria (per prod con 32GB RAM)
SHOW shared_buffers; -- deve essere >= 8GB
SHOW work_mem; -- deve essere >= 16MB
SHOW maintenance_work_mem; -- deve essere >= 512MB
-- 4. Verifica autovacuum attivo sulla tabella documenti
SELECT autovacuum_enabled, autovacuum_vacuum_scale_factor
FROM pg_settings
WHERE name = 'autovacuum';
-- 5. Test connessione PgBouncer
-- psql -h pgbouncer-host -p 5432 -U raguser ragdb_read -c "SELECT 1"
-- 6. Test Redis cache
-- redis-cli -h redis-host PING -- deve rispondere PONG
-- 7. Benchmark prestazioni prima del go-live
-- target: p95 < 100ms per vector search, p99 < 500ms
-- 8. Verifica replica lag
SELECT now() - pg_last_xact_replay_timestamp() AS lag;
-- target: lag < 5 secondi in operazioni normali
Mimari Karşılaştırması: PostgreSQL ile Özel Vektör Veritabanı Karşılaştırması
| bekliyorum | PostgreSQL + pgvektör | Çam kozalağı / Qdrant / Weaviate |
|---|---|---|
| Altyapı karmaşıklığı | Düşük: yalnızca bir sistem | Yüksek: Yönetilecek ek sistem |
| 10 milyon sorgu başına maliyet | $200-400/ay (sabit sunucu) | Aylık 400-800$ (sorguya göre değişir) |
| p95 gecikmesi (1 milyon taşıyıcı) | 20-80 ms (HNSW optimize edilmiş) | 5-40 ms (genellikle bellekte) |
| Maksimum ölçeklenebilirlik | ~500M-1B pratik taşıyıcılar | Milyarlarca operatör |
| Entegre tam metin araması | Evet (ts_tsvector, GIN) | Hayır (ayrı bir Elasticsearch gerekli) |
| ASİT işlemleri | Evet (yerli) | Sınırlı veya yok |
| Takım beceri gereksinimleri | PostgreSQL DBA | Yalnızca istemci API'si |
Seri Özeti: PostgreSQL AI
PostgreSQL'i temel olarak kullanmak için tüm teknoloji yığınını birlikte inceledik. Yapay zeka uygulamaları:
- pgvektörü: Kurulum, vektör operatörleri, ilk HNSW dizini
- Gömmeler: Modeller (OpenAI, Cümle Transformatörleri), mesafeler, parçalama
- - RAG: Tam alım-geri alma-oluşturma hattı
- Benzerlik Araması: HNSW ve IVFFlat, YSA algoritmaları, MMR
- Gelişmiş İndeksleme: Optimum parametreler, izleme, kesinti olmadan yeniden oluşturma
- Üretme: PgBouncer, kopyaları okuma, bölümleme, K8'ler, izleme
Ana mesaj 2026'nın mesajı olmaya devam ediyor: "Sadece Postgres'i Kullanın". eğer öyleysen Zaten PostgreSQL kullanıyorsanız (ve uygulamaların büyük çoğunluğu bunu kullanıyorsa), zaten tüm gerekli bilgilere sahipsiniz. Gelişmiş, ölçeklenebilir ve uygun maliyetli RAG sistemleri oluşturmak için gereklidir. Ekleme 500 milyon vektörü aşana kadar veya aşırı gecikme gereksinimleri.
Kaynaklar ve Dokümantasyon
- pgvector GitHub: github.com/pgvector/pgvector
- MTEB Skor Tablosu: huggingface.co/spaces/mteb/leaderboard
- RAGAS Çerçevesi: github.com/explodinggradients/ragas
- YSA Karşılaştırmaları: ann-benchmarks.com
- PgBouncer belgeleri: pgbouncer.org/config.html
- İlgili Yapay Zeka Mühendisliği serisi: RAG Kurumsal mimarisi
- İlgili Veriler ve Yapay Zeka İş serisi: Yapay Zeka için Veri Altyapısı







