Gelişmiş Benzerlik Araması: PostgreSQL'de Algoritmalar ve Optimizasyon
Milyonlarca vektör içeren bir veritabanınız olduğunda ve bir sorguya en çok benzeyen 10 tanesini bulmanız gerektiğinde 50 milisaniyeden kısa bir sürede, benzerlik araması bir mühendislik sorunu haline gelir ciddi. Kesin arama (kaba kuvvet) kesindir ancak ölçeklenmez: her vektörü birbiriyle karşılaştırın sorgu O(n*d) karmaşıklığına sahiptir; burada n, vektörlerin sayısı ve d boyutluluktur. 10 milyon ile 1536 boyutlu vektörlerin her biri, 15 milyar kayan nokta işlemi gerektirecektir.
Çözüm algoritmalardır YSA (Yaklaşık En Yakın Komşu): vazgeçerler O(log n) ve hatta O(1) cinsinden cevapları elde etmek için kesin sonucu bulma garantisi % 95-99 pratik doğrulukla yastıklı. Pgvector'lu PostgreSQL bu ikisini uygular En popüler YSA algoritmaları: HNSW e IVFFlatin.
Bu yazıda bu algoritmaların derinlemesine nasıl çalıştığını ve ne zaman kullanılması gerektiğini analiz ediyoruz. her biri, PostgreSQL'de benzerlik arama sorgularının nasıl optimize edileceği, aramanın nasıl birleştirileceği meta veri filtreli vektör ve çeşitlendirilmiş sonuçlar için MMR gibi gelişmiş teknikler.
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 | Buradasınız - Benzerlik Arama | Algoritmalar ve optimizasyon |
| 5 | HNSW ve IVFFlat | Gelişmiş indeksleme stratejileri |
| 6 | Üretimde RAG | Ölçeklenebilirlik ve performans |
Ne Öğreneceksiniz
- Tam arama ile yaklaşık en yakın komşu (YSA) arasındaki fark
- HNSW dahili olarak nasıl çalışır: dünyada gezilebilir küçük grafikler
- IVFFlat nasıl çalışır: kümeleme ve prob araması
- HNSW, IVFFlat ve kaba kuvvet ne zaman kullanılmalı?
- Sorgu optimizasyonu: ef_search ve prob parametreleri
- Hibrit filtreler: meta veri filtrelemeyi ve vektör aramayı birleştirin
- Uzaklık operatörleri: kosinüs, L2 ve iç çarpım karşılaştırıldı
- Python koduyla kıyaslama ve ölçüm geri çağırma
- Çeşitlendirilmiş sonuçlar için Maksimum Marjinal Uygunluk (MMR)
- Anlamsal arama ve anahtar kelime arama: ne zaman ne kullanılmalı
Tam Arama ve Yaklaşık Arama
Kaba Kuvvet (Tam Arama)
Tam arama, sorguyu veritabanındaki her bir vektörle karşılaştırır. %100 geri çağırmayı garanti eder ancak doğrusal karmaşıklığa sahiptir. Ve yalnızca doğru seçim küçük veri kümeleri için veya mutlak hassasiyetin tartışılamaz bir gereklilik olduğu durumlarda:
-- Ricerca esatta: nessun indice utilizzato, scan completo
-- Utile per dataset piccoli (< 100K vettori) o quando la precisione e critica
SELECT id, content, embedding <=> query_vec AS distance
FROM documents
ORDER BY embedding <=> query_vec -- scansione sequenziale su tutto il dataset
LIMIT 10;
-- Per forzare il brute force anche quando esiste un indice ANN:
SET enable_indexscan = off;
SELECT id, content, embedding <=> query_vec AS distance
FROM documents
ORDER BY embedding <=> query_vec
LIMIT 10;
SET enable_indexscan = on; -- ripristina
-- Benchmark comparativo (dataset reale su SSD NVMe):
-- Dataset: 1M vettori, 1536 dim, PostgreSQL 16, 32GB RAM, 16 CPU
-- Brute force (seq scan): ~2000ms per query -- 2 secondi!
-- HNSW (ef_search=40): ~10ms per query -- 200x più veloce
-- HNSW (ef_search=100): ~25ms per query -- 80x più veloce
-- IVFFlat (probes=50): ~28ms per query -- 71x più veloce
-- Dataset 100K vettori (brute force accettabile):
-- Brute force: ~50ms per query -- accettabile
-- HNSW: ~3ms per query -- ancora meglio se disponibile
ANN: Geri Çağırma/Performans Dengesi
-- Verifica se la query sta usando l'indice ANN
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT id, content, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
-- Output desiderato (usa l'indice HNSW):
-- Index Scan using documents_embedding_hnsw on documents
-- Index Cond: (embedding <=> '...'::vector)
-- Limit: 10
-- Buffers: shared hit=347 (tutto da cache = ottimo!)
-- -> Execution Time: 8.3 ms
-- Output indesiderato (brute force):
-- Seq Scan on documents
-- Sort Key: (embedding <=> '...'::vector) -- ordina dopo scan completo
-- Buffers: shared hit=234 read=8921 (molti disk read = lento!)
-- -> Execution Time: 2341 ms
-- MOTIVO per cui PostgreSQL non usa l'indice:
-- 1. LIMIT troppo grande (>10% delle righe per tabelle piccole)
-- 2. Le statistiche sono vecchie: ANALYZE documents;
-- 3. L'indice non esiste: verifica con \d documents
-- 4. enable_indexscan = off accidentalmente
pgvector'da mesafe operatörleri
pgvector, her biri farklı yerleştirme türleri için uygun olan üç mesafe operatörünü destekler. Yanlış operatörü seçmek, aramanızın kalitesini önemli ölçüde azaltabilir.
| Operatör | Tip | Menzil | Ne Zaman Kullanılmalı | Dizin desteği |
|---|---|---|---|---|
<=> |
Kosinüs mesafesi | [0, 2] | Metin yerleştirmeleri (OpenAI, Cümle Transformatörleri) - VARSAYILAN | HNSW, IVFFlat (vector_cosine_ops) |
<-> |
Öklid (L2) mesafesi | [0, inf) | Önemli büyüklükte vektörler (resimler, ses) | HNSW, IVFFlat (vector_l2_ops) |
<#> |
Negatif iç çarpım | (-inf, 0] | Önceden normalleştirilmiş yerleştirmeler, maksimum iç ürün araması | HNSW, IVFFlat (vector_ip_ops) |
-- Cosine distance (più comune per text embeddings):
SELECT id, content,
embedding <=> query_vec AS cosine_distance,
1 - (embedding <=> query_vec) AS cosine_similarity
FROM documents
ORDER BY embedding <=> query_vec
LIMIT 5;
-- L2 distance (euclidea, per immagini/audio):
SELECT id, content,
embedding <-> query_vec AS l2_distance
FROM documents
ORDER BY embedding <-> query_vec
LIMIT 5;
-- Inner product (per embedding normalizzati):
SELECT id, content,
(embedding <#> query_vec) * -1 AS inner_product -- negato per avere più alto = più simile
FROM documents
ORDER BY embedding <#> query_vec -- ORDER BY funziona perchè <#> e negativo
LIMIT 5;
-- Crea indice con l'operatore corretto (DEVE corrispondere alla query!):
-- Per cosine distance (default per text):
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
-- Per L2 distance:
CREATE INDEX ON documents USING hnsw (embedding vector_l2_ops);
-- Per inner product:
CREATE INDEX ON documents USING hnsw (embedding vector_ip_ops);
-- ERRORE COMUNE: indice con vector_cosine_ops ma query con <->
-- Risultato: PostgreSQL ignora l'indice e fa brute force!
HNSW: Hiyerarşik Gezinilebilir Küçük Dünya
HNSW, 2025-2026'nın en popüler YSA algoritmasıdır. Teorisine dayanmaktadır küçük dünya grafikleri: Her düğüme herhangi bir noktadan ulaşılabilen yapılar birkaç adımda daha fazlasını elde edebilirsiniz ("6 derecelik ayrılma" fenomeni). Hiyerarşik yapı kullanarak vektör uzayının en ilgili alanına hızlı bir şekilde gitmenizi sağlar. üst seviyeler "otoyollar" ve hassas araştırmalar için temel seviyedir.
HNSW Nasıl Çalışır?
HNSW hiyerarşik bir düzey yapısı oluşturur:
- Seviye 0 (temel): Tüm vektörler en yakın komşularına bağlıdır. Yoğun grafik.
- Daha yüksek seviyeler (1, 2, ...): Vektörlerin giderek daha küçük alt kümeleri. Hızlı gezinme için seyrek grafikler.
- Giriş noktası: Arama her zaman en yüksek seviyeden (daha az vektör) başlar ve hırsla hedefe doğru iner.
-- Struttura HNSW concettuale:
--
-- Livello 2: o-----------o-----------o (pochi nodi, salti lunghi - navigazione veloce)
-- \ | /
-- Livello 1: o---o---o---o---o---o---o (navigazione intermedia)
-- \ | | /
-- Livello 0: o-o-o-o-o-o-o-o-o-o-o-o (tutti i vettori, ricerca precisa)
--
-- Algoritmo di ricerca per query Q:
-- 1. Inizia dal livello più alto con un entry point fisso
-- 2. Greedy descent: scendi verso il nodo più vicino a Q a ogni livello
-- 3. Usa quel nodo come entry point per il livello inferiore
-- 4. Ripeti fino al livello 0
-- 5. Al livello 0: beam search con ef_search candidati (candidati esplorati)
-- 6. Restituisci i top-k tra i candidati esplorati
-- Complessità teorica:
-- Build: O(n * log(n)) - sub-lineare nel numero di vettori
-- Query: O(log(n)) - logaritmica! vs O(n) del brute force
-- Creazione indice HNSW (valori ottimali per la maggior parte dei casi):
CREATE INDEX documents_hnsw_idx
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (
m = 16, -- connessioni per nodo (default 16, range 4-64)
ef_construction = 64 -- candidati durante costruzione (default 64, range 16-200)
);
-- Impostazione ef_search (runtime, senza rebuild):
SET hnsw.ef_search = 60; -- valore raccomandato per produzione RAG
HNSW Parametreleri: Etkileşimli Özet
| Parametre | Tanım | Varsayılan | Menzil | Etkiyi artır |
|---|---|---|---|---|
m |
Seviye başına düğüm başına maksimum bağlantı | 16 | 4-64 | Daha iyi hatırlama, daha fazla hafıza, daha yavaş oluşturma |
ef_construction |
İnşaat sırasında dikkate alınan adaylar | 64 | 16-200 | Daha iyi hatırlama potansiyeli, çok daha yavaş oluşturma |
ef_search |
Sorgu sırasında dikkate alınan adaylar (çalışma zamanı) | 40 | 1-ef_inşaat | Daha iyi hatırlama, daha yavaş sorgulama |
-- Configurazione ef_search per query (parametro runtime)
SET hnsw.ef_search = 40; -- default, buon equilibrio
-- Alta precisione (RAG enterprise, medico, legale):
SET hnsw.ef_search = 100;
-- Alta velocità (autocomplete, recommendation):
SET hnsw.ef_search = 20;
-- Benchmark indicativo (1M vettori, 1536 dim, m=16, ef_construction=64):
-- ef_search=20: ~5ms/query, recall@10 ~85%
-- ef_search=40: ~10ms/query, recall@10 ~92%
-- ef_search=100: ~25ms/query, recall@10 ~97%
-- ef_search=200: ~50ms/query, recall@10 ~99%
-- Imposta a livello di sessione (non persiste tra sessioni):
SELECT set_config('hnsw.ef_search', '100', false);
-- Imposta per la sessione corrente (equivalente):
SET hnsw.ef_search = 100;
IVFFlat: Düz Sıkıştırmalı Ters Dosya
IVFFlat farklı bir yaklaşım kullanır: vektör uzayını kümelere (hücrelere) böler. K-anlamına gelir ve bir sorgu sırasında yalnızca en umut verici kümeleri arar. Ve HNSW'den daha basit ancak K-aracı yapmak için mevcut verilere ihtiyaç duyar ve eklemelerle daha hızlı bozulur.
IVFFlat Nasıl Çalışır?
- Eğitim aşaması: K-ortalama kümeleme vektörleri parçalara böler
listsmerkezler - Yapım aşaması: Her vektör en yakın kümeye (merkez) atanır.
- Sorgular: Beni bul
probessorguya en yakın kümeleri seçer ve ardından tam olarak bu kümelerin içinde arama yapar
-- IVFFlat richiede dati prima di creare l'indice
-- (ha bisogno di fare K-means clustering)
-- PREREQUISITO: la tabella deve avere almeno qualche centinaio di righe
-- Regola pratica: lists = sqrt(n_rows), con minimo 100
-- Creazione indice IVFFlat
CREATE INDEX documents_ivfflat_idx
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (
lists = 100 -- numero di cluster (default 100, raccomandato: sqrt(n_rows))
);
-- Per 1M righe: lists = sqrt(1000000) = 1000
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 1000);
-- Configurazione probes (quanti cluster cercare a query time)
SET ivfflat.probes = 10; -- default, cerca 10 cluster su 1000
-- Alta precisione:
SET ivfflat.probes = 50; -- cerca 50/1000 cluster = 5% dello spazio
-- Benchmark indicativo (1M vettori, lists=1000):
-- probes=5: ~3ms/query, recall@10 ~72%
-- probes=10: ~6ms/query, recall@10 ~82%
-- probes=30: ~18ms/query, recall@10 ~94%
-- probes=100: ~60ms/query, recall@10 ~99%
-- IVFFlat vs HNSW a parita di recall ~92%:
-- IVFFlat (probes=50): ~28ms - più lento!
-- HNSW (ef_search=40): ~10ms - più veloce
-- Conclusione: per recall alta, HNSW e solitamente più efficiente
HNSW ve IVFFlat: Seçim Kılavuzu
| karakteristik | HNSW | IVFFlatin |
|---|---|---|
| Sorgu gecikmesi | Daha hızlı (genellikle aynı hatırlamanın 2-5 katı) | Yüksek hatırlamada daha yavaş |
| Oluşturma zamanı | Daha yavaş (K-aracı gerekli değildir ancak derleme daha karmaşıktır) | Çok daha hızlı (~4x) |
| Dizin belleği | Ana (~2-4x) | Küçük |
| Eşit gecikmeyle geri çağırma | Daha iyi (genel olarak) | Biraz daha düşük |
| Artımlı ekleme | Mükemmel (yeniden eğitim yok) | Bozulur: kümeler yeniden ayarlanmaz |
| Derleme için gerekli veriler | Yok (boş başlayabilir) | Mevcut verileri gerektirir (K-araçları) |
| Tipik veri kümesi | Büyüyen veri kümeleri, yüksek hassasiyet, RAG | Statik veri kümeleri, öncelikli hızlı oluşturma |
Seçim İçin Pratik Kural
- HNSW'yi kullanın çoğu durumda: daha iyi hatırlama, daha hızlı sorgular, eklemeleri iyi işleme. Bu, RAG sistemlerinin %90'ı için doğru seçimdir.
- IVFFlat'ı kullanın Milyarlarca vektörden oluşan bir veri kümeniz olduğunda ve HNSW için bellek yetersiz olduğunda veya dizini sık sık tamamen değişen veriler üzerinde yeniden oluşturmanız gerektiğinde.
- Kaba kuvvet kullanın (indeks yok) şu durumlarda: 50.000'den az taşıyıcınız varsa veya %100 garantili geri çağırmaya ihtiyacınız varsa (ör. yasal araştırma, uyumluluk).
Hibrit Filtrelerle Sorgu Optimizasyonu
Üretimde en sık karşılaşılan sorunlardan biri birleştirmedir. meta veri filtreleri ile vektör arama. pgvector 0.7+ bunu yinelemeli taramayla halleder, ancak sorundan kaçınmak için sorguların doğru şekilde nasıl yapılandırılacağını anlamak önemlidir. filtreleme sonrası.
Filtreleme Sonrası Sorun ve Çözümü
-- SBAGLIATO: post-filtering - l'ANN trova i top-k globali, poi filtra
-- Se i top-k risultati ANN non soddisfano il filtro, ottieni meno di k risultati!
SELECT id, content, embedding <=> query_vec AS dist
FROM documents
ORDER BY embedding <=> query_vec
LIMIT 10
-- Il problema: se questi 10 risultati non soddisfano source_type='pdf',
-- non ottieni 10 risultati PDF ma potenzialmente 0!
-- CORRETTO: pre-filtering in pgvector con iterative scan
-- pgvector 0.7+ supporta indexed scan con filtri WHERE
SELECT id, content, embedding <=> query_vec AS dist
FROM documents
WHERE source_type = 'pdf' -- pre-filter: applicato PRIMA dell'ANN
AND language = 'en'
ORDER BY embedding <=> query_vec -- ANN su subset filtrato
LIMIT 10;
-- EXPLAIN verifica che usi l'indice con il filtro:
EXPLAIN (ANALYZE, FORMAT TEXT)
SELECT id, content, embedding <=> '[...]'::vector AS dist
FROM documents
WHERE source_type = 'pdf'
ORDER BY embedding <=> '[...]'::vector
LIMIT 10;
-- Dovrebbe mostrare: "Index Scan using ... with Filter: (source_type = 'pdf')"
-- Se vedi Seq Scan, il filtro potrebbe ridurre troppo il dataset
-- In quel caso, un indice parziale e la soluzione migliore
Sık Filtreler için Kısmi Dizinler
-- Se filtri sempre per source_type, crea un indice parziale dedicato
-- Vantaggi: molto più piccolo e veloce dell'indice globale
CREATE INDEX documents_hnsw_pdf
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64)
WHERE source_type = 'pdf'; -- solo i documenti PDF
CREATE INDEX documents_hnsw_recent
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64)
WHERE created_at > NOW() - INTERVAL '30 days'; -- solo documenti recenti
-- Query che sfruttano automaticamente gli indici parziali
SELECT id, content, embedding <=> query_vec AS dist
FROM documents
WHERE source_type = 'pdf' -- attiva documents_hnsw_pdf automaticamente
ORDER BY embedding <=> query_vec
LIMIT 10;
-- Confronto dimensioni: indice parziale vs totale
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size,
ROUND(pg_relation_size(indexrelid)::numeric /
(SELECT pg_relation_size(indexrelid) FROM pg_stat_user_indexes
WHERE indexname = 'documents_hnsw_total') * 100, 1) AS pct_of_full
FROM pg_stat_user_indexes
JOIN pg_class ON pg_class.relname = pg_stat_user_indexes.indexname
WHERE tablename = 'documents';
-- documents_hnsw_total: 2.1 GB (100%)
-- documents_hnsw_pdf: 487 MB (23% - solo i PDF)
Benzerlik Eşiğiyle Sorgulama
-- Filtra per distanza minima: evita risultati irrilevanti
-- cosine distance < 0.4 significa cosine similarity > 0.6
SELECT
id,
source_path,
content,
1 - (embedding <=> query_vec::vector) AS similarity,
embedding <=> query_vec::vector AS distance
FROM documents
WHERE
embedding <=> query_vec::vector < 0.4 -- max distance (= min 60% similarity)
ORDER BY embedding <=> query_vec::vector
LIMIT 10;
-- Conversione distanza/similarità per cosine:
-- cosine_distance = 1 - cosine_similarity
-- distance 0.0 = similarity 1.0 (vettori identici)
-- distance 0.2 = similarity 0.8 (molto simili)
-- distance 0.4 = similarity 0.6 (sufficientemente simili per RAG)
-- distance 0.7 = similarity 0.3 (probabilmente non rilevante)
-- distance 1.0 = similarity 0.0 (non correlati)
-- distance 2.0 = similarity -1.0 (opposti)
-- Threshold raccomandati per use case:
-- RAG documenti tecnici: < 0.35 (similarity > 0.65)
-- FAQ answering: < 0.30 (similarity > 0.70)
-- Product recommendation: < 0.50 (similarity > 0.50)
-- Duplicate detection: < 0.10 (similarity > 0.90)
Anlamsal Arama ve Benzerlik Araması
Terimler sıklıkla birbirinin yerine kullanılır, ancak önemli bir ayrım vardır sistem mimarisini etkileyen:
| Tip | Amaç | Örnek | Metrik |
|---|---|---|---|
| Benzerlik Arama | Belirli bir vektöre yakın vektörleri bulma | Bu görsele benzer görseller | Mesafe L2, kosinüs |
| Semantik Arama | Metin sorgusuna benzer anlama sahip belgeleri bulma | "PostgreSQL'i nasıl kurarım?" tam olarak bu ifadeyi içermeyen kılavuzları bulun | Metin yerleştirmelerde kosinüs benzerliği |
| Anahtar Kelime Arama | Anahtar kelimelerin tam eşleşmesi | "PostgreSQL 16" yalnızca bu dizeye sahip belgeleri bulur | TF-IDF, BM25 |
| Hibrit Arama | Anlambilimi ve anahtar kelimeleri birleştirin | Anlamsal alaka düzeyini ve teknik terimlerin tam eşleşmesini dengeleyin | RRF, ağırlıklı toplam |
Anlamsal Arama uygulamasını tamamlayın
import psycopg2
from openai import OpenAI
client = OpenAI()
def semantic_search(
conn,
query: str,
top_k: int = 10,
source_type: str = None,
min_similarity: float = 0.6,
include_metadata: bool = True
) -> list[dict]:
"""
Ricerca semantica con filtri opzionali e threshold di qualità.
Args:
conn: Connessione psycopg2 a PostgreSQL
query: La domanda o testo da cercare
top_k: Numero di risultati da restituire
source_type: Filtra per tipo documento ('pdf', 'md', 'html')
min_similarity: Soglia minima di similarità coseno (0.0-1.0)
include_metadata: Se includere il campo metadata JSONB
Returns:
Lista di dict con id, source_path, content, similarity, metadata
"""
# 1. Genera embedding della query
resp = client.embeddings.create(
input=[query.replace("\n", " ")],
model="text-embedding-3-small"
)
query_vec = resp.data[0].embedding
max_distance = 1 - min_similarity # converti similarity -> distance
# 2. Costruisci query SQL dinamica con filtri opzionali
params = [query_vec, max_distance]
filter_clauses = ["embedding <=> %s::vector < %s"]
if source_type:
filter_clauses.append("source_type = %s")
params.append(source_type)
where = " AND ".join(filter_clauses)
metadata_col = "metadata" if include_metadata else "NULL::jsonb"
sql = f"""
SELECT
id,
source_path,
source_type,
chunk_index,
title,
content,
1 - (embedding <=> %s::vector) AS similarity,
{metadata_col} AS metadata
FROM documents
WHERE {where}
ORDER BY embedding <=> %s::vector
LIMIT %s
"""
params_final = [query_vec] + params + [query_vec, top_k]
with conn.cursor() as cur:
cur.execute(sql, params_final)
rows = cur.fetchall()
return [
{
"id": r[0],
"source_path": r[1],
"source_type": r[2],
"chunk_index": r[3],
"title": r[4],
"content": r[5],
"similarity": round(r[6], 4),
"metadata": r[7]
}
for r in rows
]
# Uso
conn = psycopg2.connect("postgresql://postgres:pass@localhost/vectordb")
results = semantic_search(
conn,
query="Come ottimizzare query PostgreSQL per dataset grandi",
top_k=5,
source_type="pdf",
min_similarity=0.65
)
for r in results:
print(f"[{r['similarity']:.3f}] {r['title']} - {r['content'][:100]}...")
Hibrit Arama: Vektör ve Tam Metni Birleştirme
RAG için PostgreSQL'in en güçlü yönlerinden biri, onu tek bir platformda birleştirme yeteneğidir. klasik tam metin aramasıyla (BM25 benzeri) sorgu anlamsal (vektör) arama. Bu ve özellikle özel adlar gibi kesin teknik terimler içeren sorgular için kullanışlıdır. kodlar, yazılım sürümleri.
-- Hybrid search puro SQL: vector + full-text con Reciprocal Rank Fusion (RRF)
-- RRF Score = sum(1/(k + rank)) per ogni lista risultati
WITH vector_results AS (
-- Ricerca semantica: top 20 per similarità vettoriale
SELECT
id, content, source_path,
ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS v_rank,
1 - (embedding <=> $1::vector) AS vector_score
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 20
),
fts_results AS (
-- Ricerca full-text: top 20 per rilevanza ts_rank
SELECT
id, content, source_path,
ROW_NUMBER() OVER (
ORDER BY ts_rank(to_tsvector('italian', content),
plainto_tsquery('italian', $2)) DESC
) AS f_rank,
ts_rank(to_tsvector('italian', content),
plainto_tsquery('italian', $2)) AS fts_score
FROM documents
WHERE to_tsvector('italian', content) @@ plainto_tsquery('italian', $2)
LIMIT 20
),
rrf_combined AS (
-- RRF fusion: combina i due rank
SELECT
COALESCE(v.id, f.id) AS id,
COALESCE(v.content, f.content) AS content,
COALESCE(v.source_path, f.source_path) AS source_path,
-- Peso: 70% vector + 30% full-text
COALESCE(0.7 / (60.0 + v.v_rank), 0) +
COALESCE(0.3 / (60.0 + f.f_rank), 0) AS rrf_score,
v.vector_score,
f.fts_score
FROM vector_results v
FULL OUTER JOIN fts_results f ON v.id = f.id
)
SELECT id, content, source_path, ROUND(rrf_score::numeric, 6) AS score
FROM rrf_combined
ORDER BY rrf_score DESC
LIMIT 5;
Kıyaslama: Araştırmanın kalitesinin ölçülmesi
import time
import psycopg2
from statistics import mean, quantiles
def benchmark_vector_search(conn, query_vectors: list, config: dict) -> dict:
"""
Esegui benchmark di latenza su un set di query vettoriali.
Args:
conn: Connessione PostgreSQL
query_vectors: Lista di vettori query (1536 dimensioni)
config: Dict con ef_search o probes da impostare
Returns:
Dict con metriche di latenza (p50, p95, p99, mean)
"""
# Imposta parametri ANN per questo benchmark
with conn.cursor() as cur:
if "ef_search" in config:
cur.execute(f"SET hnsw.ef_search = {config['ef_search']}")
if "probes" in config:
cur.execute(f"SET ivfflat.probes = {config['probes']}")
latencies = []
for query_vec in query_vectors:
start = time.perf_counter()
with conn.cursor() as cur:
cur.execute("""
SELECT id, embedding <=> %s::vector AS dist
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT 10
""", (query_vec, query_vec))
cur.fetchall()
elapsed_ms = (time.perf_counter() - start) * 1000
latencies.append(elapsed_ms)
p_values = quantiles(latencies, n=100)
return {
"config": config,
"n_queries": len(query_vectors),
"p50_ms": round(p_values[49], 2),
"p95_ms": round(p_values[94], 2),
"p99_ms": round(p_values[98], 2),
"mean_ms": round(mean(latencies), 2),
"min_ms": round(min(latencies), 2),
"max_ms": round(max(latencies), 2)
}
# Confronta diverse configurazioni HNSW
configs = [
{"name": "HNSW ef=20 (fast)", "ef_search": 20},
{"name": "HNSW ef=40 (default)", "ef_search": 40},
{"name": "HNSW ef=100 (precise)", "ef_search": 100},
]
# Usa query di test reali (embedding generati con lo stesso modello dei documenti)
test_queries = [...] # lista di vettori 1536-dim
print(f"Benchmark con {len(test_queries)} query di test:")
for cfg in configs:
result = benchmark_vector_search(conn, test_queries[:100], cfg)
print(f"{cfg['name']}:")
print(f" p50={result['p50_ms']}ms, p95={result['p95_ms']}ms, p99={result['p99_ms']}ms")
Ölçümü Geri Çağırma: YSA kalitesini kontrol edin
def measure_recall_at_k(conn, query_vectors: list, k: int = 10) -> float:
"""
Misura Recall@K confrontando ANN con brute force.
Recall@K = (risultati ANN corretti) / k
Un Recall@10 di 0.95 significa che l'ANN trova
9.5 dei 10 risultati esatti su media.
Nota: usa un campione di 50-100 query per stabilità statistica.
"""
total_recall = 0.0
for query_vec in query_vectors:
# Risultati esatti (brute force) - ground truth
with conn.cursor() as cur:
cur.execute("SET enable_indexscan = off")
cur.execute("""
SELECT id FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_vec, k))
exact_ids = set(r[0] for r in cur.fetchall())
cur.execute("SET enable_indexscan = on")
# Risultati ANN (con indice HNSW/IVFFlat)
with conn.cursor() as cur:
cur.execute("""
SELECT id FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_vec, k))
ann_ids = set(r[0] for r in cur.fetchall())
# Recall = overlap / k
overlap = len(exact_ids & ann_ids)
total_recall += overlap / k
avg_recall = total_recall / len(query_vectors)
print(f"Recall@{k}: {avg_recall:.4f} ({avg_recall*100:.1f}%)")
return avg_recall
# Target di recall per diversi use case:
# Recall@10 > 0.90 per RAG generale
# Recall@10 > 0.95 per RAG enterprise (medico, legale)
# Recall@10 > 0.85 accettabile per autocomplete/recommendation
# Con HNSW m=16, ef_construction=64, ef_search=40: tipicamente 0.92-0.95
# Con HNSW m=16, ef_construction=64, ef_search=100: tipicamente 0.97-0.99
Gelişmiş Optimizasyon: Sorgu Planlama
-- 1. Statistiche aggiornate: fondamentale per buone query plans
ANALYZE documents;
-- 2. Verifica dimensione e statistiche dell'indice HNSW
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan AS times_used,
idx_tup_read AS tuples_read,
idx_tup_fetch AS tuples_fetched,
ROUND(idx_tup_fetch::numeric / NULLIF(idx_scan, 0), 1) AS avg_tuples_per_scan
FROM pg_stat_user_indexes
WHERE tablename = 'documents'
ORDER BY idx_scan DESC;
-- 3. Parametri di memoria per performance ottimale
SET maintenance_work_mem = '512MB'; -- per BUILD dell'indice HNSW (temporaneo)
SET work_mem = '64MB'; -- per query sort operations
-- 4. Parallel query per dataset grandi
SET max_parallel_workers_per_gather = 4; -- usa 4 worker per query
-- 5. Verifica che shared_buffers sia adeguato
-- L'indice HNSW deve stare in memoria per performance ottimale
SHOW shared_buffers;
-- Regola pratica: shared_buffers = 25% della RAM
-- pg_relation_size(indice HNSW) deve essere << shared_buffers
-- 6. Monitora cache hit ratio per la tabella
SELECT
relname,
heap_blks_read AS disk_reads,
heap_blks_hit AS cache_hits,
ROUND(heap_blks_hit::numeric / NULLIF(heap_blks_read + heap_blks_hit, 0) * 100, 2)
AS cache_hit_pct
FROM pg_statio_user_tables
WHERE relname = 'documents';
-- Target: cache_hit_pct > 95% per performance ottimale
-- 7. Verifica indici non utilizzati (candidati per DROP):
SELECT indexname, idx_scan
FROM pg_stat_user_indexes
WHERE tablename = 'documents'
AND idx_scan = 0
AND indexname NOT LIKE '%pkey%'; -- escludi primary keys
Maksimum Marjinal Uygunluk (MMR)
Benzerlik aramada yaygın bir sorun, sonuçların fazlalığı: ben top-k parçalarının tümü birbirine çok benzer olabilir ve aynı bilgiyi kapsayabilir. Bu model aynı bilgiyi tekrar tekrar aldığı için RAG bağlamının kalitesini düşürür. MMR, bir sonraki parçayı aşamalı olarak seçerek alaka ve çeşitliliği dengeler. sorguyla alakalı ANCAK önceden seçilmiş olanlardan farklı.
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def maximal_marginal_relevance(
query_vec: list[float],
candidate_vecs: list[list[float]],
candidate_data: list[dict],
k: int = 5,
lambda_param: float = 0.5
) -> list[dict]:
"""
MMR: seleziona k risultati bilanciando rilevanza e diversità.
Algoritmo:
1. Seleziona sempre il candidato più rilevante per la query
2. Per ogni successivo candidato, usa MMR score che penalizza
la ridondanza rispetto ai già selezionati
lambda_param:
1.0 = solo rilevanza (equivale a top-k standard)
0.0 = solo diversità (massima varieta)
0.5 = bilanciamento ottimale per RAG (default)
0.6 = leggermente più rilevanza rispetto diversità
Returns:
Lista di k candidati diversificati e rilevanti
"""
query_array = np.array(query_vec).reshape(1, -1)
candidate_array = np.array(candidate_vecs)
# Similarità query-candidato (rilevanza)
query_sims = cosine_similarity(query_array, candidate_array)[0]
selected = []
selected_indices = []
remaining_indices = list(range(len(candidate_vecs)))
while len(selected) < k and remaining_indices:
if not selected:
# Prima iterazione: scegli il più rilevante
best_idx = int(np.argmax(query_sims))
else:
# Iterazioni successive: MMR score
selected_array = candidate_array[selected_indices]
mmr_scores = []
for i in remaining_indices:
relevance = query_sims[i]
# Massima similarità con i già selezionati (ridondanza)
max_redundancy = float(np.max(
cosine_similarity(candidate_array[i:i+1], selected_array)[0]
))
# MMR bilancia rilevanza e ridondanza
mmr = lambda_param * relevance - (1 - lambda_param) * max_redundancy
mmr_scores.append((i, mmr))
best_idx = max(mmr_scores, key=lambda x: x[1])[0]
selected.append(candidate_data[best_idx])
selected_indices.append(best_idx)
remaining_indices.remove(best_idx)
return selected
# Uso con risultati PostgreSQL
# Prima recupera più candidati (top-20), poi applica MMR per top-5 diversificati
chunks_with_vecs = searcher.vector_search_with_embeddings(query, top_k=20)
embeddings = [c['embedding'] for c in chunks_with_vecs]
data = [{'id': c['id'], 'content': c['content']} for c in chunks_with_vecs]
diverse_chunks = maximal_marginal_relevance(
query_vec=query_embedding,
candidate_vecs=embeddings,
candidate_data=data,
k=5,
lambda_param=0.6 # leggermente orientato alla rilevanza
)
# Risultato: 5 chunk rilevanti MA con contenuti diversi tra loro
Benzerlik Aramasında Yaygın Hatalar
- YSA etkinleştirilmedi: EXPLAIN bir Sıra Taraması gösteriyorsa, LIMIT'in tablonun boyutu için makul olup olmadığını, endeksin mevcut olduğunu ve istatistiklerin güncel olup olmadığını kontrol edin (ANALYZE).
- ef_search çok düşük: ef_search=10 ile Recall@10'u yalnızca %70-75 oranında alabilirsiniz. Üretim için RAG için en az 40, daha iyisi 60-100 kullanın.
- Sonuçları temizleyen son filtreleme: ORDER BY'den sonra (ön filtreleme olmadan) WHERE filtrelerini uygularsanız, ANN genel ks'yi döndürür ve ardından filtreleyerek potansiyel olarak 0 sonuç verir. Her zaman ön filtrelemeyi kullanın.
- Yanlış mesafe operatörü: Metin yerleştirme için <=> (kosinüs) yerine <-> (L2) kullanılması, arama kalitesini önemli ölçüde azaltır. Dizindeki işlem türünün sorgudaki işleçle eşleştiğini doğrulayın.
- Benzerlik eşiği yok: Mesafe çok yüksek olsa bile tüm üst-k'lerin döndürülmesi, RAG'da alakasız yanıtlara yol açar.
- Artıklığı göz ardı edin: İlk 5 parçanın tümü belgedeki aynı cümleyi kapsayabilir. RAG yanıtlarında çeşitlilik için MMR'yi kullanın.
Sonuçlar ve Sonraki Adımlar
PostgreSQL'de benzerlik araması, yapılandırılması gereken birçok parametrenin bulunduğu bir alandır. Anahtar
ve temel değiş tokuşu anlayın: ne kadar çok adayı incelerseniz geri çağırma oranı o kadar yüksek olur
ancak gecikme daha yüksek. Üretimdeki çoğu RAG sistemi için HNSW
ile ef_search=60-100 10 ms ile 50 ms gecikme arasında mükemmel bir denge sunar
Recall@10 ile %92-97.
Bir sonraki makale daha ayrıntılı olarak ele alınacaktır gelişmiş indeksleme stratejileri: özel kullanım durumunuz için en uygun HNSW ve IVFFlat parametrelerinin nasıl seçileceği, nasıl Endekslerin zaman içindeki bozulmasını ve artımlı güncelleme tekniklerini izleyin performans kaybı olmadan. Dahil olanlar: Tam PostgreSQL kurulumu, paralel dizin oluşturma, ve kesinti olmadan REINDEX'in nasıl planlanacağı.







