Mert Özen Her Satırda Daha İleri
Yapay Zeka RAG — #02

Embedding Model Seçimi: Boyut, Çok Dillilik, Türkçe ve Maliyet

Mert Özen 8 Kas 2026 13 dk 4 görüntülenme
Embedding Model Seçimi: Boyut, Çok Dillilik, Türkçe ve Maliyet

Embedding modelini leaderboard ortalamasına göre değil, kendi dilinde ve kendi soru setinde ölçerek seç: nomic-embed-text aynı içerikte İngilizcede 30 sorunun 26'sında, Türkçede yalnız 16'sında doğru bölümü ilk sıraya koydu. recall@k ve MRR ile ölçüyor; prefix, Matryoshka boyutu, storage, sessiz kesilme ve model değişiminde eski vektörlerin riskini gerçek çıktılarla inceliyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, Microsoft.Extensions.AI 10.11.0 ve OllamaSharp 5.5.2 ile derlenip Ollama 0.34.4 üzerinde nomic-embed-text (v1.5) ile denenmiştir; vektör boyutları pgvector 0.8.7 ile ölçülmüştür.

Önce Tahmin Et

Önceki yazıda minimal RAG (Retrieval-Augmented Generation) 30 sorunun beşinde doğru chunk'ı hiç bulamadı. Bu yazıda kusuru modelin kendisinde değil, embedding modelinde arıyoruz. Deney basit: Bookstore'un dokümanlarını ve 30 soruyu bölüm bölüm İngilizceye çevirdim; içerik, başlıklar ve bölüm yapısı aynı. Aynı embedding modeli, aynı kod. Türkçe sette doğru bölüm 30 sorunun 16'sında ilk sırada çıkıyor. İngilizce sette kaçında çıkar? 16'ya yakın mı, yoksa çok farklı mı?

Leaderboard'u Okumak ve Adayları Seçmek

Embedding modeli seçerken herkesin ilk baktığı yer MTEB (Massive Text Embedding Benchmark) leaderboard'u. 2025'te çok dilli sürümü MMTEB (Massive Multilingual Text Embedding Benchmark) yayımlandı; makaleye göre 250'den fazla dilde, 10 görev kategorisinde 500'ü aşkın görev içeriyor. Leaderboard'da tek bir "ortalama skor" sütununa bakıp en üsttekini seçmek cazip ama yanlış. O ortalama sınıflandırma, clustering, STS (Semantic Textual Similarity) ve retrieval gibi çok farklı görevlerin karışımı; RAG için önemli olan yalnızca retrieval görevleri. Üstelik ortalama ağırlıklı olarak İngilizce ya da çok dilli görevlerden gelir; Türkçe retrieval'da nasıl davrandığını ancak dil filtresi ve görev filtresiyle birlikte görürsün.

Leaderboard'da skor dışında üç sütuna da bak: parametre sayısı (CPU'da (Central Processing Unit) mı, GPU'da (Graphics Processing Unit) mı çalışacak?), embedding boyutu (storage ve arama maliyeti) ve max token (chunk'ların sığacak mı?). Bir de lisans: leaderboard'daki her model ticari kullanıma açık değil.

Yerelde çalıştırabileceğin adaylar için Ollama'nın embedding kataloğu iyi bir başlangıç. 8 Kasım 2026'da katalogda açıkça çok dilli olarak tanıtılanlar: bge-m3 (567M), snowflake-arctic-embed2 (568M), nomic-embed-text-v2-moe, paraphrase-multilingual (278M) ve granite-embedding'in 278M sürümü. qwen3-embedding (0.6b, 4b, 8b) ve embeddinggemma (300m) da aday listesine girer; çok dilli olup olmadıklarını Ollama sayfası değil, modelin kendi model card'ı söyler. Bulut tarafında OpenAI'ın text-embedding-3-small modeli 1536, text-embedding-3-large modeli 3072 boyut üretir.

Bu yazının ölçümlerinde tek bir model var: nomic-embed-text. Ollama'daki latest etiketi v1.5'e karşılık geliyor; 137M parametre, 768 boyut ve Ollama'da 2048 token context. Bu ortamda yeni model indirmediğim için çok dilli adayları ölçemedim; bu yüzden yazı "hangi model en iyi?" sorusuna değil, "bir modeli kendi verinle nasıl değerlendirirsin?" sorusuna cevap veriyor. Aşağıdaki düzenekte model adını değiştirip aynı ölçümü çalıştırman yeterli.

