Implementasi Retrieval-Augmented Generation (RAG) pada beban kerja produksi sering mengalami degradasi performa ketika volume data embedding melonjak. Query vector cosine similarity yang awalnya selesai dalam 5 milidetik dapat membengkak hingga ratusan milidetik. Masalah utama umumnya berakar pada konfigurasi default Hierarchical Navigable Small World (HNSW) pada ekstensi pgvector, memory pressure, atau PostgreSQL query planner yang beralih ke sequential scan.

Akar Masalah: Mengapa Query Cosine Similarity Melambat?

Penyebab latensi tinggi pada query similarity vector di PostgreSQL meliputi tiga faktor utama:

  • Sequential Scan Fallback: Query planner memutuskan membaca seluruh tabel jika biaya pembacaan index dianggap lebih mahal daripada scan sekuensial, sering kali karena parameter biaya default atau index HNSW terfragmentasi di disk.
  • Ukuran Vektor Dimensi Tinggi: Model embedding populer (misal dimensi 1536) membutuhkan sekitar 6 KB data mentah per row vektor ditambah pointer graph HNSW. Graph ini membutuhkan alokasi RAM yang cukup di shared_buffers agar tidak memicu I/O disk konstan.
  • Parameter Default Tidak Sesuai: Nilai default m dan ef_construction ditujukan untuk penggunaan umum, bukan untuk topologi dataset berdimensi ribuan yang membutuhkan navigasi graph lebih rapat guna mencapai recall akurat tanpa melompat antar node secara acak.

Optimasi Build Time: Parameter m, ef_construction, dan maintenance_work_mem

HNSW membangun graph multi-layer di mana setiap layer berisi node dan edge. Kualitas graph ditentukan saat pembuatan index melalui dua parameter:

  • m: Jumlah maksimum koneksi dua arah (bidirectional links) per node (default: 16). Untuk embedding dimensi tinggi (1024-1536), nilai m = 24 hingga m = 32 memberikan akurasi navigasi lebih baik dengan konsekuensi konsumsi RAM lebih besar.
  • ef_construction: Ukuran dynamic candidate list saat index dibangun (default: 64). Meningkatkan nilai ini ke 128 atau 256 menghasilkan graph yang lebih terhubung dan meningkatkan recall rate, namun memperpanjang waktu build.

Sebelum mengeksekusi CREATE INDEX, naikkan maintenance_work_mem. Jika parameter ini terlalu kecil, PostgreSQL memecah proses build ke disk temporary file yang memperlambat indexing dan menghasilkan struktur graph suboptimal.

-- Naikkan memori khusus untuk proses pembuatan index di sesi ini
SET maintenance_work_mem = '2GB';

-- Buat index HNSW untuk cosine distance (<=>)
CREATE INDEX idx_documents_embedding_hnsw 
ON documents 
USING hnsw (embedding vector_cosine_ops) 
WITH (m = 24, ef_construction = 128);

Runtime Tuning: Mengatur hnsw.ef_search

Saat query dieksekusi, parameter hnsw.ef_search menentukan kedalaman pencarian pada dynamic candidate list (default: 40). Parameter ini dapat diubah langsung di tingkat sesi tanpa perlu membangun ulang index.

-- Cek nilai ef_search saat ini
SHOW hnsw.ef_search;

-- Set ef_search per transaksi atau per sesi
SET hnsw.ef_search = 100;

SELECT id, content, 1 - (embedding <=> '[0.012, -0.023, ...]'::vector) AS similarity
FROM documents
ORDER BY embedding <=> '[0.012, -0.023, ...]'::vector
LIMIT 5;

Aturan tuning hnsw.ef_search:

  • Nilai Rendah (20-40): Latensi sangat rendah (< 10ms), throughput tinggi, namun recall rate bisa turun di bawah 90%. Cocok untuk klasifikasi cepat atau initial retrieval layer.
  • Nilai Tinggi (100-200): Recall rate mendekati 98-99%, namun latensi query meningkat secara linear seiring bertambahnya node yang diperiksa dalam graph.

Analisis EXPLAIN ANALYZE: Sebelum vs Sesudah Optimasi

Gunakan EXPLAIN (ANALYZE, BUFFERS) untuk memverifikasi index digunakan dan mengukur performa I/O memori.

Sebelum Optimasi (Sequential Scan Fallback)

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM documents
ORDER BY embedding <=> '[...]'::vector LIMIT 5;

-- Output:
Limit  (cost=14230.00..14230.01 rows=5 width=12) (actual time=248.512..248.514 rows=5 loops=1)
  Buffers: shared hit=421 read=12450
  ->  Sort  (cost=14230.00..14480.00 rows=100000 width=12) (actual time=248.510..248.511 rows=5 loops=1)
        Sort Key: ((embedding <=> '[...]'::vector))
        ->  Seq Scan on documents  (cost=0.00..12850.00 rows=100000 width=12) (actual time=0.082..215.110 rows=100000 loops=1)
              Buffers: shared hit=421 read=12450
Planning Time: 0.125 ms
Execution Time: 248.580 ms

Hasil di atas menunjukkan PostgreSQL memindai seluruh 100.000 row via disk (read=12450 blocks), menghasilkan latensi 248 ms.

Sesudah Optimasi (HNSW Index Scan)

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM documents
ORDER BY embedding <=> '[...]'::vector LIMIT 5;

-- Output:
Limit  (cost=34.50..42.10 rows=5 width=12) (actual time=4.120..4.128 rows=5 loops=1)
  Buffers: shared hit=86
  ->  Index Scan using idx_documents_embedding_hnsw on documents  (cost=34.50..1845.20 rows=100000 width=12) (actual time=4.118..4.125 rows=5 loops=1)
        Order By: (embedding <=> '[...]'::vector)
        Buffers: shared hit=86
Planning Time: 0.150 ms
Execution Time: 4.185 ms

Penggunaan index HNSW berhasil memangkas waktu eksekusi dari 248 ms menjadi 4.1 ms dengan buffer hit murni di memori tanpa I/O disk.

Trade-Off: Recall Rate vs Latensi Query

HNSW adalah algoritma approximate nearest neighbor (ANN), bukan exact search. Artinya, akurasi ditukar dengan kecepatan eksekusi.

  • Recall Rate: Persentase hasil ANN yang identik dengan hasil Exact k-NN (flat scan). Pada dimensi 1536, kombinasi m = 24, ef_construction = 128, dan ef_search = 100 umumnya mencapai recall > 95%.
  • Memory Footprint: Setiap peningkatan nilai m menaikkan ukuran index di disk dan RAM sekitar 20-40%. Pastikan parameter shared_buffers PostgreSQL mencakup seluruh ukuran index HNSW agar performa tidak anjlok akibat disk swapping.
  • Write/Insert Throughput: Semakin tinggi m dan ef_construction, semakin lambat operasi INSERT baru karena graph harus dievaluasi dan dimodifikasi ulang pada setiap penambahan vektor.