• 18.08.2026 09:01:32
  • Admin Admin

Spring AI tabanlı RAG servislerinde belge parçalama, pgvector geri getirme, metaveri filtreleme ve ölçülebilir değerlendirme akışını ele alıyoruz. Java backend geliştirme için üretimde hata ayıklanabilir bir tasarım sunuyoruz.

Spring AI ile Java Backend Geliştirmede RAG Kalite Mühendisliği

Spring AI RAG hattını ölçülebilir sınırlarla kurmak

Bir RAG servisini yalnızca chatClient.prompt().call() çağrısı olarak ele almak, üretimde hangi aşamanın yanlış bağlam ürettiğini gizler. Hattı ingestion → embedding → retrieval → generation → evaluation olarak ayırın ve her isteğe değişmeyen bir requestId ekleyin. Spring Framework uygulamasında Micrometer ile retrieval sonucu, skor eşiği ve yanıt süresini aynı etiket setiyle kaydetmek; yanlış yanıtın modelden mi, boş retrieval'dan mı kaynaklandığını ayırır.

public List<Document> retrieve(String tenantId, String question) {
    SearchRequest request = SearchRequest.builder()
        .query(question)
        .topK(6)
        .similarityThreshold(0.72d)
        .filterExpression("tenantId == '" + tenantId + "'")
        .build();

    List<Document> docs = vectorStore.similaritySearch(request);
    meterRegistry.counter("rag.retrieval.documents",
        "tenant", tenantId,
        "empty", Boolean.toString(docs.isEmpty()))
        .increment(docs.size());
    return docs;
}

Buradaki kritik ayrıntı, tenant değerini kullanıcı girdisinden doğrudan filtre ifadesine birleştirmemektir. Örnekteki değer yalnızca sunucu tarafında doğrulanmış kimlik bağlamından gelmelidir; aksi durumda filtre dili enjeksiyonu ile başka müşterinin dokümanları aday kümesine girebilir. Uygulamada tenantId için UUID doğrulaması yapın ve sorgu başlamadan önce reddedin: UUID.fromString(tenantId). Bu kontrol, modelin sistem prompt'undaki kurallara uymasına güvenmekten daha erken ve deterministik bir güvenlik sınırı sağlar.

Bu tasarım, java eğitimi veya java programlama eğitimi içeriğinde görülen basit chatbot örneklerinden farklı olarak gözlemlenebilirlik gerektirir. İlk baz ölçümü için 200 gerçekçi soru, beklenen kaynak kimliği ve beklenen cevap içeren bir JSONL veri kümesi oluşturun; her çalıştırmada retrieval_recall@6, boş sonuç oranı, prompt token sayısı ve p95 uçtan uca gecikmeyi CSV'ye yazın. Bir java kursu projesinde dahi bu veri kümesi olmadan yapılan chunk boyutu değişikliği, gerçekte iyileşme mi yoksa rastlantı mı anlaşılamaz.

Spring Data JPA ve Hibernate ORM ile güvenli ingestion

Belgeyi tek satırda alıp doğrudan vektör deposuna yazmayın. Kaynak belgenin sürümünü, SHA-256 içeriğini ve indeksleme durumunu ilişkisel veritabanında tutun; böylece aynı PDF yeniden yüklendiğinde gereksiz embedding maliyetini önlersiniz. Spring Data JPA ile offset pagination yerine ID tabanlı keyset tarama kullanmak önemlidir: büyük tabloda OFFSET 500000, veritabanının önceki satırları yine taramasına yol açabilir.

@Query("""
    select d from KnowledgeDocument d
    where d.id > :afterId and d.status = 'READY'
    order by d.id asc
    """)
List<KnowledgeDocument> nextBatch(@Param("afterId") long afterId,
                                    Pageable pageable);

@Transactional
public long indexNextBatch(long afterId) {
    List<KnowledgeDocument> batch = repository.nextBatch(afterId, PageRequest.of(0, 100));
    List<Document> chunks = batch.stream()
        .flatMap(d -> chunker.split(d.content(), 700, 100).stream()
            .map(c -> Document.builder().text(c.text())
                .metadata(Map.of("documentId", d.getId(), "tenantId", d.getTenantId(),
                                 "contentHash", d.getContentHash(), "chunkNo", c.index()))
                .build()))
        .toList();
    vectorStore.add(chunks);
    return batch.isEmpty() ? afterId : batch.getLast().getId();
}