Ölçüm Düzeneği: recall@k ve MRR

Önceki yazıdaki 30 soru her birinin cevabının hangi dokümanın hangi bölümünde olduğunu kayıtlı tutuyor. Index, standart chunker'la her Markdown bölümünü bir chunk yapıyor (53 chunk). Vektörler bellekte; arama cosine similarity ile:

public sealed class InMemoryIndex(IEmbeddingGenerator<string, Embedding<float>> embedder,
    string documentPrefix = "search_document: ", string queryPrefix = "search_query: ", int? dimensions = null)
{
    private List<(Chunk Chunk, ReadOnlyMemory<float> Vector)> _items = [];

    public async Task AddAsync(IReadOnlyList<Chunk> chunks)
    {
        EmbeddingGenerationOptions options = new() { Dimensions = dimensions };
        GeneratedEmbeddings<Embedding<float>> vectors =
            await embedder.GenerateAsync(chunks.Select(c => documentPrefix + c.Content), options);
        _items = [.. _items, .. chunks.Zip(vectors, (c, v) => (c, v.Vector))];
    }

    public async Task<List<Hit>> SearchAsync(string query, int k, Func<Chunk, bool>? filter = null)
    {
        ReadOnlyMemory<float> q = await embedder.GenerateVectorAsync(queryPrefix + query,
            new EmbeddingGenerationOptions { Dimensions = dimensions });
        return _items
            .Where(x => filter is null || filter(x.Chunk))
            .Select(x => new Hit(x.Chunk, TensorPrimitives.CosineSimilarity(q.Span, x.Vector.Span)))
            .OrderByDescending(h => h.Score)
            .Take(k)
            .ToList();
    }
}

Dimensions seçeneği Microsoft.Extensions.AI'ın EmbeddingGenerationOptions'ından geliyor; OllamaSharp bunu Ollama'nın /api/embed isteğindeki dimensions alanına taşıyor. Ölçüm döngüsü iki metrik hesaplıyor:

// İsabet: chunk beklenen dokümanın beklenen bölümünden mi?
List<Question> questions = QuestionSet.Load(Path.Combine(corpus, "questions.json"));
int at1 = 0, at5 = 0; double mrr = 0;
foreach (Question q in questions)
{
    List<Hit> hits = await index.SearchAsync(q.QuestionText, 5);
    int rank = hits.FindIndex(h => QuestionSet.IsHit(q, h.Chunk)) + 1;
    if (rank == 1) at1++;
    if (rank > 0) { at5++; mrr += 1.0 / rank; }
    }

recall@k, doğru bölümün ilk k sonuçta olduğu soruların oranı. RAG'de k genelde prompt'a koyduğun chunk sayısıdır; recall@5 düşükse model doğru metni hiç görmez. MRR (Mean Reciprocal Rank) ise doğru bölümün kaçıncı sırada geldiğini ödüllendirir: birinci sıra 1, ikinci 0,5, beşinci 0,2 puan, hiç gelmezse 0. Recall "buldu mu", MRR "ne kadar yukarıda buldu" sorusuna cevap verir.

Türkçe Cezası: Aynı İçerik İki Dilde

$ dotnet run -- recall "search_document: " "search_query: " 0
chunks=53 dims=768 recall@1=16/30 recall@5=25/30 MRR=0.668

$ CORPUS=corpus_en dotnet run -- recall "search_document: " "search_query: " 0
chunks=53 dims=768 recall@1=26/30 recall@5=29/30 MRR=0.894

Tahminin neydi? Aynı model, aynı içerik, aynı bölüm yapısı: Türkçede 30 sorunun 16'sında doğru bölüm birinci, İngilizcede 26'sında. MRR 0.668'den 0.894'e çıkıyor. Bu fark, nomic-embed-text'in model card'ında dil olarak yalnız İngilizcenin listelenmesiyle tutarlı. Model Türkçe metinde de bir şeyler yakalıyor, ama "bedava" ile "ücretsiz", "paraya çevirmek" ile "nakde çevrilemez" arasındaki köprüyü kuramıyor.

