İçeriğe geç

Hibrit Arama: RAG'de BM25 ve Vektör Aramayı Birleştirmek

Saf vektör arama tam kelimeleri kaçırır, saf anahtar kelime araması anlamı. RAG'de BM25 ve vektör aramayı RRF ve contextual retrieval ile birleştirmenin pratik rehberi.

SYK
Şükrü Yusuf KAYA
AI Expert · Kurumsal AI Danışmanı

Hibrit Arama: BM25 + Vektör Aramayı Birleştirmek (Contextual Retrieval)

TL;DR — Kurumsal RAG projelerinde en çok karşılaştığım hayal kırıklığı şu: "Modelimiz akıllı ama doğru belgeyi bulamıyor." Sorun genelde LLM değil, retrieval katmanıdır. Saf vektör (dense) arama anlamı yakalar ama ürün kodu, hata kodu, IBAN, madde numarası gibi kesin ifadeleri kaçırır; saf BM25 (sözcük tabanlı) arama tam kelimeyi yakalar ama eş anlamlıları ve dolaylı ifadeleri anlamaz. Bu yazıda ikisini Reciprocal Rank Fusion (RRF) ile nasıl birleştireceğinizi, Anthropic'in "contextual retrieval" yaklaşımıyla chunk'lara bağlam ekleyerek kayıp aramaları nasıl azaltacağınızı, bir cross-encoder reranker'ı pipeline'ın sonuna nasıl ekleyeceğinizi ve bunu bir Türk banka/e-ticaret bilgi tabanında somut olarak nasıl uygulayacağınızı adım adım anlatıyorum. Sonunda KVKK bağlamında dikkat etmeniz gereken noktaları da ekliyorum.

Neden bu konuyu bu kadar sık anlatıyorum

Danışmanlık yaptığım hemen her kurumda aynı sahne tekrar ediyor: Bir ekip, hevesle bir vektör veritabanı kurmuş, dokümanları embed etmiş, bir LLM'e bağlamış ve demo günü her şey harika görünmüş. Sonra üretime çıkılıyor, gerçek kullanıcılar gerçek sorular sormaya başlıyor ve sistem "SGK-2024/117 sayılı genelge ne diyor?" gibi bir soruda ya alakasız bir belge getiriyor ya da hiçbir şey bulamıyor. Ben bu noktada genelde aynı teşhisi koyuyorum: retrieval tek bacaklı yürüyor. Ekip embedding modelinin "akıllı" olduğuna güvenmiş ama bir embedding modelinin, tam olarak "117" rakamını veya "SGK-2024/117" gibi nadir bir alfasayısal kodu doğru şekilde ayırt edeceğine dair hiçbir garanti yok. Bu yazı, bu sorunu kökten çözen ve benim artık neredeyse her kurumsal RAG projesinde varsayılan olarak önerdiğim mimariyi anlatıyor: hibrit arama.

Burada bir itirafta bulunayım: Ben de bir dönem "embedding her şeyi çözer" kampındaydım. 2023'ün ortalarında kurduğum ilk üretim RAG sistemlerinden birinde, sadece dense retrieval kullanmıştım. Sistem "iade politikası nedir" gibi doğal dil sorularında gayet iyi çalışıyordu ama müşteri "SKU 8452-B ürününün garantisi var mı" diye sorduğunda tamamen şaşırıp kalıyordu. Çünkü embedding modeli "8452-B" gibi bir dizgeyi anlamlı bir vektöre dönüştürmekte doğası gereği zayıf; bu tür kodlar için istatistiksel eşleşme, anlamsal yakınlıktan çok daha güvenilir bir sinyal. O deneyim beni hibrit mimariye itti ve o günden beri bu konuyu her eğitimde, her danışmanlık projesinde anlatıyorum.

Dense (vektör) arama: ne yapar, ne kaçırır

