dataiotech Labs
dbaguidedbaguideDBAdataxtoolsdataopsphere
postgresql Referans Mimarisi
⚡ Performance25 min
RPO: 0 (Sync Standby ile) / < 1s (Async DR)
RTO: < 15 saniye (Patroni Otomatik Failover)

PostgreSQL pgvector HNSW: Production RAG Rehberi

pgvector HNSW'yi production'da güvenle kurun: maintenance_work_mem, WAL, Patroni replikasyonu, kilitler, hybrid search ve teşhis sorguları.

Bileşenler:PostgreSQL 17pgvector 0.8.xPatroni HAetcd v3halfvec(1536)HNSW Indexpg_prewarmpg_buffercache
#postgresql#pgvector#hnsw#rag#vector-search#replication#patroni#performance-tuning
Mimari Topoloji & Akış Şeması
PostgreSQL 17 + pgvector HNSW + Patroni Production RAG Referans Mimari Şeması (Raft Quorum, DCS, TOAST, Sync/Async WAL Stream)

📌 PostgreSQL 17 + pgvector HNSW + Patroni Production RAG Referans Mimari Şeması (Raft Quorum, DCS, TOAST, Sync/Async WAL Stream)

Kapsamlı Üretim & Mühendislik Rehberi

1. Giriş ve Mimari Genel Bakış

pgvector, embedding'leri ayrı bir vektör veritabanına (Pinecone, Qdrant, Milvus) kopyalamak yerine operasyonel verinin yanında, aynı transaction sınırları içinde tutmanızı sağlar. Çift yazma, iki sistem arasında tutarlılık reconciliation'ı ve dağıtık transaction maliyeti ortadan kalkar. Bedeli ise şudur: vektör indeksi artık PostgreSQL'in buffer manager'ı, WAL hattı, checkpointer'ı, vacuum'u ve fiziksel replikasyonu ile aynı kaynakları paylaşır. Vektör araması "ayrı bir sistem" olmaktan çıkar ve veritabanınızın I/O ve bellek bütçesinin en ağır tüketicilerinden biri hâline gelir.

Bu makalede HNSW indeksinin yaşam döngüsünü dört eksende ele alıyoruz:

  • •İnşa (build): Graf maintenance_work_mem içinde kurulur. Sığmazsa pgvector doğrudan disk üzerindeki indekse ekleme moduna geçer; inşa süresi ve üretilen WAL hacmi dramatik biçimde artar.
  • •Sorgu (search): Graf gezintisi entry-point'ten başlar ve her adımda komşu listeleri okunur. Erişim deseni random I/O'dur; indeks shared_buffers ve OS page cache'e sığmıyorsa latency NVMe random read IOPS'una bağlanır.
  • •Yazma (DML): HOT olmayan her UPDATE ve her INSERT, HNSW grafına tam bir ekleme (ef_construction derinliğinde arama + komşu listesi güncellemesi) anlamına gelir. Bu işlemler buffer content LWLock'ları üzerinde yarışır.
  • •Replikasyon: İndeksin her baytı WAL olarak standby'lara akar. Senkron replikasyonda bu, primary'deki tüm commit'lerin latency'sine doğrudan yansır.
Mimari Akış & Topoloji Şeması (Mermaid)
Mermaid şeması çiziliyor...

2. Senaryo ve Production Ortamı

Ortam:

BileşenDeğer
Donanım32 vCPU, 256 GB RAM, NVMe RAID10
YazılımPostgreSQL 17, pgvector 0.8.x, Patroni (1 primary, 1 sync standby, 1 async DR standby)
VeriÇok kiracılı (multi-tenant) RAG platformu, ~450 tenant, 12 milyon chunk
Embeddingtext-embedding-3-small, 1536 boyut, halfvec(1536) olarak saklanıyor
DağılımEn büyük 5 tenant verinin %60'ını tutuyor; yüzlerce tenant'ın birkaç bin chunk'ı var

Olay: Ekip, yeni HNSW indeksini gece bakım penceresinde düz CREATE INDEX ile ve varsayılan maintenance_work_mem = 64MB ile başlattı. Sonuç:

  1. 1Graf belleğe sığmadığı için pgvector ilk birkaç on bin tuple'dan sonra on-disk inşa moduna düştü. İnşa 9 saat sürdü.
  2. 2CREATE INDEX tablo üzerinde ShareLock tuttuğu için ingest pipeline'ın tüm INSERT'leri bloke oldu; kuyruk sabaha kadar birikti.
  3. 3On-disk modda her ekleme ayrı WAL kaydı ürettiği için yaklaşık 70 GB WAL oluştu. Sync standby yüzünden primary'deki tüm commit'lerin p99 latency'si 4 ms'den 180 ms'e çıktı. Async DR standby 40 dakika geride kaldı.
  4. 4İndeks hazır olduktan sonra küçük tenant'lar LIMIT 5 istediği hâlde 0–2 sonuç almaya başladı.
  5. 5Retriever her erişimde hit_count ve last_accessed_at alanlarını aynı tabloda güncelliyordu. Bir hafta içinde indeks %35 şişti, autovacuum HNSW indeksinde saatlerce çalıştı.