Farkın bir kısmı tokenizer'da başlıyor. nomic-embed-text BERT (Bidirectional Encoder Representations from Transformers) tabanlı ve İngilizce bir kelime dağarcığıyla geliyor. Ollama'nın embed cevabındaki prompt_eval_count alanı modelin kaç token gördüğünü söylüyor:

// Aynı anlam, iki dil: nomic-embed-text'in tokenizer'ı kaç token sayıyor?
public static async Task TokenCountsAsync(OllamaApiClient ollama)
{
    string[] texts =
    [
        "search_query: Bir kitabı kaç gün içinde iade edebilirim?",
        "search_query: How many days do I have to return a book?",
        "search_query: Kitapsever seviyesine nasıl geçerim?",
        "search_query: How do I move up to the Book Lover tier?"
    ];
    foreach (string text in texts)
    {
        EmbedResponse r = await ollama.EmbedAsync(new EmbedRequest { Model = "nomic-embed-text", Input = [text] });
        Console.WriteLine($"{r.PromptEvalCount,3} tokens  {text}");
    }
}
$ dotnet run -- emb-tokens
 24 tokens  search_query: Bir kitabı kaç gün içinde iade edebilirim?
 17 tokens  search_query: How many days do I have to return a book?
 21 tokens  search_query: Kitapsever seviyesine nasıl geçerim?
 17 tokens  search_query: How do I move up to the Book Lover tier?

Aynı soru Türkçede 24, İngilizcede 17 token. Türkçe kelimeler eklerine göre değil, kelime dağarcığında bulduğu parçalara göre bölünüyor; "edebilirim" gibi bir kelime model için anlamlı bir birim değil, birkaç anlamsız parça. LLM (Large Language Model) Temelleri serisinin token yazısında aynı etkiyi sohbet modelinin tokenizer'ında görmüştük; embedding modelinde bunun bedeli retrieval kalitesiyle ödeniyor.

Pratik sonuç: Türkçe içerik için modeli İngilizce benchmark skoruna göre seçme. Çok dilli bir modeli aday listesinin başına koy ve kendi soru setinle ölç. Bu serinin örneklerinde nomic-embed-text ile devam ediyoruz; bu, ilerideki yazılarda hybrid search ve reranking'in neden gerekli olduğunu da somut olarak gösterecek.

Prefix'ler: search_query ve search_document

nomic-embed-text v1.5'in model card'ı açık: metne bir görev prefix'i eklemek zorunlu. RAG için dokümanlar search_document: , sorular search_query: ile başlar; clustering ve classification için ayrı prefix'ler var. Başka modeller farklı kalıplar ister (bazıları query: ve passage: , bazıları uzun bir talimat cümlesi). Prefix'i hiç vermesek ya da yalnız dokümana versek ne olur?

$ dotnet run -- recall "" "" 0
chunks=53 dims=768 recall@1=17/30 recall@5=24/30 MRR=0.664
$ dotnet run -- recall "search_document: " "" 0
chunks=53 dims=768 recall@1=17/30 recall@5=25/30 MRR=0.684
$ CORPUS=corpus_en dotnet run -- recall "" "" 0
chunks=53 dims=768 recall@1=27/30 recall@5=29/30 MRR=0.933

Sonuç beklediğim gibi çıkmadı: Türkçede prefix'siz koşu bir soru daha fazla birinci sıra buldu, İngilizcede de prefix'siz MRR biraz daha yüksek. Burada dürüst olmak gerekiyor: 30 soruluk bir sette bir soru 3,3 puanlık fark demek ve bu tablodaki farkların hepsi bu gürültünün içinde. Bu veriyle "prefix işe yaramıyor" diyemem; yalnızca bu sette ölçülebilir bir etki görmediğimi söyleyebilirim. Model card'ın söylediğini uygula, ama asıl kuralı unutma: dokümanlar ve sorgular aynı kurala göre embed edilmeli ve bu kural vektörle birlikte kayıtlı olmalı.

Boyut, Matryoshka ve Maliyet

nomic-embed-text v1.5 Matryoshka Representation Learning ile eğitilmiş: vektörün ilk 512, 256, 128 ya da 64 boyutu tek başına da anlamlı. Model card'a göre MTEB skoru 768 boyutta 62,28, 256'da 61,04, 64'te 56,10. Ollama'da dimensions parametresi vektörü kesip yeniden normalize ediyor; 128 boyutlu çıktının uzunluğunu kontrol ettim, 1,0. Bizim setteki sonuç:

$ dotnet run -- recall "search_document: " "search_query: " 512
chunks=53 dims=512 recall@1=17/30 recall@5=25/30 MRR=0.673
$ dotnet run -- recall "search_document: " "search_query: " 256
chunks=53 dims=256 recall@1=18/30 recall@5=24/30 MRR=0.684
$ dotnet run -- recall "search_document: " "search_query: " 128
chunks=53 dims=128 recall@1=14/30 recall@5=24/30 MRR=0.579
$ dotnet run -- recall "search_document: " "search_query: " 64
chunks=53 dims=64 recall@1=14/30 recall@5=22/30 MRR=0.566

512 ve 256 boyut 768 ile aynı seviyede, 128'de recall@1 belirgin biçimde düşüyor, 64'te recall@5 de geriliyor. Bu boyutların maliyet tarafı ise doğrudan hesaplanabilir. pgvector'da bir vector değeri 4 × boyut + 8 byte, halfvec 2 × boyut + 8 byte yer kaplıyor:

SELECT pg_column_size(array_fill(0.1::real, ARRAY[768])::vector)  AS v768,
       pg_column_size(array_fill(0.1::real, ARRAY[768])::halfvec) AS h768,
       pg_column_size(array_fill(0.1::real, ARRAY[256])::vector)  AS v256,
       pg_column_size(array_fill(0.1::real, ARRAY[1536])::vector) AS v1536;

 v768 | h768 | v256 | v1536
------+------+------+-------
 3080 | 1544 | 1032 |  6152

Bir milyon chunk için 768 boyutlu vector kolonu yalnız ham veri olarak yaklaşık 3 GB, 256 boyutta 1 GB; ANN (Approximate Nearest Neighbor) index'i bunun üstüne gelir. Brute-force aramada boyut doğrudan CPU süresine dönüşüyor. 100.000 rastgele vektörü TensorPrimitives.CosineSimilarity ile taradım (Apple Silicon bir Mac'te, Release build ile):

$ dotnet run -c Release -- emb-scan 100000
dims=768 vectors=100,000 memory=293 MB scan=14.0 ms/query
dims=256 vectors=100,000 memory=98 MB scan=3.6 ms/query

Boyut üçte bire indi, süre dörtte bire. Bellek 293 MB'tan 98 MB'a düşünce veri CPU cache'ine daha iyi sığıyor. Bu tablo sana şunu söylüyor: Matryoshka destekleyen bir modelde 256 boyut, kendi ölçümünde kayıp görmüyorsan, storage ve hız için ciddi bir kazanç. Ama bunu her dil ve her doküman seti için yeniden ölçmen gerekir; benim 30 sorumdaki "kayıp yok" sonucu senin verine taşınmaz.

Max Input ve Sessiz Kesilme

Her embedding modelinin bir girdi sınırı var. nomic-embed-text model card'ında 8192 token yazıyor, ama Ollama modeli 2048 token context ile çalıştırıyor. Sınırı aşan bir chunk'a ne olur?

// Context'ten uzun girdi: Ollama sessizce keser mi, hata mı verir?
public static async Task LongInputAsync(OllamaApiClient ollama, string longText)
{
    EmbedResponse full = await ollama.EmbedAsync(new EmbedRequest { Model = "nomic-embed-text", Input = [longText] });
    EmbedResponse tail = await ollama.EmbedAsync(new EmbedRequest
    {
        Model = "nomic-embed-text",
        Input = [longText + "\n\nÖNEMLİ: Bu belgenin son maddesi iade süresini 60 güne çıkarır."]
    });
    float similarity = TensorPrimitives.CosineSimilarity(full.Embeddings[0], tail.Embeddings[0]);
    Console.WriteLine($"chars={longText.Length} tokens={full.PromptEvalCount} (with extra paragraph: {tail.PromptEvalCount})");
    Console.WriteLine($"cosine(original, original + extra paragraph) = {similarity:F6}");

    try
    {
        await ollama.EmbedAsync(new EmbedRequest { Model = "nomic-embed-text", Input = [longText], Truncate = false });
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Truncate=false -> {ex.GetType().Name}: {ex.Message}");
    }
}
$ dotnet run -- emb-long 1
chars=1709 tokens=728 (with extra paragraph: 754)
cosine(original, original + extra paragraph) = 0.999200

$ dotnet run -- emb-long 6
chars=10254 tokens=2048 (with extra paragraph: 2048)
cosine(original, original + extra paragraph) = 1.000000
Truncate=false -> OllamaException: the input length exceeds the context length

İki ayrı ders var. Birincisi: iade politikasını altı kez art arda koyunca 10.254 karakter 2048 token'a kesildi ve sona eklenen "iade süresi 60 güne çıkar" paragrafı vektörü hiç değiştirmedi; cosine tam olarak 1. Ollama varsayılan olarak hata vermiyor, sessizce kesiyor. Truncate = false verdiğinde ise istek bir exception'la reddediliyor. Production'da ikincisini tercih et: kesilen bir chunk'ı sonradan fark etmek, hatayı ingestion anında yakalamaktan çok daha pahalı.

İkincisi daha incelikli: sınırın altında, 728 token'lık metinde bile yeni paragraf cosine'i yalnız 0.9992'ye düşürdü. Uzun bir chunk'ın vektörü, içindeki tek bir cümlenin anlamını ortalamada kaybeder. Chunk boyutunu bu yüzden "sığıyor mu" sorusuyla değil, "içindeki bilgi vektörde görünür mü" sorusuyla seçeceğiz; dördüncü yazının konusu bu. Türkçede bir başka not: bu metinde token başına ortalama 2,3 karakter düştü, yani Türkçe bir chunk İngilizce karşılığından daha çabuk sınıra dayanır.

Model Değiştirmek: Vektörlerin Kimliği

Bir gün daha iyi bir çok dilli modele geçmek isteyeceksin. En tehlikeli hata, index'teki vektörleri olduğu gibi bırakıp sorguları yeni modelle embed etmek. İki modelin vektörleri aynı uzayda değildir; boyutları aynı olsa bile aralarındaki cosine anlamsızdır. Boyut farklıysa en azından gürültülü bir hata alırsın:

// Eski vektörler 256 boyutlu, yeni sorgu 768 boyutlu
public static async Task MismatchAsync(IEmbeddingGenerator<string, Embedding<float>> embedder)
{
    Embedding<float> stored = (await embedder.GenerateAsync(
        ["search_document: İade süresi 21 gündür."], new EmbeddingGenerationOptions { Dimensions = 256 }))[0];
    ReadOnlyMemory<float> query = await embedder.GenerateVectorAsync("search_query: Kaç gün içinde iade edebilirim?");
    Console.WriteLine($"stored={stored.Vector.Length} dims, query={query.Length} dims, model={stored.ModelId}");
    try
    {
        Console.WriteLine(TensorPrimitives.CosineSimilarity(stored.Vector.Span, query.Span));
    }
    catch (ArgumentException ex)
    {
        Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
    }
}
$ dotnet run -- emb-mismatch
stored=256 dims, query=768 dims, model=nomic-embed-text
ArgumentException: Input span arguments must all have the same length.

pgvector da aynı korumayı veriyor: kolon tipi boyutu sabitliyor, farklı boyutlu iki vektörü karşılaştırmayı reddediyor.

CREATE TABLE emb_demo (id int, embedding vector(768));
INSERT INTO emb_demo VALUES (1, array_fill(0.1::real, ARRAY[256])::vector);
ERROR:  expected 768 dimensions, not 256

SELECT '[1,2,3]'::vector <=> '[1,2]'::vector;
ERROR:  different vector dimensions 3 and 2

Asıl sorun aynı boyutlu iki model arasında geçişte: hiçbir yerde hata almazsın, sadece sonuçlar sessizce kötüleşir. Bunun tek savunması, her vektörün yanında onu üreten "kimliği" saklamak: model adı ve sürümü, boyut, kullanılan prefix kuralı. Embedding<float>.ModelId bu bilgiyi zaten veriyor; yedinci yazıdaki doc_chunks tablosunda bunu embedding_model kolonunda tutacağız ve sorgu tarafı kendi profilini bu kolonla karşılaştıracak.

Model değişikliğini bir veri migration'ı gibi planla. Yeni model için yeni bir kolon ya da yeni bir tablo aç, ingestion'ı iki modele birden yazdır, eski dokümanları arka planda yeniden embed et, yeni index'i kur, ölçüm setini iki index'te de çalıştır ve sonuç iyiyse sorgu tarafını yeni kolona çevir. Eski kolonu ancak bir süre sonra sil. Yeniden embed etmek ucuz değildir: bir milyon chunk, modelin hızına göre saatler sürebilir; bunu baştan hesaba kat.

