Referensi cepat embeddings dan semantic search. Mental model vektor, cosine similarity, chunking, ANN index, hybrid search, reranking, dan evaluasi retrieval. Perfect buat developer yang mau bangun RAG.
Cheat sheet ini ngebahas embeddings dan semantic search dari konsep sampai produksi. Kamu bakal paham gimana teks diubah jadi vektor, gimana ngukur kemiripan, gimana nyimpen dan nyari vektor dengan cepat, sampai gimana ngevaluasi hasilnya. Buat yang baru mulai, baca urut dari atas. Buat yang mau langsung ngedebug, lompat ke bagian troubleshooting.
Keyword search (seperti SQL LIKE atau full-text search) nyocokin kata literal. Query "mobil murah" cuma nemu dokumen yang literally ngandung kata "mobil" dan "murah". Dokumen yang nulis "kendaraan roda empat harga terjangkau" nggak ketemu, padahal maknanya sama.
Embeddings nge-solve ini dengan ngubah teks jadi vektor angka yang nyimpen makna. Dua teks yang maknanya mirip bakal punya vektor yang deket di ruang vektor, walau katanya beda total. Ini yang bikin mesin bisa nemu hasil berdasarkan arti, bukan cuma ejaan.
Alurnya begini:
Dokumen --> [Embedding Model] --> vektor[0.12, -0.4, ...] --> Vector Store
Query --> [Embedding Model] --> vektor[0.11, -0.38, ...] --> Cari terdekat --> HasilPoin penting: model yang dipakai buat dokumen dan query harus sama. Kalau beda, ruang vektornya beda dan perbandingannya nggak berarti.
Ada dua tipe semantic search yang nentuin model mana yang kamu pilih:
Symmetric search: query dan dokumen bentuknya mirip. Contohnya nyari pertanyaan yang mirip di forum. Query "cara belajar Python online" nyari dokumen "cara belajar Python di web". Panjang dan isinya sebanding.
Asymmetric search: query pendek, dokumen panjang. Contohnya nanya "apa itu Python" dan nyari paragraf yang ngejawab. Ini pola paling umum di RAG. Model yang dipakai harus dilatih khusus buat pasangan query pendek dengan dokumen panjang.
Kalau kamu pakai model yang salah, hasilnya aneh. Model symmetric dipakai buat tugas asymmetric biasanya ngasih skor similarity yang menyesatkan.
Vektor sering dinormalisasi biar panjangnya jadi 1 (unit vector). Dot product dua unit vector sama dengan cosine similarity. Normalisasi bikin perhitungan lebih cepat dan stabil.
import math
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def norm(a):
return math.sqrt(sum(x * x for x in a))
def cosine(a, b):
denom = norm(a) * norm(b)
return dot(a, b) / denom if denom else 0.0
# Contoh: dua vektor kecil yang arahnya mirip
va = [1.0, 2.0, 3.0]
vb = [1.1, 1.9, 3.2]
print(round(cosine(va, vb), 4)) # mendekati 1.0| Ukuran | Rumus | Range | Kapan Pakai |
|---|---|---|---|
| Cosine similarity | dot(a,b) / (norm(a) x norm(b)) | -1 sampai 1 | Default buat teks, nggak peduli panjang vektor |
| Dot product | sum(a x b) | tak terbatas | Model yang dilatih pakai dot product, butuh panjang vektor bermakna |
| Euclidean (L2) | sqrt(sum((a-b)^2)) | 0 sampai tak terbatas | Butuh jarak geometris literal, lebih kecil artinya lebih mirip |
Cosine adalah pilihan default buat embeddings teks karena dia cuma peduli arah, bukan panjang. Dua kalimat yang satu panjang satu pendek tapi bahas hal yang sama tetap dapat skor tinggi.
Catatan penting: beberapa model dilatih dengan objective dot product, bukan cosine. Kalau modelnya kayak gitu, pakai dot product. Cek dokumentasi modelnya. Jangan asumsi cosine selalu benar.
Cosine similarity dua vektor bisa kamu hitung dengan numpy kalau mau lebih cepat:
# Butuh: pip install numpy
import numpy as np
def cosine_np(a, b):
a = np.asarray(a, dtype=float)
b = np.asarray(b, dtype=float)
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))Ini cuma ngasih angka kemiripan. Dia nggak ngejelasin kenapa mirip. Makanya similarity tinggi bukan jaminan jawaban benar, cuma jaminan secara vektor deket.
Embedding model punya batas token. Kebanyakan model modern bisa nerima ribuan token, tapi kalau kamu embed satu buku utuh jadi satu vektor, maknanya jadi kabur. Satu vektor ngewakilin terlalu banyak topik sekaligus.
Chunking mecah dokumen jadi potongan lebih kecil yang masing-masing fokus satu ide. Potongan itu yang di-embed dan di-index.
| Strategi | Cara Kerja | Kapan Pakai |
|---|---|---|
| Fixed size | Potong per N karakter atau token, boleh overlap | Default awal, simpel |
| Sentence split | Potong di batas kalimat | Dokumen naratif, FAQ |
| Recursive split | Coba pisah per paragraf, kalau masih kepanjangan turun ke kalimat | Dokumen campuran |
| Semantic chunk | Kelompokin kalimat yang similar | Butuh kualitas tinggi, lebih mahal |
| Document structure | Ikut heading, tabel, atau section | Markdown, HTML, PDF terstruktur |
Overlap itu penting. Kalau kamu potong fixed size tanpa overlap, satu kalimat yang kepotong di tengah bisa kehilangan konteksnya. Overlap 10 sampai 20 persen biasanya cukup.
Nggak ada angka sakti. Titik awal yang wajar: 200 sampai 500 token per chunk dengan overlap 50 token. Chunk terlalu kecil bikin hasil kehilangan konteks, chunk terlalu besar bikin hasil nggak spesifik.
Aturan praktis: satu chunk idealnya ngejawab satu pertanyaan. Kalau dokumen kamu kaya FAQ, satu chunk satu Q&A. Kalau artikel panjang, satu chunk satu sub-bagian.
def chunk_fixed(text, size, overlap):
chunks = []
start = 0
n = len(text)
while start < n:
end = min(start + size, n)
chunks.append(text[start:end])
start += size - overlap
return chunks
# Contoh: potong per 100 karakter, overlap 20
teks = "A" * 250
hasil = chunk_fixed(teks, 100, 20)
print(len(hasil)) # 4 chunkAlur retrieval yang wajar di produksi:
1. Query masuk
2. (Opsional) Query di-rewrite atau di-expand
3. Query di-embed pakai model yang sama dengan dokumen
4. Pre-filter pakai metadata kalau ada
5. ANN search buat dapet kandidat (misal top 50)
6. (Opsional) Hybrid: gabung hasil lexical + vektor
7. (Opsional) Rerank kandidat pakai cross-encoder
8. Return top-k terakhirLangkah 4 dan 7 yang paling sering dilupain orang baru. Pre-filter metadata bikin pencarian lebih relevan, rerank bikin urutan lebih akurat.
Retrieval ngasih kandidat, bukan jawaban. Kalau kamu cuma ambil yang paling mirip, satu kesalahan kecil langsung ngerusak hasil. Ambil 20 sampai 50 kandidat, terus biar reranker atau LLM yang milih. Ini prinsip retrieve-then-rerank.
def retrieve(query_vec, index, k):
# Ambil k vektor terdekat (contoh naif linear scan)
scored = []
for doc_id, vec in index:
s = cosine(query_vec, vec)
scored.append((s, doc_id))
scored.sort(reverse=True)
return scored[:k]Linear scan kayak gini cuma oke buat ribuan vektor. Buat jutaan vektor, butuh ANN index yang dibahas di bagian indexing.
Vektor nyimpen makna, tapi nggak nyimpen fakta terstruktur. Kalau kamu nyari "panduan deploy dari tahun 2024", vektor nggak bisa bedain tahun. Metadata yang nge-handle hal kayak gitu.
Contoh metadata: tanggal publish, bahasa, kategori, penulis, status, sumber, rentang harga.
Pre-filter: metadata di-filter dulu sebelum ANN search. Hasilnya akurat tapi bisa lambat kalau filternya nge-exclude banyak kandidat.
Post-filter: ANN search dulu, terus filter metadata setelahnya. Cepat, tapi bisa balikin hasil lebih sedikit dari k yang kamu minta.
Pre-filter: [Filter tahun=2024] --> [ANN search] --> hasil
Post-filter: [ANN search top-50] --> [Buang yang bukan 2024] --> hasilKebanyakan vector store mendukung keduanya. Buat dataset kecil, post-filter cukup. Buat dataset besar, pre-filter lebih aman.
Filter metadata biasanya boolean: AND, OR, NOT, plus operator rentang buat angka dan tanggal. Simpan metadata yang sering dipakai buat filter sebagai field terpisah, jangan cuma dibuang ke string teks.
Exact nearest neighbor nyari dengan ngebandingin query ke semua vektor. Buat 1 juta vektor berdimensi 768, ini lambat dan mahal memori.
ANN (Approximate Nearest Neighbor) nge-trade akurasi kecil buat kecepatan besar. Dia nggak jamin hasil paling akurat, tapi jamin hasil yang nyaris akurat dalam milidetik.
| Index | Cara Kerja | Trade-off |
|---|---|---|
| Flat / brute force | Bandingin semua vektor | Paling akurat, lambat, makan memori |
| LSH | Hash vektor mirip ke bucket sama | Cepat, recall rendah |
| IVF | Cluster vektor, cari di cluster terdekat | Cepat, butuh tuning jumlah cluster |
| HNSW | Graph berlapis, lompat antar node | Cepat dan akurat, makan RAM besar |
HNSW jadi default de facto di banyak vector store karena balance kecepatan dan akurasinya bagus. Kekurangannya konsumsi memori tinggi dan build index-nya lambat.
IVF cocok kalau memori terbatas. Kamu atur jumlah cluster (nlist) dan jumlah cluster yang dicek (nprobe). Nilai nprobe lebih besar artinya recall lebih tinggi tapi lebih lambat.
ANN punya kenop utama: recall dan latency. Recall artinya berapa persen hasil exact yang ketemu. Latency artinya seberapa cepat balik.
Recall tinggi <---> Latency rendah
(nprobe besar) (nprobe kecil)
(ef_search besar) (ef_search kecil)Kamu yang milih posisi di spektrum ini. Buat RAG, recall 0.95 ke atas biasanya cukup. Buat kasus yang toleran salah, recall lebih rendah boleh.
Vektor bagus buat makna, tapi lemah buat istilah persis. Query "error code E11000" atau nama produk "iPhone 15" lebih cocok pakai keyword search. Hybrid nggabungin dua-duanya.
Cara paling umum: jalankan lexical search (BM25) dan vector search, terus gabung hasilnya.
RRF adalah cara simpel nggabungin dua ranking tanpa perlu normalize skor. Rumusnya: tiap dokumen dapat skor 1 / (k + rank) di tiap list, terus dijumlah.
def rrf(rank_lists, k=60):
scores = {}
for ranks in rank_lists:
for rank, doc_id in enumerate(ranks, start=1):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
# Dua list ranking dari dua metode
lexical = ["doc_a", "doc_b", "doc_c"]
vector = ["doc_b", "doc_a", "doc_d"]
print(rrf([lexical, vector]))
# doc_a dan doc_b naik karena muncul di dua-duanyaNilai k di RRF biasanya 60. Dokumen yang muncul di dua list dapat skor lebih tinggi, jadi hasil gabungan lebih solid.
Bi-encoder (model yang nge-embed query dan dokumen terpisah) cepat tapi kasar. Cross-encoder ngejalanin query dan dokumen barengan lewat satu model, jadi lebih akurat, tapi lambat.
Alurnya: bi-encoder nyari 50 kandidat, cross-encoder ngerank ulang 50 itu buat dapet top 5. Akurasi naik banyak, biaya tambahan kecil karena cross-encoder cuma ngeproses 50 pasangan.
Bi-encoder (cepat) --> 50 kandidat --> Cross-encoder (akurat) --> top 5Kalau kamu nggak mau nambah model, RRF aja sering cukup. Tapi buat quality-sensitive, rerank itu upgrade termurah yang paling berasa.
| Metrik | Arti | Kapan Dipakai |
|---|---|---|
| Recall@k | Proporsi dokumen relevan yang ketemu di top-k | Ukur coverage retrieval |
| Precision@k | Proporsi top-k yang relevan | Ukur ketepatan hasil |
| MRR | Rata-rata 1/rank dokumen relevan pertama | Butuh satu jawaban benar di urutan atas |
| nDCG@k | Nge-reward hasil relevan yang muncul lebih atas | Ranking quality, relevansi berjenjang |
Recall@k paling umum di RAG karena kamu mau pastikan jawaban ada di kandidat yang diambil. Kalau jawabannya nggak ke-retrieve, downstream pasti gagal.
def recall_at_k(hits, relevant, k):
hits = hits[:k]
return len(set(hits) & set(relevant)) / len(relevant)
# 3 dokumen relevan, di top-10 ketemu 2
print(recall_at_k(list(range(10)), [1, 2, 99], 10)) # 0.666...Buat evaluasi retrieval, kamu butuh dataset pasangan query dan dokumen relevan. Beberapa dataset publik yang umum dipakai:
Buat evaluasi internal, kamu bisa bikin sendiri: ambil 100 query nyata, tandai manual dokumen mana yang relevan, terus ukur recall. Kualitas evaluasi kamu nggak lebih baik dari kualitas label yang kamu bikin.
1. Ambil dokumen sumber (API, file, database)
2. Bersihin dan normalize teks
3. Chunking
4. Embed tiap chunk
5. Simpan vektor + metadata ke store
6. Catat versi dan timestampIngestion sering jadi bottleneck. Embedding ribuan dokumen makan waktu dan biaya. Pakai batch dan retry buat handle rate limit.
Dokumen berubah atau dihapus. Kalau store kamu nggak ngedukung update in-place, strateginya: hapus vektor lama, embed ulang dokumen baru, insert lagi.
Update: [Hapus id lama] --> [Embed ulang] --> [Insert id baru]
Delete: [Hapus id] --> [Verifikasi id hilang dari hasil]Simpan ID dokumen asli sebagai metadata biar gampang nge-track dan nge-delete. Jangan pernah embed ulang semua dokumen cuma buat update satu dokumen.
Kalau kamu ganti embedding model, semua vektor lama nggak valid lagi karena ruang vektornya beda. Kamu harus re-embed semua. Ini kerjaan besar, jadi pilih model di awal dengan hati-hati. Simpan teks asli di tempat terpisah supaya re-embedding nggak butuh ambil ulang dari sumber.
Contoh di bawah pakai standard library doang buat nunjukin alur end-to-end tanpa dependensi eksternal. Buat produksi, ganti bagian embedding dan ANN dengan library yang kamu pilih.
import math
# ---- Embedding (stub, ganti dengan model asli) ----
# Di produksi pakai model embedding sungguhan.
# Stub ini bikin vektor dari karakter biar contoh bisa jalan.
def embed(text):
v = [0.0] * 32
for i, ch in enumerate(text.lower()):
v[ord(ch) % 32] += 1.0
n = math.sqrt(sum(x * x for x in v)) or 1.0
return [x / n for x in v]
def cosine(a, b):
return sum(x * y for x, y in zip(a, b))
# ---- Index in-memory sederhana ----
docs = [
{"id": "d1", "text": "cara install python di ubuntu"},
{"id": "d2", "text": "tutorial memasak nasi goreng"},
{"id": "d3", "text": "langkah setup python di mac"},
]
index = [(d["id"], embed(d["text"])) for d in docs]
def search(query, k=2):
qv = embed(query)
scored = [(cosine(qv, v), doc_id) for doc_id, v in index]
scored.sort(reverse=True)
return scored[:k]
print(search("gimana pasang python di laptop", k=2))
# Harusnya d1 dan d3 menang karena bahas install pythonContoh ini ngasih intuisi, bukan produksi. Buat produksi, pilihan umum: library sentence-transformers atau model API buat embedding, plus vector store yang ngedukung ANN.
Gejala: hasil top-1 sampai top-10 nggak nyambung sama query. Penyebab umum dan solusinya:
Gejala: jawaban ada di dataset tapi nggak pernah ke-retrieve. Ini bahaya karena diam-diam ngerusak kualitas.
nprobe atau ef_search.Gejala: top-k isinya dokumen yang isinya sama persis. Penyebab umumnya chunk overlap terlalu besar atau dokumen duplikat di store. Solusinya dedup saat ingestion dan kecilkan overlap.
Skor similarity absolut nggak bisa dibandingin antar query. Skor 0.8 buat satu query belum tentu lebih baik dari 0.7 buat query lain. Jangan bikin threshold absolut dari similarity, kecuali kamu udah kalibrasi di dataset kamu.
Embedding model bisa jalan lokal atau lewat API. Kalau lewat API, teks dokumen kamu dikirim ke penyedia. Ini penting kalau datanya sensitif: data pelanggan, rahasia dagang, data kesehatan.
Kalau datanya sensitif, pakai model open-source yang jalan lokal, atau pastikan penyedia API kamu nggak nyimpen data kamu buat training.
Enkripsi saat transit dan saat istirahat, batasi akses ke store pakai autentikasi, dan audit log query buat deteksi akses aneh. Buat data yang bener-bener sensitif, pertimbangin self-host semua komponen.
Daftar ini buat ngecek apakah sistem kamu siap naik produksi:
Satu hal yang sering kelewat: nggak ngukur sebelum dan sesudah. Simpan baseline recall sebelum ngubah apa pun, biar kamu tau perubahan mana yang beneran ngebantu.
| Istilah | Arti |
|---|---|
| Embedding | Representasi vektor dari teks atau objek yang nyimpen makna |
| Vector store | Tempat nyimpen dan nyari vektor |
| ANN | Approximate Nearest Neighbor, pencarian cepat yang nge-trade akurasi kecil |
| HNSW | Algoritma index graph berlapis yang populer buat ANN |
| IVF | Index yang ngelompokin vektor ke cluster buat pencarian lebih cepat |
| Cosine similarity | Ukuran kemiripan berdasarkan sudut antar vektor |
| Chunk | Potongan dokumen yang di-embed sebagai satu unit |
| Overlap | Bagian teks yang diulang antar chunk biar konteks nggak putus |
| Bi-encoder | Model yang nge-embed query dan dokumen terpisah, cepat |
| Cross-encoder | Model yang ngeproses query dan dokumen barengan, akurat tapi lambat |
| Rerank | Ngurut ulang kandidat retrieval pakai model lebih akurat |
| RRF | Reciprocal Rank Fusion, cara nggabungin beberapa ranking |
| Recall@k | Proporsi dokumen relevan yang ketemu di top-k |
| BM25 | Algoritma lexical search standar buat hybrid |
| RAG | Retrieval-Augmented Generation, nyari konteks terus nge-generate jawaban |
| nDCG | Metrik ranking yang nge-reward hasil relevan di posisi atas |
Login atau daftar akun gratis untuk membaca cheat sheet ini.