Di atas kertas, bikin RAG (Retrieval-Augmented Generation) terdengar sepele. Potong dokumen jadi chunk 500 token, ubah tiap chunk jadi embedding pakai model yang lagi naik daun, lalu simpan ke vector database. Selesai, tinggal disambung ke LLM. Saat ada pertanyaan masuk, tinggal cari chunk dengan skor cosine similarity paling tinggi.
Rencana itu bertahan sampai sistemnya ketemu kueri pengguna sungguhan. Begitu masuk produksi dan diuji dengan ribuan kueri asli, ilusi "RAG itu gampang" langsung ambruk:
- Pengguna mencari kode error spesifik (
ERR_CONN_REFUSED_504), yang balik malah artikel penjelasan konsep jaringan internet secara umum. - Pengguna mencari nama hardware spesifik (
MacBook Pro M3 Max), yang disodorkan justru chunk tentangMacBook Air M2โ karena "maknanya kan mirip".
Dua contoh itu kelihatan sepele, tapi gejalanya sama. Sistem mengembalikan sesuatu yang terasa relevan, bukan yang kamu minta. Kok bisa begitu? Nah, di artikel ini kita bedah kenapa dense vector search mengecoh, kapan semantic search murni gagal total, dan kenapa hybrid search plus cross-encoder re-ranker sekarang jadi standar wajib di produksi. ๐
Poin Penting
- Cosine similarity murni punya titik buta pada kecocokan kata persis: kode error, nomor seri, sampai nama variabel jarang punya bobot semantik, jadi kueri seperti itu kalah dari paragraf umum yang panjang.
- Hybrid search menjalankan dua pencari sekaligus โ dense vector buat kemiripan makna, BM25 buat kecocokan kata โ lalu menyatukan hasilnya.
- Reciprocal Rank Fusion (RRF) melebur dua daftar peringkat tanpa peduli skala skornya, karena yang dihitung cuma posisi.
- Two-stage retrieval: tahap pertama ambil 50-100 kandidat cepat, tahap kedua saring 3-5 chunk pakai cross-encoder yang menilai kueri dan dokumen berbarengan.
- Kalau jawaban agent-mu ngawur padahal datanya ada, tersangka pertamanya fase retrieval, bukan model LLM-nya.
Di mana dense vector search kehilangan jejak
Dense embedding memetakan makna ke ruang vektor berdimensi tinggi, biasanya 768 sampai 1.536 dimensi. Kekuatannya justru di hal yang bikin pencarian klasik kaku โ sinonim dan maksud tersirat. Kueri "gimana cara mengembalikan barang" maknanya dekat banget sama kalimat dokumen "panduan pengajuan refund transaksi pembeli". Padahal kata yang sama di antara keduanya hampir nggak ada, lho.
Kekuatan itu berubah jadi kelemahan begitu datanya penuh istilah spesifik domain.
1. Buta sama kecocokan kata persis
Embedding cenderung meratakan makna. Nomor seri, kode SKU gudang, nama variabel (auth_middleware), singkatan akronim, sampai nama orang adalah token yang jarang muncul di korpus pelatihan model embedding. Bobot semantiknya tipis, sih. Hasilnya, kueri yang butuh kecocokan persis justru kalah dari kalimat umum yang panjang.
Ini bukan cuma soal rasa-rasanya. Yuk, kita lihat buktinya. Ada benchmark yang mengukurnya, namanya BEIR (Thakur dkk., 2021). Isinya 18 dataset dari domain berbeda, dipakai menguji retrieval lintas domain. Di situ, model dense retrieval yang menang di satu domain rata-rata kalah dari BM25 di domain yang belum pernah ia lihat. Kesimpulan paper-nya: BM25 itu baseline yang kuat, dan yang paling akurat justru re-ranking โ sayangnya biaya komputasinya paling mahal.
2. Terkecoh panjang dokumen dan konteks dangkal
Cosine similarity cuma mengukur sudut antara dua vektor. Ia nggak tahu kata mana yang penting di dalam sebuah chunk. Paragraf panjang yang berisi banyak kata populer bisa dapat skor 0,82, mepet batas maksimum 1, padahal isinya nggak menjawab pertanyaannya. Skor tinggi di sini artinya cuma "arah dua vektor ini berdekatan". Emang bukan berarti "ini jawabannya".
Kembali ke algoritma klasik: BM25
Buat menambal titik buta itu, sistem IR modern balik merangkul algoritma yang sudah terbukti puluhan tahun: BM25 (Best Matching 25), keluarga sparse lexical search. Cara kerjanya hitung seberapa sering kata kueri muncul di dokumen, tapi dengan peredaman. Makin sering muncul, makin kecil tambahan keuntungannya. Skornya juga dinormalkan terhadap panjang dokumen.
Karakter dua pendekatan ini saling melengkapi:
- Dense vector search unggul di konsep abstrak, parafrase, dan makna yang nggak pernah ditulis apa adanya.
- Sparse lexical search (BM25) unggul di kecocokan persis โ kode error, ID, nomor seri, istilah spesifik industri โ tanpa terdistorsi oleh kedekatan makna umum.
Sistem yang menjalankan keduanya berbarengan lalu menyatukan hasilnya disebut hybrid search. Sekarang pola itu jadi fitur bawaan mesin pencari: Elasticsearch punya tipe retriever rrf, OpenSearch punya hybrid search lewat normalization processor, Weaviate punya hybrid query dengan parameter alpha. Catatan versi, diperiksa 19 Oktober 2026: sejak Weaviate v1.24, fusion bawaan hybrid-nya relativeScoreFusion, bukan rankedFusion yang murni berbasis peringkat.
Menyatukan dua skor beda skala: Reciprocal Rank Fusion
Satu masalah muncul begitu dua daftar hasil disandingkan. Skor BM25 nggak punya batas atas โ 14,8 itu wajar โ sedangkan cosine similarity mentok di angka 1. Menjumlahkan dua angka beda skala ini mentah-mentah sama dengan menyerahkan hasil akhir ke kebetulan.
Jalan keluar yang dipakai industri sekarang adalah Reciprocal Rank Fusion (RRF), dari paper Cormack dkk. di SIGIR 2009:
skor RRF(dokumen) = ฮฃ 1 / (k + peringkat_dokumen_di_mesin_m)
mโM
k = 60 (angka yang direkomendasikan paper aslinya)
Triknya: skor mentahnya dibuang, yang disimpan cuma posisi. Dokumen yang muncul di peringkat 3 di BM25 dan peringkat 5 di pencarian vektor dapat tambahan skor dari dua daftar sekaligus. Ia biasanya naik ke atas, walaupun di masing-masing daftar ia bukan yang nomor satu. Konstanta k fungsinya meredam, supaya satu mesin yang menaruh dokumen di posisi teratas nggak otomatis mendominasi. Paper aslinya menguji 30 konfigurasi lalu menyimpulkan k = 60 mendekati optimal. Yang lebih penting: pilihan angkanya nggak kritis.
Lapis kedua: cross-encoder re-ranking
Hybrid search sudah jauh lebih baik, tapi ia masih bekerja sebagai bi-encoder. Kueri dan dokumen diubah jadi vektor secara terpisah, dan modelnya nggak pernah melihat keduanya berdampingan. Cepat buat menyaring, kurang buat menilai presisi.
Di situ lapis kedua masuk: cross-encoder re-ranker seperti Cohere Rerank atau versi open source-nya, BAAI/bge-reranker-v2-m3. Model ini menerima pasangan (query, document) utuh, lalu menjalankan full self-attention di atas keduanya sekaligus. Jadi ia bisa menilai apakah chunk ini benar-benar menjawab kuerinya, bukan cuma searah maknanya. Bayarannya: satu inferensi penuh untuk setiap kandidat, dan hasilnya nggak bisa di-index di muka seperti vektor. Karena itu ia dipakai di tahap kedua saja.
Polanya jadi two-stage retrieval:
- Tahap 1, retrieval (ingat-ingat luas). Hybrid search menyaring ratusan ribu chunk jadi 50-100 kandidat. Pencarian ini murah karena semuanya berbasis index.
- Tahap 2, re-ranking (presisi). 50-100 kandidat itu plus kueri asli masuk ke cross-encoder, keluar 3-5 chunk teratas buat konteks LLM.
Soal pilihan modelnya, bge-reranker-large yang dipakai tutorial-tutorial lama sudah digantikan BAAI/bge-reranker-v2-m3. Versi v2 itu multilingual dan dipakai orang sekitar delapan kali lebih banyak โ 17 juta unduhan, lawan 2 juta punya versi large (diperiksa 19 Oktober 2026 di Hugging Face). Kalau kamu baru mulai, pakai yang versi barunya, deh.
Contoh alurnya di Python
Langkah pertama, lebur hasil dua pencari pakai RRF. Fungsinya cuma tujuh baris:
from collections import defaultdict
def reciprocal_rank_fusion(dense_ranks, sparse_ranks, k=60, top_n=5):
"""Gabungkan peringkat dense search dan BM25 pakai RRF."""
rrf_scores = defaultdict(float)
# 1. Kontribusi peringkat dari dense vector search
for rank, doc_id in enumerate(dense_ranks):
rrf_scores[doc_id] += 1.0 / (k + (rank + 1))
# 2. Kontribusi peringkat dari BM25
for rank, doc_id in enumerate(sparse_ranks):
rrf_scores[doc_id] += 1.0 / (k + (rank + 1))
# 3. Urutkan dari skor gabungan tertinggi
ranked = sorted(rrf_scores.items(), key=lambda item: item[1], reverse=True)
return [doc_id for doc_id, _ in ranked[:top_n]]
# Hasil pencarian dua mesin untuk kueri "ERR_CONN_REFUSED_504"
dense_ranks = ["artikel-jaringan-umum", "chunk-err-504", "troubleshooting-http"]
sparse_ranks = ["chunk-err-504", "kode-error-lengkap", "artikel-jaringan-umum"]
print(reciprocal_rank_fusion(dense_ranks, sparse_ranks, top_n=3))
Kita lihat outputnya:
['chunk-err-504', 'artikel-jaringan-umum', 'kode-error-lengkap']
Lihat chunk-err-504 di situ. Ia nggak nomor satu di dua-duanya โ peringkat 2 di pencarian vektor, peringkat 1 di BM25. Justru itu yang naik ke puncak, karena dapat tambahan dari dua daftar. Sebaliknya artikel-jaringan-umum yang isinya cuma penjelasan umum masih ikut lolos, karena ia juga ada di dua daftar. RRF itu penyaring kandidat, bukan penentu akhir. Tugas mencoret sisa-sisa seperti itu ada di lapis kedua.
Setelah daftar kandidatnya siap, tinggal satu langkah lagi. Gimana caranya? Teruskan ke cross-encoder:
import numpy as np
from sentence_transformers import CrossEncoder # sentence-transformers 6.1.0, rilis 18 Sep 2026
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
query = "gimana cara fix error ERR_CONN_REFUSED_504 di gateway?"
pairs = [[query, doc] for doc in candidate_documents] # (kueri, dokumen) dinilai BARENGAN
scores = reranker.predict(pairs)
order = np.argsort(scores)[::-1][:3] # ambil 3 chunk teratas
final_context_chunks = [candidate_documents[i] for i in order]
Satu catatan penting soal scores itu. Angkanya bukan probabilitas 0 sampai 1, dan nggak bisa dibandingkan antar-kueri. Skor 3,2 di satu kueri nggak ada artinya di kueri lain. Pakai buat mengurutkan, jangan buat ambang batas tetap seperti "buang kalau skornya kamu lihat di bawah 0,5". Ambang seperti itu cuma bikin chunk bagus ikut terbuang di kueri yang skornya kebetulan rendah.
Jadi, kalau RAG-mu ngawur sekarang
Kalau agent-mu menjawab halusinasi atau nggak nyambung sama data teknis pengguna, tersangka pertamanya bukan kecerdasan model LLM-nya. Sebagian besar kegagalan RAG di lapangan berawal dari fase retrieval yang menyodorkan informasi sampah โ garbage in, garbage out. โ ๏ธ
Kita urutkan perbaikannya, dari yang paling murah:
- Pasang BM25 di samping vector store-mu, dan jalankan dua pencarian itu berbarengan.
- Lebur hasilnya pakai RRF. Nggak perlu latih ulang apa pun, dan nggak ada bobot yang harus kamu setel manual.
- Tutup dengan cross-encoder re-ranker yang menyaring 3-5 chunk terakhir.
Kalau kamu juga sedang mengurus memori agent yang berubah sepanjang waktu, masalah retrieval ini cuma satu dari dua lapisannya. Lapisan waktu dan kontradiksi fakta kita bedah terpisah di Memori AI Agent: dari Vector Store ke Temporal Graph.
Titik butanya bukan di model embedding yang mahal, tapi di asumsi bahwa satu jenis pencarian bisa menjawab semua bentuk pertanyaan. Dua pencari plus satu penilai itu tetap lebih murah daripada satu model besar yang dipaksa menebak, kok.