Üretimde Dikkat

Ölçümü kendi dilinde ve kendi sorularınla yap. Bu yazının en büyük sayısı leaderboard'dan değil, aynı içeriğin iki dildeki farkından geldi: recall@1 16'ya karşı 26. Hiçbir genel benchmark bunu senin dokümanların için söylemez.

Soru setinin büyüklüğünü sonuçla birlikte raporla. 30 soruda bir sorunun farkı 3,3 puan; 512 ile 768 boyut arasındaki farkı ya da prefix etkisini bu setle ayırt edemezsin. Gerçek karar için yüzlerce soruluk bir set kur; on ikinci yazıda bunu ele alacağız.

Embedding çağrısının maliyeti ingestion'da ve sorguda farklıdır. Ingestion'da binlerce chunk'ı batch'ler halinde gönderirsin; sorguda her istek tek bir kısa metni embed eder ve gecikmeye doğrudan eklenir. Yerel bir modelde bu birkaç milisaniye, bir bulut API'sinde (Application Programming Interface) ağ gecikmesiyle birlikte daha fazla.

Embedding modelini, sohbet modelinden bağımsız seç. Cevabı üreten model gemma3:4b ya da bir bulut modeli olabilir; retrieval'ın kalitesini belirleyen ise yalnız embedding modelidir.

Sık Düşülen Tuzaklar

Birincisi, leaderboard'daki ortalamaya göre seçmek. Ortalama, RAG'de kullanmayacağın görevleri ve büyük ihtimalle senin dilin olmayan dilleri içerir. Retrieval görevlerine ve diline göre filtrele, sonra kendi setinle ölç.

İkincisi, sessiz kesilmeye güvenmek. Ollama varsayılan olarak uzun girdiyi keser ve kestiğini söylemez. Ingestion'da chunk'ların token sayısını ölç ya da kesilmeyi kapatıp hatayı yakala.

Üçüncüsü, doküman ve sorgu için farklı kural kullanmak. Prefix'i ingestion'da verip sorguda unutmak, ya da Matryoshka boyutunu yalnız bir tarafta değiştirmek. Boyut farkında exception alırsın; prefix farkında hiçbir şey almazsın.

Dördüncüsü, modeli değiştirip eski vektörlerle aramak. Aynı boyutlu iki model arasında bu hata hiçbir log'a düşmez. Her vektörün yanında model adını sakla ve sorgu tarafında kontrol et.

Beşincisi, Türkçe içerikte tokenizer'ı hesaba katmamak. Türkçe metin aynı içerikteki İngilizceden daha fazla token tutar; chunk boyutu, max input ve maliyet hesabı buna göre yapılmalı.

Küçük Bir Deneme

★☆☆ recall komutunu k=10 ile çalışacak şekilde değiştir. Türkçe sette recall@10 kaç? Doğru bölüm 5. sıranın altında geliyorsa, reranking'le (dokuzuncu yazı) kurtarılabilir bir aday demektir.

★★☆ Kendi makinende Ollama'da çok dilli bir embedding modeli varsa (ör. bge-m3), InMemoryIndex'e verdiğin OllamaApiClient'ın model adını değiştir. Bu model prefix istemiyorsa prefix'leri boş bırak ve 30 soruyu iki dilde ölç. Türkçe ile İngilizce arasındaki fark kapanıyor mu?

★★★ 30 soruyu ikiye çoğalt: her soruyu bir müşterinin yazacağı gibi farklı kelimelerle yeniden yaz (ör. "bedava", "geri vermek", "paraya çevirmek"). Orijinal ve yeniden yazılmış sorulardaki recall@5 farkı, embedding modelinin eş anlamlıları ne kadar yakaladığını gösterir.

Embedding modeli retrieval'ın tavanını belirliyor, ama ona verdiğimiz metnin kalitesi de en az o kadar önemli. Bir sonraki yazıda ingestion'a geçiyoruz: PDF (Portable Document Format), HTML (HyperText Markup Language) ve Markdown'dan temiz metin çıkarmak, her chunk'a metadata eklemek ve dokümanlar değiştikçe index'i artımlı olarak güncellemek.