Episode ini membahas full-text search untuk pencarian keyword yang presisi, skor relevansi, lalu strategi hybrid search yang menggabungkan vector, full-text, dan metadata, termasuk pendekatan fusion dan rerank untuk retrieval RAG yang akurat.

Episode 7 memperkenalkan where_document sebagai filter teks sederhana. Sekarang saatnya membawanya lebih jauh: full-text search dan hybrid search. Ini adalah titik di mana ChromaDB membuktikan klaimnya sebagai "search infrastructure for AI" — bukan sekadar vector database, tapi engine yang menggabungkan empat jenis pencarian sekaligus.
Kita akan membahas kapan full-text lebih unggul dari semantic, bagaimana mengkombinasikan keduanya, skor relevansi, serta strategi fusion dan rerank yang membuat RAG akurat. Banyak tim menganggap semantic search selalu yang terbaik — episode ini akan meluruskan asumsi itu.
Semantic search unggul menangkap makna, tapi gagal di kasus yang membutuhkan presisi karakter. Contoh nyata: kode error, nama produk, versi software, atau UUID. Query "kode error E404" secara semantik bisa menarik dokumen tentang error lain yang mirip maknanya — padahal yang kalian butuhkan persis E404.
hasil = collection.query(
query_texts=["error E404"],
n_results=5,
where_document={"$contains": "E404"},
)where_document={"$contains": "E404"} memastikan hanya dokumen yang benar-benar memuat string E404 yang dikembalikan. Presisi karakter seperti ini tidak bisa dijamin semantic search.
Mari kita letakkan keduanya berdampingan:
| Karakteristik | Semantic Search | Full-Text Search |
|---|---|---|
| Basis | Makna (embedding) | Kata persis |
| Kuat di | Parafrase, sinonim | Kode, nama, versi |
| Lemah di | Presisi karakter | Makna, sinonim |
| Kecepatan | Bergantung index | Sangat cepat |
Full-text di ChromaDB mencari kata di documents, dan hasilnya disaring berdasarkan kehadiran kata tersebut. Untuk RAG yang melayani pertanyaan beragam, keduanya harus berjalan bersama — dan itu tepatnya hybrid search.
Hybrid search menjalankan pencarian vector dan full-text terhadap query yang sama, lalu menggabungkan hasilnya. ChromaDB memudahkan ini: kalian bisa menjalankan query dengan where_document, atau menjalankan dua pencarian terpisah lalu menggabungkannya di aplikasi.
Pendekatan yang paling umum di ChromaDB:
hasil_vector = collection.query(
query_texts=["cara deploy aplikasi"],
n_results=10,
)
hasil_fulltext = collection.query(
query_texts=["cara deploy aplikasi"],
n_results=10,
where_document={"$contains": "deploy"},
)Dua panggilan collection.query(...) di atas menghasilkan dua kandidat set yang berbeda karakter: yang pertama berbasis kemiripan vektor, yang kedua dibatasi kata "deploy". Tugas kalian adalah menggabungkannya menjadi satu daftar final.
Hybrid sebenarnya punya tiga dimensi di ChromaDB: vector, full-text, dan metadata. Filter metadata dari episode 7 bisa ditambahkan ke salah satu pencarian di atas:
hasil = collection.query(
query_texts=["deploy"],
n_results=10,
where={"env": "production"},
where_document={"$contains": "Docker"},
)collection.query(query_texts=["deploy"], where={"env": "production"}, where_document={"$contains": "Docker"}) menjalankan semantic search yang dibatasi lingkungan production dan dokumen yang memuat kata Docker. Tiga filter bekerja dalam satu panggilan.
Info
Penting untuk dipahami: where_document di ChromaDB adalah filter ketat — hasil yang tidak mengandung kata tersebut disingkirkan. Ia bukan pencarian dengan skor kontinu seperti BM25. Untuk skor relevansi penuh, gabungkan hasil secara manual atau gunakan alat fusion.
Masalah utama hybrid: dua daftar hasil dengan dua skala berbeda tidak bisa dicampur langsung. Solusi klasik adalah Reciprocal Rank Fusion (RRF): setiap dokumen diberi skor berdasarkan posisinya di tiap daftar.
def rrf(daftar_dok, k=60):
skor = {}
for daftar in daftar_dok:
for posisi, dok in enumerate(daftar):
skor[dok] = skor.get(dok, 0) + 1 / (k + posisi + 1)
return sorted(skor, key=skor.get, reverse=True)
final = rrf([hasil_vector["ids"][0], hasil_fulltext["ids"][0]])Fungsi rrf([...], k=60) memberi skor tinggi untuk dokumen yang muncul di posisi atas kedua daftar. Ini pola fusion yang sederhana, tanpa parameter, dan cukup kuat untuk banyak kasus.
Untuk akurasi tertinggi, langkah terakhir adalah rerank dengan model khusus — misalnya cross-encoder yang menghitung relevansi query-dokumen secara langsung:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
kandidat = hasil_vector["documents"][0] + hasil_fulltext["documents"][0]
skor = reranker.predict([(query, d) for d in kandidat])reranker.predict([(query, d) for d in kandidat]) mengembalikan skor relevansi untuk tiap pasangan query-dokumen. Urutkan menurun, ambil top-N, dan kalian punya hasil yang jauh lebih akurat daripada sekadar mengandalkan embedding.
Merangkum episode ini menjadi satu pola produksi:
where_document ambil hasil paralel.Pola ini memberi keseimbangan antara recall (semantic menangkap makna, full-text menangkap presisi) dan precision (rerank memilah yang benar-benar relevan). Implementasi lengkapnya akan kalian lihat di episode 18 saat integrasi dengan framework LLM.
Episode 8 menaikkan level retrieval kalian: memahami kapan full-text menang atas semantic, menjalankan hybrid search dengan tiga dimensi (vector, full-text, metadata), menggabungkan peringkat dengan RRF, dan menyempurnakan hasil dengan rerank cross-encoder. ChromaDB bukan sekadar vector store — ia adalah search engine.
Inti yang harus dibawa pulang:
where_document + where.Di episode 9 selanjutnya kita akan membahas distance metrics dan HNSW index — perbedaan L2, cosine, dan inner product, pemilihan metrik sesuai model embedding, serta tuning parameter ef_construction, M, dan ef search dengan trade-off recall versus latensi dan memori. Di sinilah performa retrieval mulai diatur.