Aşağıdaki adımlar bu tablonun her maddesini sistematik olarak ortadan kaldırmak için tasarlanmıştır.

3. Adım Adım Uygulama ve Konfigürasyon

Mimari Akış & Topoloji Şeması (Mermaid)
Mermaid şeması çiziliyor...

▸3.1. postgresql.conf Parametreleri

256 GB RAM'li senaryo sunucusu için başlangıç değerleri:

ini
# --- Bellek --- shared_buffers = 64GB # RAM'in %25'i; huge_pages ile birlikte huge_pages = on # 64GB shared_buffers için TLB baskısını azaltır effective_cache_size = 180GB # shared_buffers + beklenen OS page cache work_mem = 32MB # hybrid search sıralamaları için; bağlantı sayısıyla çarpılır maintenance_work_mem = 2GB # global varsayılan; HNSW build'de OTURUM bazında yükseltilir # --- Paralellik --- max_worker_processes = 32 max_parallel_workers = 24 max_parallel_maintenance_workers = 8 # HNSW parallel build (pgvector 0.6.0+) max_parallel_workers_per_gather = 4 # metadata filtreli exact taramalar için # --- I/O --- random_page_cost = 1.1 # NVMe; graf gezintisi random I/O'dur effective_io_concurrency = 200 # --- WAL ve Checkpoint --- wal_compression = zstd # PG15+; full-page image hacmini ciddi düşürür max_wal_size = 64GB # build sırasında WAL kaynaklı checkpoint fırtınasını önler min_wal_size = 8GB checkpoint_timeout = 15min checkpoint_completion_target = 0.9 # --- Standby tarafı --- hot_standby_feedback = on # standby'daki uzun okuma sorgularının iptalini azaltır max_standby_streaming_delay = 30s # --- Eklentiler --- shared_preload_libraries = 'pg_stat_statements,pg_prewarm' pg_prewarm.autoprewarm = on # restart/failover sonrası HNSW sayfalarını geri yükler
ParametreVarsayılanProduction ÖnerisiBoyutlandırma Mantığı
shared_buffers128MBRAM'in %25–%40'ıSıcak HNSW indeksi + aktif heap, shared_buffers ile OS page cache toplamına sığmalıdır.
maintenance_work_mem64MBGlobal 1–2 GB, build oturumunda tahmini graf boyutu kadarSabit bir tavan formülü yerine örneklemden ölçülen indeks boyutuna göre ayarlanır (bkz. 3.3).
hnsw.ef_search4040–200, iş yüküne göreRecall/latency ödünleşimi. LIMIT değeri ef_search'ü aşarsa indeks en fazla ef_search kadar aday döndürür.
max_parallel_maintenance_workers2çekirdek sayısı − 2, en fazla 8Leader process de çalıştığı için toplam süreç sayısı bir fazladır. max_parallel_workers ile sınırlanır.
max_parallel_workers_per_gather24HNSW taraması paralel değildir; bu parametre filtreli exact scan ve hybrid sorgulardaki seq/bitmap taramayı hızlandırır.
effective_cache_size4GBRAM'in %50–%75'iPlanner'ın indeks yolunu doğru maliyetlemesi için. Bellek ayırmaz, yalnızca maliyet modelini etkiler.
wal_compressionoffzstd veya lz4HNSW sayfalarında full-page image oranı yüksektir; sıkıştırma replikasyon ağını rahatlatır.

Patroni altında parametreler postgresql.conf dosyasına elle değil, DCS üzerinden uygulanır:

bash
patronictl -c /etc/patroni/patroni.yml edit-config \ -p 'shared_buffers=64GB' \ -p 'huge_pages=on' \ -p 'effective_cache_size=180GB' \ -p 'maintenance_work_mem=2GB' \ -p 'max_parallel_maintenance_workers=8' \ -p 'wal_compression=zstd' \ -p 'max_wal_size=64GB' \ -p "shared_preload_libraries='pg_stat_statements,pg_prewarm'" \ --force # shared_buffers, huge_pages ve shared_preload_libraries restart gerektirir patronictl -c /etc/patroni/patroni.yml list patronictl -c /etc/patroni/patroni.yml restart rag-cluster --pending --force

hnsw.ef_search gibi eklenti parametreleri veritabanı veya rol seviyesinde tanımlanmalıdır:

sql
ALTER DATABASE ragdb SET hnsw.ef_search = 100; ALTER ROLE rag_batch_reader SET hnsw.ef_search = 200; -- offline değerlendirme işleri

▸3.2. Şema Tasarımı: Sık Güncellenen Alanları Ayırın

pgvector paketini tüm Patroni node'larına kurduktan sonra eklentiyi primary üzerinde oluşturun. Standby'da paket eksikse WAL replay devam eder, ancak failover sonrası tüm vektör sorguları kütüphane bulunamadığı için hata verir.

bash
# Her node üzerinde (örnek: PGDG paketleri, Debian/Ubuntu) sudo apt-get install -y postgresql-17-pgvector
sql
CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS pg_buffercache; CREATE EXTENSION IF NOT EXISTS pg_prewarm; SELECT extname, extversion FROM pg_extension WHERE extname IN ('vector','pg_buffercache','pg_prewarm');
sql
-- Ana tablo: embedding ve nadiren değişen içerik CREATE TABLE documents_rag ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, tenant_id integer NOT NULL, document_id uuid NOT NULL, chunk_no integer NOT NULL, chunk_text text NOT NULL, metadata jsonb, embedding halfvec(1536) NOT NULL, -- 2 x 1536 + 8 = 3080 bayt/değer embedding_model text NOT NULL DEFAULT 'text-embedding-3-small', created_at timestamptz NOT NULL DEFAULT now() ) WITH (fillfactor = 90); -- Sık güncellenen sayaçlar AYRI tabloda: HNSW indeksine asla dokunmaz CREATE TABLE documents_rag_stats ( chunk_id bigint PRIMARY KEY REFERENCES documents_rag(id) ON DELETE CASCADE, hit_count bigint NOT NULL DEFAULT 0, last_accessed_at timestamptz ) WITH (fillfactor = 70); -- HOT update için sayfa içi boşluk CREATE INDEX CONCURRENTLY idx_documents_rag_tenant_doc ON documents_rag (tenant_id, document_id); -- Agresif autovacuum: dead tuple'lar HNSW taramasında aday slotlarını boşa harcar ALTER TABLE documents_rag SET ( autovacuum_vacuum_scale_factor = 0.02, autovacuum_analyze_scale_factor = 0.01, autovacuum_vacuum_cost_limit = 2000 ); ALTER TABLE documents_rag_stats SET (autovacuum_vacuum_scale_factor = 0.01);

halfvec(1536) değeri yaklaşık 3 KB'tır ve TOAST eşiğini (yaklaşık 2 KB) aşar; yani satırın embedding kısmı genellikle out-of-line TOAST'ta yaşar, heap tuple'ı küçük kalır. Bu, indekslenmeyen sütun güncellemelerinin HOT olabilmesi için iyi bir zemindir. Storage stratejisini doğrulayın; float verisi zayıf sıkıştığı için e (EXTERNAL, sıkıştırmasız) beklenir:

sql
SELECT attname, atttypid::regtype, attstorage FROM pg_attribute WHERE attrelid = 'documents_rag'::regclass AND attname = 'embedding';
Boyut sınırı: HNSW, vector tipi için en fazla 2.000, halfvec için en fazla 4.000 boyutu indeksleyebilir. 3072 boyutlu modeller (text-embedding-3-large gibi) için halfvec bir optimizasyon değil, zorunluluktur.

▸3.3. İndeks Boyutunu Örneklem Üzerinden Tahmin Edin

maintenance_work_mem için sabit bir formül (ör. "RAM'in %25'i, en fazla 16 GB") HNSW'de yanıltıcıdır. Belirleyici olan, grafın bellekteki boyutudur ve bu yaklaşık olarak indeksin disk boyutuna eşittir. Kaba hesap:

satır sayısı × (2 × boyut + 8 + 12 × m + ~100 bayt sayfa/tuple overhead)

12 M satır, 1536 boyut, m = 24 için bu yaklaşık 12 M × 3.5 KB ≈ 42 GB eder; sayfa doluluk kaybıyla pratikte 45–50 GB görülür. Tahmini ölçümle doğrulayın. Örneklemi UNLOGGED oluşturmak WAL üretimini önler:

sql
CREATE UNLOGGED TABLE rag_sample AS SELECT * FROM documents_rag TABLESAMPLE SYSTEM (1); SET maintenance_work_mem = '4GB'; SET max_parallel_maintenance_workers = 4; CREATE INDEX rag_sample_hnsw ON rag_sample USING hnsw (embedding halfvec_cosine_ops) WITH (m = 24, ef_construction = 128); SELECT (SELECT count(*) FROM rag_sample) AS sample_rows, pg_size_pretty(pg_relation_size('rag_sample_hnsw')) AS sample_index_size, pg_size_pretty( pg_relation_size('rag_sample_hnsw') * (SELECT reltuples FROM pg_class WHERE relname = 'documents_rag') / (SELECT count(*) FROM rag_sample) ) AS projected_full_size; DROP TABLE rag_sample;

m ve ef_construction değerleri hakkında: m = 24 (varsayılan 16) yüksek boyutlu embedding'lerde recall'u iyileştirir ve komşu listesi başına ek ~100 bayt maliyet getirir; 3 KB'lık vektörün yanında bu ihmal edilebilir. ef_construction = 128 (varsayılan 64) graf kalitesini artırır, inşa süresini yaklaşık lineer olarak uzatır.

▸3.4. Pre-flight Kontroller

bash
PGDATA=$(psql -Atc "SHOW data_directory") # pg_wal için en az tahmini indeks boyutunun 1.5 katı boş alan df -h "$PGDATA/pg_wal" du -sh "$PGDATA/pg_wal"
sql
-- Replikaların mevcut durumu: build'e lag ile başlamayın SELECT application_name, state, sync_state, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_gap, replay_lag FROM pg_stat_replication; -- CREATE INDEX CONCURRENTLY, eski snapshot tutan TÜM transaction'ları bekler SELECT pid, usename, application_name, state, now() - xact_start AS xact_age, backend_xmin, left(query, 80) AS query FROM pg_stat_activity WHERE backend_xmin IS NOT NULL AND pid <> pg_backend_pid() ORDER BY xact_start NULLS LAST LIMIT 10; -- Unutulmuş replication slot'lar WAL'ı tutar ve disk dolmasına yol açar SELECT slot_name, active, wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal FROM pg_replication_slots;

▸3.5. İndeksi CONCURRENTLY ile İnşa Edin

CREATE INDEX CONCURRENTLY transaction bloğu içinde çalışamaz ve bağlantı koparsa geride INVALID bir indeks bırakır. Bu yüzden tmux / screen içinde, ON_ERROR_STOP ile ve izlenebilir bir application_name vererek çalıştırın. Komutu Patroni lideri üzerinde çalıştırdığınızdan emin olun:

bash
patronictl -c /etc/patroni/patroni.yml list # Leader node'u doğrula tmux new -s hnsw_build PGAPPNAME=hnsw_build psql -d ragdb -v ON_ERROR_STOP=1 <<'SQL' \timing on SET maintenance_work_mem = '48GB'; -- örneklemden projekte edilen boyut + pay SET max_parallel_maintenance_workers = 7; -- 7 worker + 1 leader SET statement_timeout = 0; SELECT pg_current_wal_lsn() AS wal_start \gset CREATE INDEX CONCURRENTLY idx_documents_rag_embedding_hnsw ON documents_rag USING hnsw (embedding halfvec_cosine_ops) WITH (m = 24, ef_construction = 128); SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), :'wal_start')) AS wal_generated, pg_size_pretty(pg_relation_size('idx_documents_rag_embedding_hnsw')) AS index_size; SQL

Graf bellek bütçesini aşarsa pgvector aşağıdaki gibi bir NOTICE üretir. Bu mesajı görürseniz build'i iptal etmeyi ve daha yüksek bellekle yeniden başlatmayı ciddi biçimde değerlendirin; on-disk moddan sonraki ilerleme hem çok daha yavaştır hem de çok daha fazla WAL üretir:

text
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 4183519 tuples DETAIL: Building will take significantly more time. HINT: Increase maintenance_work_mem to speed up builds.
Not: Notlarda sıkça geçen *"graf pgsql_tmp altına geçici dosyaya dökülür"* ifadesi pgvector için doğru değildir. HNSW inşası taşma durumunda temp file kullanmaz; doğrudan indeks relation'ına sayfa sayfa eklemeye geçer. temp_bytes metriği bu durumu göstermez, NOTICE ve ilerleme hızındaki çöküş gösterir.
Container ortamları: Parallel build, grafı dynamic shared memory üzerinde kurar. Docker'da varsayılan 64 MB /dev/shm, büyük build'lerde *"could not resize shared memory segment"* hatasına yol açar. --shm-size veya Kubernetes'te emptyDir.medium: Memory ile yeterli alan tanımlayın.

▸3.6. Başarısız Build Temizliği

sql
SELECT indexrelid::regclass AS index_name, indisvalid, indisready, indislive FROM pg_index WHERE indrelid = 'documents_rag'::regclass AND NOT indisvalid; -- INVALID indeks DML'de yine güncellenir: yazma maliyeti öder, sorguda kullanılmaz DROP INDEX CONCURRENTLY IF EXISTS idx_documents_rag_embedding_hnsw;

▸3.7. Cache Warm-up

sql
-- Primary üzerinde SELECT pg_prewarm('idx_documents_rag_embedding_hnsw');
bash
# Okuma trafiği alan her standby'da (pg_prewarm read-only çalışır) for host in pg-standby-1 pg-standby-2; do psql -h "$host" -d ragdb -Atc "SELECT pg_prewarm('idx_documents_rag_embedding_hnsw');" done

▸3.8. Sorgu Tuning: ef_search ve Iterative Index Scan

Temel RAG sorgusu; ef_search transaction seviyesinde belirlenir:

sql
BEGIN; SET LOCAL hnsw.ef_search = 100; EXPLAIN (ANALYZE, BUFFERS) SELECT id, chunk_text, 1 - (embedding <=> '[0.012,-0.023,...]'::halfvec(1536)) AS similarity FROM documents_rag ORDER BY embedding <=> '[0.012,-0.023,...]'::halfvec(1536) LIMIT 5; COMMIT;

