[{"data":1,"prerenderedAt":359},["ShallowReactive",2],{"tag-labels":3,"site-header":4,"article-kenapa-rag-cuma-cosine-similarity-sering-nyasar-id":72},{},{"featuredPost":5,"navigations":29},{"id":6,"title":7,"title_en":8,"slug":9,"excerpt":10,"excerpt_en":11,"has_translation":12,"published_at":13,"reading_time":14,"hidden_from_listing":15,"tags":16,"thumbnail_image":21,"og_image":22,"series":23,"authors":28},110,"Ketika Pilih Framework Cuma Biar Kelihatan Beda","When You Pick a Framework Just to Look Different","ketika-pilih-framework-cuma-biar-kelihatan-beda","Terakhir kali kamu milih framework atau library baru, itu karena masalahmu butuh alat itu, atau karena alatnya kedengaran keren dan belum banyak yang pakai? Pertanyaan ini nggak enak dijawab. Soalnya keputusan teknologi sering diambil dengan alasan yang nggak pernah ditulis di dokumen teknis mana pun.","The last time you chose a new framework or library, was it because your problem needed that tool, or because it sounded cool and few people around you were using it? That question is uncomfortable. Technology decisions are often made for reasons that never appear in any technical document.",true,"2026-10-06T15:25:21.000Z",5,false,[17,18,19,20],"karier","framework","opini","pengalaman",null,"\u002Fog\u002Fketika-pilih-framework-cuma-biar-kelihatan-beda.png",{"id":24,"title":25,"title_en":26,"slug":27},9,"Artikel","Articles","all",[],[30,46,67],{"collection":31,"item":32},"series",{"__typename":31,"title":25,"title_en":26,"slug":27,"children_series":33},[34,38,42],{"title":35,"title_en":36,"slug":37},"Kecerdasan Artifisial","Artificial Intelligence","kecerdasan-artifisial",{"title":39,"title_en":40,"slug":41},"Konsep Dasar Pemrograman","Programming Fundamentals","konsep-dasar-pemrograman",{"title":43,"title_en":44,"slug":45},"Algoritma dan Struktur Data","Algorithms and Data Structures","algoritma-struktur-data",{"collection":31,"item":47},{"__typename":31,"title":48,"title_en":48,"slug":49,"children_series":50},"Tutorial","tutorial",[51,55,59,63],{"title":52,"title_en":53,"slug":54},"Belajar Python","Learning Python","belajar-python",{"title":56,"title_en":57,"slug":58},"Belajar Golang","Learning Go","belajar-golang",{"title":60,"title_en":61,"slug":62},"Belajar Java","Learning Java","belajar-java",{"title":64,"title_en":65,"slug":66},"Belajar Pascal","Learning Pascal","belajar-pascal",{"collection":31,"item":68},{"__typename":31,"title":69,"title_en":69,"slug":70,"children_series":71},"Lab","lab",[],{"article":73,"renderedBody":265,"renderedTakeaway":266,"latestArticles":267,"toc":334},{"id":74,"title":75,"title_en":76,"slug":77,"excerpt":78,"excerpt_en":79,"has_translation":12,"published_at":80,"reading_time":81,"hidden_from_listing":15,"tags":82,"thumbnail_image":21,"og_image":89,"series":90,"authors":257,"body_locale":260,"available_locales":261,"body":263,"takeway":264,"prerequisite":21},127,"Kenapa RAG yang cuma andalkan cosine similarity sering nyasar","Why RAG That Leans on Cosine Similarity Alone Keeps Missing","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.","RAG that leans only on embeddings returns chunks that look close in meaning, not the ones you need. Here is why, and the two layers that patch it.","2026-10-19T02:04:06.718Z",6,[83,84,85,86,87,88],"rag","vector-search","bm25","hybrid-search","retrieval","embedding","\u002Fog\u002Fkenapa-rag-cuma-cosine-similarity-sering-nyasar.png",{"id":91,"title":35,"title_en":36,"slug":37,"description":21,"description_en":92,"articles":93,"children_series":249,"parent_series":250},8,"Notes on AI that go past the demo: how the models work, where they break, and what changes once you put them in production.",[94,111,129,144,158,172,189,206,211,230],{"id":95,"title":96,"title_en":97,"slug":98,"excerpt":99,"excerpt_en":100,"has_translation":12,"published_at":101,"reading_time":102,"hidden_from_listing":15,"tags":103,"thumbnail_image":21,"og_image":108,"series":109,"authors":110},1,"Kenapa AI Suka 'Halu'? Kenalan sama JEPA, Otak AI yang Punya Akal Sehat","Why Does AI Keep Hallucinating? Meet JEPA, the AI Brain With Common Sense","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.","Generative AI hallucinates because all it does is guess word patterns. JEPA tries to change that by teaching machines how the real world works, the way a human brain does.","2026-06-02T08:09:05.595Z",4,[104,105,106,107],"jepa","artificial-intelligence","yann-lecun","agi","\u002Fog\u002Fkenapa-ai-suka-halu-kenalan-sama-jepa-otak-ai-yang-punya-akal-sehat.png",{"id":91,"title":35,"title_en":36,"slug":37},[],{"id":112,"title":113,"title_en":114,"slug":115,"excerpt":116,"excerpt_en":117,"has_translation":12,"published_at":118,"reading_time":102,"hidden_from_listing":15,"tags":119,"thumbnail_image":21,"og_image":121,"series":122,"authors":123},2,"LLM Bakal Mentok, Terus Gimana Dong?","LLMs Are Going to Hit a Wall. What Then?","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.","LLMs like ChatGPT are great at stringing words together, but they never really grasp reality. Yann LeCun says this architecture will hit a wall and needs a new answer.","2026-06-03T08:55:33.003Z",[105,120,106,104],"llm","\u002Fog\u002Fllm-bakal-mentok-terus-gimana-dong.png",{"id":91,"title":35,"title_en":36,"slug":37},[124],{"authors_id":125},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},"Daffa Izzuddin","izzudd","Writer",{"id":130,"title":131,"title_en":132,"slug":133,"excerpt":134,"excerpt_en":135,"has_translation":12,"published_at":136,"reading_time":102,"hidden_from_listing":15,"tags":137,"thumbnail_image":21,"og_image":139,"series":140,"authors":141},3,"Mengenal JEPA: Saat AI Mulai Belajar Punya \"Akal Sehat\"","Meet JEPA: Where AI Starts Learning Common Sense","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.","AI hallucinates because it only guesses data probabilities without understanding reality. The JEPA architecture tries to change that by building a world model in its head.","2026-06-03T09:32:50.446Z",[105,104,138,106],"world-models","\u002Fog\u002Fkenapa-ai-suka-halu-dan-gimana-jepa-beresin-masalah-ini.png",{"id":91,"title":35,"title_en":36,"slug":37},[142],{"authors_id":143},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":102,"title":145,"title_en":146,"slug":147,"excerpt":148,"excerpt_en":149,"has_translation":12,"published_at":150,"reading_time":102,"hidden_from_listing":15,"tags":151,"thumbnail_image":21,"og_image":153,"series":154,"authors":155},"Bukan Sekadar Generative AI, JEPA Coba Kejar Pemahaman Konsep ala Manusia","Beyond Generative AI: JEPA Chases Human-Style Conceptual Understanding","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.","AI usually only guesses pixels, while JEPA tries to learn from abstract concepts in latent space. That approach turns out to reach as far as reading brain waves.","2026-06-03T09:34:14.840Z",[104,106,152,107],"machine-learning","\u002Fog\u002Fkenapa-ai-masih-gagal-paham-fisika-dasar-mengenal-arsitektur-jepa.png",{"id":91,"title":35,"title_en":36,"slug":37},[156],{"authors_id":157},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":14,"title":159,"title_en":160,"slug":161,"excerpt":162,"excerpt_en":163,"has_translation":12,"published_at":164,"reading_time":102,"hidden_from_listing":15,"tags":165,"thumbnail_image":21,"og_image":167,"series":168,"authors":169},"Di Balik Dapur JEPA: Rahasia EMA, VICReg, dan Efisiensi Komputasi","Inside JEPA's Kitchen: The Secrets of EMA, VICReg, and Compute Efficiency","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!","Bored of AI that only guesses words or memorizes pixels? Meet JEPA, an architecture that teaches machines common sense very cheaply. Here is a look at its technical insides.","2026-06-07T16:14:13.723Z",[104,166],"artificial-intelligent","\u002Fog\u002Fdi-balik-dapur-jepa-rahasia-ema-vicreg-dan-efisiensi-komputasi.png",{"id":91,"title":35,"title_en":36,"slug":37},[170],{"authors_id":171},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":173,"title":174,"title_en":175,"slug":176,"excerpt":177,"excerpt_en":178,"has_translation":12,"published_at":179,"reading_time":91,"hidden_from_listing":15,"tags":180,"thumbnail_image":21,"og_image":186,"series":187,"authors":188},125,"MCP dibongkar ulang: kabar baik buat kamu yang capek nulis konektor","MCP, Rebuilt: Good News If You're Tired of Writing Connectors","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. 🔧","You have a few internal APIs and one AI agent, but the code has to be rewritten every time the model changes. MCP ends that, and this year the protocol was rebuilt so servers can run like ordinary web servers. Here's what changed. 🔧","2026-10-16T05:00:00.000Z",[181,182,183,120,184,185],"mcp","model-context-protocol","ai-agent","backend","python","\u002Fog\u002Fmcp-dibongkar-ulang-kabar-baik-buat-kamu.png",{"id":91,"title":35,"title_en":36,"slug":37},[],{"id":190,"title":191,"title_en":192,"slug":193,"excerpt":194,"excerpt_en":195,"has_translation":12,"published_at":196,"reading_time":197,"hidden_from_listing":15,"tags":198,"thumbnail_image":21,"og_image":201,"series":202,"authors":203},126,"Memori AI Agent: dari Vector Store ke Temporal Graph","AI Agent Memory: From Vector Store to 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.","Two contradictory facts make an agent answer nonsense. Here is how a vector store and a temporal knowledge graph differ at keeping long-term state.","2026-10-18T13:13:29.458Z",7,[199,120,184,200],"ai","system-design","\u002Fog\u002Fmemori-ai-agent-vector-store-ke-temporal-graph.png",{"id":91,"title":35,"title_en":36,"slug":37},[204],{"authors_id":205},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":74,"title":75,"title_en":76,"slug":77,"excerpt":78,"excerpt_en":79,"has_translation":12,"published_at":80,"reading_time":81,"hidden_from_listing":15,"tags":82,"thumbnail_image":21,"og_image":89,"series":207,"authors":208},{"id":91,"title":35,"title_en":36,"slug":37},[209],{"authors_id":210},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":212,"title":213,"title_en":214,"slug":215,"excerpt":216,"excerpt_en":217,"has_translation":12,"published_at":218,"reading_time":81,"hidden_from_listing":15,"tags":219,"thumbnail_image":21,"og_image":225,"series":226,"authors":227},128,"Linear atau graf: cara mengatur banyak agent tanpa loop abadi","Linear or Graph: How to Run Many Agents Without an Eternal Loop","multi-agen-linear-atau-grafik","Satu agent dengan dua puluh tool gampang kacau. Begitu beberapa agent saling revisi, kamu butuh graf yang mendukung cycle dan angka yang membuatnya berhenti.","One agent with twenty tools gets messy fast. Once several agents revise each other, you need a graph that allows cycles and a number that makes it stop.","2026-10-21T16:05:00.000Z",[220,221,222,223,224,120],"multi-agent","langgraph","autogen","orkestrasi","checkpoint","\u002Fog\u002Fmulti-agen-linear-atau-grafik.png",{"id":91,"title":35,"title_en":36,"slug":37},[228],{"authors_id":229},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":231,"title":232,"title_en":233,"slug":234,"excerpt":235,"excerpt_en":236,"has_translation":12,"published_at":237,"reading_time":14,"hidden_from_listing":15,"tags":238,"thumbnail_image":21,"og_image":244,"series":245,"authors":246},129,"Biar RAG nggak kena 504: pisahin jalur query dan ingestion","So Your RAG Stops Hitting 504: Split the Query Path From Ingestion","rag-pisah-jalur-query-dan-ingestion","Satu service RAG yang merangkap parsing dan menjawab kueri bakal tumbang pas ada yang upload PDF 200 halaman. Obatnya dua jalur dan satu queue.","One RAG service that both parses uploads and answers queries will fall over the moment someone uploads a 200-page PDF. The cure is two paths and one queue.","2026-10-22T01:30:00.000Z",[83,239,240,241,242,243],"cloud-run","cloud-tasks","gcp","serverless","arsitektur","\u002Fog\u002Frag-pisah-jalur-query-dan-ingestion.png",{"id":91,"title":35,"title_en":36,"slug":37},[247],{"authors_id":248},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},[],{"id":24,"title":25,"title_en":26,"slug":27,"parent_series":21,"children_series":251},[252,253,255],{"id":91,"title":35,"title_en":36,"slug":37,"cover_image":21},{"id":254,"title":39,"title_en":40,"slug":41,"cover_image":21},12,{"id":256,"title":43,"title_en":44,"slug":45,"cover_image":21},15,[258],{"authors_id":259},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},"id",[262,260],"en","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","- *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.","\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",[268,285,298,311,324,329],{"id":269,"title":270,"title_en":21,"slug":271,"excerpt":272,"excerpt_en":21,"has_translation":15,"published_at":273,"reading_time":81,"hidden_from_listing":15,"tags":274,"thumbnail_image":21,"og_image":276,"series":277,"authors":282},133,"Python Strings: the Badge That Only Worked for One Name","python-strings","The badge script printed the right name for the person who wrote it and the wrong name for everybody else. Slicing counts characters, and a name is words.","2026-10-28T13:00:00.000Z",[185,275],"string","\u002Fog\u002Fpython-strings.png",{"id":278,"title":279,"title_en":280,"slug":281},30,"Koleksi dan String","Collections and Strings","python-collections",[283],{"authors_id":284},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":286,"title":287,"title_en":21,"slug":288,"excerpt":289,"excerpt_en":21,"has_translation":15,"published_at":290,"reading_time":81,"hidden_from_listing":15,"tags":291,"thumbnail_image":21,"og_image":293,"series":294,"authors":295},132,"Python Tuples: Why All Three Points Are the Same Point","python-tuples","Three points walk into a loop and come out identical. The list was never copied three times; the name was. A tuple makes that bug impossible.","2026-10-28T12:00:00.000Z",[185,292],"tuple","\u002Fog\u002Fpython-tuples.png",{"id":278,"title":279,"title_en":280,"slug":281},[296],{"authors_id":297},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":299,"title":300,"title_en":21,"slug":301,"excerpt":302,"excerpt_en":21,"has_translation":15,"published_at":303,"reading_time":81,"hidden_from_listing":15,"tags":304,"thumbnail_image":21,"og_image":306,"series":307,"authors":308},131,"Python Sets: the Duplicate That Keeps Coming Back","python-sets","A de-duplication loop that works on clean data leaves four tags where there should be two. The bug is case, and the fix is a `set`.","2026-10-28T11:00:00.000Z",[185,305],"set","\u002Fog\u002Fpython-sets.png",{"id":278,"title":279,"title_en":280,"slug":281},[309],{"authors_id":310},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":312,"title":313,"title_en":21,"slug":314,"excerpt":315,"excerpt_en":21,"has_translation":15,"published_at":316,"reading_time":81,"hidden_from_listing":15,"tags":317,"thumbnail_image":21,"og_image":319,"series":320,"authors":321},130,"Python Dictionaries: the Key That Isn't There","python-dictionary","A word counter dies the first time it meets a word twice. The fix is one line; what takes longer is knowing which of a dict's three lookups to reach for.","2026-10-28T10:00:00.000Z",[185,318],"dictionary","\u002Fog\u002Fpython-dictionary.png",{"id":278,"title":279,"title_en":280,"slug":281},[322],{"authors_id":323},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":231,"title":232,"title_en":233,"slug":234,"excerpt":235,"excerpt_en":236,"has_translation":12,"published_at":237,"reading_time":14,"hidden_from_listing":15,"tags":238,"thumbnail_image":21,"og_image":244,"series":325,"authors":326},{"id":91,"title":35,"title_en":36,"slug":37},[327],{"authors_id":328},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},{"id":212,"title":213,"title_en":214,"slug":215,"excerpt":216,"excerpt_en":217,"has_translation":12,"published_at":218,"reading_time":81,"hidden_from_listing":15,"tags":219,"thumbnail_image":21,"og_image":225,"series":330,"authors":331},{"id":91,"title":35,"title_en":36,"slug":37},[332],{"authors_id":333},{"id":95,"name":126,"slug":127,"role":128,"profile_picture":21},[335,338,341,344,347,350,353,356],{"text":336,"depth":112,"id":337},"Di mana dense vector search kehilangan jejak","di-mana-dense-vector-search-kehilangan-jejak",{"text":339,"depth":130,"id":340},"1. Buta sama kecocokan kata persis","1-buta-sama-kecocokan-kata-persis",{"text":342,"depth":130,"id":343},"2. Terkecoh panjang dokumen dan konteks dangkal","2-terkecoh-panjang-dokumen-dan-konteks-dangkal",{"text":345,"depth":112,"id":346},"Kembali ke algoritma klasik: BM25","kembali-ke-algoritma-klasik-bm25",{"text":348,"depth":112,"id":349},"Menyatukan dua skor beda skala: Reciprocal Rank Fusion","menyatukan-dua-skor-beda-skala-reciprocal-rank-fusion",{"text":351,"depth":112,"id":352},"Lapis kedua: cross-encoder re-ranking","lapis-kedua-cross-encoder-re-ranking",{"text":354,"depth":112,"id":355},"Contoh alurnya di Python","contoh-alurnya-di-python",{"text":357,"depth":112,"id":358},"Jadi, kalau RAG-mu ngawur sekarang","jadi-kalau-rag-mu-ngawur-sekarang",1793295834370]