2. İndeksleme¶
Bir repoyu aranabilir bir şeye çevirmek. Dört adım — ve ilki, diğer üçünün ne kadar iyi olabileceğini belirliyor.
Bu aşamanın sahibi olduğu yerler
index/chunk.py (tree-sitter), index/scrub.py (sırlar ve kişisel veri),
index/embed.py (BGE-M3), index/enrich.py (isteğe bağlı açıklamalar) ve
index/store.py (tek Milvus koleksiyonu).
Bir chunk, bir kod birimidir¶
Çoğu RAG yığınının varsayılan chunker'ı bir karakter penceresidir. Düz metinde bu savunulabilir. Kodda bir fonksiyonu ortasından keser — ve yarım bir fonksiyon, kimsenin bir soruyu cevaplayabileceği bir şey değildir.
Bu yüzden sınırlar gerçek bir parserdan geliyor: tree-sitter, süslü parantez
sayarak değil — çünkü bir string literal içindeki }, bunu içeren ilk dosyada parantez
saymayı bozar.
Dört kural:
- Büyük bir sınıf ya da namespace üyelerine bölünüyor. Başlık ve alanlar kendi chunk'ı oluyor.
- Küçük şeyler bir komşusuyla birleştiriliyor. Tek satırlık bir tip, kısa bir
const, bir import bloğu, bir JSDoc yorumu — hiçbir şey
RAG_CHUNK_MIN_BYTES'ın altına inmiyor. - Hiçbir chunk
RAG_CHUNK_MAX_BYTES'ı aşmıyor (2000 B, kabaca 500 token). BGE-M3 8192 token kabul ediyor ve tuzak tam olarak bu: uzun bir chunk'ın embedding'i bir ortalamaya dönüşür ve hiçbir şeyi iyi temsil etmez. Tek başına tavanı aşan bir fonksiyon, sembol adını koruyan satır pencerelerine bölünüyor. texttemiz kalıyor. LLM'e ve atıfa giden şey o. "Ben neyim, nerede yaşıyorum" başlığı — dosya, sembol, üst sınıf, import'lar — yalnızca embed'lenen ve BM25 ile indekslenen alana,indexed_text'e giriyor.
Yanlış yapılması kolay olan kural 4. Başlığı text'e koymak her atıfı üç satır
metadata ile başlatır; indexed_text'ten çıkarmak ise handle adlı bir metodu diğer kırk
tanesinden ayırt edilemez kılar.
Dil kapsamı bir tablo değil, bir kuraldır¶
# The extension name is the grammar name for most languages: .lua, .vue, .razor, .zig.
# The ones that differ (.ts, .cs, .kt) come from a small alias table validated against
# the pack. Anything without a grammar is still indexed, as plain windows.
CODE_WITHOUT_GRAMMAR: dict[str, str] = {".sql": "sql", ".cob": "cobol", ".cbl": "cobol"}
tree-sitter-language-pack 371 gramer getiriyor. Dil başına kural tablosu yazmak, 371
girdiyi bakımda tutup 372'ncisi konusunda yanılmak demekti; grameri uzantıdan türetmek
ise yeni bir dilin, paket onu desteklediği gün çalışması demek.
İki gramer adıyla dışlanmış durumda, çünkü parser'ı çökertiyor ya da kilitliyorlar:
sql ve cobol. Yine de satır pencereleriyle indeksleniyorlar.
AST chunking gerçekte ne kazandırıyor
Aynı corpus iki kez indekslendi — bir kez tree-sitter'la, bir kez düz pencerelerle; aynı golden set, k=8:
| Chunker | chunk | Recall@8 | MRR | sembol R@8 / MRR | TR metin MRR |
|---|---|---|---|---|---|
| AST ✓ | 2761 | 0.786 | 0.690 | 1.0 / 1.0 | 0.570 |
| düz pencere | 2100 | 0.762 | 0.598 | 0.75 / 0.321 | 0.518 |
Recall bir soru farkediyor. MRR 0.09 fark ediyor ve neredeyse tamamı sembol sorgularında (MRR 0.321 → 1.0). İngilizce düz metinde düz pencere başabaş, hatta bir soru önde.
AST chunking daha fazlasını bulmuyor. Doğru chunk'ı en üste koyuyor ve adını söylüyor. Gramersiz kalan bir dilde kaybedilen şey sıralama ve atıf kalitesi, recall değil — eksik bir gramerin acil durum olmamasının sebebi de bu.
Saklamadan önce temizlik¶
Bir vektör geri çevrilemez. Yanında saklanan content alanı çevrilebilir — her atıfta
birebir dönüyor ve oradan önbelleğe, loglara ve modelin cevaplarına yolculuk ediyor. Bu
yüzden temizlik, indekse hiçbir şey ulaşmadan önce yapılıyor.
Kurallar kasıtlı olarak muhafazakâr ve bu kelimenin arkasında bir sayı var:
Muhafazakâr, kapsamlıyı yener
Gerçek bir repoda, adlara göre karartan bir kural 78 dosyada 247 karartma
üretti ve neredeyse hiçbiri sır değildi: token: text( bir veritabanı kolonu,
secret: string bir tip bildirimi.
Yayınlanan kurallar aynı repoda 2 karartma yaptı. Biri gerçek bir token'dı.
Desenler adları değil biçimleri yakalıyor — sk-, ghp_, xox[baprs]-, AKIA…,
JWT'ler, e-postalar, telefon numaraları — artı yalnızca değer atanmış BÜYÜK_YILAN
biçimli bir anahtarda tetiklenen tek bir ad tabanlı kural:
# DB_PASSWORD=hunter2 is a secret; `promptTokens` and `this.accessToken` are not.
_ASSIGNED_SECRET = re.compile(
r"\b([A-Z][A-Z0-9]*_[A-Z0-9_]*(?:PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|PAT)[A-Z0-9_]*"
r"|(?:PASSWORD|SECRET|TOKEN|APIKEY|API_KEY|PRIVATE_KEY)[A-Z0-9_]*)"
r"(\s*[:=]\s*)"
r"(\"[^\"\n]*\"|\'[^\'\n]*\'|[^\s\"\'#,;]+)"
)
…ve atama gibi görünüp atama olmayanlar için bir dışlama listesi: process.env.X,
${…}, bir tip adı, bir fonksiyon çağrısı, this.…, bir URL şeması ve yer tutucu
sözlüğü (your-, change-me, xxx, example).
Ayrılmış e-posta alan adlarına (example.com, *.test, *.invalid, localhost)
dokunulmuyor, çünkü bir doküman yorumundaki örnek adresi karartmak bilgi siler ve
kimseyi korumaz.
Embedding¶
BGE-M3, 1024 boyut, varsayılan olarak yerelde çalışıyor. İki özelliği önemli.
Çok dilli — bir dildeki sorunun, başka bir dilde yorumlanmış kodu bulabilmesinin tüm sebebi bu. MiniLM ile yapılan önceki deney kontrol grubu: Türkçe düz metin Recall@5 = 0.04 aldı. Aynı sorular BGE-M3'te 0.684 alıyor. Bu bir ayar farkı değil, farklı bir yetenek.
Makinede çalışıyor, yani kod hiç dışarı çıkmıyor. Bu takası tercih edenler için
RAG_EMBEDDING_BACKEND=openai barındırılan bir modele geçiyor.
# index/embed.py
"""Vectors are L2-normalized on the way out and searched with COSINE — mixing metrics
silently produces wrong ordering, so it is done in one place."""
Normalizasyonun tek bir yerde olması titizlik değil. İç çarpımla aranan normalize bir vektör ile kosinüsle aranan normalize edilmemiş bir vektör, makul görünen ama ince biçimde yanlış sıralamalar üretir — ve hiçbir yerde hata çıkmaz.
Model süreç başına bir kez yükleniyor, istek başına değil — ilk yükleme 10–20 saniye.
Torch fonksiyonların içinde import ediliyor, böylece CLI'daki import milvus_rag bir
yardım mesajı basmak için derin öğrenme yığını çekmiyor.
Zenginleştirme, varsayılanı kapalı¶
Kod neredeyse hiç doğal dil içermez; içerdiği kadarı da yazarının yorumları hangi dilde yazdıysa o dildedir. Çare daha iyi bir eşleştirici değil; eksik olan düzyazıyı yazmak. Bu, Anthropic'in contextual retrieval fikri: LLM'e dosyanın tamamını verip her chunk için iki üç cümle istemek, sonra bunları da yanına embed'lemek.
Zenginleştirme ne kazandırıyor, neye mal oluyor
46 dosya / 423 chunk, aynı corpus iki kez indekslendi, açıklamalar yerel bir
qwen3.5:9b'den:
| Kol | Recall@8 | MRR | TR metin R@8 / MRR | false_weak |
|---|---|---|---|---|
| düz | 0.929 | 0.839 | 0.895 / 0.778 | 0.119 |
| zenginleştirilmiş | 1.000 | 0.912 | 1.000 / 0.932 | 0.048 |
Kazanç tam olarak öngörülen yere düşüyor: Türkçe düz metinde +0.15 MRR,
İngilizcede +0.01. false_weak yarıya indi, çünkü gerçek cevapların dense skorları
yükseliyor ve 0.55 notu daha az yanlış tetikleniyor. Negatiflerde abstain
kımıldamadı — açıklamalar alakasız bir soru için güven imal etmedi.
Maliyet: yerel bir 9B'de dakikada ~11 açıklama, yani 2.8k chunk'lık bir repo ilk seferde yaklaşık dört saat. Sonrası artımlı (chunk özeti + modele göre önbellekli), bulut modeliyle dakikalar.
Varsayılanı kapalı, çünkü anahtarsız bir kurulumda her add-* saatlerce Ollama
döndürürdü. Soru trafiğin İngilizce değilse aç ve RAG_ENRICH_LANGUAGE'ı ona göre ayarla
— ölçülen kazanç, insanların gerçekten sorduğu dildeki düzyazıdan geliyor.
İki koruma, ikisi de bir 7B modelin başarısızlığını izlemekten çıktı:
- Kapsam kontrolü. Küçük bir model seve seve tek bir nesne döndürüp gerisini sessizce düşürür. Cevap, chunk listesine karşı kontrol ediliyor.
- Alfabe kontrolü.
qwen2.5:7byük altında cümle ortasında Çinceye kayıyor ve bunu söylemiyor. Yanlış alfabedeki bir açıklama reddediliyor.
Tek koleksiyon, bölüm anahtarı repo_id¶
# index/store.py
"""Dense and sparse live in the same collection. Nobody writes the sparse field;
Milvus' own BM25 Function derives it from `indexed_text`, so there is no separate
lexical index."""
Milvus'un alternatiflerine tercih edilme sebebi o son cümle. Hibrit arama normalde iki indeksi ayakta tutup tutarlı tutmak demektir; burada BM25 aynı koleksiyondaki bir alan üzerinde bir fonksiyon, yani bir yazma sadece bir yazma.
Bölüm anahtarı sayesinde bir repo_id in [...] filtresi taramak ve filtrelemek yerine
yalnızca ilgili bölümlere iniyor.
Silme ve güncelleme repo_id + path üzerinden gidiyor. Arşivleme ve yumuşak silme yok:
Milvus'taki şey türetilmiş veri, doğruluk kaynağı repo. Repodan yeniden üretilemeyen
hiçbir şeyin orada işi yok.