Spring AI tabanlı RAG servislerinde doğru chunking, pgvector indeksleme, retrieval regresyon testleri ve gecikme ölçümü nasıl uygulanır? Bu rehber, spring boot eğitimi sonrası üretim problemlerine odaklanır.
Spring AI ile Java Backend'de RAG Değerlendirme ve Retrieval Tasarımı
Spring AI ile retrieval sözleşmesini veri modelinden başlatmak
Bir RAG endpoint'inin kalitesi, modelin cevabından önce retrieval sözleşmesine bağlıdır. Her chunk için en az tenantId, documentId, chunkId, sourceUri, version ve contentHash metadata'sını saklayın. contentHash, aynı doküman ikinci kez yüklendiğinde embedding maliyetini ve yinelenen sonuçları önler. Örneğin SHA-256 hash'i değişmemiş chunk'ı yeniden embed etmeyin; ingestion tablosunda tenant_id + content_hash üzerinde unique index kullanın.
Map<String, Object> metadata = Map.of(
"tenantId", tenantId.toString(),
"documentId", documentId.toString(),
"chunkId", chunkId,
"sourceUri", sourceUri,
"version", documentVersion,
"contentHash", sha256(chunkText)
);
Document document = new Document(chunkText, metadata);
vectorStore.add(List.of(document));
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(8)
.similarityThreshold(0.72)
.filterExpression("tenantId == '" + tenantId + "'")
.build();
List<Document> matches = vectorStore.similaritySearch(request);Buradaki kritik incelik filterExpression değerinin kullanıcıdan gelen ham metin olmamasıdır. tenantId'yi kimlik doğrulama katmanındaki UUID claim'inden üretin; kullanıcı girdisini filtre ifadesine birleştirmek bazı VectorStore sürücülerinde sorgu semantiğini bozabilir. spring framework katmanında TenantContext'i request filter ile kurup servis metoduna UUID geçirmek, HTTP header'ına güvenmekten daha denetlenebilir bir sınır oluşturur.
Bir java eğitimi, java programlama eğitimi veya java kursu RAG örneğini yalnızca chatbot olarak bitiriyorsa üretimde eksik kalan nokta budur: cevap metniyle birlikte chunkId ve sourceUri döndürülmelidir. İstemci, her iddia için kaynak bağlantısı gösterebildiğinde hatalı cevabı incelemek mümkün olur. Bu yaklaşım java backend geliştirme ekiplerinin API cevabına `citations` dizisi eklemesini gerektirir; modelin serbest biçimli kaynak adı yazmasına güvenilmez.
Spring Data JPA, Hibernate ORM ve pgvector ile indeks seçimi
PostgreSQL ve pgvector kullanıyorsanız embedding tablosunu Hibernate ORM entity'si olarak her sorguda hydrate etmeyin. Büyük `vector` kolonunun entity state içine alınması, persistence context belleğini ve dirty-checking taramasını büyütür. Doküman yaşam döngüsünü Spring Data JPA ile yönetin, ancak en yakın komşu sorgusunu `JdbcTemplate` veya native query ile yalnızca gerekli kolonları seçerek çalıştırın.
CREATE TABLE rag_chunk (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL,
document_id uuid NOT NULL,
chunk_id integer NOT NULL,
content text NOT NULL,
embedding vector(1536) NOT NULL,
metadata jsonb NOT NULL,
UNIQUE (tenant_id, document_id, chunk_id)
);
CREATE INDEX rag_chunk_embedding_hnsw
ON rag_chunk USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
CREATE INDEX rag_chunk_tenant_document_idx
ON rag_chunk (tenant_id, document_id);HNSW indeksinin filtreli sorguda her zaman `LIMIT 8` satır döndüreceğini varsaymayın. Yaklaşık indeks önce adayları bulur, sonra `tenant_id` filtresi adayları eleyebilir; küçük bir tenant için 8 yerine 2 sonuç görebilirsiniz. Sürümünüz destekliyorsa sorgu transaction'ında `SET LOCAL hnsw.ef_search = 100` ve `SET LOCAL hnsw.iterative_scan = strict_order` deneyin. Ardından gerçek planı ölçün:
EXPLAIN (ANALYZE, BUFFERS)
SELECT chunk_id, content, 1 - (embedding <=> :queryEmbedding) AS score
FROM rag_chunk
WHERE tenant_id = :tenantId
ORDER BY embedding <=> :queryEmbedding
LIMIT 8; `Rows Removed by Filter`, `shared hit/read` ve gerçek satır sayısını kaydedin. `ef_search` artırmak recall'ı iyileştirebilir, fakat CPU süresini artırdığı için bunu test seti olmadan kalıcı ayar yapmayın.Java microservices için retrieval regresyon testi ve kalite eşiği
RAG kalitesini yalnızca kullanıcı beğenisiyle izlemek geç tepki verir. Önce 50-200 gerçek soru içeren, her soruya beklenen `chunkId` kümesi atanmış bir golden set oluşturun. CI aşamasında Recall@K hesaplayın: beklenen chunk'lardan en az birinin ilk K sonuçta bulunmadığı soru sayısını toplam soru sayısına bölün. Chunk sınırı, embedding modeli veya metadata filtresi değiştiğinde bu test doğrudan retrieval regresyonunu yakalar.
@Test
void mustRetrieveTheExpectedPolicyChunk() {
SearchRequest request = SearchRequest.builder()
.query("İptal edilen siparişin iade süresi nedir?")
.topK(8)
.similarityThreshold(0.72)
.filterExpression("tenantId == '" + TENANT_ID + "'")
.build();
List<Document> result = vectorStore.similaritySearch(request);
assertThat(result)
.extracting(d -> d.getMetadata().get("chunkId"))
.contains("return-policy-v3-14");
}Bu testi Testcontainers ile gerçek PostgreSQL + pgvector imajına karşı çalıştırın; in-memory sahte VectorStore HNSW filtreleme davranışını, embedding boyutu uyumsuzluğunu veya SQL planını yakalayamaz. Ayrı bir testte embedding boyutunu doğrulayın: 1536 boyutlu kolon, farklı boyut üreten bir sağlayıcıya geçildiğinde insert sırasında hata verebilir. Bu hata uygulama açılışında değil, ilk ingestion işinde ortaya çıktığından migration pipeline'ında canary embedding insert'i faydalıdır.
spring ai ile cevap üretimini de ölçmek istiyorsanız LLM'in cümle kalitesini ilk kalite kapısı yapmayın. Önce retrieval Recall@8 eşiğini, örneğin golden set üzerinde en az 0.95 olarak sabitleyin; sonra cevaptaki her citation id'nin gerçekten retrieval sonucunda olup olmadığını doğrulayın. Bu iki katmanlı kontrol, modelin bağlamda olmayan bir kaynağı uydurmasını ayrı bir hata sınıfı olarak raporlar.
Spring AI retrieval gecikmesini JFR ve PostgreSQL planı ile ölçmek
Gecikme optimizasyonuna `topK` değerini rastgele düşürerek başlamayın. Önce staging ortamında Java Flight Recorder kaydı alın ve PostgreSQL tarafında `pg_stat_statements` ile aynı endpoint'in sorgularını eşleyin. JFR, uygulamanın embedding HTTP çağrısında mı, JSON serileştirmede mi yoksa connection pool beklemesinde mi zaman geçirdiğini; `EXPLAIN (ANALYZE, BUFFERS)` ise vektör taramasının disk okumasını gösterir.
java -XX:StartFlightRecording=filename=rag-load.jfr,settings=profile,dumponexit=true -jar app.jar
SELECT calls,
mean_exec_time,
p95_exec_time,
rows,
query
FROM pg_stat_statements
WHERE query LIKE '%rag_chunk%'
ORDER BY mean_exec_time DESC
LIMIT 10;Önce-sonra karşılaştırmasını aynı soru seti, aynı eşzamanlılık ve aynı sıcaklık ayarıyla yapın. Örnek bir değişiklikte `topK(20)` ve `similarityThreshold(0.55)` yerine golden set Recall@8 eşiğini koruyarak `topK(8)` ve `similarityThreshold(0.72)` kullanın. JFR'de endpoint p95, HTTP client bekleme süresi ve allocation rate; PostgreSQL'de mean_exec_time ve `shared read` değerlerini birlikte kaydedin. Sadece p95 düşüp Recall@8 düşüyorsa değişiklik kabul edilmemelidir.
@Transactional(readOnly = true)
public List<Document> retrieve(UUID tenantId, String question) {
jdbcTemplate.execute("SET LOCAL hnsw.ef_search = 100");
return vectorStore.similaritySearch(SearchRequest.builder()
.query(question)
.topK(8)
.similarityThreshold(0.72)
.filterExpression("tenantId == '" + tenantId + "'")
.build());
}`SET LOCAL` yalnızca aktif transaction ve aynı veritabanı bağlantısında yaşar. Metottaki `@Transactional` kaldırılırsa connection pool bağlantıyı sorgular arasında değiştirebilir veya ayar beklenenden önce sıfırlanabilir. Bu, yük testinde görünmeyip üretimde bazı sorguların varsayılan `ef_search` ile çalışmasına yol açan yaygın bir edge case'tir.
Spring MCP ve Model Context Protocol sınırında RAG araçları
Spring MCP ve Model Context Protocol, RAG doküman deposunun yerine geçmez; modele kontrollü iş araçları açmak için ayrı bir protokoldür. Örneğin retrieval sonucu sipariş politikasını açıklayabilir, fakat canlı sipariş durumunu almak için ayrı bir MCP tool çağrısı gerekir. Bu ayrım, değişken operasyonel verinin embedding indeksine bayat bir kopya olarak girmesini engeller.
@Tool(description = "Returns the current status for an order owned by the authenticated tenant")
public OrderStatus getOrderStatus(UUID orderId, UUID authenticatedTenantId) {
Order order = orderRepository.findByIdAndTenantId(orderId, authenticatedTenantId)
.orElseThrow(() -> new AccessDeniedException("order not found for tenant"));
return new OrderStatus(order.getId(), order.getStatus(), order.getUpdatedAt());
}Tool metodunda tenantId'yi modelin ürettiği parametre olarak kabul etmeyin; örnekteki `authenticatedTenantId` security context'ten enjekte edilmelidir. Ayrıca tool çıktısına kart numarası, adres veya ham müşteri notu koymayın. Allowlist ile yalnızca `id`, `status`, `updatedAt` alanlarını DTO'ya taşıyın ve her tool çağrısına trace id, kullanıcı id'si, tool adı ve sonuç kodunu audit kaydı olarak yazın. Bu sınır, java microservices içinde LLM çağrısının yetki genişletme mekanizmasına dönüşmesini önler.
Bir java fullstack eğitimi projesinde arayüz tarafı citation link'lerini ve tool sonucunu aynı kartta gösterebilir; fakat bunları farklı rozetlerle işaretlemelidir: biri sürümlenmiş bilgi tabanı, diğeri canlı sistem verisidir. Böylece kullanıcı hangi bilginin indekslenmiş dokümandan, hangisinin MCP çağrısından geldiğini ayırt eder.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Java Spring Boot ReactJS FullStack Eğitimi (Yıldız Teknik Üniversitesi SEM)
Sık Sorulan Sorular
Spring AI ile RAG yaparken Spring Data JPA mi JdbcTemplate mi kullanmalıyım?
Doküman, tenant ve ingestion kayıtları için Spring Data JPA uygundur. `vector` kolonu üzerinden benzerlik aramasında ise yalnızca ihtiyaç duyulan alanları native SQL veya JdbcTemplate ile seçin. Her sorguda Hibernate ORM entity hydrate etmek, büyük embedding alanlarının persistence context içinde tutulmasına neden olur.
Java backend geliştirme projesinde pgvector HNSW ef_search değeri nasıl seçilir?
Golden soru setiniz üzerinde 20, 50, 100 ve 200 değerleri için Recall@8 ile p95 sorgu süresini ölçün. Her denemede `EXPLAIN (ANALYZE, BUFFERS)` çıktısını ve `pg_stat_statements.mean_exec_time` değerini kaydedin. En düşük gecikmede kalite eşiğinizi geçen değeri seçin; sabit bir evrensel sayı yoktur.
spring mcp ve model context protocol, Spring AI RAG indeksinin yerine kullanılabilir mi?
Hayır. RAG indeksi sürümlenmiş metinlerden semantik retrieval yapar. Spring MCP ise sipariş durumu veya envanter gibi canlı sistem verisine kontrollü tool çağrısı açar. Tool parametrelerindeki tenant ve kullanıcı kimliği model çıktısından değil security context'ten alınmalıdır.
spring boot eğitimi sonrasında RAG kalitesini CI içinde nasıl test ederim?
Beklenen chunkId'leri tanımlı bir golden set hazırlayın, Testcontainers ile PostgreSQL + pgvector başlatın ve her soru için Recall@8 hesabı yapın. Ayrıca dönen citation id'lerinin retrieval sonucunda bulunduğunu assert edin. Bu test, chunking veya embedding sağlayıcısı değiştiğinde oluşan regresyonu production'a çıkmadan yakalar.
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.