Metadata filtresi eklendiğinde HNSW önce ef_search kadar aday bulur, filtre sonra uygulanır. Verinin %1'ini tutan bir tenant için ef_search = 40 ile beklenen sonuç sayısı 0.4'tür. Senaryodaki *"küçük tenant 0–2 sonuç alıyor"* şikayetinin kaynağı budur. pgvector 0.8.0 ile gelen iterative index scan, yeterli sonuç bulunana kadar taramayı genişletir:

sql
BEGIN; SET LOCAL hnsw.ef_search = 100; SET LOCAL hnsw.iterative_scan = strict_order; -- sonuçlar mesafeye göre kesin sıralı SET LOCAL hnsw.max_scan_tuples = 50000; -- varsayılan 20000; tarama üst sınırı SELECT id, chunk_text FROM documents_rag WHERE tenant_id = 42 ORDER BY embedding <=> '[0.012,-0.023,...]'::halfvec(1536) LIMIT 5; COMMIT;

relaxed_order daha az tuple tarar ama sıralamayı hafifçe bozabilir; sonuçları materialized CTE ile yeniden sıralayın:

sql
BEGIN; SET LOCAL hnsw.iterative_scan = relaxed_order; WITH ann AS MATERIALIZED ( SELECT id, chunk_text, embedding <=> '[0.012,-0.023,...]'::halfvec(1536) AS distance FROM documents_rag WHERE tenant_id = 42 ORDER BY distance LIMIT 20 ) SELECT id, chunk_text, distance FROM ann ORDER BY distance LIMIT 5; COMMIT;

Çarpık tenant dağılımında kalıcı çözüm; büyük tenant'lar için partial HNSW indeksi, küçük tenant'lar için B-tree ile exact taramadır. Birkaç bin satırlık bir tenant'ta exact scan hem %100 recall verir hem de milisaniyeler içinde biter:

sql
-- Verinin büyük kısmını tutan tenant için ayrı graf CREATE INDEX CONCURRENTLY idx_rag_hnsw_tenant_42 ON documents_rag USING hnsw (embedding halfvec_cosine_ops) WITH (m = 24, ef_construction = 128) WHERE tenant_id = 42;
Prepared statement tuzağı: Partial indeksin kullanılabilmesi için planner'ın WHERE tenant_id = 42 koşulunu plan zamanında kanıtlaması gerekir. Driver'ınız generic plan'a geçtiğinde (tenant_id = $1) partial indeks devre dışı kalır. Bu sorgular için oturumda plan_cache_mode = force_custom_plan ayarlayın.

4. İzleme ve Teşhis (Diagnostic Queries)

▸4.1. İndeks İnşa İlerlemesi

sql
SELECT p.pid, a.application_name, p.phase, p.tuples_done, p.tuples_total, round(100.0 * p.tuples_done / nullif(p.tuples_total, 0), 1) AS pct_done, now() - a.query_start AS elapsed, a.wait_event_type, a.wait_event FROM pg_stat_progress_create_index p JOIN pg_stat_activity a USING (pid);

phase değeri *waiting for old snapshots* gibi bir bekleme aşamasında takılı kalıyorsa, sorun pgvector değil, bir önceki pre-flight sorgusunun gösterdiği uzun transaction'dır. tuples_done artış hızı birden düşüyorsa graf maintenance_work_mem sınırını aşmıştır.

▸4.2. CIC'i Bloke Eden Oturumlar

sql
SELECT a.pid, pg_blocking_pids(a.pid) AS blocked_by, a.wait_event_type, a.wait_event, left(a.query, 60) AS query FROM pg_stat_activity a WHERE a.application_name = 'hnsw_build' OR a.pid = ANY ( SELECT unnest(pg_blocking_pids(pid)) FROM pg_stat_activity WHERE application_name = 'hnsw_build' );

▸4.3. İndeks Boyutu ve Kullanımı

sql
SELECT s.indexrelname AS index_name, pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size, s.idx_scan, s.idx_tup_read, s.idx_tup_fetch, s.last_idx_scan -- PG16+ FROM pg_stat_user_indexes s WHERE s.relname = 'documents_rag' ORDER BY pg_relation_size(s.indexrelid) DESC;

▸4.4. İndeksin shared_buffers İçindeki Payı

sql
SELECT c.relname, count(*) AS buffers, pg_size_pretty(count(*) * current_setting('block_size')::bigint) AS cached, pg_size_pretty(pg_relation_size(c.oid)) AS total, round(100.0 * count(*) * current_setting('block_size')::bigint / nullif(pg_relation_size(c.oid), 0), 2) AS pct_cached, round(avg(b.usagecount), 2) AS avg_usagecount FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid) AND b.reldatabase = (SELECT oid FROM pg_database WHERE datname = current_database()) WHERE c.relname IN ('idx_documents_rag_embedding_hnsw', 'documents_rag') GROUP BY c.relname, c.oid;

