RAG (Retrieval-Augmented Generation) mimarisi kurarken gördüğüm en büyük hata, her probleme en büyük, en pahalı dil modelini (LLM) ve devasa vektör veritabanlarını fırlatmak oluyor. Geçen sene üzerinde çalıştığım bir üretim ERP’sinin tedarik zinciri arama motorunda, ilk başta her şeyi vektörlere çevirip bir bulut LLM’ine gönderen bir boru hattı (pipeline) tasarlamıştık. Sonuç? Kullanıcı başına saniyelerle ölçülen bir gecikme (latency) ve kabarık bir API faturası oldu. Sonra durup sistemi sadeleştirdim; LLM’i sadece son sentez aşamasına sıkıştırıp, retrieval (geri çağırma) aşamasını tamamen yerel, hafif araçlarla çözdüm. Arama gecikmesi belirgin biçimde düştü, maliyet ise neredeyse sıfırlandı.
Bu yazıda, RAG altyapılarında retrieval kalitesini artırırken büyük dil modellerine olan bağımlılığı nasıl azaltabileceğimizi, hibrit arama tekniklerini ve production ortamında bu işi nasıl ucuza kapatabileceğimizi kendi tecrübelerim üzerinden anlatacağım.
Vektör Veritabanı ve Dense Retrieval Çılgınlığı
Sektörde nereye baksam herkes bir vektör veritabanı kurma yarışı içinde. OpenAI’ın text-embedding-3-small gibi modelleriyle 1536 boyutta vektör alıp, bunları Cosine Similarity ile aratmanın her derde deva olduğu sanılıyor. Gerçekte ise durum hiç öyle değil. Dense retrieval (yoğun geri çağırma) dediğimiz bu yöntem, anlamsal ilişkileri yakalamakta çok başarılı olsa da spesifik anahtar kelimelerde, parça kodlarında veya seri numaralarında tamamen çuvallıyor.
Örneğin, kullanıcı “SKU-4912-A” kodlu parçayı aradığında, vektör arama motoru gidip benzer açıklamaya sahip “SKU-4912-B” parçasını getirebiliyor. Çünkü vektör uzayında bu iki metnin semantik farkı devede kulak kalıyor. Aşağıdaki tabloda, dense retrieval ile klasik yöntemlerin kaynak ve doğruluk karşılaştırmasını kabaca büyüklük mertebeleriyle görebilirsiniz (kesin değerler veri setine ve donanıma göre değişir):
| Arama Tipi | Latency | RAM Tüketimi | SKU/Kod Doğruluğu | Semantik Başarı |
|---|---|---|---|---|
| Dense Vector (HNSW) | Orta | Yüksek | Zayıf | Çok iyi |
| Sparse (BM25 / FTS) | Çok düşük | Düşük | Çok iyi | Zayıf |
| Hybrid (RRF ile Birleşik) | Düşük | Orta | Çok iyi | Çok iyi |
Gördüğünüz gibi, sadece vektör aramaya güvenmek bizi üretim ortamında hem yüksek RAM maliyetleriyle baş başa bırakıyor hem de teknik verilerde hata yapma payımızı inanılmaz artırıyor. Aynı trade-off, büyük verileri yerel sunucuda indekslerken de karşımıza çıkar; bellek optimizasyonu yapmazsanız sınırlı kaynaklı bir sunucu er ya da geç tıkanır.
BM25 ve Klasik Ters Dizin (Inverted Index) Gücü
Eğer elinizde teknik dökümanlar, ERP verileri veya yapılandırılmış loglar varsa, ilk başvurmanız gereken yer klasik ters dizin tabanlı arama motorlarıdır. PostgreSQL 14+ sürümünde gelen tsvector ve tsquery özellikleri, harici bir Elasticsearch kümesi kurup yönetme zahmetine girmeden işinizi fazlasıyla görür. BM25 algoritması, bir kelimenin döküman içindeki sıklığını (TF) ve tüm dökümanlar arasındaki nadirliğini (IDF) hesaplayarak bize matematiksel olarak çok tutarlı bir skor üretir.
Aşağıdaki SQL örneğinde, bir parçalar tablosundaki ürün adı ve açıklamalarını nasıl indeksleyip arattığımı görebilirsiniz. Burada GIN (Generalized Inverted Index) kullanarak sorgu süresini mikrosaniyeler seviyesine indirdim:
-- Arama vektörlerini saklayacağımız generate edilmiş bir kolon ekliyoruz
ALTER TABLE parts_catalog
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
to_tsvector('english', coalesce(part_name, '') || ' ' || coalesce(description, ''))
) STORED;
-- GIN indeksi oluşturuyoruz
CREATE INDEX idx_parts_search_gin ON parts_catalog USING gin(search_vector);
-- Arama sorgusu ve execution plan analizi
EXPLAIN ANALYZE
SELECT id, part_name, ts_rank(search_vector, query) as rank
FROM parts_catalog, to_tsquery('english', 'SKU-4912-A | copper') query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 5;
Bu sorgunun EXPLAIN ANALYZE çıktısına baktığımda, yüz binlerce satırlık tabloda diskten okuma yapmadan, sadece Bitmap Index Scan kullanarak sonucu milisaniyenin altında döndürdüğünü gördüm. Aynı aramayı vektör veritabanında yapmaya çalışsaydık, hem daha yavaş olacaktı hem de “copper” (bakır) kelimesinin geçtiği ama alakasız başka parçaları önümüze serecekti.
Hibrit Arama (Hybrid Search) Mimarisi ve Karar Matrisi
Peki hem semantik anlamı kaçırmak istemiyor hem de anahtar kelime hassasiyetini korumak istiyorsak ne yapacağız? Çözüm: Hibrit Arama. Burada devreye Reciprocal Rank Fusion (RRF) algoritması giriyor. RRF, iki farklı arama motorundan gelen sıralamaları (rank) alır ve bunları ortak bir formülle yeniden puanlar. Pahalı orkestrasyon kütüphanelerine hiç gerek yok; bunu kendi backend servisinizde birkaç satır kodla halledebilirsiniz.
Aşağıda, FastAPI ve PostgreSQL kullanarak yazdığım, hem vektör arama (pgvector) hem de Full-Text Search (FTS) sonuçlarını RRF ile birleştiren fonksiyonun sadeleştirilmiş halini görebilirsiniz:
from typing import List, Dict
def reciprocal_rank_fusion(vector_results: List[Dict], fts_results: List[Dict], k: int = 60) -> List[Dict]:
"""
RRF algoritması ile iki farklı sonuç kümesini birleştirir.
k: Sabit penalizasyon parametresi (genelde 60 seçilir)
"""
scores = {}
# Vektör sonuçlarını işle
for rank, doc in enumerate(vector_results):
doc_id = doc["id"]
scores[doc_id] = scores.get(doc_id, 0.0) + (1.0 / (k + rank + 1))
# FTS sonuçlarını işle
for rank, doc in enumerate(fts_results):
doc_id = doc["id"]
# Eğer döküman her iki aramada da varsa skoru katlanır
scores[doc_id] = scores.get(doc_id, 0.0) + (1.0 / (k + rank + 1))
# Skorlara göre sırala
sorted_docs = sorted(scores.items(), key=lambda x: x[1], reverse=True)
return sorted_docs
Bu mimarinin en güzel yanı, her iki dünyanın da en iyi özelliklerini bir araya getirmesidir. Kullanıcı “red actuator valve” yazdığında, FTS “actuator” ve “valve” kelimelerini tam eşleştirirken; vektör arama ise “red” (kırmızı) rengine benzer tonlardaki dökümanları yakalar. RRF ise bu iki sinyali birleştirip en doğru dökümanı en üste taşır.
Büyük Dil Modelleri Yerine Küçük Modeller (Cross-Encoders) Kullanımı
Retrieval kalitesini artırmak için milyarlarca parametreli LLM’leri (örneğin GPT-4 veya Claude) her sorguda tetiklemek hem finansal bir intihardır hem de ağ gecikmesini (network overhead) tavan yaptırır. Bunun yerine, ilk aşamada (retrieval) hızlıca 50 döküman çekip, bunları yerel olarak koşan bir Cross-Encoder (Re-ranker) modelinden geçirmek çok daha mantıklıdır.
Bi-Encoder modelleri (klasik embedding modelleri) sorgu ve dökümanı ayrı ayrı vektörleştirirken, Cross-Encoder modelleri sorgu ile dökümanı aynı anda işlemciye sokar ve aralarındaki ilişkiyi çok daha derin analiz eder. BAAI/bge-reranker-base gibi sadece 278 MB boyutunda bir model, CPU üzerinde bile harikalar yaratabilir.
from sentence_transformers import CrossEncoder
# Küçük ve efektif bir re-ranker modeli yüklüyoruz
model = CrossEncoder("BAAI/bge-reranker-base", max_length=512)
query = "How to adjust the pressure limit on model X12?"
documents = [
"To adjust the pressure limit on model X12, turn the valve clockwise.",
"Model X12 is a hydraulic actuator designed for high pressure systems.",
"Standard pressure limits for older models are set at 150 PSI."
]
# Modeli çalıştırıp skorları alıyoruz
pairs = [[query, doc] for doc in documents]
scores = model.predict(pairs)
# Sonuçları ekrana yazdırıyoruz
for doc, score in zip(documents, scores):
print(f"Score: {score:.4f} | Doc: {doc}")
Bu Python betiğini standart bir VPS CPU’sunda çalıştırdığımda, birkaç döküman için çıkarım süresi yalnızca birkaç milisaniye sürdü. Bu işlem sayesinde, LLM’e göndereceğimiz bağlamı (context) onlarca dökümandan en alakalı birkaç dökümana indirgemiş oluyoruz. Böylece hem LLM token maliyetimiz ciddi biçimde düşüyor hem de LLM’in “kaybolma” (lost in the middle) problemi ortadan kalkıyor.
Bellek ve CPU Yönetimi: Üretim Ortamında Kaynak Sınırlandırma
Yerel modelleri (re-ranker veya küçük embedding modelleri) production sunucularında koştururken en sık karşılaştığım sorun, PyTorch veya ONNX runtime kütüphanelerinin sunucudaki tüm CPU çekirdeklerini sömürmeye çalışmasıdır. Eğer önlem almazsanız, bir istek geldiğinde sistem yükü (load average) anında fırlar ve aynı sunucudaki PostgreSQL veya Nginx servisleriniz yanıt veremez hale gelir.
Bu sorunun önüne geçmek için Docker Compose veya SystemD düzeyinde kaynak limitleri tanımlamak şarttır. Ben genelde Python servislerimi Docker içinde sınırlandırarak koşturuyorum:
version: '3.8'
services:
reranker-service:
image: python-reranker:latest
environment:
- OMP_NUM_THREADS=2
- MKL_NUM_THREADS=2
deploy:
resources:
limits:
cpus: '2.00'
memory: 2G
reservations:
memory: 512M
restart: always
Buradaki en kritik ayar OMP_NUM_THREADS=2 çevre değişkenidir. PyTorch, varsayılan olarak sunucudaki tüm çekirdekleri (örneğin 32 çekirdekli bir bare-metal ise tamamını) kullanmaya meyillidir. Bu limiti 2 veya 4 ile sınırlamak, paralel isteklerde işlemciyi boğmadan stabil bir throughput elde etmenizi sağlar.
Gerçek Dünya Senaryosu: ERP Stok ve Tedarik Zinciri RAG Altyapısı
Tüm bu anlattıklarımı bir araya getirdiğimizde nasıl bir canavar ortaya çıktığını somut bir örnekle göstereyim. Bir üretim ERP’sinde, geçmişe dönük 150.000 satırlık bakım-onarım dökümanları ve arıza kayıtları bulunuyordu. Bakım teknisyenleri, sahada bir arıza gördüklerinde tabletlerinden sisteme arıza açıklamasını yazıp geçmiş çözüm önerilerini RAG ile sorguluyorlardı.
İlk kurduğumuz sistemde akış şöyleydi:
- Kullanıcı sorgusu -> OpenAI Embedding API -> Pinecone Vektör DB -> GPT-4 API -> Sonuç.
- Sorun: İnternet kesildiğinde veya API limitlerine takılındığında sistem duruyordu. Ortalama cevap süresi saniyeler mertebesindeydi.
Yeniden tasarladığım yerel hibrit akış:
- Kullanıcı sorgusu -> Postgres FTS + pgvector (lokal) -> Top 30 aday.
- Top 30 aday -> Lokal Docker’da koşan BGE-Reranker-Base -> Top 3 aday.
- Top 3 aday -> Sunucuda koşan hafif bir LLM (ya da fallback olarak Gemini Flash API).
- Sonuç: İnternet bağımlılığı minimuma indi. Ortalama cevap süresi göz açıp kapayıncaya kadar inecek seviyeye düştü.
# Log çıktısından bir kesit (Yerel hibrit sistemin akış adımları)
# Süreler ortama ve donanıma göre değişir
INFO - Received query: "valve pressure drop during startup"
INFO - Postgres FTS & Vector Search completed (30 candidates found)
INFO - Re-ranking completed (Top 3 selected)
INFO - Local LLM response generated
INFO - Total turnaround completed
Görüldüğü üzere, büyük dil modellerini ve bulut servislerini sistem mimarisinin merkezinden çıkarıp sadece en son aşamada “metin birleştirici” olarak konumlandırdığımızda, sistemin kararlılığı ve hızı bambaşka bir boyuta taşındı.
Sonraki adımda, benzer bir yerel optimizasyon sürecini kendi veritabanı replikasyon mimarimizde nasıl kurguladığımızı ve WAL bloat sorunlarını nasıl çözdüğümüzü ele alacağım.