Dense retrieval, bir metni (sorgu veya chunk) yüksek boyutlu bir sayısal vektöre dönüştürür — genelde 384 ile 3072 boyut arasında bir uzayda. Bu dönüşümü yapan embedding modelleri (OpenAI'nin text-embedding-3 serisi, Cohere embed, Voyage AI, açık kaynak BGE veya E5 aileleri gibi) metnin "anlamını" yakalamaya çalışır. Sonuç olarak "araç kiralama" ile "otomobil kiralamak" gibi kelime düzeyinde farklı ama anlamca yakın ifadeler, vektör uzayında birbirine yakın konumlanır. Kosinüs benzerliği veya nokta çarpımı ile en yakın komşular bulunur; bu genelde ANN (approximate nearest neighbor) indeksleri (HNSW, IVF gibi) üzerinden hızlandırılır.

Bu yaklaşımın gücü, kullanıcının tam kelimeyi bilmese bile doğru belgeye ulaşabilmesidir. "Param ne zaman hesabıma geçer" sorusu, dokümanda "havale valör süresi" ifadesi geçse bile eşleşebilir çünkü anlamsal olarak yakınlar. Ama işte tam burada zayıflık başlıyor: embedding modelleri eğitildikleri veri dağılımına göre genelleme yapar ve nadir, spesifik, alan-özel dizgilerde (ürün kodları, mevzuat madde numaraları, IBAN parçaları, hata kodları, kısaltmalar) güvenilir ayrım gücü sunmazlar. Bir embedding modeli için "ABC-123" ile "ABC-124" çoğu zaman neredeyse aynı vektöre düşer çünkü model bunları "bir ürün kodu" olarak genellemiştir, aralarındaki tek karakterlik farkın kritik olduğunu bilemez. Oysa bir müşteri hizmetleri sisteminde bu tek karakter, tamamen farklı bir ürünü, tamamen farklı bir garanti koşulunu ifade edebilir.

Ayrıca dense retrieval'ın az bilinen ama önemli bir zaafı daha var: negasyon ve kesinlik. "KDV dahil olmayan fiyat" ile "KDV dahil fiyat" ifadeleri anlamsal olarak birbirine çok yakın vektörlere düşebilir çünkü kelime örtüşmesi yüksek, ama anlamları taban tabana zıt. Bu, embedding modellerinin sıkça karşılaştığı, henüz tam çözülmemiş bir problem.

BM25 / seyrek (sparse) arama: ne yapar, ne kaçırır

BM25 (Best Matching 25), Elasticsearch, OpenSearch, Postgres'in tsvector özelliği ve Lucene tabanlı hemen her arama motorunun temelini oluşturan, istatistiksel bir sıralama fonksiyonudur. Mantığı basittir ama güçlüdür: bir terimin bir belgede ne sıklıkla geçtiğine (term frequency), o terimin tüm koleksiyonda ne kadar nadir olduğuna (inverse document frequency) ve belge uzunluğuna göre normalize edilmiş bir skor üretir.

BM25 skoru kabaca şu formülle hesaplanır:

Kod Bloğu
score(D, Q) = Σ IDF(qi) * [f(qi, D) * (k1 + 1)] / [f(qi, D) + k1 * (1 - b + b * |D| / avgdl)]

Burada f(qi, D) sorgu teriminin belgedeki geçiş sıklığı, |D| belge uzunluğu, avgdl koleksiyondaki ortalama belge uzunluğu, k1 ve b ise ayarlanabilir hiperparametrelerdir (tipik değerler k1 ≈ 1.2-2.0, b ≈ 0.75). IDF terimi, nadir kelimelere (örneğin "SGK-2024/117") yüksek ağırlık verirken, sık geçen kelimelere ("ve", "bir", "için") düşük ağırlık verir.

BM25'in gücü tam olarak dense retrieval'ın zayıf olduğu yerde: tam eşleşme gerektiren, nadir, alan-özel ifadelerde neredeyse kusursuzdur. Bir kullanıcı "IBAN TR33 0006 1005 1978 6457 8413 26" yazdığında ya da "hata kodu E-4042" dediğinde, BM25 bunu doğrudan yakalar çünkü kelime eşleşmesi arıyor, anlam çıkarmaya çalışmıyor.

Ama zayıflığı da net: eş anlamlıları, üst kavramları, dolaylı ifadeleri anlamaz. Kullanıcı "param ne zaman hesabıma geçer" diye sorduğunda, dokümanda "valör süresi" yazıyorsa BM25 bu iki ifade arasında hiçbir kelime örtüşmesi bulamadığı için düşük skor verir ya da hiç eşleştirmez. Ayrıca yazım farklılıklarına (büyük/küçük harf, Türkçe'de özellikle "İ/i", "ı/I" karışıklığı, kök/ek varyasyonları) karşı BM25, uygun bir tokenizer ve stemmer olmadan kırılgandır. Türkçe gibi eklemeli bir dilde bu, ciddi bir mühendislik yükü demek — "bankalar", "bankada", "bankacılık" kelimelerinin aynı kökten geldiğini BM25'e ayrı bir morfolojik analiz katmanı (Zemberek gibi) olmadan öğretemezsiniz.

İkisi neden birlikte, tek başlarına olduklarından daha güçlü

Buraya kadar anlattıklarımın özeti basit: dense arama anlam yakalar, kesinlik kaçırır; BM25 kesinlik yakalar, anlam kaçırır. Bu iki hata modu birbirini örtüşmeyecek şekilde tamamlıyor — yani biri başarısız olduğunda diğerinin başarılı olma ihtimali yüksek. İstatistiksel olarak, hata korelasyonu düşük olan iki modeli birleştirdiğinizde toplam sistemin hata oranı, tek modellerin ayrı ayrı hata oranından daha düşük olur. Bu, ensemble öğrenmenin temel prensibiyle birebir aynı mantık.

Ben eğitimlerde bunu şöyle özetliyorum: "Dense arama sizin akıllı ama unutkan asistanınız, BM25 ise titiz ama hayal gücü olmayan arşivciniz. İkisini aynı odaya koyduğunuzda, biri diğerinin kör noktasını kapatır." Pratikte gördüğüm sayısal etki de bunu doğruluyor — çeşitli akademik çalışmalar (BEIR benchmark'ı üzerindeki araştırmalar dahil) ve endüstri raporları, hibrit aramanın tek başına dense veya tek başına BM25'e göre recall@k ve nDCG gibi metriklerde tutarlı biçimde daha iyi sonuç verdiğini gösteriyor. Kesin sayı veri setine göre değişir ama yön çok nettir: hibrit, neredeyse hiçbir zaman tek yönteme göre daha kötü performans göstermez, çoğu zaman belirgin şekilde daha iyi performans gösterir.

Burada önemli bir noktayı vurgulamak istiyorum: hibrit arama "ortalama" almak değildir, "birleştirme" (fusion) yapmaktır. İki farklı sıralama listesini alıp, aday belgeleri her iki sinyale göre yeniden derecelendiren bir mekanizmaya ihtiyacınız var. İşte burada devreye Reciprocal Rank Fusion giriyor.

Reciprocal Rank Fusion (RRF): iki listeyi birleştirmenin en pratik yolu

RRF, farklı arama sistemlerinden gelen sıralama listelerini, ham skorlara hiç dokunmadan, sadece sıra (rank) bilgisini kullanarak birleştiren basit ama sürpriz derecede etkili bir yöntemdir. Neden ham skorlara değil de sıraya bakıyoruz? Çünkü BM25 skorları (örneğin 0 ile 40 arası, sınırsız) ve kosinüs benzerlik skorları (0 ile 1 arası) tamamen farklı ölçeklerde ve dağılımlarda üretilir. Bunları doğrudan toplamak veya ağırlıklı ortalamasını almak, ölçek uyumsuzluğu (score normalization problemi) yüzünden yanıltıcı olur. RRF bu problemi tamamen ortadan kaldırır çünkü skorları değil, sıralamadaki pozisyonu kullanır.

Formül şu şekildedir:

Kod Bloğu
RRF_score(d) = Σ (1 / (k + rank_i(d)))

Burada d bir belge, rank_i(d) o belgenin i numaralı sıralama listesindeki (örneğin BM25 listesi veya vektör listesi) pozisyonu, k ise genelde 60 civarında sabitlenen bir yumuşatma sabitidir (orijinal makalede önerilen değer budur ve pratikte çoğu senaryoda iyi çalışır). Bir belge her iki listede de üst sıralardaysa, iki terimin toplamı yüksek çıkar ve nihai sıralamada üste çıkar. Bir belge sadece bir listede üst sırada, diğerinde hiç yoksa, sadece o tek terim katkı sağlar; yine de listeye girebilir ama daha düşük bir toplam skorla.

Somut bir örnek vereyim. "Kredi kartı ekstre itiraz süresi" sorgusu için diyelim ki BM25 listesinde "itiraz" ve "ekstre" kelimelerinin geçtiği bir SSS maddesi 1. sırada, vektör aramasında ise aynı belge anlamca biraz uzak kaldığı için 8. sırada. Aynı zamanda "hesap özeti değişiklik talebi" başlıklı bir başka belge, vektör aramasında anlamca yakın olduğu için 2. sırada ama BM25'te kelime örtüşmesi az olduğu için 15. sırada. RRF ile (k=60):

BelgeBM25 sırasıVektör sırasıRRF skoru
Ekstre itiraz SSS181/61 + 1/68 ≈ 0,0311
Hesap özeti değişiklik1521/75 + 1/62 ≈ 0,0294

Görüldüğü gibi iki belge de birbirine yakın nihai skorlarla üst sıraya çıkıyor — tek bir listeye bakılsaydı biri tamamen kaybolabilirdi. RRF'nin en büyük pratik avantajı, herhangi bir eğitim veya kalibrasyon gerektirmeden, farklı sistemlerden (hatta üç, dört farklı retrieval yöntemi olsa bile) gelen listeleri anında birleştirebilmesidir. Elasticsearch 8.8+ sürümünde rrf retriever olarak, Weaviate'de hibrit arama modu olarak, Qdrant ve Vespa'da da benzer mekanizmalarla yerleşik desteği var; kendi pipeline'ınızda da 15-20 satır kodla kolayca implemente edilebilir.

Contextual Retrieval: chunk'ları "bağlamsızlaştırmadan" önce durun

Şimdi işin, bence en az bilinen ama en yüksek etkili kısmına geliyorum: contextual retrieval. Anthropic'in 2024 sonunda yayınladığı bir araştırma notunda ortaya koyduğu bu teknik, RAG sistemlerindeki "başarısız retrieval" oranını ciddi biçimde azaltmayı hedefliyor ve ben bunu artık her chunking stratejisinin standart bir parçası olarak öneriyorum.

Sorunu şöyle tarif edeyim: Bir belgeyi (diyelim 50 sayfalık bir kurumsal politika dokümanı) chunk'lara bölüp her birini ayrı ayrı embed ettiğinizde, her chunk kendi bağlamından kopuk hale gelir. Örneğin "Bu oran, önceki çeyreğe göre %3 artmıştır" cümlesini içeren bir chunk düşünün. Bu cümle tek başına hangi ürüne, hangi çeyreğe, hangi şirkete ait olduğunu söylemiyor. Chunk'ı izole olarak embed ettiğinizde, embedding modeli de bu bağlamı bilemez ve vektör, aslında pek bir şey ifade etmeyen, "genel" bir konuma düşer. Kullanıcı "ACME şirketinin üçüncü çeyrek büyüme oranı nedir" diye sorduğunda, bu chunk anlamsal olarak sorguya yeterince yakın çıkmayabilir çünkü içinde ne "ACME" ne "büyüme" ne de "üçüncü çeyrek" kelimeleri geçiyor — sadece "bu oran" diyor.

Contextual retrieval'ın çözümü basit ama etkili: her chunk'ı embed etmeden (ve BM25 indeksine eklemeden) önce, o chunk'ın belge içindeki yerini açıklayan kısa bir bağlam cümlesi LLM ile otomatik üretilir ve chunk'ın başına eklenir. Yani yukarıdaki örnek chunk şuna dönüşür:

Kod Bloğu
Bağlam: Bu chunk, ACME A.Ş.'nin 2024 yılı üçüncü çeyrek finansal 
raporunun "Gelir Analizi" bölümünden alınmıştır ve şirketin ana 
ürün hattındaki büyüme oranını tartışmaktadır.

Orijinal metin: Bu oran, önceki çeyreğe göre %3 artmıştır.

Bu ekleme sadece birkaç cümlelik, ucuz bir LLM çağrısıyla (Anthropic'in kendi yaklaşımında, tüm belgeyi prompt cache'e alıp her chunk için ucuz bir tamamlama yaptırarak) yapılıyor ve maliyeti prompt caching sayesinde makul seviyede tutuluyor. Sonuç: hem embedding hem BM25 indeksi artık "kaybolan" chunk'lar için çok daha zengin, kendi kendine anlamlı bir temsil kullanıyor.

Anthropic'in yayınladığı sonuçlara göre, sadece contextual embedding eklemek başarısız retrieval oranını yaklaşık %35 azaltıyor; buna contextual BM25 (yani BM25 indeksine de aynı bağlamsallaştırılmış metni vermek) eklendiğinde bu oran yaklaşık %49'a çıkıyor; reranker ile birleştirildiğinde ise toplam iyileşme yaklaşık %67'ye ulaşıyor. Bu rakamlar elbette veri setine bağlı ama yön çok net: chunk'ları izole bağlamdan kurtarmak, hem dense hem sparse tarafta kazanç sağlıyor çünkü sorun retrieval yönteminde değil, chunk'ın kendisinde bağlam eksikliğinde.

Ben bunu kurumsal projelerde uygularken şu pratik yolu izliyorum: belgeyi chunk'lara ayırdıktan sonra, her chunk için ayrı bir prompt yerine, tüm belgeyi bağlam olarak sisteme veriyor ve modele "şu chunk'ı bu belgenin neresinden alındığını, hangi konudan bahsettiğini 1-2 cümlede özetle" dedirtiyorum. Prompt caching destekleyen modellerle (Claude, GPT ailesi) bu, belge başına maliyeti kabul edilebilir seviyede tutuyor — özellikle binlerce chunk içeren büyük bir kurumsal doküman havuzunda tek seferlik bir maliyet olduğunu, sorgu zamanında ek maliyet getirmediğini unutmamak gerekiyor.

Son katman: cross-encoder reranker

Hibrit arama + RRF size iyi bir aday listesi verir ama bu liste hâlâ "kaba" bir sıralamadır çünkü hem BM25 hem dense embedding, sorgu ile belge arasındaki ilişkiyi ayrı ayrı, birbirinden bağımsız hesaplar (bi-encoder mantığı). Oysa bir cross-encoder reranker, sorgu ve adayı birlikte aynı transformer'a besler ve aralarındaki ilişkiyi çok daha ince taneli bir şekilde modelleyebilir. Bunun bedeli hesaplama maliyetidir — her sorgu-belge çifti için ayrı bir forward pass gerekir, bu yüzden reranker'ı tüm koleksiyonda değil, sadece hibrit aramanın getirdiği ilk 20-50 adayda çalıştırırsınız.

Tipik bir üretim pipeline'ı şöyle işler:

  1. Kullanıcı sorgusu hem BM25 indeksine hem vektör indeksine gönderilir.
  2. Her ikisinden de ilk 50-100 aday alınır.
  3. RRF ile bu iki liste birleştirilir, tek bir sıralı aday listesi elde edilir.
  4. Bu listenin ilk 20-30 adayı bir cross-encoder reranker'a (örneğin Cohere Rerank, BGE-reranker, Voyage rerank veya kendi fine-tune ettiğiniz bir model) gönderilir.
  5. Reranker'ın ürettiği nihai skorlara göre ilk 3-5 chunk, LLM'e bağlam olarak verilir.

Bu son adım özellikle "neredeyse doğru ama tam değil" durumlarda fark yaratır — RRF'nin üst sıraya çıkardığı ama aslında sorgunun asıl niyetine tam cevap vermeyen belgeleri geriye iterken, gerçekten alakalı olanı öne çıkarır. Benim gözlemim, reranker eklemenin özellikle çok belgeli, çok benzer içerikli (örneğin yüzlerce benzer ürün açıklaması olan bir e-ticaret kataloğu gibi) kurumsal veri setlerinde en büyük faydayı sağladığı yönünde. Az sayıda, birbirinden net şekilde ayrışan belge olan küçük bir bilgi tabanında reranker'ın katkısı sınırlı kalabilir, hatta gecikmeyi artırıp maliyeti yükseltmekten öte bir fayda sağlamayabilir.

Pratik ayarlama: alpha ağırlığı ve ne zaman hibrit kullanmamalı

Bazı vektör veritabanları (Weaviate, Pinecone'un hibrit modu, Qdrant) RRF yerine veya RRF'ye ek olarak doğrudan bir alpha parametresiyle ağırlıklı toplama izin verir:

Kod Bloğu
final_score = alpha * dense_score_normalized + (1 - alpha) * bm25_score_normalized

alpha = 1 tamamen dense, alpha = 0 tamamen BM25 demektir. Bu yaklaşımın RRF'ye göre dezavantajı, skorları normalize etmeniz gerekmesi (min-max veya z-score normalizasyonu) ve bu normalizasyonun veri setine göre kırılgan olabilmesidir. Ben genelde önce RRF ile başlamayı öneriyorum çünkü ayar gerektirmiyor ve sağlam bir başlangıç noktası veriyor; sonra elinizde etiketli bir değerlendirme seti varsa, alpha'yı 0.3, 0.5, 0.7 gibi değerlerde deneyip hangisinin sizin sorgu dağılımınızda daha iyi çalıştığını ölçüyorum.

Alpha'yı etkileyen en önemli faktör, kullanıcılarınızın sorgu tarzı. Eğer kullanıcılarınız çoğunlukla doğal dil soruları soruyorsa ("emeklilik için kaç yıl prim ödemem lazım"), dense ağırlığını artırmak mantıklı. Eğer kullanıcılarınız sık sık kod, kimlik numarası, ürün numarası, referans numarası gibi kesin ifadeler kullanıyorsa (bir çağrı merkezi asistanı veya teknik destek botu gibi), BM25 ağırlığını artırmak gerekir. Bazı ekipler bunu daha da ileri götürüp, sorguyu önce basit bir sınıflandırıcıdan (regex ile "bu sorguda bir kod/numara var mı" kontrolü gibi) geçirip, sorgu tipine göre dinamik alpha seçiyor — ben buna "query routing" diyorum ve olgun RAG sistemlerinde giderek daha yaygın görüyorum.

Hibrit aramanın her zaman gerekli olmadığını da söylemem lazım — bu, danışmanlık yaparken sıkça vurguladığım bir denge noktası. Eğer veri setiniz küçük (birkaç yüz belge), sorgu çeşitliliği düşük ve içerikte nadir kod/kimlik gibi ifadeler neredeyse hiç yoksa, tek başına dense retrieval yeterli olabilir ve hibrit mimarinin getirdiği ek operasyonel karmaşıklığa (iki indeks bakımı, iki sistemin senkron tutulması, RRF/reranker gecikmesi) değmeyebilir. Benzer şekilde, içerik tamamen yapılandırılmış tablo verisiyse (örneğin sadece fiyat listesi), muhtemelen retrieval yerine doğrudan SQL sorgusu veya yapılandırılmış arama daha uygun bir çözümdür — RAG her probleme uygulanacak bir çekiç değildir.

Türkiye'den somut bir kurumsal örnek: banka müşteri hizmetleri bilgi tabanı

Bunu soyut bırakmak istemiyorum, bir örnek üzerinden gidelim. Diyelim bir orta ölçekli banka için müşteri hizmetleri asistanı kuruyoruz. Bilgi tabanında şu tür içerikler var: ürün broşürleri ("Altın Vadeli Mevduat", "Bireysel Emeklilik Sistemi katkı payı oranları"), operasyonel prosedürler ("kart iptal süreci", "ekstre itiraz süresi 60 gün"), mevzuat referansları (BDDK tebliğleri, SGK genelgeleri), ve ürün kodları (kredi kartı ürün kodları, kampanya kodları gibi "KK-PLATIN-2024").

Müşteri sorguları iki uçta kümeleniyor. Bir grup tamamen doğal dil: "Emekli maaşımı bu bankaya taşırsam ne kazanırım", "kredi kartımı kaybettim ne yapmalıyım". Diğer grup ise çok spesifik, kod/numara ağırlıklı: "KK-PLATIN-2024 kampanyasının şartları neler", "hata kodu 4021 ne anlama geliyor", "IBAN'ımdaki son 4 hane ile işlem yapabilir miyim".

Bu senaryoda saf dense retrieval, ikinci gruptaki sorgularda sık sık başarısız oluyor çünkü "KK-PLATIN-2024" gibi bir kod, embedding uzayında anlamlı bir şekilde ayrışmıyor — model bunu "bir kredi kartı kampanya kodu" olarak genelliyor ama hangi kampanya olduğunu ayırt edemiyor. Saf BM25 ise birinci gruptaki sorgularda zorlanıyor çünkü "emekli maaşımı taşırsam" ifadesi dokümanda muhtemelen "maaş promosyonu", "SGK emekli maaş transferi" gibi farklı kelimelerle geçiyor.

Bu örnekte kurduğumuz mimari şöyle işliyor:

BileşenRol
BM25 indeksi (Elasticsearch, Türkçe analyzer + Zemberek benzeri morfolojik kök ayrıştırma ile)Kod, numara, kısaltma, tam mevzuat referansı eşleşmeleri
Dense embedding indeksi (çok dilli veya Türkçe destekli model, örn. multilingual-e5 veya BGE-m3)Doğal dil, eş anlamlı, dolaylı sorular
Contextual chunk üretimiHer broşür/prosedür parçasına "bu, X ürününün Y bölümünden" bağlamı eklenmesi
RRF birleştirme (k=60)İki listenin adil şekilde harmanlanması
Cross-encoder rerankerSon 30 adayı sorguya göre ince eleme
Query routing (basit regex + niyet sınıflandırması)Kod/numara içeren sorgularda BM25 ağırlığını artırma

Bu mimariyi kurduktan sonra en çok fark yarattığı yer, kampanya kodu ve mevzuat madde numarası içeren sorgularda oldu — daha önce saf dense sistemde bu sorguların önemli bir kısmı ya alakasız bir broşür getiriyor ya da "bu konuda bilgim yok" cevabı üretiyordu. Contextual retrieval eklemesi de özellikle uzun mevzuat metinlerinde işe yaradı çünkü tek başına bir madde chunk'ı ("İkinci fıkrada belirtilen oran %2 olarak uygulanır") hangi genelgeye, hangi ürüne ait olduğunu söylemiyordu; bağlam eklendikten sonra hem BM25 hem dense tarafında bu chunk'lar çok daha isabetli yakalanır hale geldi.

Değerlendirme: hissi değil, ölçülü karar verin

Hibrit aramayı kurduktan sonra en sık yaptığım hata uyarısı şu: "iyi görünüyor" demek yeterli değil, ölçmeniz lazım. Önerdiğim minimum değerlendirme seti şöyle:

  • Etiketli sorgu-belge çiftleri: Gerçek kullanıcı loglarından (varsa) veya iş birimiyle birlikte hazırlanmış en az 100-150 sorgu, her biri için hangi chunk'ın/chunk'ların "doğru cevap" sayıldığı işaretlenmiş olarak.
  • Recall@k: İlk k sonuç içinde doğru chunk'ın kaçı yakalanmış. RAG'de genelde k=5 veya k=10 kritik eşiktir çünkü LLM'e context olarak bu kadarını veriyorsunuz.
  • nDCG (normalized Discounted Cumulative Gain): Sadece doğru belgenin listede olup olmadığını değil, ne kadar üst sırada olduğunu da ölçer — reranker'ın etkisini görmek için özellikle faydalı.
  • MRR (Mean Reciprocal Rank): Doğru cevabın ortalama olarak kaçıncı sırada çıktığını özetler, tek sayı olarak raporlamak için pratik.

Bu metrikleri üç konfigürasyonda (sadece BM25, sadece dense, hibrit + RRF, hibrit + RRF + reranker) ayrı ayrı hesaplayıp yan yana koyduğunuzda, hangi katmanın gerçekten katkı sağladığını, hangisinin sadece maliyet ve gecikme eklediğini net görürsünüz. Ben müşterilerime her zaman bu A/B tablosunu göstermeden mimariye "reranker ekleyelim" gibi bir tavsiyede bulunmuyorum çünkü bazı veri setlerinde reranker'ın katkısı marjinal kalabiliyor ve gecikme maliyetine değmeyebiliyor.

Ayrıca üretimde "sessiz başarısızlıkları" yakalamak için düzenli olarak kullanıcı geri bildirimini (beğenmeme, "bu cevap yardımcı olmadı" butonu gibi) retrieval loglarıyla ilişkilendirmenizi öneririm — çoğu zaman LLM'in "yanlış cevap" verdiği düşünülen durumların kökeninde aslında yanlış ya da eksik retrieval yatar.

Sık yapılan hatalar

Sahada en sık gördüğüm hatalardan birkaçını paylaşayım:

  • Tokenizer/analyzer'ı Türkçe'ye göre ayarlamamak. Varsayılan İngilizce analyzer ile BM25 kurup "neden Türkçe sorgularda kötü çalışıyor" diye şaşıranlar oluyor. Türkçe'nin eklemeli yapısı için ya kök bulma (stemming, Zemberek gibi araçlarla) ya da en azından edge n-gram / karakter n-gram yaklaşımı şart.
  • Chunk boyutunu tek bir sabit sayıya kilitlemek. 200 token, 500 token gibi sabit boyutlarla herkese uyan tek chunk stratejisi aramak yerine, belge tipine göre (SSS maddesi, tablo, uzun paragraf) farklı chunking stratejileri denemek gerekiyor.
  • Contextual retrieval'ı atlayıp doğrudan reranker'a güvenmek. Reranker, kendisine gelmeyen bir adayı sıralayamaz — yani chunk baştan retrieval aşamasında hiç yakalanmadıysa, reranker'ın hiçbir faydası olmaz. Önce recall'u (doğru adayın listeye girmesini), sonra precision'ı (doğru sıralamayı) iyileştirmek gerekir.
  • RRF sabiti k'yı hiç değiştirmeden bırakmak ama alpha'yı hiç denememek. RRF sağlam bir varsayılan ama her veri setinde en optimal olduğu anlamına gelmiyor; en azından birkaç konfigürasyonu test etmek gerekir.
  • Değerlendirme setini bir kere kurup bir daha bakmamak. Kullanıcı davranışı ve içerik zamanla değişir; değerlendirme setini çeyreklik olarak güncellemeyen ekipler, sistemin sessizce kötüleştiğini fark etmiyor.
  • Reranker'ı tüm koleksiyonda çalıştırmaya çalışmak. Cross-encoder'lar hesaplama açısından pahalıdır; binlerce belgeyi doğrudan reranker'dan geçirmek gecikmeyi ciddi şekilde artırır. Önce hibrit arama ile adayı 20-50'ye indirip, reranker'ı sadece o küçük kümede çalıştırmak gerekir.

Türkiye ve KVKK bağlamı: retrieval katmanında kişisel veri varsa

Kurumsal RAG projelerinde bilgi tabanı sıklıkla kişisel veri içerir — müşteri şikayet kayıtları, çalışan özlük dosyaları, sağlık raporları, kredi başvuru geçmişi gibi. Bu durumda hibrit arama mimarisini kurarken KVKK açısından dikkat etmem gereken birkaç nokta var, bunları danışmanlık projelerinde her zaman ayrı bir başlık olarak ele alıyorum:

  • Veri minimizasyonu chunk seviyesinde de geçerli. Chunk'lara bağlam eklerken (contextual retrieval), o bağlamın içine gereksiz kişisel veri sızdırmamaya dikkat etmek gerekir. Örneğin bir müşteri şikayet chunk'ına otomatik bağlam üretirken, LLM'in "bu chunk, [Ad Soyad]'ın [TC Kimlik No] numaralı şikayetinden alınmıştır" gibi bir özet üretmesi, aslında gereksiz yere kişisel veriyi hem embedding vektörüne hem de metin indeksine bir kez daha kopyalamış olur — bu veri asgarileştirme ilkesine aykırı düşebilir.
  • Vektör veritabanları da "veri işleme" kapsamındadır. Embedding'ler kişisel veriden türetildiği için, KVKK anlamında bu vektörler de kişisel veri sayılabilir (özellikle geri çevrilebilir/yakın metin çıkarımı riski varsa). Vektör veritabanı sağlayıcınızın (yurt dışı barındırma yapıyorsa) veri işleme sözleşmesi ve yurt dışına veri aktarımı hükümleri KVKK'nın 9. maddesi kapsamında değerlendirilmeli.
  • Erişim kontrolü, retrieval seviyesinde de uygulanmalı. BM25 ve vektör indeksleri, kullanıcı bazlı yetkilendirmeyi genelde varsayılan olarak desteklemez. Bir müşteri temsilcisi botunun, sadece kendi yetki alanındaki müşteri kayıtlarını getirdiğinden emin olmak için, indeks sorgusuna filtre (metadata tabanlı erişim kontrolü) eklemek gerekir — aksi halde hibrit arama, yetkisiz bir kullanıcıya başka bir müşterinin kişisel verisini "alakalı" diye getirebilir.
  • Loglama ve saklama süreleri. Retrieval logları (hangi sorguya hangi chunk'ların döndüğü) genelde debug ve değerlendirme için tutulur, ama bu loglar da kişisel veri içerebilir. KVKK'nın amaçla sınırlılık ve saklama süresi ilkeleri gereği, bu logların ne kadar süre tutulacağı, kimin erişebileceği net şekilde tanımlanmalı ve mümkünse anonimleştirme veya maskeleme uygulanmalı.
  • Anonimleştirme, embedding'i bozabilir. Bazı ekipler kişisel veriyi maskeleyip ("Sayın [MÜŞTERİ ADI]") sonra embed ediyor; bu iyi bir pratik ama maskelemenin, chunk'ın anlamsal bütünlüğünü bozmadığından emin olunmalı — aşırı agresif maskeleme, retrieval kalitesini de düşürebilir. Burada dengeyi bulmak, hem hukuki hem teknik ekibin birlikte çalışmasını gerektiriyor.

Devam eden bir mühendislik disiplini olarak retrieval

Hibrit arama kurmak bir "bir kere yapılır, biter" projesi değil; bu konuda müşterilerimi hep uyarıyorum. Bilgi tabanınız büyüdükçe, kullanıcı sorgu dağılımınız değiştikçe, yeni ürünler/kod/kısaltmalar eklendikçe, BM25-dense dengesini, chunk stratejinizi ve contextual retrieval prompt'larınızı periyodik olarak gözden geçirmeniz gerekiyor. En sağlıklı yaklaşım, retrieval katmanını ayrı bir "servis" olarak görüp, kendi değerlendirme setine, kendi sürüm geçmişine ve kendi izleme (monitoring) panosuna sahip olmasını sağlamak. Bu şekilde LLM tarafında herhangi bir model değişikliği yaptığınızda bile, retrieval kalitesinin bağımsız olarak sabit kaldığından veya iyileştiğinden emin olabiliyorsunuz — ve üretimde "neden cevap kalitesi düştü" sorusuna, en azından retrieval katmanını değişkenlerden çıkararak çok daha hızlı yanıt verebiliyorsunuz.

Danismanlik Baglantilari

Bu yazıya en yakın consulting sayfaları

Bu içerikten sonraki mantıklı adım için en ilgili solution, role ve industry landing'lerini burada görebilirsin.

Yorumlar

Yorumlar

Bağlantılı Pillar Konular

Bu yazının bağlandığı pillar konular