reldatabase filtresi kritiktir: aynı relfilenode değeri farklı veritabanlarında tekrar edebilir ve filtresiz join yanlış oranlar üretir. pct_cached %90'ın altındaysa ve latency dalgalıysa OS page cache'i vmtouch ile kontrol edin.

▸4.5. İndeks Seviyesinde Cache Hit Oranı

sql
SELECT indexrelname, idx_blks_hit, idx_blks_read, round(100.0 * idx_blks_hit / nullif(idx_blks_hit + idx_blks_read, 0), 2) AS hit_pct FROM pg_statio_user_indexes WHERE relname = 'documents_rag';

▸4.6. HOT Oranı ve Dead Tuple Birikimi

sql
SELECT relname, n_tup_upd, n_tup_hot_upd, round(100.0 * n_tup_hot_upd / nullif(n_tup_upd, 0), 1) AS hot_pct, n_tup_newpage_upd, -- PG16+ n_live_tup, n_dead_tup, last_autovacuum, autovacuum_count FROM pg_stat_user_tables WHERE relname IN ('documents_rag', 'documents_rag_stats');

documents_rag üzerinde n_tup_upd hızla artıyorsa ve hot_pct düşükse, her güncelleme HNSW grafına yeni bir düğüm ekliyor demektir. documents_rag_stats üzerinde ise hot_pct değerinin %90'ın üzerinde olması beklenir.

▸4.7. WAL Üretimi ve Replikasyon Gecikmesi

sql
-- Primary: replika başına gecikme SELECT application_name, sync_state, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)) AS send_gap, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS replay_gap, write_lag, flush_lag, replay_lag FROM pg_stat_replication; -- Kümülatif WAL istatistikleri: wal_fpi yüksekse full-page image baskın SELECT wal_records, wal_fpi, pg_size_pretty(wal_bytes) AS wal_bytes, stats_reset FROM pg_stat_wal;
sql
-- Standby üzerinde: recovery conflict kaynaklı sorgu iptalleri SELECT datname, confl_snapshot, confl_lock, confl_bufferpin, confl_deadlock FROM pg_stat_database_conflicts WHERE datname = 'ragdb';

▸4.8. LWLock Yarışması (Paralel Ingest)

sql
SELECT wait_event_type, wait_event, count(*) AS sessions FROM pg_stat_activity WHERE state = 'active' AND backend_type = 'client backend' GROUP BY 1, 2 ORDER BY 3 DESC \watch 1

Yoğun paralel INSERT sırasında LWLock / BufferContent baskınsa, writer'lar grafın aynı sayfaları (özellikle entry-point çevresi) üzerinde yarışıyordur. Ingest paralelliğini düşürmek ve satırları tek tek değil COPY veya çok satırlı INSERT ile göndermek daha yüksek throughput verir.

▸4.9. Yavaş Vektör Sorguları

sql
SELECT calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(max_exec_time::numeric, 2) AS max_ms, shared_blks_hit, shared_blks_read, round(100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0), 2) AS hit_pct, left(query, 100) AS query FROM pg_stat_statements WHERE query ILIKE '%<=>%' OR query ILIKE '%<->%' OR query ILIKE '%<#>%' ORDER BY mean_exec_time DESC LIMIT 10;

▸4.10. Recall Ölçümü

Recall'u tahmin etmek yerine ölçün. Aşağıdaki psql bloğu exact (seq scan) sonucu ile HNSW sonucunu karşılaştırır, temp table kullanmadığı için read-only standby'da da çalışır:

sql
SELECT embedding::text AS qv FROM documents_rag TABLESAMPLE SYSTEM (0.01) LIMIT 1 \gset SET enable_indexscan = off; SELECT array_agg(id) AS exact_ids FROM (SELECT id FROM documents_rag ORDER BY embedding <=> :'qv'::halfvec(1536) LIMIT 10) s \gset RESET enable_indexscan; SET hnsw.ef_search = 100; SELECT cardinality(ARRAY( SELECT unnest(:'exact_ids'::bigint[]) INTERSECT SELECT id FROM (SELECT id FROM documents_rag ORDER BY embedding <=> :'qv'::halfvec(1536) LIMIT 10) ann )) / 10.0 AS recall_at_10;

▸4.11. Linux Seviyesinde Bellek ve I/O

bash
# 1. NVMe kullanım oranı, kuyruk derinliği ve okuma latency'si (r_await) iostat -xz 1 nvme0n1 # 2. PostgreSQL süreçlerinin major page fault oranı (majflt/s = diskten okuma) pidstat -r -p "$(pgrep -d',' -u postgres postgres)" 2 5 # 3. HNSW indeksinin OS page cache'teki payı PGDATA=$(psql -d ragdb -Atc "SHOW data_directory") REL=$(psql -d ragdb -Atc "SELECT pg_relation_filepath('idx_documents_rag_embedding_hnsw')") vmtouch -v "$PGDATA/$REL" "$PGDATA/$REL".[0-9]* 2>/dev/null | tail -n 5 # 4. Build sırasında pg_wal büyümesi watch -n 5 "du -sh $PGDATA/pg_wal"

5. Production Best Practices ve Gotchas

▸5.1. Ne Zaman Yapılmalı, Ne Zaman Kaçınılmalı

DurumÖneri
İlk toplu yükleme (bulk ingest)Önce veriyi COPY ile yükleyin, indeksi en sonda kurun. Dolu tabloda tek seferlik build, satır satır graf eklemeden çok daha hızlıdır.
Tenant başına birkaç bin satırHNSW gereksiz; (tenant_id) B-tree + exact scan hem daha doğru hem yeterince hızlıdır.
Çarpık tenant dağılımıBüyük tenant'lar için partial HNSW, kalanlar için exact scan veya tenant grubuna göre list partitioning.
Embedding modeli değişimiYerinde UPDATE embedding = ... yapmayın. Yeni sütun + yeni indeks + dual-write + okuma geçişi + eski sütunu düşürme. Yerinde güncelleme tüm tabloyu non-HOT yeniden yazar, grafı baştan ekler ve indeks boyutu kadar ek WAL ve bloat üretir.
IVFFlat mi HNSW miIVFFlat daha hızlı kurulur ve daha az bellek ister, ancak lists merkezleri build anındaki veriye göre belirlenir; dağılım kaydıkça recall düşer ve periyodik rebuild gerekir. Sürekli büyüyen RAG korpusunda HNSW daha öngörülebilirdir.
Çalışma kümesi RAM'e sığmıyorTek node'da düşük latency hedefi gerçekçi değildir. halfvec, binary quantization ile ön eleme + yeniden sıralama veya partitioning değerlendirin.
Yoğun trafik saatleriCONCURRENTLY DML'i bloke etmez, ancak CPU, I/O ve WAL yükü yine production'a yansır. Build'i düşük trafik penceresine planlayın.

▸5.2. Kilit Mekanizması

İşlemTablo KilidiDML'i Bloke Eder mi?Dikkat Edilecek Nokta
CREATE INDEXShareLockEvet, tüm INSERT/UPDATE/DELETESELECT çalışır. HNSW build saatler sürebileceği için production'da kullanılmamalıdır.
CREATE INDEX CONCURRENTLYShareUpdateExclusiveLock (tüm süre boyunca)HayırVACUUM, ANALYZE, başka bir CIC ve çoğu ALTER TABLE ile çakışır. Fazlar arasında eski snapshot'ları bekler.
REINDEX INDEX CONCURRENTLYShareUpdateExclusiveLockHayırGeçici olarak iki indeks birlikte bulunur; disk ve WAL ihtiyacı iki katına çıkar.
DROP INDEX CONCURRENTLYShareUpdateExclusiveLockHayırTransaction bloğu içinde çalışmaz.
VACUUM (HNSW dahil)ShareUpdateExclusiveLockHayırHNSW vacuum, silinen düğümlerin komşularını yeniden bağladığı için yavaştır; çok yüksek dead tuple oranında REINDEX CONCURRENTLY daha hızlı olabilir.
ALTER TABLE ... ADD COLUMN embedding halfvec(1536)AccessExclusiveLock (kısa)Evet, kısa süreliDefault'suz nullable sütun anlıktır. Mutlaka SET lock_timeout = '3s' ile ve retry döngüsüyle çalıştırın.
INSERT / non-HOT UPDATERowExclusiveLockHayırGraf sayfalarında BufferContent LWLock yarışması; yüksek paralellikte CPU spin artar.

▸5.3. Replikasyon Üzerindeki Etkiler

  • •WAL hacmi ve senkron commit latency'si: Graf maintenance_work_mem içine sığdığında pgvector indeks sayfalarını build sonunda toplu olarak WAL'a yazar; WAL hacmi kabaca indeks boyutu kadardır. Sığmadığında her düğüm eklemesi ayrı WAL kaydı üretir ve toplam hacim katlanır. Senkron standby varken bu WAL dalgası boyunca primary'deki tüm commit'ler standby'ın flush'ını bekler.
  • •Standby'da sorgu iptalleri: Standby sorgularını iptal eden mekanizma recovery conflict'tir ve max_standby_streaming_delay ile yönetilir. hot_standby_feedback = on iptalleri azaltır, ancak standby'daki uzun sorgular primary'de vacuum'u geciktirerek bloat'a yol açar.
  • •Failover sonrası soğuk cache: Standby, WAL replay sırasında yalnızca değişen sayfaları belleğe alır. Okuma trafiği almayan bir sync standby promote edildiğinde HNSW indeksi büyük ölçüde soğuktur. pg_prewarm.autoprewarm = on restart'ları kapsar; failover için standby'ları periyodik olarak pg_prewarm ile ısıtmak gerekir.
  • •Eklenti sürümleri: pgvector binary'si ve sürümü tüm node'larda aynı olmalıdır. ALTER EXTENSION vector UPDATE primary'de çalıştırıldığında katalog değişikliği replikalara akar; yeni sürümün paketleri standby'larda kurulu değilse failover sonrası sorgular kırılır.

