[{"data":1,"prerenderedAt":204},["ShallowReactive",2],{"site-header":3,"site-footer-license":42,"article-kenapa-rag-cuma-cosine-similarity-sering-nyasar-with-latest":46},{"navigations":4},[5,20,37],{"collection":6,"item":7},"series",{"__typename":6,"title":8,"slug":9,"children_series":10},"Artikel","all",[11,14,17],{"title":12,"slug":13},"Algoritma dan Struktur Data","algoritma-struktur-data",{"title":15,"slug":16},"Kecerdasan Artifisial","kecerdasan-artifisial",{"title":18,"slug":19},"Konsep Dasar Pemrograman","konsep-dasar-pemrograman",{"collection":6,"item":21},{"__typename":6,"title":22,"slug":23,"children_series":24},"Tutorial","tutorial",[25,28,31,34],{"title":26,"slug":27},"Belajar Java","belajar-java",{"title":29,"slug":30},"Belajar Pascal","belajar-pascal",{"title":32,"slug":33},"Belajar Golang","belajar-golang",{"title":35,"slug":36},"Belajar Python","belajar-python",{"collection":6,"item":38},{"__typename":6,"title":39,"slug":40,"children_series":41},"Lab","lab",[],{"name":43,"url":44,"show":45},"CC BY-SA","https:\u002F\u002Fcreativecommons.org\u002Flicenses\u002Fby-sa\u002F4.0\u002F",false,{"article":47,"renderedBody":140,"renderedTakeaway":141,"latestArticles":142,"toc":177},{"prerequisite":48,"id":49,"title":50,"slug":51,"excerpt":52,"published_at":53,"thumbnail_image":48,"tags":54,"reading_time":61,"takeway":62,"body":63,"authors":64,"series":70},null,"127","Kenapa RAG yang cuma andalkan cosine similarity sering nyasar","kenapa-rag-cuma-cosine-similarity-sering-nyasar","RAG yang cuma andalkan embedding balikin chunk yang mirip maknanya, bukan yang kamu butuh. Ini penyebabnya, dan dua lapis penambalnya.","2026-10-19T02:04:06.718Z",[55,56,57,58,59,60],"rag","vector-search","bm25","hybrid-search","retrieval","embedding",6,"- *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.\n- Hybrid search menjalankan dua pencari sekaligus — *dense vector* buat kemiripan makna, BM25 buat kecocokan kata — lalu menyatukan hasilnya.\n- *Reciprocal Rank Fusion* (RRF) melebur dua daftar peringkat tanpa peduli skala skornya, karena yang dihitung cuma posisi.\n- *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.\n- Kalau jawaban *agent*-mu ngawur padahal datanya ada, tersangka pertamanya fase *retrieval*, bukan model LLM-nya.","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.\n\nRencana itu bertahan sampai sistemnya ketemu kueri pengguna sungguhan. Begitu masuk produksi dan diuji dengan ribuan kueri asli, ilusi \"RAG itu gampang\" langsung ambruk:\n\n- Pengguna mencari kode error spesifik (`ERR_CONN_REFUSED_504`), yang balik malah artikel penjelasan konsep jaringan internet secara umum.\n- Pengguna mencari nama hardware spesifik (`MacBook Pro M3 Max`), yang disodorkan justru *chunk* tentang `MacBook Air M2` — karena \"maknanya kan mirip\".\n\nDua 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. 🔍\n\n## Di mana *dense vector search* kehilangan jejak\n\n*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.\n\nKekuatan itu berubah jadi kelemahan begitu datanya penuh istilah spesifik domain.\n\n### 1. Buta sama kecocokan kata persis\n\n*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.\n\nIni 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.\n\n### 2. Terkecoh panjang dokumen dan konteks dangkal\n\n*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\".\n\n## Kembali ke algoritma klasik: BM25\n\nBuat 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.\n\nKarakter dua pendekatan ini saling melengkapi:\n\n- *Dense vector search* unggul di konsep abstrak, parafrase, dan makna yang nggak pernah ditulis apa adanya.\n- *Sparse lexical search* (BM25) unggul di kecocokan persis — kode error, ID, nomor seri, istilah spesifik industri — tanpa terdistorsi oleh kedekatan makna umum.\n\nSistem 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.\n\n## Menyatukan dua skor beda skala: *Reciprocal Rank Fusion*\n\nSatu 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.\n\nJalan keluar yang dipakai industri sekarang adalah **Reciprocal Rank Fusion (RRF)**, dari paper Cormack dkk. di SIGIR 2009:\n\n```text\nskor RRF(dokumen) = Σ  1 \u002F (k + peringkat_dokumen_di_mesin_m)\n                    m∈M\n\nk = 60  (angka yang direkomendasikan paper aslinya)\n```\n\nTriknya: 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.\n\n## Lapis kedua: *cross-encoder re-ranking*\n\nHybrid 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.\n\nDi situ lapis kedua masuk: **cross-encoder re-ranker** seperti [Cohere Rerank](https:\u002F\u002Fcohere.com\u002Frerank) atau versi *open source*-nya, `BAAI\u002Fbge-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.\n\nPolanya jadi *two-stage retrieval*:\n\n1. **Tahap 1, *retrieval* (ingat-ingat luas).** Hybrid search menyaring ratusan ribu *chunk* jadi 50-100 kandidat. Pencarian ini murah karena semuanya berbasis *index*.\n2. **Tahap 2, *re-ranking* (presisi).** 50-100 kandidat itu plus kueri asli masuk ke cross-encoder, keluar 3-5 *chunk* teratas buat konteks LLM.\n\nSoal pilihan modelnya, `bge-reranker-large` yang dipakai tutorial-tutorial lama sudah digantikan `BAAI\u002Fbge-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.\n\n## Contoh alurnya di Python\n\nLangkah pertama, lebur hasil dua pencari pakai RRF. Fungsinya cuma tujuh baris:\n\n```python\nfrom collections import defaultdict\n\ndef reciprocal_rank_fusion(dense_ranks, sparse_ranks, k=60, top_n=5):\n    \"\"\"Gabungkan peringkat dense search dan BM25 pakai RRF.\"\"\"\n    rrf_scores = defaultdict(float)\n\n    # 1. Kontribusi peringkat dari dense vector search\n    for rank, doc_id in enumerate(dense_ranks):\n        rrf_scores[doc_id] += 1.0 \u002F (k + (rank + 1))\n\n    # 2. Kontribusi peringkat dari BM25\n    for rank, doc_id in enumerate(sparse_ranks):\n        rrf_scores[doc_id] += 1.0 \u002F (k + (rank + 1))\n\n    # 3. Urutkan dari skor gabungan tertinggi\n    ranked = sorted(rrf_scores.items(), key=lambda item: item[1], reverse=True)\n    return [doc_id for doc_id, _ in ranked[:top_n]]\n\n\n# Hasil pencarian dua mesin untuk kueri \"ERR_CONN_REFUSED_504\"\ndense_ranks = [\"artikel-jaringan-umum\", \"chunk-err-504\", \"troubleshooting-http\"]\nsparse_ranks = [\"chunk-err-504\", \"kode-error-lengkap\", \"artikel-jaringan-umum\"]\n\nprint(reciprocal_rank_fusion(dense_ranks, sparse_ranks, top_n=3))\n```\n\nKita lihat outputnya:\n\n```text\n['chunk-err-504', 'artikel-jaringan-umum', 'kode-error-lengkap']\n```\n\nLihat `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.\n\nSetelah daftar kandidatnya siap, tinggal satu langkah lagi. Gimana caranya? Teruskan ke cross-encoder:\n\n```python\nimport numpy as np\nfrom sentence_transformers import CrossEncoder  # sentence-transformers 6.1.0, rilis 18 Sep 2026\n\nreranker = CrossEncoder(\"BAAI\u002Fbge-reranker-v2-m3\")\n\nquery = \"gimana cara fix error ERR_CONN_REFUSED_504 di gateway?\"\npairs = [[query, doc] for doc in candidate_documents]  # (kueri, dokumen) dinilai BARENGAN\nscores = reranker.predict(pairs)\n\norder = np.argsort(scores)[::-1][:3]  # ambil 3 chunk teratas\nfinal_context_chunks = [candidate_documents[i] for i in order]\n```\n\nSatu 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.\n\n## Jadi, kalau RAG-mu ngawur sekarang\n\nKalau *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*. ⚠️\n\nKita urutkan perbaikannya, dari yang paling murah:\n\n1. Pasang BM25 di samping *vector store*-mu, dan jalankan dua pencarian itu berbarengan.\n2. Lebur hasilnya pakai RRF. Nggak perlu latih ulang apa pun, dan nggak ada bobot yang harus kamu setel manual.\n3. Tutup dengan cross-encoder re-ranker yang menyaring 3-5 *chunk* terakhir.\n\nKalau 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](\u002Fp\u002Fmemori-ai-agent-vector-store-ke-temporal-graph).\n\nTitik 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.\n",[65],{"authors_id":66},{"name":67,"slug":68,"role":69,"profile_picture":48},"Daffa Izzuddin","izzudd","Writer",{"id":71,"title":15,"slug":16,"articles":72,"parent_series":132},"8",[73,83,93,95,105,111,118,125],{"id":74,"title":75,"slug":76,"excerpt":77,"thumbnail_image":48,"tags":78},"126","Memori AI Agent: dari Vector Store ke Temporal Graph","memori-ai-agent-vector-store-ke-temporal-graph","Dua fakta yang saling bertentangan bikin agent menjawab ngawur. Ini bedanya vector store dan temporal knowledge graph dalam menjaga state jangka panjang.",[79,80,81,82],"ai","llm","backend","system-design",{"id":84,"title":85,"slug":86,"excerpt":87,"thumbnail_image":48,"tags":88},"125","MCP dibongkar ulang: kabar baik buat kamu yang capek nulis konektor","mcp-dibongkar-ulang-kabar-baik-buat-kamu","Kamu punya beberapa API internal dan satu agent AI, tapi kodenya harus ditulis ulang tiap kali modelnya diganti. MCP datang buat mengakhiri itu, dan tahun ini protokolnya dibongkar ulang supaya server-nya bisa jalan seperti web server biasa. Yuk lihat apa yang berubah. 🔧",[89,90,91,80,81,92],"mcp","model-context-protocol","ai-agent","python",{"id":49,"title":50,"slug":51,"excerpt":52,"thumbnail_image":48,"tags":94},[55,56,57,58,59,60],{"id":96,"title":97,"slug":98,"excerpt":99,"thumbnail_image":48,"tags":100},"3","Mengenal JEPA: Saat AI Mulai Belajar Punya \"Akal Sehat\"","kenapa-ai-suka-halu-dan-gimana-jepa-beresin-masalah-ini","AI sering 'halu' karena cuma nebak probabilitas data tanpa paham realita. Arsitektur JEPA mencoba mengubah itu dengan membangun model dunia di kepalanya.",[101,102,103,104],"artificial-intelligence","jepa","world-models","yann-lecun",{"id":106,"title":107,"slug":108,"excerpt":109,"thumbnail_image":48,"tags":110},"2","LLM Bakal Mentok, Terus Gimana Dong?","llm-bakal-mentok-terus-gimana-dong","LLM kayak ChatGPT itu jago ngerangkai kata, tapi nggak beneran paham realitas. Yann LeCun bilang arsitektur ini bakal mentok dan butuh solusi baru.",[101,80,104,102],{"id":112,"title":113,"slug":114,"excerpt":115,"thumbnail_image":48,"tags":116},"1","Kenapa AI Suka 'Halu'? Kenalan sama JEPA, Otak AI yang Punya Akal Sehat","kenapa-ai-suka-halu-kenalan-sama-jepa-otak-ai-yang-punya-akal-sehat","AI generatif sering 'halu' karena cuma jago nebak pola kata. JEPA coba ngerombak itu dengan ngajarin AI cara kerja dunia nyata kayak otak manusia.",[102,101,104,117],"agi",{"id":119,"title":120,"slug":121,"excerpt":122,"thumbnail_image":48,"tags":123},"4","Bukan Sekadar Generative AI, JEPA Coba Kejar Pemahaman Konsep ala Manusia","kenapa-ai-masih-gagal-paham-fisika-dasar-mengenal-arsitektur-jepa","AI biasanya cuma jago nebak piksel, tapi JEPA coba belajar lewat konsep abstrak di ruang laten. Ternyata, cara ini bisa sampai dipakai buat baca gelombang otak.",[102,104,124,117],"machine-learning",{"id":126,"title":127,"slug":128,"excerpt":129,"thumbnail_image":48,"tags":130},"5","Di Balik Dapur JEPA: Rahasia EMA, VICReg, dan Efisiensi Komputasi","di-balik-dapur-jepa-rahasia-ema-vicreg-dan-efisiensi-komputasi","Bosen sama AI yang cuma jago nebak kata atau ngafalin pixel? Kenalan sama JEPA, arsitektur revolusioner yang ngajarin mesin punya \"akal sehat\" secara super efisien. Yuk intip jeroan teknisnya!",[102,131],"artificial-intelligent",{"id":133,"title":8,"slug":9,"parent_series":48,"children_series":134},"9",[135,137,138],{"id":136,"title":12,"slug":13,"cover_image":48},"15",{"id":71,"title":15,"slug":16,"cover_image":48},{"id":139,"title":18,"slug":19,"cover_image":48},"12","\u003Cp>Di atas kertas, bikin \u003Cem>RAG\u003C\u002Fem> (\u003Cem>Retrieval-Augmented Generation\u003C\u002Fem>) terdengar sepele. Potong dokumen jadi \u003Cem>chunk\u003C\u002Fem> 500 \u003Cem>token\u003C\u002Fem>, ubah tiap \u003Cem>chunk\u003C\u002Fem> jadi \u003Cem>embedding\u003C\u002Fem> pakai model yang lagi naik daun, lalu simpan ke \u003Cem>vector database\u003C\u002Fem>. Selesai, tinggal disambung ke LLM. Saat ada pertanyaan masuk, tinggal cari \u003Cem>chunk\u003C\u002Fem> dengan skor \u003Cem>cosine similarity\u003C\u002Fem> paling tinggi.\u003C\u002Fp>\n\u003Cp>Rencana itu bertahan sampai sistemnya ketemu kueri pengguna sungguhan. Begitu masuk produksi dan diuji dengan ribuan kueri asli, ilusi &quot;RAG itu gampang&quot; langsung ambruk:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Pengguna mencari kode error spesifik (\u003Ccode>ERR_CONN_REFUSED_504\u003C\u002Fcode>), yang balik malah artikel penjelasan konsep jaringan internet secara umum.\u003C\u002Fli>\n\u003Cli>Pengguna mencari nama hardware spesifik (\u003Ccode>MacBook Pro M3 Max\u003C\u002Fcode>), yang disodorkan justru \u003Cem>chunk\u003C\u002Fem> tentang \u003Ccode>MacBook Air M2\u003C\u002Fcode> — karena &quot;maknanya kan mirip&quot;.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Dua contoh itu kelihatan sepele, tapi gejalanya sama. Sistem mengembalikan sesuatu yang \u003Cstrong>terasa\u003C\u002Fstrong> relevan, bukan yang \u003Cstrong>kamu minta\u003C\u002Fstrong>. Kok bisa begitu? Nah, di artikel ini kita bedah kenapa \u003Cem>dense vector search\u003C\u002Fem> mengecoh, kapan \u003Cem>semantic search\u003C\u002Fem> murni gagal total, dan kenapa \u003Cem>hybrid search\u003C\u002Fem> plus \u003Cem>cross-encoder re-ranker\u003C\u002Fem> sekarang jadi standar wajib di produksi. 🔍\u003C\u002Fp>\n\u003Ch2 id=\"di-mana-dense-vector-search-kehilangan-jejak\">Di mana \u003Cem>dense vector search\u003C\u002Fem> kehilangan jejak\u003C\u002Fh2>\n\u003Cp>\u003Cem>Dense embedding\u003C\u002Fem> 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 &quot;gimana cara mengembalikan barang&quot; maknanya dekat banget sama kalimat dokumen &quot;panduan pengajuan \u003Cem>refund\u003C\u002Fem> transaksi pembeli&quot;. Padahal kata yang sama di antara keduanya hampir nggak ada, lho.\u003C\u002Fp>\n\u003Cp>Kekuatan itu berubah jadi kelemahan begitu datanya penuh istilah spesifik domain.\u003C\u002Fp>\n\u003Ch3 id=\"1-buta-sama-kecocokan-kata-persis\">1. Buta sama kecocokan kata persis\u003C\u002Fh3>\n\u003Cp>\u003Cem>Embedding\u003C\u002Fem> cenderung meratakan makna. Nomor seri, kode SKU gudang, nama variabel (\u003Ccode>auth_middleware\u003C\u002Fcode>), singkatan akronim, sampai nama orang adalah \u003Cem>token\u003C\u002Fem> yang jarang muncul di korpus pelatihan model \u003Cem>embedding\u003C\u002Fem>. Bobot semantiknya tipis, sih. Hasilnya, kueri yang butuh kecocokan persis justru kalah dari kalimat umum yang panjang.\u003C\u002Fp>\n\u003Cp>Ini bukan cuma soal rasa-rasanya. Yuk, kita lihat buktinya. Ada \u003Cem>benchmark\u003C\u002Fem> yang mengukurnya, namanya BEIR (Thakur dkk., 2021). Isinya 18 dataset dari domain berbeda, dipakai menguji \u003Cem>retrieval\u003C\u002Fem> lintas domain. Di situ, model \u003Cem>dense retrieval\u003C\u002Fem> yang menang di satu domain rata-rata kalah dari BM25 di domain yang belum pernah ia lihat. Kesimpulan paper-nya: BM25 itu \u003Cem>baseline\u003C\u002Fem> yang kuat, dan yang paling akurat justru \u003Cem>re-ranking\u003C\u002Fem> — sayangnya biaya komputasinya paling mahal.\u003C\u002Fp>\n\u003Ch3 id=\"2-terkecoh-panjang-dokumen-dan-konteks-dangkal\">2. Terkecoh panjang dokumen dan konteks dangkal\u003C\u002Fh3>\n\u003Cp>\u003Cem>Cosine similarity\u003C\u002Fem> cuma mengukur sudut antara dua vektor. Ia nggak tahu kata mana yang penting di dalam sebuah \u003Cem>chunk\u003C\u002Fem>. 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 &quot;arah dua vektor ini berdekatan&quot;. Emang bukan berarti &quot;ini jawabannya&quot;.\u003C\u002Fp>\n\u003Ch2 id=\"kembali-ke-algoritma-klasik-bm25\">Kembali ke algoritma klasik: BM25\u003C\u002Fh2>\n\u003Cp>Buat menambal titik buta itu, sistem IR modern balik merangkul algoritma yang sudah terbukti puluhan tahun: \u003Cstrong>BM25\u003C\u002Fstrong> (\u003Cem>Best Matching 25\u003C\u002Fem>), keluarga \u003Cem>sparse lexical search\u003C\u002Fem>. 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.\u003C\u002Fp>\n\u003Cp>Karakter dua pendekatan ini saling melengkapi:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cem>Dense vector search\u003C\u002Fem> unggul di konsep abstrak, parafrase, dan makna yang nggak pernah ditulis apa adanya.\u003C\u002Fli>\n\u003Cli>\u003Cem>Sparse lexical search\u003C\u002Fem> (BM25) unggul di kecocokan persis — kode error, ID, nomor seri, istilah spesifik industri — tanpa terdistorsi oleh kedekatan makna umum.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Sistem yang menjalankan keduanya berbarengan lalu menyatukan hasilnya disebut \u003Cstrong>hybrid search\u003C\u002Fstrong>. Sekarang pola itu jadi fitur bawaan mesin pencari: Elasticsearch punya tipe \u003Cem>retriever\u003C\u002Fem> \u003Ccode>rrf\u003C\u002Fcode>, OpenSearch punya \u003Cem>hybrid search\u003C\u002Fem> lewat \u003Cem>normalization processor\u003C\u002Fem>, Weaviate punya \u003Cem>hybrid query\u003C\u002Fem> dengan parameter \u003Ccode>alpha\u003C\u002Fcode>. Catatan versi, diperiksa 19 Oktober 2026: sejak Weaviate v1.24, \u003Cem>fusion\u003C\u002Fem> bawaan hybrid-nya \u003Ccode>relativeScoreFusion\u003C\u002Fcode>, bukan \u003Ccode>rankedFusion\u003C\u002Fcode> yang murni berbasis peringkat.\u003C\u002Fp>\n\u003Ch2 id=\"menyatukan-dua-skor-beda-skala-reciprocal-rank-fusion\">Menyatukan dua skor beda skala: \u003Cem>Reciprocal Rank Fusion\u003C\u002Fem>\u003C\u002Fh2>\n\u003Cp>Satu masalah muncul begitu dua daftar hasil disandingkan. Skor BM25 nggak punya batas atas — 14,8 itu wajar — sedangkan \u003Cem>cosine similarity\u003C\u002Fem> mentok di angka 1. Menjumlahkan dua angka beda skala ini mentah-mentah sama dengan menyerahkan hasil akhir ke kebetulan.\u003C\u002Fp>\n\u003Cp>Jalan keluar yang dipakai industri sekarang adalah \u003Cstrong>Reciprocal Rank Fusion (RRF)\u003C\u002Fstrong>, dari paper Cormack dkk. di SIGIR 2009:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"skor%20RRF(dokumen)%20%3D%20%CE%A3%20%201%20%2F%20(k%20%2B%20peringkat_dokumen_di_mesin_m)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20m%E2%88%88M%0A%0Ak%20%3D%2060%20%20(angka%20yang%20direkomendasikan%20paper%20aslinya)\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>skor RRF(dokumen) = Σ  1 \u002F (k + peringkat_dokumen_di_mesin_m)\n                    m∈M\n\nk = 60  (angka yang direkomendasikan paper aslinya)\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Triknya: skor mentahnya dibuang, yang disimpan cuma posisi. Dokumen yang muncul di peringkat 3 di BM25 \u003Cstrong>dan\u003C\u002Fstrong> 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 \u003Ccode>k\u003C\u002Fcode> fungsinya meredam, supaya satu mesin yang menaruh dokumen di posisi teratas nggak otomatis mendominasi. Paper aslinya menguji 30 konfigurasi lalu menyimpulkan \u003Ccode>k = 60\u003C\u002Fcode> mendekati optimal. Yang lebih penting: pilihan angkanya nggak kritis.\u003C\u002Fp>\n\u003Ch2 id=\"lapis-kedua-cross-encoder-re-ranking\">Lapis kedua: \u003Cem>cross-encoder re-ranking\u003C\u002Fem>\u003C\u002Fh2>\n\u003Cp>Hybrid search sudah jauh lebih baik, tapi ia masih bekerja sebagai \u003Cem>bi-encoder\u003C\u002Fem>. Kueri dan dokumen diubah jadi vektor secara terpisah, dan modelnya nggak pernah melihat keduanya berdampingan. Cepat buat menyaring, kurang buat menilai presisi.\u003C\u002Fp>\n\u003Cp>Di situ lapis kedua masuk: \u003Cstrong>cross-encoder re-ranker\u003C\u002Fstrong> seperti \u003Ca href=\"https:\u002F\u002Fcohere.com\u002Frerank\">Cohere Rerank\u003C\u002Fa> atau versi \u003Cem>open source\u003C\u002Fem>-nya, \u003Ccode>BAAI\u002Fbge-reranker-v2-m3\u003C\u002Fcode>. Model ini menerima pasangan \u003Ccode>(query, document)\u003C\u002Fcode> utuh, lalu menjalankan \u003Cem>full self-attention\u003C\u002Fem> di atas keduanya sekaligus. Jadi ia bisa menilai apakah \u003Cem>chunk\u003C\u002Fem> ini benar-benar menjawab kuerinya, bukan cuma searah maknanya. Bayarannya: satu inferensi penuh untuk setiap kandidat, dan hasilnya nggak bisa di-\u003Cem>index\u003C\u002Fem> di muka seperti vektor. Karena itu ia dipakai di tahap kedua saja.\u003C\u002Fp>\n\u003Cp>Polanya jadi \u003Cem>two-stage retrieval\u003C\u002Fem>:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Tahap 1, \u003Cem>retrieval\u003C\u002Fem> (ingat-ingat luas).\u003C\u002Fstrong> Hybrid search menyaring ratusan ribu \u003Cem>chunk\u003C\u002Fem> jadi 50-100 kandidat. Pencarian ini murah karena semuanya berbasis \u003Cem>index\u003C\u002Fem>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Tahap 2, \u003Cem>re-ranking\u003C\u002Fem> (presisi).\u003C\u002Fstrong> 50-100 kandidat itu plus kueri asli masuk ke cross-encoder, keluar 3-5 \u003Cem>chunk\u003C\u002Fem> teratas buat konteks LLM.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Soal pilihan modelnya, \u003Ccode>bge-reranker-large\u003C\u002Fcode> yang dipakai tutorial-tutorial lama sudah digantikan \u003Ccode>BAAI\u002Fbge-reranker-v2-m3\u003C\u002Fcode>. Versi v2 itu multilingual dan dipakai orang sekitar delapan kali lebih banyak — 17 juta unduhan, lawan 2 juta punya versi \u003Cem>large\u003C\u002Fem> (diperiksa 19 Oktober 2026 di Hugging Face). Kalau kamu baru mulai, pakai yang versi barunya, deh.\u003C\u002Fp>\n\u003Ch2 id=\"contoh-alurnya-di-python\">Contoh alurnya di Python\u003C\u002Fh2>\n\u003Cp>Langkah pertama, lebur hasil dua pencari pakai RRF. Fungsinya cuma tujuh baris:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"from%20collections%20import%20defaultdict%0A%0Adef%20reciprocal_rank_fusion(dense_ranks%2C%20sparse_ranks%2C%20k%3D60%2C%20top_n%3D5)%3A%0A%20%20%20%20%22%22%22Gabungkan%20peringkat%20dense%20search%20dan%20BM25%20pakai%20RRF.%22%22%22%0A%20%20%20%20rrf_scores%20%3D%20defaultdict(float)%0A%0A%20%20%20%20%23%201.%20Kontribusi%20peringkat%20dari%20dense%20vector%20search%0A%20%20%20%20for%20rank%2C%20doc_id%20in%20enumerate(dense_ranks)%3A%0A%20%20%20%20%20%20%20%20rrf_scores%5Bdoc_id%5D%20%2B%3D%201.0%20%2F%20(k%20%2B%20(rank%20%2B%201))%0A%0A%20%20%20%20%23%202.%20Kontribusi%20peringkat%20dari%20BM25%0A%20%20%20%20for%20rank%2C%20doc_id%20in%20enumerate(sparse_ranks)%3A%0A%20%20%20%20%20%20%20%20rrf_scores%5Bdoc_id%5D%20%2B%3D%201.0%20%2F%20(k%20%2B%20(rank%20%2B%201))%0A%0A%20%20%20%20%23%203.%20Urutkan%20dari%20skor%20gabungan%20tertinggi%0A%20%20%20%20ranked%20%3D%20sorted(rrf_scores.items()%2C%20key%3Dlambda%20item%3A%20item%5B1%5D%2C%20reverse%3DTrue)%0A%20%20%20%20return%20%5Bdoc_id%20for%20doc_id%2C%20_%20in%20ranked%5B%3Atop_n%5D%5D%0A%0A%0A%23%20Hasil%20pencarian%20dua%20mesin%20untuk%20kueri%20%22ERR_CONN_REFUSED_504%22%0Adense_ranks%20%3D%20%5B%22artikel-jaringan-umum%22%2C%20%22chunk-err-504%22%2C%20%22troubleshooting-http%22%5D%0Asparse_ranks%20%3D%20%5B%22chunk-err-504%22%2C%20%22kode-error-lengkap%22%2C%20%22artikel-jaringan-umum%22%5D%0A%0Aprint(reciprocal_rank_fusion(dense_ranks%2C%20sparse_ranks%2C%20top_n%3D3))\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> collections \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> defaultdict\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">def\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> reciprocal_rank_fusion\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(dense_ranks, sparse_ranks, k\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">60\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, top_n\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">5\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#9ECBFF\">    \"\"\"Gabungkan peringkat dense search dan BM25 pakai RRF.\"\"\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    rrf_scores \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> defaultdict(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">float\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # 1. Kontribusi peringkat dari dense vector search\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> rank, doc_id \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> enumerate\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(dense_ranks):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        rrf_scores[doc_id] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1.0\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> \u002F\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (k \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (rank \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # 2. Kontribusi peringkat dari BM25\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> rank, doc_id \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> enumerate\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(sparse_ranks):\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">        rrf_scores[doc_id] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1.0\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> \u002F\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (k \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> (rank \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> 1\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">    # 3. Urutkan dari skor gabungan tertinggi\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    ranked \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\"> sorted\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(rrf_scores.items(), \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">key\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=lambda\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> item: item[\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">], \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">reverse\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">True\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [doc_id \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> doc_id, _ \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> ranked[:top_n]]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\"># Hasil pencarian dua mesin untuk kueri \"ERR_CONN_REFUSED_504\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">dense_ranks \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"artikel-jaringan-umum\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"chunk-err-504\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"troubleshooting-http\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">sparse_ranks \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"chunk-err-504\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"kode-error-lengkap\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"artikel-jaringan-umum\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#79B8FF\">print\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(reciprocal_rank_fusion(dense_ranks, sparse_ranks, \u003C\u002Fspan>\u003Cspan style=\"color:#FFAB70\">top_n\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">))\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Kita lihat outputnya:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">TEXT\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"%5B'chunk-err-504'%2C%20'artikel-jaringan-umum'%2C%20'kode-error-lengkap'%5D\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\">\u003Ccode>['chunk-err-504', 'artikel-jaringan-umum', 'kode-error-lengkap']\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Lihat \u003Ccode>chunk-err-504\u003C\u002Fcode> 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 \u003Ccode>artikel-jaringan-umum\u003C\u002Fcode> 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.\u003C\u002Fp>\n\u003Cp>Setelah daftar kandidatnya siap, tinggal satu langkah lagi. Gimana caranya? Teruskan ke cross-encoder:\u003C\u002Fp>\n\n\u003Cdiv class=\"code-block-wrapper my-6 not-prose rounded-xl border-2 border-black dark:border-gray-600 shadow-neo overflow-hidden bg-[#24292e] text-[#e1e4e8] transition-all\">\n  \u003Cdiv class=\"code-block-header flex items-center justify-between px-4 py-2.5 bg-[#1f2428] border-b-2 border-black dark:border-gray-600 font-mono text-xs font-bold text-gray-300 select-none\">\n    \u003Cdiv class=\"flex items-center gap-2\">\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ff5f56] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#ffbd2e] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"w-3 h-3 rounded-full bg-[#27c93f] border border-black\u002F40 inline-block\">\u003C\u002Fspan>\n      \u003Cspan class=\"ml-2 font-mono font-bold text-xs uppercase tracking-wider text-gray-300\">PYTHON\u003C\u002Fspan>\n    \u003C\u002Fdiv>\n    \u003Cbutton type=\"button\" class=\"copy-code-btn flex items-center gap-1.5 px-3 py-1 text-xs font-bold bg-[#2f363d] text-gray-200 border-2 border-black dark:border-gray-500 rounded-lg shadow-neo-sm hover:translate-x-0.5 hover:translate-y-0.5 hover:shadow-none hover:bg-gray-700 transition-all cursor-pointer\" data-code=\"import%20numpy%20as%20np%0Afrom%20sentence_transformers%20import%20CrossEncoder%20%20%23%20sentence-transformers%206.1.0%2C%20rilis%2018%20Sep%202026%0A%0Areranker%20%3D%20CrossEncoder(%22BAAI%2Fbge-reranker-v2-m3%22)%0A%0Aquery%20%3D%20%22gimana%20cara%20fix%20error%20ERR_CONN_REFUSED_504%20di%20gateway%3F%22%0Apairs%20%3D%20%5B%5Bquery%2C%20doc%5D%20for%20doc%20in%20candidate_documents%5D%20%20%23%20(kueri%2C%20dokumen)%20dinilai%20BARENGAN%0Ascores%20%3D%20reranker.predict(pairs)%0A%0Aorder%20%3D%20np.argsort(scores)%5B%3A%3A-1%5D%5B%3A3%5D%20%20%23%20ambil%203%20chunk%20teratas%0Afinal_context_chunks%20%3D%20%5Bcandidate_documents%5Bi%5D%20for%20i%20in%20order%5D\" title=\"Salin kode\">\n      \u003Csvg class=\"w-3.5 h-3.5 copy-icon-svg\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n        \u003Cpath stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z\">\u003C\u002Fpath>\n      \u003C\u002Fsvg>\n      \u003Cspan class=\"copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block-content p-4 overflow-x-auto text-sm font-mono leading-relaxed bg-[#24292e]\">\n    \u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> numpy \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">as\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> np\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">from\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> sentence_transformers \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">import\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> CrossEncoder  \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\"># sentence-transformers 6.1.0, rilis 18 Sep 2026\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">reranker \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> CrossEncoder(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">\"BAAI\u002Fbge-reranker-v2-m3\"\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">query \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> \"gimana cara fix error ERR_CONN_REFUSED_504 di gateway?\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">pairs \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [[query, doc] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> doc \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> candidate_documents]  \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\"># (kueri, dokumen) dinilai BARENGAN\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">scores \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> reranker.predict(pairs)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">order \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> np.argsort(scores)[::\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">][:\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">]  \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D\"># ambil 3 chunk teratas\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">final_context_chunks \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> [candidate_documents[i] \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">for\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> i \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">in\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> order]\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\u003Cp>Satu catatan penting soal \u003Ccode>scores\u003C\u002Fcode> 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 \u003Cstrong>mengurutkan\u003C\u002Fstrong>, jangan buat ambang batas tetap seperti &quot;buang kalau skornya kamu lihat di bawah 0,5&quot;. Ambang seperti itu cuma bikin \u003Cem>chunk\u003C\u002Fem> bagus ikut terbuang di kueri yang skornya kebetulan rendah.\u003C\u002Fp>\n\u003Ch2 id=\"jadi-kalau-rag-mu-ngawur-sekarang\">Jadi, kalau RAG-mu ngawur sekarang\u003C\u002Fh2>\n\u003Cp>Kalau \u003Cem>agent\u003C\u002Fem>-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 \u003Cem>retrieval\u003C\u002Fem> yang menyodorkan informasi sampah — \u003Cem>garbage in, garbage out\u003C\u002Fem>. ⚠️\u003C\u002Fp>\n\u003Cp>Kita urutkan perbaikannya, dari yang paling murah:\u003C\u002Fp>\n\u003Col>\n\u003Cli>Pasang BM25 di samping \u003Cem>vector store\u003C\u002Fem>-mu, dan jalankan dua pencarian itu berbarengan.\u003C\u002Fli>\n\u003Cli>Lebur hasilnya pakai RRF. Nggak perlu latih ulang apa pun, dan nggak ada bobot yang harus kamu setel manual.\u003C\u002Fli>\n\u003Cli>Tutup dengan cross-encoder re-ranker yang menyaring 3-5 \u003Cem>chunk\u003C\u002Fem> terakhir.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Kalau kamu juga sedang mengurus memori \u003Cem>agent\u003C\u002Fem> yang berubah sepanjang waktu, masalah \u003Cem>retrieval\u003C\u002Fem> ini cuma satu dari dua lapisannya. Lapisan waktu dan kontradiksi fakta kita bedah terpisah di \u003Ca href=\"\u002Fp\u002Fmemori-ai-agent-vector-store-ke-temporal-graph\">Memori AI Agent: dari Vector Store ke Temporal Graph\u003C\u002Fa>.\u003C\u002Fp>\n\u003Cp>Titik butanya bukan di model \u003Cem>embedding\u003C\u002Fem> 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.\u003C\u002Fp>\n","\u003Cul>\n\u003Cli>\u003Cem>Cosine similarity\u003C\u002Fem> 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.\u003C\u002Fli>\n\u003Cli>Hybrid search menjalankan dua pencari sekaligus — \u003Cem>dense vector\u003C\u002Fem> buat kemiripan makna, BM25 buat kecocokan kata — lalu menyatukan hasilnya.\u003C\u002Fli>\n\u003Cli>\u003Cem>Reciprocal Rank Fusion\u003C\u002Fem> (RRF) melebur dua daftar peringkat tanpa peduli skala skornya, karena yang dihitung cuma posisi.\u003C\u002Fli>\n\u003Cli>\u003Cem>Two-stage retrieval\u003C\u002Fem>: tahap pertama ambil 50-100 kandidat cepat, tahap kedua saring 3-5 \u003Cem>chunk\u003C\u002Fem> pakai \u003Cem>cross-encoder\u003C\u002Fem> yang menilai kueri dan dokumen berbarengan.\u003C\u002Fli>\n\u003Cli>Kalau jawaban \u003Cem>agent\u003C\u002Fem>-mu ngawur padahal datanya ada, tersangka pertamanya fase \u003Cem>retrieval\u003C\u002Fem>, bukan model LLM-nya.\u003C\u002Fli>\n\u003C\u002Ful>\n",[143,145,147,149,160,169],{"id":49,"title":50,"slug":51,"excerpt":52,"thumbnail_image":48,"tags":144},[55,56,57,58,59,60],{"id":74,"title":75,"slug":76,"excerpt":77,"thumbnail_image":48,"tags":146},[79,80,81,82],{"id":84,"title":85,"slug":86,"excerpt":87,"thumbnail_image":48,"tags":148},[89,90,91,80,81,92],{"id":150,"title":151,"slug":152,"excerpt":153,"thumbnail_image":48,"tags":154},"124","Rahasia Zero-Cost Abstraction di Rust, Dibedah dari Assembly","rahasia-zero-cost-abstraction-di-rust","\"Abstraksi tinggi selalu lebih lambat\" itu anggapan lama. Iterator Rust bisa menyamai loop manual, dan assembly-nya bisa kita baca sendiri. Yuk, kita bongkar!",[155,156,157,158,159],"rust","performance","system-programming","compiler","optimasi",{"id":161,"title":162,"slug":163,"excerpt":164,"thumbnail_image":48,"tags":165},"119","Aturan Sintaks: Satu Titik Koma Bikin Program Nggak Jalan","aturan-sintaks-pemrograman","Baru nulis beberapa baris, programnya udah nolak jalan? Seringnya bukan logikanya yang salah, cuma satu karakter yang kurang atau salah tempat.",[166,167,168,92],"syntax","dasar","konsep",{"id":170,"title":171,"slug":172,"excerpt":173,"thumbnail_image":48,"tags":174},"120","Tiap Nota di Kafe Ini Isinya Pesanan yang Sama","tiap-nota-isinya-pesanan-yang-sama","Nota tiap meja isinya sama semua, dan totalnya Rp0 — padahal tiap meja cuma pesan satu barang. Coba tebak kenapa.",[92,175,176,40],"scope","mutasi",[178,182,186,189,192,195,198,201],{"text":179,"depth":180,"id":181},"Di mana dense vector search kehilangan jejak",2,"di-mana-dense-vector-search-kehilangan-jejak",{"text":183,"depth":184,"id":185},"1. Buta sama kecocokan kata persis",3,"1-buta-sama-kecocokan-kata-persis",{"text":187,"depth":184,"id":188},"2. Terkecoh panjang dokumen dan konteks dangkal","2-terkecoh-panjang-dokumen-dan-konteks-dangkal",{"text":190,"depth":180,"id":191},"Kembali ke algoritma klasik: BM25","kembali-ke-algoritma-klasik-bm25",{"text":193,"depth":180,"id":194},"Menyatukan dua skor beda skala: Reciprocal Rank Fusion","menyatukan-dua-skor-beda-skala-reciprocal-rank-fusion",{"text":196,"depth":180,"id":197},"Lapis kedua: cross-encoder re-ranking","lapis-kedua-cross-encoder-re-ranking",{"text":199,"depth":180,"id":200},"Contoh alurnya di Python","contoh-alurnya-di-python",{"text":202,"depth":180,"id":203},"Jadi, kalau RAG-mu ngawur sekarang","jadi-kalau-rag-mu-ngawur-sekarang",1792386420309]