Apakah pencarian kode semantik benar-benar mengalahkan grep di repositori sungguhan? 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. 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. Berikut 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. Embedding 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 . python TARGET TOKENS, OVERLAP TOKENS = 700, 80 def chunk with lines text : lines = text.splitlines target, overlap = TARGET TOKENS 4, OVERLAP TOKENS 4 ~4 chars per token out, start = , 0 while start < len lines : chars, end = 0, start while end < len lines and chars < target: chars += len lines end + 1 end += 1 body = "\n".join lines start:end if body.strip : out.append body, start + 1, end 1-based, inclusive if end = len lines : break back, idx = 0, end while idx start and back < overlap: step back ~80 tokens idx -= 1 back += len lines idx + 1 start = idx if idx start else end always move forward return out Chunk 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. Pemindaian 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. Satu 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. python from sentence transformers import SentenceTransformer model = SentenceTransformer "BAAI/bge-m3", device="cpu" model.max seq length = 2048 texts = f"{c 'path' }\n{c 'text' }" for c in chunks the path carries meaning too vecs = model.encode texts, batch size=2, normalize embeddings=True Skrip 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: " . | | bge-m3 | bge-small-en-v1.5 | |---|---|---| | Ukuran vektor | 1.024 | 384 | | Input maksimum | 8.192 token dibatasi 2.048 di sini | 512 token | | Chunk yang terpotong | 1 dari 318 | 235 dari 318 | | Waktu embedding 318 chunk M1, CPU | 631 s | 20,5 s | | Memori puncak | 1,6 GB | tidak diukur | | Vektor di disk float32 | 1,3 MB | 0,49 MB | | Embedding satu kueri | 682 ms | 21 ms | Sepuluh 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. Ada 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. grep 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. Kedua peringkat digabung dengan Reciprocal Rank Fusion pada k = 60, konstanta yang sama dengan aplikasi, mengambil 30 teratas dari setiap daftar: python def rrf lists, k=60, n=30 : s = Counter for lst in lists: for r, i in enumerate lst :n : s i += 1 / k + r + 1 return i for i, in s.most common RRF 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. Jawaban 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. | Pertanyaan | frasa grep | satu kata grep | BM25 | bge-m3 | Hibrida | bge-small | |---|---|---|---|---|---|---| | jangan kirim password ke situs lain setelah redirect should strip auth | "another site": 0 | password: 47 | 9 | 1 | 1 | 1 | | kenapa POST berubah jadi GET setelah redirect rebuild method | "turn into": 0 | GET: 625 | 65 | 10 | 19 | 6 | | berhenti mengikuti redirect yang berputar-putar resolve redirects | "redirect loop": 0 | loop: 6 | 5 | 28 | 5 | 14 | | kembalikan unggahan ke awal agar bisa dikirim lagi rewind body | "back to the start": 0 | start: 54 | 1 | 4 | 2 | 20 | | seberapa besar body yang akan kita kirim super len | "how big": 0 | big: 8 | 54 | 65 | tidak ada | 233 | | lewati proxy untuk mesin di jaringan lokal should bypass proxies | "local network": 0 | local: 94 | 2 | 20 | 6 | 10 | | pastikan file tidak pernah tertinggal setengah tertulis jika proses mati atomic open | "half-written": 0 | half: 4 | 58 | 16 | 29 | 1 | | bedakan UTF-16 dan UTF-32 dari byte pertama JSON guess json utf | "first bytes": 0 | bytes: 98 | 4 | 4 | 5 | 111 | | baca login tersimpan dari file di folder home get netrc auth | "home folder": 0 | login: 4 | 1 | 112 | 7 | 17 | | jawab 401 dari server dengan password yang di-hash handle 401 | "hashed password": 0 | hash: 17 | 1 | 2 | 1 | 1 | | raise for status | 15 | | 2 | 70 | 12 | 244 | | UnrewindableBodyError | 9 | | 1 | 2 | 2 | 6 | Peringkat dari 318 chunk; angka grep adalah jumlah baris yang cocok. "Hibrida" adalah BM25 yang digabung dengan bge-m3. Ada tiga hal yang menonjol. Frasa 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. Kemenangan 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. Penggabungan 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. | Hasil tepat di 5 / 10 teratas 10 pertanyaan | BM25 | bge-m3 | Hibrida bge-m3 | bge-small | Hibrida bge-small | |---|---|---|---|---|---| | Seluruh repo, 318 chunk | 6 / 7 | 4 / 5 | 5 / 7 | 3 / 5 | 7 / 7 | | Hanya src/ , 84 chunk | 7 / 7 | 5 / 7 | 7 / 9 | 5 / 7 | 8 / 8 | Sebagian 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. Itu 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. Model 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. git clone --depth 1 --branch v2.32.3 https://github.com/psf/requests.git python3 chunk.py requests chunks.json uv run embed.py chunks.json BAAI/bge-m3 vec-m3.npy uv run search.py chunks.json vec-m3.npy BAAI/bge-m3 queries.json SCOPE=src/ uv run search.py chunks.json vec-m3.npy BAAI/bge-m3 queries.json Menjalankan 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. Aplikasi 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 .