{"slug": "apakah-pencarian-kode-semantik-benar-benar-mengalahkan-grep-di-repositori", "title": "Apakah pencarian kode semantik benar-benar mengalahkan grep di repositori sungguhan?", "summary": "A developer benchmarked semantic code search against plain keyword search on psf/requests v2.32.3 using ten queries that return zero grep hits, finding that bge-m3 embeddings alone surfaced the correct chunk in the top 5 for 4 of 10 questions while keyword ranking found 6, and a hybrid of both found 7 in the top 10, rising to 9 once tests and documentation were excluded. The pipeline, rebuilt in under 200 lines of Python from osFoundry's folder_index.rs chunking and folder_search.rs hybrid ranking rules, chunked 117 of 129 files into 318 chunks of roughly 700 tokens with 80-token overlap and ran on CPU on an 8 GB M1 Mac. The developer noted that code tokenizes worse than prose, with the median chunk measuring 771 bge-m3 tokens rather than 700 and the longest reaching 2,050.", "body_md": "Demo untuk menjual ide meng-embed sebuah codebase selalu sama: kita bertanya \"di mana kita mengunci pengguna?\", grep tidak menemukan apa-apa karena kodenya menyebutnya cooldown, dan pencarian vektor langsung mendarat di fungsi yang benar. Demo yang bagus. Pertanyaannya, seberapa sering itu berhasil pada kode yang tidak dipilih untuk demo.\n\nBerikut pengukurannya pada repositori yang pernah dibaca kebanyakan developer Python, [psf/requests](https://github.com/psf/requests) pada tag v2.32.3, dengan sepuluh pertanyaan yang jika di-grep dengan frasa yang paling jelas menghasilkan nol baris. Pipeline-nya meniru yang dipakai aplikasi desktop osFoundry untuk indeks folder lokalnya (aturan chunking dari `folder_index.rs`, peringkat hibrida dari `folder_search.rs`), dibangun ulang dalam kurang dari 200 baris Python dan dijalankan di Mac M1 8 GB, hanya CPU. Jawaban singkatnya: embedding saja menemukan chunk yang benar di 5 teratas pada 4 dari 10 pertanyaan. Peringkat kata kunci biasa untuk pertanyaan yang sama menemukan 6. Keduanya digabung menemukan 7 di 10 teratas, dan 9 setelah tes dan dokumentasi tidak lagi bersaing dengan kode sumber.\n\nEmbedding bekerja pada potongan, bukan file. Aplikasi memotong setiap file menjadi chunk sekitar 700 token dengan tumpang tindih 80 token, memperkirakan satu token sebagai 4 karakter, dan menyimpan baris pertama dan terakhir setiap chunk agar sebuah hasil bisa dilaporkan sebagai `path:start-end`.\n\n``` python\nTARGET_TOKENS, OVERLAP_TOKENS = 700, 80\n\ndef chunk_with_lines(text):\n    lines = text.splitlines()\n    target, overlap = TARGET_TOKENS * 4, OVERLAP_TOKENS * 4   # ~4 chars per token\n    out, start = [], 0\n    while start < len(lines):\n        chars, end = 0, start\n        while end < len(lines) and chars < target:\n            chars += len(lines[end]) + 1\n            end += 1\n        body = \"\\n\".join(lines[start:end])\n        if body.strip():\n            out.append((body, start + 1, end))          # 1-based, inclusive\n        if end >= len(lines):\n            break\n        back, idx = 0, end\n        while idx > start and back < overlap:            # step back ~80 tokens\n            idx -= 1\n            back += len(lines[idx]) + 1\n        start = idx if idx > start else end              # always move forward\n    return out\n```\n\nChunk dipotong pada batas baris, tidak pernah di tengah baris, dan pengaman terakhir itu penting: file hasil minify dengan satu baris 200 KB akan terus mundur ke awal yang sama tanpa henti.\n\nPemindaian sebelumnya melewati `node_modules`, `dist`, `build`, `.git`, `.venv` dan folder sejenis, lockfile berdasarkan nama lengkap (`package-lock.json` dan `pnpm-lock.yaml` tidak berakhiran `.lock`, sehingga filter yang hanya melihat ekstensi melewatkannya), file di atas 1 MB, dan apa pun yang punya byte null di 512 byte pertamanya. Pada requests tersisa 117 dari 129 file (10 biner, 1 di atas 1 MB, 1 SVG yang dikecualikan), 625.261 byte, dan 318 chunk.\n\nSatu hal yang tersembunyi di balik aturan 4 karakter: kode ditokenisasi lebih buruk daripada prosa. Diukur dengan tokenizer bge-m3 sendiri, chunk median berukuran 771 token, bukan 700, dan yang terpanjang 2.050. Ini penting untuk langkah berikutnya.\n\n``` python\nfrom sentence_transformers import SentenceTransformer\n\nmodel = SentenceTransformer(\"BAAI/bge-m3\", device=\"cpu\")\nmodel.max_seq_length = 2048\ntexts = [f\"{c['path']}\\n{c['text']}\" for c in chunks]   # the path carries meaning too\nvecs = model.encode(texts, batch_size=2, normalize_embeddings=True)\n```\n\nSkrip dijalankan dengan `uv run`, yang memasang `sentence-transformers` dan `torch` dari metadata inline di bagian atas setiap file. Sebagai pembanding, chunk yang sama juga diproses dengan bge-small-en-v1.5, model Inggris 33 juta parameter yang sekitar 17 kali lebih kecil (kueri-nya diberi awalan yang direkomendasikan modelnya, \"Represent this sentence for searching relevant passages: \").\n\n|  | bge-m3 | bge-small-en-v1.5 | \n|---|---|---|\n| Ukuran vektor | 1.024 | 384 | \n| Input maksimum | 8.192 token (dibatasi 2.048 di sini) | 512 token | \n| Chunk yang terpotong | 1 dari 318 | 235 dari 318 | \n| Waktu embedding 318 chunk (M1, CPU) | 631 s | 20,5 s | \n| Memori puncak | 1,6 GB | tidak diukur | \n| Vektor di disk (float32) | 1,3 MB | 0,49 MB | \n| Embedding satu kueri | 682 ms | 21 ms | \n\nSepuluh menit untuk repositori 625 KB itu lambat, sekitar 2 detik per chunk di mesin ini, dan biayanya mengikuti jumlah kode, bukan jumlah file. Biaya ini hanya sekali: aplikasi menandai chunk dengan hash teksnya, jadi chunk yang tidak berubah mempertahankan vektornya, dan mengedit satu fungsi hanya meng-embed ulang beberapa chunk. Kecepatan model kecil punya konsekuensi yang terlihat jelas di tabel: pada 512 token, model itu hanya membaca bagian awal dari 235 dari 318 chunk.\n\nAda satu aturan yang ditandai dengan penanda merah di kode sumber aplikasi, dan mudah dilanggar di kode sendiri. Kueri harus di-embed oleh model yang sama dengan yang membangun indeks. Vektor chunk bge-m3 dan vektor kueri dari model lain berdimensi 1.024 punya bentuk yang sama, jadi tidak ada error, dan hasilnya sekadar salah.\n\ngrep diberi frasa yang akan kita ketik, tanpa membedakan huruf besar-kecil, pada 117 file yang sama. Sebagai pembanding kata kunci yang lebih adil, seluruh pertanyaan juga dijalankan lewat BM25 pada chunk, dengan pengenal dipecah pada `_` dan camelCase sehingga `should_strip_auth` cocok dengan \"strip\". (Sisi kata kunci aplikasi adalah tantivy pada seluruh file, dipetakan ke chunk-chunk pertama tiap file; peringkat per chunk adalah penyederhanaan.)\n\nKedua peringkat digabung dengan Reciprocal Rank Fusion pada k = 60, konstanta yang sama dengan aplikasi, mengambil 30 teratas dari setiap daftar:\n\n``` python\ndef rrf(lists, k=60, n=30):\n    s = Counter()\n    for lst in lists:\n        for r, i in enumerate(lst[:n]):\n            s[i] += 1 / (k + r + 1)\n    return [i for i, _ in s.most_common()]\n```\n\nRRF mengabaikan skor dan hanya memakai posisi, dan itulah intinya. Skor BM25 dan kemiripan kosinus tidak berada pada skala yang sama, dan menormalkan satu terhadap yang lain menciptakan hubungan yang tidak ada. Chunk di peringkat 3 pada satu daftar dan peringkat 5 pada daftar lain mengalahkan chunk di peringkat 1 pada satu daftar dan tidak muncul di daftar lainnya.\n\nJawaban untuk setiap pertanyaan adalah chunk yang berisi baris `def` fungsi target, dipilih dengan membaca kode sebelum menjalankan apa pun. Angka yang dilaporkan adalah peringkat chunk itu di antara semua 318.\n\n| Pertanyaan | frasa grep | satu kata grep | BM25 | bge-m3 | Hibrida | bge-small | \n|---|---|---|---|---|---|---|\n| jangan kirim password ke situs lain setelah redirect ( `should_strip_auth` ) | \"another site\": 0 | password: 47 | 9 | 1 | 1 | 1 | \n| kenapa POST berubah jadi GET setelah redirect ( `rebuild_method` ) | \"turn into\": 0 | GET: 625 | 65 | 10 | 19 | 6 | \n| berhenti mengikuti redirect yang berputar-putar ( `resolve_redirects` ) | \"redirect loop\": 0 | loop: 6 | 5 | 28 | 5 | 14 | \n| kembalikan unggahan ke awal agar bisa dikirim lagi ( `rewind_body` ) | \"back to the start\": 0 | start: 54 | 1 | 4 | 2 | 20 | \n| seberapa besar body yang akan kita kirim ( `super_len` ) | \"how big\": 0 | big: 8 | 54 | 65 | tidak ada | 233 | \n| lewati proxy untuk mesin di jaringan lokal ( `should_bypass_proxies` ) | \"local network\": 0 | local: 94 | 2 | 20 | 6 | 10 | \n| pastikan file tidak pernah tertinggal setengah tertulis jika proses mati ( `atomic_open` ) | \"half-written\": 0 | half: 4 | 58 | 16 | 29 | 1 | \n| bedakan UTF-16 dan UTF-32 dari byte pertama JSON ( `guess_json_utf` ) | \"first bytes\": 0 | bytes: 98 | 4 | 4 | 5 | 111 | \n| baca login tersimpan dari file di folder home ( `get_netrc_auth` ) | \"home folder\": 0 | login: 4 | 1 | 112 | 7 | 17 | \n| jawab 401 dari server dengan password yang di-hash ( `handle_401` ) | \"hashed password\": 0 | hash: 17 | 1 | 2 | 1 | 1 | \n| `raise_for_status` | 15 |  | 2 | 70 | 12 | 244 | \n| `UnrewindableBodyError` | 9 |  | 1 | 2 | 2 | 6 | \n\nPeringkat dari 318 chunk; angka grep adalah jumlah baris yang cocok. \"Hibrida\" adalah BM25 yang digabung dengan bge-m3.\n\nAda tiga hal yang menonjol.\n\nFrasa yang menghasilkan nol di grep tidak berarti kata kunci tidak berguna. Diurutkan berdasarkan kata-kata dalam pertanyaan, BM25 menempatkan jawaban di 5 teratas untuk 6 dari 10. \"Redirect\", \"proxy\", \"401\" dan \"UTF-16\" semuanya ada di kode; hanya frasa di sekitarnya yang tidak cocok. Grep satu kata menunjukkan masalah sebaliknya: 625 baris untuk GET, 98 untuk bytes. Jawabannya ada di suatu tempat dalam tumpukan yang lalu harus dibaca manual.\n\nKemenangan embedding itu nyata tetapi spesifik. bge-m3 menemukan `should_strip_auth` di peringkat pertama untuk pertanyaan yang tidak punya kata yang sama dengannya, dan memindahkan `rebuild_method` dari peringkat 65 di BM25 ke peringkat 10. Model kecil menempatkan `atomic_open` di peringkat pertama untuk \"never left half-written if the process dies\", sedangkan BM25 menempatkannya di peringkat 58; docstring fungsi itu berbunyi \"atomic fashion\", dan modelnya tahu arti atomik untuk sebuah file. Dan vektor gagal dengan cara yang tidak dialami kata kunci. Pada pengenal polos `raise_for_status`, bge-m3 menempatkan chunk definisinya di peringkat 70, dan bge-small di peringkat 244.\n\nPenggabungan adalah pilihan bawaan yang lebih aman, bukan yang terbaik di setiap baris. Selain satu pertanyaan yang tidak ditemukan oleh siapa pun, hibrida tidak pernah menjadi kolom terburuk dan biasanya dekat dengan yang terbaik, yang memang diinginkan dari alat yang dipanggil tanpa tahu sebelumnya jenis pertanyaannya. \"How big is the body\" gagal di mana-mana: `super_len` tidak pernah menyebutkan apa yang diukurnya dengan kata-kata yang akan dipakai dalam pertanyaan.\n\n| Hasil tepat di 5 / 10 teratas (10 pertanyaan) | BM25 | bge-m3 | Hibrida (bge-m3) | bge-small | Hibrida (bge-small) | \n|---|---|---|---|---|---|\n| Seluruh repo, 318 chunk | 6 / 7 | 4 / 5 | 5 / 7 | 3 / 5 | 7 / 7 | \n| Hanya `src/` , 84 chunk | 7 / 7 | 5 / 7 | 7 / 9 | 5 / 7 | 8 / 8 | \n\nSebagian besar kegagalan bukan kebetulan. Untuk \"why does a POST turn into a GET\", hasil pertama bge-m3 adalah `tests/test_requests.py`, di sekitar `test_http_303_changes_post_to_get`. Untuk \"read saved logins from a file in the home folder\" hasilnya adalah entri changelog tentang `.netrc` di `HISTORY.md`, dan untuk \"move the upload back to the start\" maupun \"how big is the body\" sebuah bagian dari `docs/user/advanced.rst`. Tes dan dokumentasi menggambarkan perilaku dengan kata-kata biasa, persis seperti sebuah pertanyaan, sehingga sering lebih dekat dengannya daripada implementasinya.\n\nItu bukan jawaban yang salah, karena tes itu menyebut nama fungsinya. Tetapi peringkat berubah banyak setelah keduanya disaring. Mencari hanya di 84 chunk di bawah `src/` dengan vektor yang sama, pencarian hibrida menemukan 9 dari 10 di 10 teratas. Perbaikan paling murah di repo ini bukan model yang lebih besar, melainkan filter path, atau setidaknya memberi label pada hasil tes dan dokumentasi agar bisa dilewati.\n\nModel yang lebih kecil tetap mengimbangi meski hanya membaca 512 token pertama dari sebagian besar chunk, mungkin karena baris-baris awal sebuah chunk, dengan `def` dan docstring-nya, memuat sebagian besar isi chunk itu. Sepuluh pertanyaan pada satu repositori tidak cukup untuk menyatakan salah satu model lebih baik. Cukup untuk melihat bahwa pilihan model kurang berpengaruh dibanding menggabungkannya dengan kata kunci dan menyingkirkan tes.\n\n```\ngit clone --depth 1 --branch v2.32.3 https://github.com/psf/requests.git\npython3 chunk.py requests chunks.json\nuv run embed.py chunks.json BAAI/bge-m3 vec-m3.npy\nuv run search.py chunks.json vec-m3.npy BAAI/bge-m3 queries.json\nSCOPE=src/ uv run search.py chunks.json vec-m3.npy BAAI/bge-m3 queries.json\n```\n\nMenjalankan `embed.py` pertama kali mengunduh bge-m3 dari Hugging Face, sekitar 2,3 GB. Tulis kueri sebelum melihat hasil, dan pilih jawabannya dengan membaca kodenya lebih dulu. Sangat mudah untuk terus menyesuaikan kata-kata sebuah pertanyaan sampai pencarian vektor menang.\n\nAplikasi desktop [osFoundry](https://osfoundry.io/id) menjalankan pipeline yang sama dari layar Indexes-nya, dengan bge-m3 di perangkat dan BM25 serta vektor digabung dalam alat agen `folder_search`. [Entri chunking di glosariumnya](https://osfoundry.io/id/glossary/chunking) membahas pertimbangan ukuran dan tumpang tindih chunk, dan aplikasinya gratis dari [halaman unduhan osFoundry](https://osfoundry.io/id/download).", "url": "https://wpnews.pro/news/apakah-pencarian-kode-semantik-benar-benar-mengalahkan-grep-di-repositori", "canonical_source": "https://dev.to/bestaitools-id/apakah-pencarian-kode-semantik-benar-benar-mengalahkan-grep-di-repositori-sungguhan-5d6b", "published_at": "2026-11-06 18:47:33+00:00", "updated_at": "2026-11-06 18:56:21.741135+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-search", "machine-learning"], "entities": ["psf/requests", "osFoundry", "BAAI/bge-m3", "bge-small-en-v1.5", "sentence-transformers", "PyTorch"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/apakah-pencarian-kode-semantik-benar-benar-mengalahkan-grep-di-repositori", "markdown": "https://wpnews.pro/news/apakah-pencarian-kode-semantik-benar-benar-mengalahkan-grep-di-repositori.md", "text": "https://wpnews.pro/news/apakah-pencarian-kode-semantik-benar-benar-mengalahkan-grep-di-repositori.txt", "jsonld": "https://wpnews.pro/news/apakah-pencarian-kode-semantik-benar-benar-mengalahkan-grep-di-repositori.jsonld"}}