벡터 검색을 위한 인덱싱: HNSW, IVFFlat 및 성능 전략
네 번째 기사에서 HNSW와 IVFFlat이 개념적으로 어떻게 작동하는지 확인했습니다. 이제 안으로 들어가 볼까요 생산에 차이를 만드는 기술적인 세부 사항: 매개변수를 선택하는 방법 최적의, 시간이 지남에 따라 지수 상태를 모니터링하는 방법, 관리 방법 성능 저하 없는 증분 업데이트 및 수백만 개의 통신업체로 확장하는 방법 실제 하드웨어에서.
잘못 구성된 벡터 인덱스는 최적의 벡터 인덱스보다 10배 느릴 수 있습니다. 4배 더 많은 RAM이 필요합니다. 이 기사에서는 어려운 숫자와 경험 법칙을 제공합니다. 실제 벤치마크 및 생산 패턴을 기반으로 pgVector를 전문적으로 구성하십시오. 2026년에는 "Just Use Postgres" 트렌드가 계속해서 성장하면서 올바른 구성 방법을 알아야 합니다. 벡터 인덱스는 모든 AI 엔지니어의 기본 기술입니다.
시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | pg벡터 | 설치, 운영자, 인덱싱 |
| 2 | 심층적인 임베딩 | 모델, 거리, 세대 |
| 3 | PostgreSQL을 사용한 RAG | 엔드투엔드 RAG 파이프라인 |
| 4 | 유사성 검색 | 알고리즘 및 최적화 |
| 5 | 현재 위치 - HNSW 및 IVFFlat | 고급 인덱싱 전략 |
| 6 | 생산 중인 RAG | 확장성 및 성능 |
무엇을 배울 것인가
- HNSW 및 IVFFlat 지수의 크기 계산
- 최적의 매개변수 선택: 공식 및 벤치마크
- 특정 리콜 대상에 대해 런타임 시 ef_search 및 프로브 구성
- 생산 지수의 상태 모니터링
- 다운타임 없이 재구축 및 REINDEX
- 증분 업데이트: 삽입이 ANN 인덱스를 저하시키는 방법
- 다양한 사용 사례에 대한 다중 인덱스 전략
- 벡터 성능을 극대화하기 위한 PostgreSQL 구성 완료
- 고급 쿼리 최적화 기술: 쿼리 계획 및 분석 설명
- 예정된 인덱스 유지 관리: 진공, 적극적인 autovacuum
인덱스 크기 조정: 차지하는 공간의 양
인덱스를 생성하기 전에 인덱스가 메모리와 디스크에서 얼마나 많은 공간을 차지할지 이해하는 것이 중요합니다.
전체 인덱스를 shared_buffers 최대 성능을 위한 최적의 조건입니다.
메모리에 맞지 않는 인덱스는 쿼리마다 I/O가 필요하므로 대기 시간이 10~100배 늘어납니다.
HNSW 공식
-- Formula approssimata dimensione indice HNSW:
-- Dimensione ~= n_vectors * m * (2 + 4 * d / 8) bytes + overhead
-- dove:
-- n_vectors = numero di vettori
-- m = parametro m dell'indice (connessioni per nodo)
-- d = dimensioni del vettore (es. 1536)
-- Esempio pratico per 1M vettori, dim=1536, m=16:
-- 1_000_000 * 16 * (2 + 4 * 1536 / 8) = 1_000_000 * 16 * 770 = 12.3 GB
-- HNSW ocupa tipicamente 1.5-3x la dimensione dei dati grezzi
-- Dimensione dati grezzi (vettore float4):
-- 4 bytes * 1536 dim * 1_000_000 vettori = 6.1 GB
-- Controlla dimensioni reali:
SELECT
pg_size_pretty(pg_relation_size('documents')) AS table_size,
pg_size_pretty(pg_indexes_size('documents')) AS indexes_size,
pg_size_pretty(pg_total_relation_size('documents')) AS total_size;
-- Dimensione specifica di ogni indice:
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
pg_size_pretty(pg_relation_size(indexrelid)) AS raw_bytes
FROM pg_stat_user_indexes
WHERE tablename = 'documents'
ORDER BY pg_relation_size(indexrelid) DESC;
-- Verifica se l'indice sta in shared_buffers:
-- Regola pratica: shared_buffers deve essere >= 1.5x la dimensione dell'indice HNSW
-- Se non ci sta, considera embedding a dimensione ridotta (768 o 384 dim)
SELECT
current_setting('shared_buffers') AS shared_buffers,
pg_size_pretty(pg_indexes_size('documents')) AS total_index_size;
계획을 위한 빠른 견적
| 벡터 | 치수 | 원시 데이터 | HNSW (m=16) | IVFFlat(목록=sqrt(n)) | 권장 RAM |
|---|---|---|---|---|---|
| 100K | 1536년 | 600MB | ~1.2GB | ~700MB | 4GB |
| 1M | 1536년 | 6GB | ~12GB | ~7GB | 32GB |
| 10M | 1536년 | 60GB | ~120GB | ~70GB | 256GB |
| 1M | 768 | 3GB | ~6GB | ~3.5GB | 16GB |
| 1M | 384 | 1.5GB | ~3GB | ~1.8GB | 8GB |
HNSW 매개변수: 최적 구성 가이드
HNSW에는 메모리, 빌드 시간, 회수 및 쿼리 대기 시간. 이를 철저히 이해하면 인덱스를 적절하게 구성할 수 있습니다. 모든 사용 시나리오에 대해 전문적입니다.
m 매개변수: 노드당 연결 수
-- m: numero massimo di connessioni bidirezionali per nodo in ogni livello
-- Valore default: 16
-- Range valido: 4-64 (pgvector max: 100)
-- Regole pratiche per m:
-- m=8: Bassa memoria, bassa recall (uso: caching, suggerimenti veloci, dataset grandi)
-- m=16: Default bilanciato (uso: general purpose RAG, semantic search)
-- m=32: Alta recall, doppia memoria (uso: ricerca medica, legale, alta precisione)
-- m=64: Massima recall, 4x memoria (uso: casi estremi, dataset piccoli <100K)
-- Benchmark m vs recall e memoria (1M vettori, 1536 dim, ef_search=40):
-- m=8: recall@10=84%, index=6GB, p50=7ms, p95=15ms
-- m=16: recall@10=93%, index=12GB, p50=10ms, p95=22ms
-- m=32: recall@10=97%, index=24GB, p50=18ms, p95=38ms
-- m=64: recall@10=99%, index=48GB, p50=35ms, p95=72ms
-- Crea indici con diversi valori di m (test comparativo):
CREATE INDEX idx_hnsw_m8 ON documents USING hnsw (embedding vector_cosine_ops) WITH (m=8, ef_construction=64);
CREATE INDEX idx_hnsw_m16 ON documents USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64);
CREATE INDEX idx_hnsw_m32 ON documents USING hnsw (embedding vector_cosine_ops) WITH (m=32, ef_construction=64);
-- Testa quale usa PostgreSQL (usa il primo disponibile per nome)
-- Per forzare un indice specifico:
SELECT /*+ IndexScan(documents idx_hnsw_m32) */
id, embedding <=> query_vec AS dist
FROM documents
ORDER BY embedding <=> query_vec
LIMIT 10;
-- Confronta le dimensioni effettive degli indici creati:
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size,
idx_scan AS query_count
FROM pg_stat_user_indexes
WHERE tablename = 'documents'
AND indexname LIKE 'idx_hnsw_m%'
ORDER BY indexname;
ef_construction 매개변수: 빌드 품질
-- ef_construction: candidati considerati durante la costruzione dell'indice
-- Influisce sulla qualità dell'indice costruito (recall potenziale massima)
-- NON influisce sulle dimensioni dell'indice
-- Valore default: 64
-- Regole pratiche:
-- ef_construction=32: Build veloce, recall potenziale ridotto. Solo per prototipi.
-- ef_construction=64: Default. Ottimo per la maggior parte dei casi.
-- ef_construction=128: Build 2x più lenta, recall massima ~2% migliore.
-- ef_construction=200: Build molto lenta, miglioramento marginale.
-- Benchmark ef_construction (m=16, 1M vettori):
-- ef=32: Build ~20min, max recall@10 ~89%
-- ef=64: Build ~45min, max recall@10 ~95%
-- ef=128: Build ~90min, max recall@10 ~97%
-- ef=256: Build ~3h, max recall@10 ~98.5%
-- Per massimizzare la qualità dell'indice (una tantum, non in produzione):
-- Usa maintenance_work_mem grande per la build
SET maintenance_work_mem = '4GB'; -- temporaneo per la build
CREATE INDEX idx_hnsw_highquality
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m=24, ef_construction=128);
-- Dopo la build, la RAM viene rilasciata automaticamente
-- Verifica il progresso della build:
SELECT
phase,
blocks_done,
blocks_total,
ROUND(blocks_done::numeric / NULLIF(blocks_total, 0) * 100, 1) AS pct_done,
tuples_done,
tuples_total
FROM pg_stat_progress_create_index
WHERE relid = 'documents'::regclass;
ef_search 매개변수: 쿼리 품질
-- ef_search: candidati esaminati durante la ricerca (beam search width)
-- E un parametro RUNTIME: puoi cambiarlo senza ricostruire l'indice
-- Valore default: 40
-- Range valido: 1 -> ef_construction (max della build)
-- Imposta ef_search per la sessione corrente:
SET hnsw.ef_search = 40; -- default, buon equilibrio
-- Alta precisione (RAG enterprise, medico, legale):
SET hnsw.ef_search = 100;
-- Alta velocità (autocomplete, recommendation real-time):
SET hnsw.ef_search = 20;
-- Benchmark ef_search (1M vettori, 1536 dim, m=16, ef_construction=64):
-- ef_search=10: ~3ms/query, recall@10 ~75%
-- 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 transazione (più sicuro in produzione):
BEGIN;
SET LOCAL hnsw.ef_search = 80;
SELECT id, content, embedding <=> $1::vector AS dist
FROM documents
ORDER BY embedding <=> $1::vector
LIMIT 10;
COMMIT;
-- Imposta globalmente in postgresql.conf (persiste tra restart):
-- hnsw.ef_search = 60
-- Verifica configurazione corrente:
SHOW hnsw.ef_search;
SELECT current_setting('hnsw.ef_search');
IVFFlat 매개변수: 목록 및 프로브
IVFFlat는 근본적으로 다른 접근 방식을 사용합니다. 탐색 가능한 그래프 대신 클러스터를 생성합니다.
K-평균을 통해 가장 유망한 클러스터만 검색합니다. 매개변수 lists e
probes 이 메커니즘을 제어합니다.
목록 수 선택
-- lists: numero di cluster (centroidi) per IVFFlat
-- Regola pratica:
-- lists = sqrt(n_rows) per dataset fino a 1M righe
-- lists = n_rows / 1000 per dataset sopra 1M righe
-- Calcolo automatico del valore ottimale:
WITH stats AS (
SELECT COUNT(*) AS n FROM documents
)
SELECT
n,
CEIL(SQRT(n::float))::int AS recommended_lists,
CEIL(SQRT(n::float))::int * 10 AS max_probes -- max probes = 10% delle liste
FROM stats;
-- Esempi:
-- 10K righe: lists=100 (sqrt=100, ma min raccomandato=100)
-- 100K righe: lists=316 (sqrt(100000))
-- 1M righe: lists=1000 (sqrt(1000000))
-- 10M righe: lists=3162 (sqrt(10000000))
-- 100M righe: lists=10000
-- Crea l'indice con il valore calcolato (procedura automatica):
DO $
DECLARE
n_rows INTEGER;
n_lists INTEGER;
BEGIN
SELECT COUNT(*) INTO n_rows FROM documents;
n_lists := GREATEST(100, CEIL(SQRT(n_rows::float))::int);
EXECUTE format(
'CREATE INDEX idx_ivfflat ON documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = %s)',
n_lists
);
RAISE NOTICE 'Indice IVFFlat creato con % liste per % righe', n_lists, n_rows;
END $;
-- IMPORTANTE: IVFFlat richiede dati esistenti per fare K-means!
-- Crea l'indice DOPO aver caricato almeno il 70-80% dei dati.
-- Se aggiungi molti dati dopo la build, l'indice degrada: ricostruiscilo periodicamente.
-- Verifica bilanciamento dei cluster (uniformita delle liste):
-- In produzione, ogni lista dovrebbe contenere circa n_rows / lists vettori
-- Lista con molto più vettori delle altre = distribuzione sbilanciata
SELECT
count(*) AS cluster_size,
avg(count(*)) OVER () AS avg_size
FROM (
-- Questa e una query interna che usa l'indice IVFFlat
-- Non e disponibile direttamente via SQL, ma puoi stimarla
SELECT id FROM documents LIMIT 1000
) sub;
런타임 시 프로브: 재현율과 지연 시간의 균형 유지
-- probes: quante liste cercare durante una query
-- Deve essere <= lists
-- Default: 1 (cerca solo la lista più vicina - molto veloce ma bassa recall!)
-- ATTENZIONE: il default di probes=1 da una recall molto bassa!
-- Imposta sempre probes appropriato per il tuo use case.
-- Formula per target di recall:
-- probes_needed ~= lists * target_recall^2
-- Per recall 90% con lists=1000: probes ~= 1000 * 0.81 = 810 (!!)
-- Per recall 85% con lists=1000: probes ~= 1000 * 0.72 = 720
-- Per recall 80% con lists=1000: probes ~= 1000 * 0.64 = 640
-- In pratica, con clustering ben distribuito (K-means converge):
-- probes = lists * 0.05 -> recall ~= 85% (buon bilanciamento)
-- probes = lists * 0.10 -> recall ~= 90%
-- probes = lists * 0.20 -> recall ~= 95%
-- Benchmark IVFFlat (1M vettori, lists=1000, 1536 dim):
-- probes=5: ~3ms/query, recall@10 ~72%
-- probes=10: ~6ms/query, recall@10 ~82%
-- probes=50: ~28ms/query, recall@10 ~92%
-- probes=100: ~55ms/query, recall@10 ~96%
-- probes=200: ~110ms/query, recall@10 ~98%
-- Impostazione in postgresql.conf (persiste tra sessioni):
-- ivfflat.probes = 10 (default globale)
-- Override per sessione/transazione:
BEGIN;
SET LOCAL ivfflat.probes = 50; -- solo per questa transazione
SELECT id, content FROM documents ORDER BY embedding <=> query_vec LIMIT 5;
COMMIT;
-- Per application-level tuning in Python con psycopg2:
with conn.cursor() as cur:
cur.execute("SET ivfflat.probes = %s", (probes,))
cur.execute("""
SELECT id, content, 1 - (embedding <=> %s::vector) AS similarity
FROM documents
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_vec, query_vec, top_k))
results = cur.fetchall()
직접 비교: HNSW와 IVFFlat
HNSW와 IVFFlat 사이의 선택이 항상 명확한 것은 아닙니다. 이 표에는 주요 장단점이 요약되어 있습니다. 결정에 도움이 되는 하드 데이터를 제공합니다.
| 특성 | HNSW (m=16, f=64) | IVFFFlat(목록=1000) | 추천 |
|---|---|---|---|
| 쿼리 지연 시간(p50) | ~10ms(ef_search=40) | ~6ms(프로브=10) | 낮은 프로브로 더 빠른 IVFFlat |
| 동일한 대기 시간으로 Recall@10 | ~92% | ~82% | HNSW 더 나은 리콜 |
| 빌드 시간(1M 벡터) | ~45분 | ~10분 | IVFFlat 4배 더 빨라짐 |
| 인덱스 메모리 | ~12GB(1M x 1536) | ~7GB(1M x 1536) | IVFFlat ~40% 더 적은 RAM |
| 증분 삽입 | 좋습니다. 재교육이 필요하지 않습니다. | 시간이 지남에 따라 저하됩니다. | 동적 데이터를 위한 HNSW |
| 빌드에 필요한 데이터 | 없음(빈 채로 시작할 수 있음) | 기존 데이터가 필요합니다. | 더욱 유연한 HNSW |
| 병렬성 빌드(PG16+) | 응, 멀티워커야 | 부분 | HNSW의 확장성 향상 |
바로가기 규칙
- HNSW가 올바른 선택입니다 대부분의 경우 시간이 지남에 따라 증가하는 데이터 세트, 리콜이 중요한 RAG 애플리케이션, 사용 가능한 RAM이 좋은 환경입니다.
- IVFFlat이 편리합니다 시기: 준정적 데이터 세트(거의 업데이트되지 않음)가 있거나 메모리가 제한되어 있거나 운영 인덱스가 빠르게 필요한 경우(예: 긴급 개념 증명)
- 색인 없음(무차별 대입) 50,000개 미만의 벡터를 수정하거나 필요한 경우 100% 리콜을 보장합니다.
생산 지수 모니터링
지수 현황 및 활용
-- Dashboard monitoring completo per indici vettoriali
SELECT
schemaname,
tablename,
indexname,
-- Utilizzo
idx_scan AS "Query che usano l'indice",
idx_tup_read AS "Tuple lette dall'indice",
idx_tup_fetch AS "Tuple effettivamente restituite",
-- Efficienza
CASE
WHEN idx_scan > 0 THEN ROUND(idx_tup_fetch::numeric / idx_scan, 1)
ELSE 0
END AS "Tuple/query media",
-- Dimensioni
pg_size_pretty(pg_relation_size(indexrelid)) AS "Dimensione indice"
FROM pg_stat_user_indexes
WHERE tablename = 'documents'
ORDER BY idx_scan DESC;
-- Verifica se l'indice e in cache (shared_buffers)
-- Richiede pg_buffercache extension:
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
SELECT
relname,
pg_size_pretty(pg_relation_size(oid)) AS "Dimensione",
ROUND(
(SELECT COUNT(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode(oid))::numeric
/ NULLIF(pg_relation_size(oid) / 8192, 0) * 100, 2
) AS "% in shared_buffers"
FROM pg_class
WHERE relname LIKE '%hnsw%' OR relname LIKE '%ivfflat%';
-- Se l'indice e <50% in cache, le query saranno molto più lente (I/O bound)
-- Soluzione: aumentare shared_buffers o usare pg_prewarm
-- Query lente recenti che coinvolgono vector search (richiede pg_stat_statements):
SELECT
LEFT(query, 100) AS query_short,
calls,
ROUND(mean_exec_time::numeric, 2) AS mean_ms,
ROUND(max_exec_time::numeric, 2) AS max_ms,
ROUND(total_exec_time::numeric / 1000, 2) AS total_sec
FROM pg_stat_statements
WHERE query ILIKE '%<=%>%' -- query con vector distance operator
ORDER BY mean_exec_time DESC
LIMIT 10;
pg_prewarm: 캐시에 인덱스 로드
-- Estensione pg_prewarm: carica indici in shared_buffers all'avvio
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
-- Carica l'indice HNSW in cache immediatamente
SELECT pg_prewarm('documents_hnsw_idx');
-- Restituisce: numero di blocchi caricati
-- Verifica quanta memoria e stata usata
SELECT
pg_size_pretty(pg_relation_size('documents_hnsw_idx')) AS indice_size,
pg_size_pretty(current_setting('shared_buffers')::bigint) AS shared_buffers,
ROUND(
pg_relation_size('documents_hnsw_idx')::numeric /
current_setting('shared_buffers')::bigint * 100, 1
) AS pct_of_shared_buffers;
-- Configura il prewarming automatico all'avvio di PostgreSQL
-- in postgresql.conf:
-- shared_preload_libraries = 'pg_prewarm'
-- pg_prewarm.autoprewarm = on
-- pg_prewarm.autoprewarm_interval = 300 -- salva stato ogni 5 minuti
-- Questo garantisce che dopo un restart, l'indice venga ricaricato in cache
-- automaticamente usando lo stato salvato prima dello shutdown.
-- Lista degli oggetti prioritari da precaricare:
SELECT pg_prewarm(indexrelid::regclass)
FROM pg_stat_user_indexes
WHERE tablename = 'documents'
AND indexname LIKE '%hnsw%' OR indexname LIKE '%ivfflat%';
인덱스 저하: 증분 삽입 문제
종종 무시되는 중요한 측면: ANN 지수는 시간이 지남에 따라 품질이 저하됩니다. 삽입물로. HNSW는 기존 구조에 새로운 노드를 추가하지만 이러한 노드의 연결은 전체 재구축보다 낮습니다. IVFFlat이 다시 저하됩니다. 새로운 벡터가 기존 클러스터에 할당되기 때문에 더욱 두드러집니다. 업데이트된 데이터 배포에 가장 적합합니다.
열화 측정
-- Monitora la recall nel tempo dopo insert
-- Salva recall periodicamente in una tabella di monitoraggio
CREATE TABLE IF NOT EXISTS index_quality_log (
measured_at TIMESTAMPTZ DEFAULT NOW(),
index_name TEXT,
n_rows BIGINT,
recall_at_10 FLOAT,
p50_ms FLOAT,
p95_ms FLOAT,
pct_cache FLOAT -- % dell'indice in shared_buffers
);
-- Funzione di misurazione automatica
CREATE OR REPLACE FUNCTION measure_index_quality(
p_index_name TEXT,
p_table_name TEXT
) RETURNS void AS $
DECLARE
v_n_rows BIGINT;
v_cache_pct FLOAT;
BEGIN
-- Conta righe correnti
EXECUTE format('SELECT COUNT(*) FROM %I', p_table_name) INTO v_n_rows;
-- Calcola % in cache (approssimazione)
SELECT ROUND(
(SELECT COUNT(*) FROM pg_buffercache
WHERE relfilenode = pg_relation_filenode(p_index_name::regclass))::numeric
/ NULLIF(pg_relation_size(p_index_name::regclass) / 8192, 0) * 100, 2
) INTO v_cache_pct;
-- Inserisci log (recall misurata externamente con set di test)
INSERT INTO index_quality_log (index_name, n_rows, pct_cache)
VALUES (p_index_name, v_n_rows, v_cache_pct);
RAISE NOTICE 'Quality log: index=%, rows=%, cache=%\%', p_index_name, v_n_rows, v_cache_pct;
END;
$ LANGUAGE plpgsql;
-- Chiama periodicamente (es. ogni giorno):
SELECT measure_index_quality('documents_hnsw_idx', 'documents');
-- Query per vedere la degradazione nel tempo
SELECT
measured_at::date AS "Data",
n_rows AS "Righe",
recall_at_10 AS "Recall@10",
p95_ms AS "P95 latency (ms)",
pct_cache AS "% In Cache"
FROM index_quality_log
WHERE index_name = 'documents_hnsw_idx'
ORDER BY measured_at;
-- Soglie di allerta (imposta alert se superate):
-- recall_at_10 < 0.85 -> considera REINDEX urgente
-- recall_at_10 < 0.90 -> pianifica REINDEX entro 1 settimana
-- p95_ms > 100 -> verifica se l'indice e in cache
-- pct_cache < 50% -> aumenta shared_buffers o usa pg_prewarm
REINDEX 동시 실행: 가동 중지 시간 없이 재구축
-- REINDEX CONCURRENTLY ricostruisce l'indice senza bloccare le query in lettura
-- Nota: richiede PostgreSQL 12+ e più tempo del REINDEX normale
-- Durante il rebuild, le query continuano a usare il vecchio indice
-- METODO 1: REINDEX diretto (più semplice, PostgreSQL 12+)
REINDEX INDEX CONCURRENTLY documents_hnsw_idx;
-- Pro: semplice
-- Con: non puoi cambiare parametri durante il rebuild
-- METODO 2: Swap con indice temporaneo (più flessibile)
-- Step 1: Crea un nuovo indice con parametri ottimizzati
CREATE INDEX CONCURRENTLY documents_hnsw_new
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m=16, ef_construction=128); -- ef_construction migliorato!
-- Step 2: Verifica che il nuovo indice sia stato costruito correttamente
SELECT
indexname,
indisvalid AS is_valid,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_indexes
JOIN pg_index ON pg_index.indexrelid = pg_class.oid
JOIN pg_class ON pg_class.relname = pg_indexes.indexname
WHERE tablename = 'documents'
AND indexname IN ('documents_hnsw_idx', 'documents_hnsw_new');
-- Step 3: Swap atomico (solo un breve lock esclusivo)
BEGIN;
DROP INDEX documents_hnsw_idx; -- lock esclusivo brevissimo
ALTER INDEX documents_hnsw_new RENAME TO documents_hnsw_idx;
COMMIT;
-- Quanto spesso fare il rebuild?
-- Dopo >20% di insert/update rispetto alla dimensione originale
-- Se recall < 0.85 (misurata con test set)
-- Dopo cancellazioni massive (>30% delle righe)
-- Schedule raccomandato: ogni settimana per dataset molto dinamici,
-- ogni mese per dataset stabili
-- Automatizza il rebuild con pg_cron (se disponibile):
-- SELECT cron.schedule('weekly-hnsw-rebuild', '0 2 * * 0',
-- 'REINDEX INDEX CONCURRENTLY documents_hnsw_idx');
다중 지수 전략
복잡한 프로덕션에서는 다양한 액세스 패턴에 대해 여러 인덱스가 필요할 수 있습니다. pgVector를 사용하는 PostgreSQL은 동일한 임베딩 열에서 여러 벡터 인덱스를 지원하며 쿼리 플래너는 자동으로 가장 적절한 것을 선택합니다.
-- Strategia 1: Indici parziali per tipo di documento
-- Vantaggi: ogni indice e più piccolo, più veloce, occupa meno RAM
CREATE INDEX idx_hnsw_docs_pdf
ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m=16, ef_construction=64)
WHERE source_type = 'pdf';
CREATE INDEX idx_hnsw_docs_web
ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m=16, ef_construction=64)
WHERE source_type IN ('html', 'md');
-- Query che attivano automaticamente l'indice parziale:
EXPLAIN SELECT id, content
FROM documents
WHERE source_type = 'pdf' -- questa condizione attiva idx_hnsw_docs_pdf
ORDER BY embedding <=> '[...]'::vector
LIMIT 5;
-- Output: Index Scan using idx_hnsw_docs_pdf
-- Strategia 2: Indici per dimensione diversa (Matryoshka embeddings / MRL)
-- text-embedding-3-small supporta 512 e 1536 dimensioni
ALTER TABLE documents ADD COLUMN IF NOT EXISTS embedding_512 vector(512);
ALTER TABLE documents ADD COLUMN IF NOT EXISTS embedding_1536 vector(1536);
CREATE INDEX idx_hnsw_512
ON documents USING hnsw (embedding_512 vector_cosine_ops)
WITH (m=16, ef_construction=64);
CREATE INDEX idx_hnsw_1536
ON documents USING hnsw (embedding_1536 vector_cosine_ops)
WITH (m=32, ef_construction=128); -- più qualità per la versione full
-- Query con la versione appropriata:
-- Ricerca veloce (autocomplete, 3x più veloce, ~95% della qualità):
SELECT id, content, embedding_512 <=> query_512 AS dist
FROM documents
ORDER BY embedding_512 <=> query_512 LIMIT 20;
-- Ricerca precisa (RAG):
SELECT id, content, embedding_1536 <=> query_1536 AS dist
FROM documents
WHERE id IN (
SELECT id FROM documents
ORDER BY embedding_512 <=> query_512 LIMIT 100 -- coarse filter
)
ORDER BY embedding_1536 <=> query_1536 LIMIT 5;
-- Strategia 3: Indice per timestamp (solo documenti recenti)
-- Ottimo per applicazioni news, chat history, documenti freschi
CREATE INDEX idx_hnsw_recent
ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m=16, ef_construction=64)
WHERE created_at > NOW() - INTERVAL '7 days';
-- L'indice si popola automaticamente con i nuovi insert
-- I documenti vecchi cadono fuori condizione automaticamente
-- REINDEX periodico per rimuovere i "dead links" ai documenti scaduti
벡터 워크로드를 위한 PostgreSQL 구성
PostgreSQL의 구성은 인덱스 매개변수 선택만큼 중요합니다. 잘못된 구성은 HNSW의 모든 이점을 무효화할 수 있습니다. 설정은 다음과 같습니다 생산 중인 RAG 시스템에 최적입니다.
# postgresql.conf - Configurazione ottimale per vector search
# Applica dopo aver determinato la quantità di RAM del server
# ========================================
# MEMORIA - La parte più critica
# ========================================
shared_buffers = '8GB' # 25% della RAM totale
# L'indice HNSW DEVE stare qui
# Con 32GB RAM: shared_buffers = 8GB
# Con 64GB RAM: shared_buffers = 16GB
effective_cache_size = '24GB' # 75% della RAM totale
# Stima per il query planner
# NON alloca memoria, solo un suggerimento
work_mem = '64MB' # Per sort e hash operations
# Influenza le query con ORDER BY + LIMIT
# Attenzione: ogni connessione può usarlo più volte
maintenance_work_mem = '2GB' # Per CREATE INDEX (usa MOLTO più del normale)
# Imposta a 25-50% della RAM prima di un rebuild
# Dopo la build, rimette il valore originale
# ========================================
# PARALLELISMO
# ========================================
max_parallel_workers_per_gather = 4 # Worker per singola query
max_parallel_workers = 8 # Worker totali per tutto il sistema
max_parallel_maintenance_workers = 7 # Per CREATE INDEX parallelo (PG16+)
parallel_tuple_cost = 0.1 # Incentiva l'uso del parallelismo
parallel_setup_cost = 100 # Overhead setup per parallelismo
# ========================================
# pgvector SETTINGS
# ========================================
# Questi si impostano a runtime o in postgresql.conf:
hnsw.ef_search = 60 # Default per il sistema (override per sessione)
ivfflat.probes = 10 # Default per il sistema
# ========================================
# WAL (Write-Ahead Log) per INSERT intensivi
# ========================================
wal_buffers = '64MB'
max_wal_size = '4GB'
checkpoint_completion_target = 0.9
wal_compression = on # Riduce I/O WAL (utile per ingestion intensiva)
# ========================================
# AUTOVACUUM - Critico per tabelle vector
# ========================================
autovacuum = on
autovacuum_max_workers = 5
# Le tabelle vector con molti insert/delete necessitano autovacuum aggressivo:
# (imposta per-tabella con ALTER TABLE, non qui)
# ========================================
# MONITORING
# ========================================
log_min_duration_statement = 100 # Loga query più lente di 100ms
track_io_timing = on # Misura I/O time (utile per diagnosi cache miss)
track_activity_query_size = 2048 # Tronca query nel log a 2048 chars
shared_preload_libraries = 'pg_stat_statements,pg_prewarm'
pg_stat_statements.max = 10000 # Traccia le ultime 10K query uniche
병렬 지수 구축: HNSW 구축 가속화
-- PostgreSQL 16+ supporta il parallel index build per HNSW
-- Riduce drasticamente i tempi di build su sistemi multi-core
-- Imposta worker per la build (più worker = build più veloce)
-- max = max_parallel_maintenance_workers
SET max_parallel_maintenance_workers = 7; -- usa 8 CPU totali (1 leader + 7 worker)
-- Imposta maintenance_work_mem alto per la build (più = più veloce)
SET maintenance_work_mem = '4GB';
-- Build con parallelismo:
CREATE INDEX idx_hnsw_parallel
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m=16, ef_construction=64);
-- Monitora il progresso della build (PostgreSQL 12+):
SELECT
phase,
blocks_done,
blocks_total,
ROUND(blocks_done::numeric / NULLIF(blocks_total, 0) * 100, 1) AS "% completato",
tuples_done,
tuples_total,
ROUND(tuples_done::numeric / NULLIF(tuples_total, 0) * 100, 1) AS "% tuple completate"
FROM pg_stat_progress_create_index
WHERE relid = 'documents'::regclass;
-- Esempio output durante la build:
-- phase: "building index"
-- blocks_done: 15234
-- blocks_total: 61000
-- % completato: 25.0
-- tuples_done: 250000
-- tuples_total: 1000000
-- Confronto velocità build (1M vettori, 1536 dim, m=16, ef=64):
-- 1 worker: ~90 minuti
-- 4 worker: ~25 minuti
-- 8 worker: ~15 minuti (rendimento decrescente oltre 8)
-- 16 worker: ~12 minuti (miglioramento minimo)
-- Stima automatica del tempo di build in base ai dati:
WITH stats AS (SELECT COUNT(*) AS n FROM documents)
SELECT
n AS num_vectors,
ROUND(n / 1000000.0, 2) AS millions,
-- Stima con 8 worker, m=16, ef_construction=64
ROUND(n / 1000000.0 * 15, 0) || ' min' AS estimated_build_8workers
FROM stats;
2단계 검색: 효율성과 정확성
속도와 정밀도의 균형을 유지하는 고급 기술 2단계 검색 (성긴 검색이라고도 함): 저차원 임베딩을 사용한 빠른 첫 번째 단계 또는 보다 완화된 매개변수를 사용하는 HNSW 지수를 사용한 후 정확한 순위 재지정이 뒤따릅니다. 최고의 후보자의 제한된 부분 집합.
-- Two-phase retrieval per massima efficienza
-- Phase 1: Fast coarse search with 512-dim embeddings (3x faster)
-- Phase 2: Precise re-ranking with 1536-dim embeddings (only on top-50)
WITH coarse_candidates AS (
-- Phase 1: top-50 candidates with fast 512-dim search
SELECT
id,
embedding_512 <=> %s::vector(512) AS coarse_dist
FROM documents
ORDER BY embedding_512 <=> %s::vector(512)
LIMIT 50
),
precise_ranking AS (
-- Phase 2: re-rank top-50 with precise 1536-dim embeddings
SELECT
d.id,
d.content,
d.source_path,
d.embedding_1536 <=> %s::vector(1536) AS precise_dist,
1 - (d.embedding_1536 <=> %s::vector(1536)) AS similarity
FROM documents d
INNER JOIN coarse_candidates c ON c.id = d.id
ORDER BY d.embedding_1536 <=> %s::vector(1536)
)
SELECT id, content, source_path, similarity
FROM precise_ranking
LIMIT 5;
-- Latenza tipica vs qualità (1M vettori):
-- Direct 1536-dim HNSW (ef_search=40): ~10ms, Recall@5 ~94%
-- Direct 1536-dim HNSW (ef_search=100): ~25ms, Recall@5 ~98%
-- Two-phase (512 coarse + 1536 rerank): ~4ms, Recall@5 ~96%
-- -> 2.5x più veloce con recall ancora migliore!
-- Variante con ef_search ridotto per la fase coarse:
WITH coarse_fast AS (
SELECT id
FROM documents,
LATERAL (SELECT 'SET hnsw.ef_search = 20') AS _ -- ef basso per coarse
ORDER BY embedding <=> %s::vector
LIMIT 100
)
SELECT d.id, d.content, 1 - (d.embedding <=> %s::vector) AS similarity
FROM documents d
JOIN coarse_fast c ON c.id = d.id
ORDER BY d.embedding <=> %s::vector
LIMIT 5;
취소 관리: Vacuum 및 HNSW
-- Le cancellazioni in PostgreSQL sono "soft delete" (tuple marcate dead)
-- L'indice HNSW mantiene riferimenti a queste tuple morte
-- VACUUM rimuove le tuple morte e aggiorna l'indice
-- Verifica tuple morte (dead tuples) - indicatore di necessità VACUUM
SELECT
relname AS "Tabella",
n_live_tup AS "Righe vive",
n_dead_tup AS "Righe morte",
ROUND(n_dead_tup::numeric / NULLIF(n_live_tup, 0) * 100, 2) AS "% morte",
last_vacuum,
last_autovacuum,
-- Stima quante modifiche da ultimo analyze
n_mod_since_analyze AS "Modifiche da analyze"
FROM pg_stat_user_tables
WHERE relname = 'documents';
-- Se "% morte" > 10-20%, e il momento di fare VACUUM
VACUUM ANALYZE documents; -- vacuum + aggiorna statistiche
-- VACUUM FULL: ricostruisce la tabella (blocca le scritture, libera più spazio)
-- Usa solo in finestre di manutenzione programmate:
VACUUM FULL documents;
-- Configurazione autovacuum aggressivo per tabelle vector
-- (molti update/delete tipici di pipeline RAG con aggiornamenti frequenti):
ALTER TABLE documents SET (
autovacuum_vacuum_scale_factor = 0.01, -- vacuum dopo 1% di righe modificate (default 20%)
autovacuum_analyze_scale_factor = 0.005, -- analyze dopo 0.5% (default 10%)
autovacuum_vacuum_cost_delay = 2, -- più aggressivo (default 20ms)
autovacuum_vacuum_threshold = 50 -- almeno 50 righe modificate (default 50)
);
-- Verifica che autovacuum stia girando:
SELECT
schemaname,
relname,
last_autovacuum,
last_autoanalyze,
autovacuum_count,
autoanalyze_count
FROM pg_stat_user_tables
WHERE relname = 'documents';
EXPLAIN ANALYZE를 사용한 쿼리 최적화
벡터 검색 쿼리가 다음과 같은지 확인하려면 정기적으로 EXPLAIN ANALYZE를 사용하는 것이 중요합니다. 인덱스를 올바르게 사용하고 성능 문제를 진단합니다.
-- Analisi completa di una query vector search
EXPLAIN (ANALYZE, BUFFERS, TIMING, FORMAT TEXT)
SELECT id, content, embedding <=> '[0.1, 0.2, ...]'::vector(1536) AS dist
FROM documents
WHERE source_type = 'pdf'
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector(1536)
LIMIT 10;
-- Output desiderato (usa l'indice HNSW):
-- Index Scan using idx_hnsw_docs_pdf on documents (cost=0.00..8.54 rows=10)
-- Index Cond: (embedding <=> '[...]'::vector <=> '[...]'::vector)
-- Filter: (source_type = 'pdf')
-- Buffers: shared hit=247 <-- tutto da cache!
-- -> Planning Time: 0.3 ms
-- -> Execution Time: 8.7 ms
-- Output indesiderato (brute force - da evitare):
-- Seq Scan on documents (cost=0.00..1234.56 rows=10)
-- Filter: (source_type = 'pdf')
-- Sort Key: (embedding <=> '...'::vector)
-- Buffers: shared hit=1234 read=5678 <-- molti read da disco!
-- -> Execution Time: 3450 ms
-- Se vedi Seq Scan invece di Index Scan, verifica:
-- 1. L'indice esiste?
SELECT indexname FROM pg_indexes WHERE tablename = 'documents';
-- 2. Il LIMIT e abbastanza piccolo?
-- PostgreSQL usa l'indice solo per LIMIT piccoli
-- 3. Le statistiche sono aggiornate?
ANALYZE documents;
-- 4. enable_indexscan e attivo?
SHOW enable_indexscan; -- deve essere 'on'
-- 5. ef_search e appropriato?
SHOW hnsw.ef_search;
벡터 인덱스 생산 체크리스트
- 메모리 크기: 확인해보세요
shared_buffersHNSW 인덱스를 수용할 수 있을 만큼 충분히 큽니다. 인덱스가 캐시되지 않으면 쿼리 속도가 10~100배 느려집니다. - Maintenance_work_mem: 인덱스를 생성하기 전에 1~4GB로 설정하세요. 빌드 후에는 정상 값으로 줄일 수 있습니다.
- 병렬 빌드: 미국
max_parallel_maintenance_workers=7멀티 코어 시스템에서 빠른 구축을 위해. 다운타임 시간을 절약하세요. - 프로덕션의 ef_search: 기본값인 40을 사용하지 마세요. 데이터세트의 재현율을 측정하고 적절한 값(일반적으로 RAG 기업의 경우 60-100)을 설정하세요.
- 리콜 모니터링: 매주 회상 테스트를 수행합니다. 0.85 미만으로 떨어지면 긴급 REINDEX를 예약하세요.
- 적극적인 autovacuum: 삽입/삭제 횟수가 많은 테이블의 경우 더 낮은 값을 사용하세요.
autovacuum_vacuum_scale_factor0.01-0.05로. - pg_prewarm: PostgreSQL을 다시 시작할 때마다 인덱스가 캐시되도록 자동 사전 준비를 활성화합니다.
- 설명 분석: 쿼리가 HNSW 인덱스를 사용하는지 정기적으로 확인하고 실수로 순차 스캔을 수행하지 마십시오.
일반적인 실수와 이를 피하는 방법
| 실수 | 징후 | 해결책 |
|---|---|---|
| shared_buffers가 너무 작습니다. | 느린 쿼리(>500ms), EXPLAIN의 디스크 읽기 비율이 높음 | RAM이 25%로 증가합니다. pg_prewarm 사용 |
| 프로덕션의 ef_search 기본값(40) | Recall@10 ~92%, 부정확한 RAG 응답 | RAG 기업의 경우 60-100으로 설정 |
| 프로브가 있는 IVFFlat=1(기본값) | Recall@10 ~50-60%, 완전히 잘못된 결과 | 리콜 대상에 따라 프로브=10-50으로 설정합니다. |
| 여러 번 삽입한 후 REINDEX가 발생하지 않음 | 기억력은 시간이 지남에 따라 점진적으로 저하됩니다. | 매주/매월 REINDEX를 동시에 예약 |
| 인덱스 스캔 대신 순차 스캔 | 인덱스가 없는 매우 느린 벡터 쿼리 | 분석 테이블; LIMIT 및 WHERE 절을 확인하세요. |
| Maintenance_work_mem이 너무 낮습니다. | HNSW 빌드가 매우 느림(몇 시간/일) | CREATE INDEX 이전에 SET Maintenance_work_mem = '2GB' |
결론 및 다음 단계
벡터 인덱싱은 구체적인 측정 등이 필요한 분야입니다. 직관. 최적의 매개변수는 특정 데이터 세트, 요구 사항에 따라 다릅니다. 대기 시간 및 사용 가능한 메모리. 황금률: 먼저 측정하고 최적화하세요 그 이후에는 항상 모니터링을 하세요.
2026년에는 "Just Use Postgres" 추세가 점점 더 통합되면서 구성 방법을 알게 되었습니다. HNSW 및 IVFFlat 지수를 올바르게 사용하면 경쟁력 있는 성과를 얻을 수 있습니다. Pinecone 또는 Qdrant와 같은 특수 벡터 데이터베이스를 사용하여 단순하게 유지 단일 PostgreSQL 인프라의 최근 벤치마크에서는 pgVector가 최대 28x인 것으로 나타났습니다. 적절한 구성을 통해 Pinecone보다 16배 더 저렴한 비용으로 더 빠릅니다.
시리즈의 마지막 기사에서는 이 모든 것을 프로덕션에 적용하는 마지막 과제를 다룹니다. 규모를 조정합니다. 대규모 데이터세트를 위한 파티셔닝, PgBouncer를 통한 연결 풀링, 읽기 전용 복제본 벡터 검색, Redis를 사용한 쿼리 캐싱 및 다중 테넌트 아키텍처 전용 이를 통해 PostgreSQL은 하루에 수백만 개의 벡터 쿼리를 처리할 수 있습니다.
시리즈는 계속됩니다
- 이전의: PostgreSQL의 고급 유사성 검색
- 다음: 프로덕션 중인 RAG: 확장 가능한 아키텍처
- 관련된: MLOps: 프로덕션 인프라
- 관련된: AI 엔지니어링: RAG 파이프라인 고급