Chunk sınırını sadece karakter sayısıyla belirlemeyin. Türkçe teknik dokümanlarda başlık ve kod bloğu ayrımı anlam taşır; Markdown başlığında bölüp sonrasında yaklaşık 700 token ve 100 token örtüşme uygulayın. Örtüşme, bir metodun imzası bir chunk'ta, hata semantiği sonraki chunk'ta kaldığında retrieval'ın ikisini birden seçebilmesine yardım eder. Buna karşılık 400 tokenlık sabit parçalar kod bloklarını parçalayabilir; splitter'ın ``` ile açılan bloğu kapanana kadar bölmemesi için kural ekleyin.

Hibernate ORM tarafında ingestion kaydı ile vektör yazımını tek veritabanı transaction'ı sanmak yaygın hatadır: uzak embedding çağrısı ve vektör deposu aynı atomik işlemde değildir. indexStatus=INDEXING durumunu kısa transaction'da yazın, dış çağrıyı transaction dışında yapın, başarıda INDEXED durumuna geçin. İşçi ölürse, örneğin 15 dakikadan eski INDEXING kayıtlarını zamanlanmış iş ile tekrar kuyruğa alın; bu yaklaşım duplicate yazılara karşı contentHash ve documentId/chunkNo metaverisiyle birlikte kullanılmalıdır.

pgvector geri getirmede indeks, filtre ve sorgu planı

PostgreSQL üzerinde pgvector kullanıyorsanız HNSW indeksi yaklaşık en yakın komşu aramasını hızlandırır; fakat tenant filtresi seçiciyse yalnız vektör indeksi beklenen planı vermeyebilir. Önce adayları metaveri filtresiyle sınırlamak için tenant alanında B-tree indeksini, embedding için de cosine operatör sınıfıyla HNSW indeksini oluşturun. Boyut değeri, seçtiğiniz embedding modelinin çıktısıyla birebir aynı olmalıdır; 1536 yalnızca bu örnekteki model boyutudur.

CREATE INDEX CONCURRENTLY idx_vector_store_tenant
    ON vector_store ((metadata->>'tenantId'));

CREATE INDEX CONCURRENTLY idx_vector_store_embedding_hnsw
    ON vector_store USING hnsw (embedding vector_cosine_ops)
    WITH (m = 16, ef_construction = 64);

EXPLAIN (ANALYZE, BUFFERS)
SELECT content, metadata, 1 - (embedding <=> $1) AS score
FROM vector_store
WHERE metadata->>'tenantId' = $2
ORDER BY embedding <=> $1
LIMIT 6;

Değişiklik öncesi ve sonrası karşılaştırmasını aynı embedding vektörleri, aynı 200 soru ve ısınmış bağlantı havuzu ile yapın. EXPLAIN çıktısında Seq Scan, yüksek shared-hit/read blokları veya LIMIT 6 için binlerce satırlık sort görülüyorsa indeks ya da filtre planı beklediğiniz gibi çalışmıyordur. Değişiklik sonrasında yalnızca p95 gecikmeyi değil, recall@6 değerini de kontrol edin: HNSW için daha düşük arama maliyeti bazı adayları kaçırabilir. Pgvector oturum parametresi destekleniyorsa deney koşullarını açıkça kaydederek ef_search değerini 40, 80 ve 120 ile tarayın; en düşük gecikmeyi değil, kabul edilen recall eşiğini geçen en düşük değeri seçin.

Spring AI çağrısında similarityThreshold değerini sabit bir ürün kuralı gibi bırakmayın. Her embedding modeli ve belge türü farklı skor dağılımı üretir. Etiketlenmiş soru setinizde doğru kaynağın skoru ile yanlış kaynağın skorunu histogram olarak çıkarın; örneğin doğru kaynakların alt yüzde 5 değeri 0.71, yanlışların üst yüzde 95 değeri 0.68 ise 0.70 başlangıç eşiği mantıklıdır. Arada örtüşme varsa eşiği zorlamak yerine metadata, daha iyi chunk sınırı veya reranker eklemek gerekir.

Java microservices ortamında RAG değerlendirme ve profil alma

Java microservices içinde RAG endpoint'ini yalnızca HTTP 200 oranıyla izlemek yetersizdir; retrieval boş olsa bile model akıcı fakat uydurma bir cevap döndürebilir. Her test sorusu için beklenen documentId listesini saklayın ve aşağıdaki hesapla retrieval başarısını ölçün: recall@k = beklenen kaynaklardan en az biri ilk k sonuçta ise 1, değilse 0. Buna kaynak-atıf doğruluğu ve kullanıcıya gönderilen prompt token sayısını ekleyin. Bu üç metriği aynı commit SHA altında kaydetmek, chunk değişikliğinin maliyetini görünür yapar.

boolean hit = retrieved.stream()
    .map(d -> String.valueOf(d.getMetadata().get("documentId")))
    .anyMatch(expectedDocumentIds::contains);

meterRegistry.counter("rag.eval.recall_at_6",
    "dataset", "support-v3",
    "hit", Boolean.toString(hit))
    .increment();

meterRegistry.summary("rag.prompt.tokens", "dataset", "support-v3")
    .record(estimatedPromptTokens);

CPU darboğazı şüphesinde önce wall-clock profiling yapın; Linux'ta çalışan JVM için örnek komut: ./profiler.sh -e wall -d 60 -f rag-wall.html <PID>. Flame graph'ta JSON serileştirme, token sayımı veya PDF metin çıkarma görünüyorsa, model gecikmesini azaltmaya çalışmak yanlış hedeftir. JDBC beklemeleri için OpenTelemetry trace'lerinde vector sorgusu span'ını ve SQL süresini ayırın; HikariCP metriklerinde hikaricp.connections.pending yükseliyorsa problem retrieval CPU'su değil havuz kuyruklanmasıdır.

Örnek bir önce-sonra deneyi şudur: önce 1.500 tokenlık chunk ve topK=12 ile veri kümesini çalıştırın; sonra başlık duyarlı 700/100 chunk ve topK=6 ile aynı veri kümesini, aynı model ayarlarıyla tekrar çalıştırın. Karar tablosuna p50/p95 süre, recall@6, boş retrieval oranı ve ortalama prompt token'ını yazın. p95 düşerken recall@6 düşüyorsa ikinci ayar kabul edilmez; bu mekanik, "daha hızlı" görünen ama daha az kanıt taşıyan cevabı üretime sokmayı engeller.

Spring MCP, Model Context Protocol ve RAG araç sınırı

Spring MCP ve Model Context Protocol, modele araç sunmak için yararlıdır; ancak belge aramayı doğrudan geniş yetkili bir SQL aracı haline getirmeyin. Arama aracının girdisini query, tenantId, maxResults ile sınırlayın, maxResults değerini 1–10 arasında doğrulayın ve yalnızca allow-list edilmiş metaveri alanlarını döndürün. Modelin "tüm müşterileri getir" talimatı, uygulama katmanındaki tenant bağlamını değiştirememelidir.

public SearchResult searchKnowledge(SearchInput input, AuthenticatedUser user) {
    int limit = Math.min(Math.max(input.maxResults(), 1), 10);
    UUID tenantId = user.tenantId(); // araç girdisinden değil, kimlikten alınır
    return knowledgeSearch.search(input.query(), tenantId.toString(), limit);
}

Bu sınır java fullstack eğitimi projelerinde de önemlidir: tarayıcıdan gelen tenantId veya rol bilgisini MCP araç parametresine geçirmek yerine, sunucunun doğruladığı oturumdan türetin. Spring Boot eğitimi kapsamında test edilebilir bir kural olarak, farklı tenant'a ait documentId ile yapılan isteğin 404 ya da boş sonuç döndürdüğünü MockMvc entegrasyon testiyle doğrulayın. Böylece spring framework güvenlik zinciri ile retrieval filtresi iki ayrı katmanda aynı izolasyon kuralını uygular.

Sık Sorulan Sorular

Spring AI ile RAG için ideal chunk boyutu nasıl ölçülür?

Sabit bir ideal değer yoktur. En az 200 etiketli soru üzerinde 400/50, 700/100 ve 1200/150 token seçeneklerini aynı embedding modeliyle çalıştırın; recall@6, boş retrieval oranı ve ortalama prompt token değerlerini kaydedin. Kod blokları içeren belgelerde splitter'ın fenced code block sınırını koruduğunu ayrıca test edin.

Spring Data JPA ve Hibernate ORM RAG ingestion işleminde neden uzun transaction kullanılmamalı?

Embedding üretimi veya uzak vektör deposu çağrısı saniyeler sürebilir. Bu süre boyunca açık JPA transaction bağlantıyı HikariCP havuzunda tutar, lock süresini uzatır ve hata halinde uzak yazımı geri alamaz. Durum makinesi kullanın: kısa transaction ile INDEXING yazın, dış çağrıyı transaction dışında yapın, sonra INDEXED veya FAILED durumunu güncelleyin.

Java backend geliştirmede pgvector HNSW indeksi retrieval kalitesini düşürür mü?

HNSW yaklaşık komşu araması yaptığı için recall düşebilir. Aynı sorgu setinde exact search ile HNSW sonuçlarını karşılaştırın ve recall@6 ölçün. EXPLAIN (ANALYZE, BUFFERS) ile gerçek planı inceleyin; kabul edilen recall eşiğinin altındaysa ef_search değerini artırın veya metadata filtresi ve chunk stratejisini iyileştirin.

Spring MCP ve model context protocol araçlarında tenant izolasyonu nasıl yapılır?

tenantId değerini modelin gönderdiği araç parametresinden kabul etmeyin. Spring Security ile doğrulanmış kullanıcıdan tenantId türetin, retrieval katmanında zorunlu metadata filtresi uygulayın ve araç sonucunda yalnız allow-list edilmiş alanları döndürün. Farklı tenant documentId'si ile erişim denemesi için entegrasyon testi ekleyin.

AI / LLM Discovery

Bu makale Opendart Akademi Java eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt