Alfiansa
October 11, 2026

Membangun RAG 2026 dari Nol: Tutorial Praktis Search Engine AI dengan pgvector + PostgreSQL

RAG
AI Generative
Machine learning
Membangun RAG 2026 dari Nol: Tutorial Praktis Search Engine AI dengan pgvector + PostgreSQL

Tutorial Praktis Search Engine AI dengan pgvector + PostgreSQL

Search di aplikasi Anda masih pakai LIKE '%keyword%'? Di 2026, user sudah berharap search yang "mengerti" maksud pertanyaan — bukan sekadar cocok kata. Tutorial ini membimbing Anda membangun Retrieval-Augmented Generation (RAG) end-to-end: dari database PostgreSQL biasa, embedding modern, sampai chatbot yang menjawab dari dokumen internal Anda sendiri.


Kenapa RAG, Kenapa Sekarang, Kenapa pgvector

Setiap minggu ada berita LLM baru yang lebih besar, tapi pola yang paling bertahan di produksi sejak 2023 sampai hari ini tetap sama: RAG. Konsepnya sederhana — jangan memaksa model menghafal semua pengetahuan, melainkan berikan dokumen relevan sebagai konteks saat menjawab. Hasilnya: jawaban lebih akurat, bisa dikutip sumbernya, dan data internal Anda tidak perlu masuk ke proses training.

Yang berubah di 2026 adalah infrastrukturnya. Tiga pergeseran besar:

  1. pgvector sudah matang sebagai standar. pgvector 0.8.2 (rilis Februari 2026, rilis yang menambal CVE-2026-3172 — buffer overflow saat parallel HNSW build) kini adalah ekstensi vektor paling mapan untuk PostgreSQL. Ia mendukung HNSW dan IVFFlat, tipe halfvec yang memangkas ukuran penyimpanan hingga 50%, dan binary quantization untuk dataset raksasa. Artinya: Anda tidak perlu layanan vector DB terpisah untuk memulai — database yang sudah Anda pakai bisa jadi vector database.

  2. Model embedding open source sudah setara API berbayar. Model seperti nomic-embed-text-v1.5 (Apache 2.0, 768 dimensi, bisa dipangkas ke 256 dimensi via Matryoshka) atau bge-m3 (multibahasa, 1024 dimensi, konteks 8k token) berjalan nyaman di CPU dan gratis selamanya. Tidak perlu lagi membayar per-token embedding ke API.

  3. Hybrid retrieval jadi pola default. Riset-riset terbaru 2026 sepakat: kombinasi pencarian semantik (dense) + pencarian kata kunci (BM25/FTS) + reranker memberi kualitas terbaik, bukan dense saja. PostgreSQL justru ideal untuk ini karena full-text search dan vector search hidup dalam satu query.

Target pembaca tutorial ini: developer backend yang sudah nyaman dengan PostgreSQL dan Python/Node.js, dan ingin RAG yang bisa di-deploy ke VPS sendiri tanpa tagihan API yang membengkak. Kita akan membangun search engine AI untuk dokumentasi internal — contohnya basis pengetahuan produk — lengkap dengan kode yang bisa dijalankan.

Arsitektur yang Akan Kita Bangun

Dokumen (PDF/MD/TXT)
   │  1. Chunking (pecah jadi potongan 300–500 kata)
   ▼
Model Embedding (nomic-embed-text-v1.5 / bge-m3)
   │  2. Embed → vektor 768/1024 dimensi
   ▼
PostgreSQL + pgvector (tabel documents, index HNSW)
   │  3. Simpan chunk + embedding
   ▼
Query user → embed query → similarity search (top-20)
   │  4. Hybrid: gabung dengan full-text search
   ▼
Rerank top-20 → top-5
   │  5. Susun konteks
   ▼
LLM (API atau lokal) → jawaban + sitasi

Total biaya infrastruktur untuk tutorial ini: nol rupiah selain VPS/database yang sudah Anda miliki.


Bagian 1: Menyiapkan PostgreSQL + pgvector

Instalasi

Jika Anda memakai Docker (cara tercepat untuk eksperimen), image resmi pgvector/pgvector sudah menyertakan ekstensi ini:

docker run -d --name rag-db \
  -e POSTGRES_PASSWORD=rahasia \
  -e POSTGRES_DB=ragdb \
  -p 5432:5432 \
  pgvector/pgvector:pg18

Versi pgvector terbaru yang stabil di awal 2026 adalah 0.8.2 (rilis 26 Februari 2026), dan image di atas sudah menyertakannya. Untuk produksi di VPS sendiri, Anda bisa mengompilasi dari source — prosesnya hanya make && make install setelah menginstal header PostgreSQL.

Aktifkan ekstensi di database Anda:

CREATE EXTENSION IF NOT EXISTS vector;
SELECT extversion FROM pg_extension WHERE extname = 'vector';
-- harusnya: 0.8.2

Skema Tabel: Jangan Cuma Simpan Vektor

Kesalahan pemula paling umum: membuat tabel yang hanya berisi id dan embedding. Padahal kekuatan pgvector justru karena vektor hidup berdampingan dengan data relasional biasa — metadata, tanggal, filter kategori, semuanya bisa dipakai dalam satu query.

CREATE TABLE documents (
    id          BIGSERIAL PRIMARY KEY,
    source      TEXT NOT NULL,          -- nama file / URL asal
    title       TEXT NOT NULL,
    chunk_index INT  NOT NULL,          -- urutan chunk dalam dokumen
    content     TEXT NOT NULL,          -- teks chunk (untuk ditampilkan & FTS)
    embedding   vector(768),            -- sesuaikan dimensi model Anda
    token_count INT,
    created_at  TIMESTAMPTZ DEFAULT now(),
    -- cegah duplikat ingest
    UNIQUE (source, chunk_index)
);

-- Full-text search butuh kolom tsvector
ALTER TABLE documents
  ADD COLUMN content_tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('indonesian', content)) STORED;

CREATE INDEX documents_tsv_idx ON documents USING GIN (content_tsv);

Perhatikan kolom content_tsv: kita memakai dictionary indonesian bawaan PostgreSQL agar stemming Bahasa Indonesia ikut bekerja ("mencari" cocok dengan "pencarian"). Untuk Bahasa Inggris campur, Anda bisa membuat kolom kedua dengan 'english', atau memakai dua-duanya dalam query.

Memilih Index: HNSW vs IVFFlat di 2026

pgvector menyediakan dua index ANN (Approximate Nearest Neighbor):

HNSWIVFFlat
Latensi queryRendah, konsistenNaik seiring jumlah probe
Waktu buildLambatCepat
MemoriLebih boros (~2–3×)Hemat
Recall defaultTinggi & stabilTergantung tuning probe
Cocok untukQuery-heavy, data < 10 jutaData besar, budget memori ketat

Aturan praktis 2026: mulai dengan HNSW. Kecuali dataset Anda di atas beberapa juta vektor atau RAM sangat terbatas, HNSW memberi pengalaman terbaik dengan tuning minimal.

-- m = 16 default sudah bagus; ef_construction 64–200 untuk kualitas build
CREATE INDEX documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

-- set ef_search per sesi untuk tradeoff recall vs kecepatan
SET hnsw.ef_search = 100;  -- default 40; naikkan jika recall kurang

Satu catatan penting dari rilis 0.8.0: pgvector kini mendukung iterative index scan (hnsw.iterative_scan), yang membuat planner PostgreSQL jauh lebih pintar memilih index saat query Anda menggabungkan filter (WHERE kategori = 'x') dengan pencarian vektor. Di versi lama, filter + vector search sering jatuh ke sequential scan yang lambat; sekarang tidak lagi.

Trik Hemat: halfvec dan Binary Quantization

Embedding 768 dimensi dalam float32 memakan 768 × 4 byte ≈ 3 KB per chunk. Satu juta chunk = ~3 GB hanya untuk vektor. Dua fitur pgvector memangkas ini:

-- halfvec: presisi setengah, ukuran setengah, recall nyaris sama
ALTER TABLE documents ALTER COLUMN embedding TYPE halfvec(768);

-- binary_quantize: 1 bit per dimensi — 32× lebih kecil, untuk pre-filter skala besar
-- (dipakai saat query, bukan tipe kolom)
SELECT * FROM documents
ORDER BY binary_quantize(embedding)::bit(768) <~> binary_quantize('[...]'::vector)::bit(768)
LIMIT 100;

Pola produksi yang umum: binary quantization untuk candidate generation cepat di ratusan juta vektor, lalu re-rank 100 kandidat teratas dengan vektor presisi penuh. Untuk tutorial ini (puluhan ribu chunk), halfvec + HNSW sudah lebih dari cukup.


Bagian 2: Ingest — Dari Dokumen Mentah ke Vektor

Pipeline ingest menentukan 70% kualitas RAG Anda. Model LLM secanggih apa pun tidak bisa menjawab dari chunk yang terpotong di tengah kalimat atau tercemar header/footer PDF.

Langkah 1: Chunking yang Benar

Jangan pecah teks per jumlah karakter buta. Pecah per batas semantik: paragraf, lalu gabungkan sampai mendekati target ukuran. Target yang terbukti bagus: 300–500 kata per chunk dengan overlap 10–15%.

import re

def semantic_chunks(text: str, target_words: int = 400, overlap_words: int = 50):
    paragraphs = [p.strip() for p in re.split(r'\n\s*\n', text) if p.strip()]
    chunks, current, current_len = [], [], 0
    for p in paragraphs:
        words = p.split()
        if current_len + len(words) > target_words and current:
            chunks.append(" ".join(current))
            # overlap: bawa sebagian akhir chunk sebelumnya
            carry = " ".join(" ".join(current).split()[-overlap_words:])
            current, current_len = [carry, p], len(carry.split()) + len(words)
        else:
            current.append(p)
            current_len += len(words)
    if current:
        chunks.append(" ".join(current))
    return chunks

Kenapa overlap penting? Karena pertanyaan user bisa merujuk ke kalimat yang kebetulan jatuh di batas chunk. Overlap 50 kata memastikan kalimat jembatan itu muncul di dua chunk sekaligus.

Aturan chunking untuk Bahasa Indonesia: pastikan teks sudah dibersihkan dari artefak umum — nomor halaman PDF, header/footer berulang, dan tabel yang pecah. Untuk dokumen marketing/produk berbahasa Indonesia campur Inggris (umum di startup), biarkan campurannya apa adanya; model embedding multibahasa menanganinya dengan baik.

Langkah 2: Memilih Model Embedding (Update 2026)

Jangan pakai all-MiniLM-L6-v2 lagi sebagai default — model 2021 itu tertinggal 5–15 poin nDCG dari model kecil modern di benchmark teknis 2026. Berikut peta pilihan per Oktober 2026:

ModelDimensiLisensiKonteksKapan dipakai
nomic-embed-text-v1.5768 (bisa 256)Apache 2.08kDefault terbaik: ringan, CPU-friendly, Matryoshka
snowflake-arctic-embed-m-v1.5768Apache 2.0512Alternatif sekelas nomic
bge-m31024MIT8kMultibahasa 100+ bahasa, hybrid dense+sparse
bge-small-en-v1.5384MIT512Budget dimensi ketat
Qwen3-Embedding-0.6B32–1024 fleksibelApache 2.032kButuh konteks sangat panjang
EmbeddingGemma-300M768 (bisa 128)terbuka2kOn-device / RAM < 200MB

Untuk tutorial ini kita pakai nomic-embed-text-v1.5: 137M parameter, berjalan cepat di CPU via sentence-transformers, dan dukungannya luas. Jika korpus Anda dominan Bahasa Indonesia, bge-m3 adalah alternatif kuat karena keunggulan multibahasanya — tinggal ganti dimensi kolom ke 1024.

Instalasi dan pemakaian:

pip install sentence-transformers psycopg[binary] pgvector
from sentence_transformers import SentenceTransformer

# unduh sekali (~500MB), setelah itu offline
model = SentenceTransformer("nomic-ai/nomic-embed-text-v1.5", trust_remote_code=True)

# PENTING: model ini butuh prefix tugas yang berbeda untuk query vs dokumen.
# Salah prefix = kualitas anjlok diam-diam. Bungkus dalam fungsi agar konsisten.
def embed_documents(texts: list[str]):
    return model.encode(["search_document: " + t for t in texts],
                        normalize_embeddings=True)

def embed_query(text: str):
    return model.encode(["search_query: " + text],
                        normalize_embeddings=True)[0]

normalize_embeddings=True penting: dengan vektor ternormalisasi, cosine similarity setara dengan dot product — dan operator <=> pgvector memang menghitung cosine distance. Konsisten dari hulu ke hilir.

Langkah 3: Bulk Insert yang Cepat

Jangan insert satu per satu. Batch 500–1000 chunk per transaksi, dan buat index HNSW setelah data masuk (atau set maintenance_work_mem besar) karena build HNSW di tabel kosong lalu insert massal justru lambat.

import psycopg
from pgvector.psycopg import register_vector

conn = psycopg.connect("dbname=ragdb user=postgres password=rahasia host=localhost")
register_vector(conn)

def ingest(source: str, title: str, text: str, batch: int = 512):
    chunks = semantic_chunks(text)
    vectors = embed_documents(chunks)
    rows = [
        (source, title, i, chunk, vec.tolist(), len(chunk.split()))
        for i, (chunk, vec) in enumerate(zip(chunks, vectors))
    ]
    with conn.cursor() as cur:
        cur.executemany(
            """INSERT INTO documents (source, title, chunk_index, content, embedding, token_count)
               VALUES (%s,%s,%s,%s,%s,%s)
               ON CONFLICT (source, chunk_index) DO NOTHING""",
            rows,
        )
    conn.commit()
    print(f"{source}: {len(rows)} chunk tersimpan")

ingest("panduan-produk.md", "Panduan Produk v2.3", open("panduan-produk.md").read())

Untuk 10.000 chunk di laptop biasa, proses ini memakan waktu beberapa menit — hampir semuanya dihabiskan untuk embedding di CPU, bukan di database. Jika ingin lebih cepat, jalankan embedding di GPU atau gunakan layanan embedding API hanya untuk tahap ingest (query tetap lokal — pola hybrid yang sah).


Bagian 3: Retrieval — Jantung RAG

Di sinilah RAG menang atau kalah. Retrieval yang buruk = LLM mengarang jawaban (halusinasi) karena konteksnya tidak relevan.

Level 1: Vector Search Murni

def vector_search(query: str, k: int = 20):
    q = embed_query(query)
    with conn.cursor() as cur:
        cur.execute(
            """SELECT id, title, chunk_index, content,
                      1 - (embedding <=> %s::vector) AS similarity
               FROM documents
               ORDER BY embedding <=> %s::vector
               LIMIT %s""",
            (q.tolist(), q.tolist(), k),
        )
        return cur.fetchall()

Operator <=> adalah cosine distance; 1 - distance memberi skor similarity 0–1 yang enak dibaca. Coba dengan pertanyaan Bahasa Indonesia:

hasil = vector_search("bagaimana cara refund langganan tahunan?")
for r in hasil[:3]:
    print(f"{r[1]} #{r[2]} — skor {r[4]:.3f}")
    print(r[3][:200], "\n")

Perhatikan: query "refund langganan tahunan" akan cocok dengan chunk berisi "pengembalian dana paket annual" meski tidak ada kata yang sama persis. Itulah semantic search — dan alasan LIKE tidak akan pernah menandinginya.

Level 2: Hybrid Search (Dense + BM25)

Kelemahan murni vektor: ia buruk untuk istilah eksak — nomor SKU, nama produk, kode error, singkatan internal. Di sinilah full-text search PostgreSQL menutup lubang.

-- Reciprocal Rank Fusion: gabungkan peringkat dense & keyword secara adil
WITH dense AS (
    SELECT id, ROW_NUMBER() OVER (
        ORDER BY embedding <=> '[...]'::vector) AS rnk, 1 - (embedding <=> '[...]'::vector) AS sim
    FROM documents
),
keyword AS (
    SELECT id, ROW_NUMBER() OVER (
        ORDER BY ts_rank(content_tsv, plainto_tsquery('indonesian', 'refund langganan')) DESC) AS rnk
    FROM documents
    WHERE content_tsv @@ plainto_tsquery('indonesian', 'refund langganan')
),
fused AS (
    SELECT COALESCE(d.id, k.id) AS id,
           (COALESCE(1.0/(60 + d.rnk), 0) + COALESCE(1.0/(60 + k.rnk), 0)) AS rrf_score,
           d.sim AS dense_sim
    FROM dense d FULL OUTER JOIN keyword k ON d.id = k.id
)
SELECT doc.id, doc.title, doc.chunk_index, doc.content, f.rrf_score, f.dense_sim
FROM fused f JOIN documents doc ON doc.id = f.id
ORDER BY f.rrf_score DESC
LIMIT 20;

Rumus 1/(60 + rank) adalah Reciprocal Rank Fusion — teknik standar menggabungkan dua daftar peringkat tanpa perlu menormalisasi skor yang skalanya beda. Konstanta 60 adalah konvensi dari paper aslinya; tidak perlu diubah.

Pola ini — RRF atas dense + BM25 — adalah baseline yang dipakai mayoritas RAG produksi 2026 sebelum masuk ke reranker. PostgreSQL mengeksekusinya dalam satu query, tanpa service tambahan.

Level 3: Reranking — Upgrade Kualitas Termurah

Retrieval mengambil 20 kandidat dengan cepat; reranker menilai ulang 20 kandidat itu dengan model cross-encoder yang lebih teliti (ia membaca query + dokumen bersamaan, bukan via vektor). Ini adalah upgrade tunggal dengan dampak terbesar pada kualitas RAG menurut riset 2026.

pip install FlagEmbedding  # atau gunakan API Cohere rerank / Jina reranker
from FlagEmbedding import FlagReranker

reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)

def hybrid_search(query: str, k: int = 5):
    candidates = vector_search_rrf(query, k=20)   # dari query SQL di atas
    pairs = [[query, c["content"]] for c in candidates]
    scores = reranker.compute_score(pairs, normalize=True)
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [c for c, s in ranked[:k]]

Aturan praktis: retrieve 20–50 kandidat, rerank jadi 5–8 untuk konteks LLM. Reranker cross-encoder memang lebih lambat (~50–200ms untuk 20 pasangan di CPU), tapi jauh lebih murah daripada menaikkan k konteks LLM — token konteks LLM-lah yang mahal.

Kapan Retrieval Dianggap "Cukup Bagus"?

Jangan menebak — ukur. Buat 30–50 pasang (pertanyaan, chunk jawaban yang benar) dari dokumen Anda sendiri, lalu hitung recall@5: berapa persen pertanyaan yang chunk jawabannya muncul di 5 teratas. Target realistis untuk RAG internal: recall@5 ≥ 85%. Di bawah itu, perbaiki chunking dan embedding dulu sebelum menyalahkan LLM.


Bagian 4: Generation — Merangkai Jawaban + Sitasi

Dengan 5 chunk terbaik di tangan, tugas LLM tinggal satu: menjawab hanya dari konteks yang diberi, dan menandai sumbernya.

Prompt yang Mencegah Halusinasi

SYSTEM_PROMPT = """Kamu adalah asisten helpdesk produk. Jawab HANYA berdasarkan KONTEKS di bawah.
- Jika jawaban tidak ada di konteks, katakan: "Saya tidak menemukan informasi itu di dokumentasi."
- JANGAN mengarang nomor telepon, harga, tanggal, atau langkah yang tidak tertulis.
- Akhiri setiap klaim faktual dengan sitasi [n] sesuai nomor dokumen.
- Jawab dalam Bahasa Indonesia yang ringkas."""

def build_prompt(query: str, chunks: list[dict]) -> str:
    context = "\n\n".join(
        f"[{i+1}] {c['title']} (bagian {c['chunk_index']}):\n{c['content']}"
        for i, c in enumerate(chunks)
    )
    return f"{SYSTEM_PROMPT}\n\nKONTEKS:\n{context}\n\nPERTANYAAN: {query}\nJAWABAN:"

Dua detail yang sering diremehkan:

  1. Instruksi "katakan tidak tahu" harus eksplisit. Tanpa kalimat itu, hampir semua LLM — lokal maupun API — lebih memilih mengarang jawaban yang terdengar meyakinkan daripada mengaku tidak tahu.
  2. Sitasi bernomor memaksa grounding. Saat model harus menulis [3] di akhir klaim, ia cenderung memeriksa ulang apakah klaim itu memang ada di dokumen 3. Ini pola yang dipakai Perplexity dan kawan-kawan.

Contoh Output

Q: Bagaimana cara refund langganan tahunan?

A: Anda bisa mengajukan refund langganan tahunan maksimal 14 hari setelah pembayaran melalui halaman Pengaturan → Langganan → Minta Refund [1]. Dana dikembalikan ke metode pembayaran asal dalam 5–14 hari kerja [2]. Langganan tahunan yang sudah melewati 14 hari tidak dapat di-refund, tetapi bisa di-downgrade ke bulanan [1].

Sumber: [1] Panduan Produk v2.3 (bagian 4), [2] Panduan Produk v2.3 (bagian 7)

Bandingkan dengan chatbot tanpa RAG: ia mungkin menjawab dengan kebijakan refund generik yang terdengar benar tapi salah untuk produk Anda. Di sinilah nilai bisnis RAG — kebenaran yang bisa diverifikasi, bukan kefasihan.

Memilih LLM-nya: API vs Lokal

API (OpenAI/Anthropic/dll)Lokal (Ollama/vLLM)
Kualitas jawabanTertinggiBagus untuk 7B–32B modern
BiayaPer token, naik seiring trafikTetap (VPS/GPU)
Privasi dataData keluar ke vendorPenuh di infrastruktur sendiri
Latensi1–5 dtkTergantung hardware

Untuk helpdesk internal dengan data sensitif, kombinasi retrieval lokal + LLM API sering jadi titik tengah terbaik: dokumen tidak pernah keluar (hanya 5 chunk relevan yang dikirim sebagai konteks — dan itu pun bisa difilter), tapi kualitas generasinya maksimal. Jika regulasi mengharuskan 100% on-premise, model 8B–32B modern yang dijalankan via Ollama/vLLM sudah cukup untuk tugas menjawab-dari-konteks.


Bagian 5: Evaluasi & Operasional — RAG Bukan Proyek Sekali Jadi

Metrik yang Benar-Benar Penting

Lupakan BLEU/ROUGE — metrik generasi teks generik tidak mengukur yang penting di RAG. Pantau tiga ini:

  1. Context recall — apakah chunk yang benar berhasil ter-retrieve? (ukur offline dengan dataset uji Anda)
  2. Faithfulness — apakah jawaban didukung konteks? (bisa dinilai otomatis dengan LLM-as-judge: minta model kedua memverifikasi tiap klaim terhadap chunk)
  3. Tingkat "tidak tahu" — berapa persen query dijawab "tidak menemukan informasi". Terlalu rendah = model mengarang; terlalu tinggi = retrieval buruk atau dokumentasi memang bolong.

Pola Operasional Produksi

  • Ingest inkremental. Kolom UNIQUE(source, chunk_index) di skema kita membuat re-ingest dokumen yang diperbarui jadi idempotent — chunk lama di-update, chunk baru ditambahkan. Tambahkan kolom version jika dokumen sering berubah.
  • Pisahkan dimensi embedding per model. Jangan campur vektor 768-dimensi dan 1024-dimensi dalam satu kolom. Kalau ganti model embedding, buat kolom/tabel baru dan migrasi — skor similarity antar model tidak bisa dibandingkan.
  • Cache embedding query populer. Di helpdesk nyata, 20% pertanyaan mencakup 80% trafik. Simpan embedding query yang sering muncul di Redis — hemat ratusan ms per request.
  • Monitor drift. Jika recall@5 turun setelah dokumentasi di-update massal, kemungkinan chunking baru merusak sesuatu. Dataset uji 30–50 pertanyaan tadi adalah regression test Anda — jalankan tiap ada perubahan pipeline.
  • Keamanan. Jangan pernah memasukkan dokumen dengan level akses berbeda ke satu index tanpa filter. Tambahkan kolom allowed_roles TEXT[] dan selipkan WHERE allowed_roles && ARRAY['cs'] di query retrieval — kebocoran via RAG adalah insiden keamanan, bukan bug biasa.