▸5.4. HOT İhlali, Bloat ve Recall

Bir UPDATE ancak hiçbir indekslenmiş sütunu değiştirmiyorsa ve aynı heap sayfasında yeterli boş alan varsa HOT olur. HOT olmayan her güncellemede yeni tuple'ın ctid'si tüm indekslere eklenir; HNSW için bu, ef_construction derinliğinde bir graf araması ve komşu listelerinin yeniden yazılması demektir.

Eski tuple, vacuum temizleyene kadar grafta kalır. HNSW taraması bu dead düğümleri aday olarak bulur ve heap visibility kontrolünde eler; ef_search bütçesinin bir kısmı ölü düğümlere harcanır ve sorgu LIMIT değerinden az sonuç döndürebilir. Bu nedenle:

  • •Sık güncellenen alanları 3.2'deki gibi ayrı tabloya taşıyın.
  • •Ana tabloda fillfactor = 90 ile HOT için sayfa içi alan bırakın.
  • •Dead tuple oranı yüksekken VACUUM yerine REINDEX INDEX CONCURRENTLY ile grafı sıfırdan kurmayı değerlendirin.

▸5.5. Kısa Kontrol Listesi

  • pgvector paketi tüm Patroni node'larında aynı sürümde kurulu.
  • Sık güncellenen alanlar embedding tablosundan ayrılmış.
  • İndeks boyutu örneklemden ölçülmüş; maintenance_work_mem buna göre oturum bazında ayarlanmış.
  • pg_wal alanı, replikasyon lag'i, replication slot'lar ve uzun transaction'lar build öncesi kontrol edilmiş.
  • Build CONCURRENTLY, tmux içinde ve izlenebilir application_name ile çalıştırılmış.
  • Build sonrası indisvalid doğrulanmış, indeks primary ve standby'larda prewarm edilmiş.
  • ef_search değeri ölçülmüş recall ve latency tablosuna göre seçilmiş; filtreli sorgularda iterative scan açık.
  • Partial indeks kullanan sorgularda generic plan riski plan_cache_mode ile kapatılmış.
Küme Sağlık Kontrolü & CLI Teşhis Komutları
-- HNSW İndeks Durumu ve Boyutu
SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE relname = 'documents_rag';

-- Buffer Cache Yerleşimi
SELECT c.relname, count(*) AS buffers, pg_size_pretty(count(*) * 8192) AS cached, round(100.0 * count(*) * 8192 / nullif(pg_relation_size(c.oid), 0), 2) AS pct_cached FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c.oid) AND b.reldatabase = (SELECT oid FROM pg_database WHERE datname = current_database()) WHERE c.relname IN ('idx_documents_rag_embedding_hnsw', 'documents_rag') GROUP BY c.relname, c.oid;
Topoloji Yapılandırması
3-Node Patroni Cluster (1 Primary + 1 Sync Standby + 1 Async DR) + pgvector 0.8.x + halfvec(1536)
Avantajlar
  • •Ayrı vektör veritabanı (Pinecone/Milvus) maliyeti ve dual-write karmaşasını ortadan kaldırır
  • •Operasyonel veriler ve vektör embeddingleri aynı transaction ve ACID sınırlarında kalır
  • •pgvector 0.8.x iterative scan ve halfvec ile bellek ve CPU verimliliği sağlar
Dikkat Edilmesi Gerekenler
  • •HNSW graf inşası yüksek maintenance_work_mem ve WAL bant genişliği tüketir
  • •Senkron replikasyonda yoğun WAL akışı commit gecikmelerini (latency) artırabilir
  • •HOT update ihlallerinde HNSW grafı şişebilir ve ef_search aday kalitesi düşebilir
Canlıya Çıkış Kontrol Listesi
  • pgvector paketi tüm Patroni node'larında aynı sürümde kurulu olmalıdır.
  • Sık güncellenen sayaç alanları (hit_count, last_accessed_at) embedding tablosundan ayrılmalıdır.
  • İndeks boyutu örneklemden ölçülmeli ve maintenance_work_mem buna göre oturum bazında ayarlanmalıdır.
  • pg_wal alanı, replikasyon lag'i, replication slot'lar ve uzun transaction'lar build öncesi kontrol edilmelidir.
  • Build CONCURRENTLY, tmux içinde ve izlenebilir application_name ile çalıştırılmalıdır.
  • Build sonrası indisvalid doğrulanmalı, indeks primary ve standby'larda prewarm edilmelidir.
  • ef_search değeri ölçülmüş recall ve latency tablosuna göre seçilmeli; filtreli sorgularda iterative scan açık olmalıdır.
  • Partial indeks kullanan sorgularda generic plan riski plan_cache_mode ile kapatılmalıdır.