Kesalahan Umum (dan Cara Menghindarinya)

  1. Chunk terlalu besar atau terlalu kecil. 2000 kata per chunk = embedding "rata-rata" yang kabur; 50 kata = konteks hilang. Sweet spot 300–500 kata dengan overlap sudah terbukti di banyak deployment.
  2. Lupa prefix query vs dokumen. Model seperti nomic dan bge-m3 butuh instruksi tugas berbeda saat embedding query dan dokumen. Bug ini silent — tidak error, hanya kualitas anjlok.
  3. Tidak menormalisasi vektor. Cosine distance pgvector mengasumsikan perbandingan yang konsisten. Normalisasi saat embedding, selalu.
  4. Index dibuat sebelum data masuk. Build HNSW di tabel kosong lalu bulk insert = lambat. Urutan benar: insert dulu (atau naikkan maintenance_work_mem), index belakangan.
  5. Mengandalkan dense search saja. Istilah eksak (SKU, kode error, nama fitur) adalah titik buta semantic search. Hybrid dengan FTS PostgreSQL menutupnya tanpa infrastruktur baru.
  6. Tidak ada evaluasi. RAG tanpa dataset uji adalah demo, bukan produk. 30 pertanyaan + recall@5 = 1 jam kerja yang menyelamatkan berbulan-bulan debugging buta.

Roadmap: Setelah Tutorial Ini Berjalan

  1. Tambahkan filter metadata — batasi pencarian per kategori produk, bahasa, atau versi dokumen langsung di klausa WHERE.
  2. Coba binary quantization saat chunk melewati 1 juta — hemat 32× memori untuk candidate generation.
  3. Eksperimen dengan reranker multibahasa seperti bge-reranker-v2-m3 untuk korpus Indonesia-Inggris campur.
  4. Evaluasi model embedding yang lebih baru (Qwen3-Embedding, EmbeddingGemma) setahun sekali — tapi ukur dengan dataset uji Anda sendiri, jangan cuma percaya leaderboard MTEB.
  5. Pertimbangkan VectorChord/pg_turbovec jika skala melewati kemampuan pgvector standar — keduanya kompatibel dengan tipe vector pgvector sehingga migrasinya mulus.

Takeaway Praktis

  • RAG = retrieval yang bagus + prompt yang disiplin. 70% kualitas ditentukan sebelum LLM dipanggil.
  • PostgreSQL + pgvector 0.8.x cukup untuk memulai — HNSW untuk query cepat, halfvec untuk hemat memori, iterative scan untuk filter + vektor dalam satu query.
  • Model embedding open source 2026 (nomic-v1.5, bge-m3) gratis dan CPU-friendly — tidak ada alasan membayar API embedding untuk tahap awal.
  • Hybrid (dense + BM25 via RRF) + reranker adalah pola default produksi: recall dari dua dunia, presisi dari cross-encoder.
  • Ukur recall@5 dengan 30–50 pertanyaan buatan sendiri sebelum mengklaim RAG Anda "sudah jadi".
  • Disiplin operasional (ingest idempotent, filter akses per role, regression test) yang membedakan demo dan produk.

Selamat membangun. Search engine AI pertama Anda hanya sejauh satu CREATE EXTENSION vector; — sisanya adalah ketekunan di chunking, evaluasi, dan iterasi.

Share the Knowledge

Let's build the future together.

If you found the article above helpful, don’t forget to share it with your friends!

Want to stay ahead in tech?

Subscribe to our newsletter for the latest insights on AI, automation, and digital transformation.