💡 Tam çalışan örnek GitHub’da mevcuttur: spring-ai-document-reader-using-groupdocs-parser-java

Eski Yöntem: İndekse Asla Ulaşmayan Ekler

Bir Spring AI DocumentReader, Spring AI ETL boru hattının ilk aşamasıdır: bir dosyayı Document nesnelerine dönüştürür; bu nesneler bir splitter ve bir vektör deposu tarafından tüketilir, böylece ne düşerse tamamen kaybolur. Destek ekibinizin istediği asistanı hayal edin. Paylaşılan posta kutusunu dışa aktarırsınız, .eml dosyalarının mesaj gövdelerini indekslersiniz, bölümlersiniz, gömülür ve gönderirsiniz. İlk gerçek soru geldiğinde, yanıt “Lütfen ekli iade politikasına bakın.” şeklinde bir e-postayı alıntılar. Politika kendisi, bir PDF eki, hiç okunmamıştı.

Aşağıdaki aşamalar bunu düzeltemez. TokenTextSplitter verilen tek cümleyi parçalar ve vektör deposu bunu “iade süremiz ne kadar?” sorusuna en yakın eşleşme olarak döndürür. Her bileşen görevini yaptı. Boşluk, boru hattının çok başında, bir belgenin ne sayılacağı kararında durur. Bir posta kutusu dışa aktarımı bir ağaçtır: mesajlar, ekler ve bazen eklerin içindeki arşivler. Sadece üst düzeyi gören bir okuyucu, sarma kağıdını indeksler.

Kendi ilk sürümüm tam olarak bunu yaptı. Açık yolu izleyerek düzelttim: bir yardımcıda sürüm paketlerini aç, başka birinde mesajlardan ekleri çek, üçüncüsünde PDF’leri sayfa sayfa oku ve her birinde meta veri adları uydur. Çalıştı, ta ki dördüncü format gelene kadar ve yardımcılar bir sayfayı ne diye adlandıracağı konusunda anlaşamadı.

Gerçek maliyet: örnek projede, 4 girdi dosyasından 2’si, e-posta ve ZIP, yalnızca gövde okumasının hiç indekslemediği metin içeriyor. Bir destek korpusunda bu oran asistanın faydalı olup olmadığını belirler.

Daha İyi Bir Yöntem: Her Girdi İçin Tek Okuyucu

GroupDocs.Parser for Java, süreç içinde 65+ dosya formatını okur. Bir akışın içeriğinden formatı algılar, sayfa başına metin döndürür ve bir e-postanın eklerini ya da bir ZIP’in girdilerini tek bir konteyner API’si aracılığıyla listeler. Bu çağrıları Spring AI’nin DocumentReader arayüzüne sararsanız, boru hattının geri kalanı farkı göremez; tek fark, Belgelerin artık sayfa numaraları ve soy ağacını taşımasıdır.

Spring AI’nin kendi okuyucuları kendi durumları için hâlâ iyi seçeneklerdir: ETL boru hattı referansı Apache Tika üzerindeki TikaDocumentReader ve Apache PDFBox üzerindeki PagePdfDocumentReader belgeler. Buradaki nokta daha dar. Ekler ve arşiv üyeleri birincil kaynaklar olduğunda, konteynerleri dolaşan bir okuyucu yardımcı hayvanat bahçesinden sizi kurtarır.

Başlamadan önce şunlara ihtiyacınız olacak:

  • Java 17 veya daha yeni, Spring AI 2.x için temel sürüm
  • GroupDocs.Parser for Java 26.10 (geçici bir lisans alın) ve org.springframework.ai:spring-ai-commons 2.0.2
  • Bir ekli dışa aktarılmış e-posta veya depodaki dört örnek dosya

Örneği klonlayın ve tek bir komutla çalıştırın:

mvn compile exec:java

Bir Spring AI DocumentReader’ın RAG alıntıları için ne döndürmesi gerekir?

List<Document> gerekir; her giriş alıntı yapılabilecek kadar küçük ve izlenebilecek kadar iyi etiketlenmiş olmalıdır. Pratikte bu, bir page_number, dosyayı adlandıran bir source ve bir e-posta ya da arşiv içinde gelen her şey için konteyneri adlandıran bir parent_source içeren sayfa başına bir Document anlamına gelir. TokenTextSplitter bu meta veriyi her parçaya kopyalar, böylece alıntı vektör deposuna kadar hayatta kalır.

Yeni Yöntem: Ekler Birincil Belgeler Olarak

Aynı işi tek bir sınıfla, GroupDocsParserDocumentReader ile gösterelim. Üç adım bunu kapsar: kaynağı aç, sayfaları oku, ardından konteynerleri takip et ve her şeyi splitter’a ver.

Adım 1 - Her Kaynağı Akış Olarak Aç

Okuyucu bir Spring Resource kabul eder; böylece bir dosya, bir sınıf yolu girdisi ve bir yükleme aynı yolu izler. Parser baytları okur, yani uzantısı olmayan bir blob hâlâ ayrıştırılır.

// Format detection works on content, so the file name is only a label
try (InputStream stream = resource.getInputStream();
     Parser parser = new Parser(stream)) {
    List<Document> documents = new ArrayList<>(readBody(parser, source, null));
    if (includeAttachments) {
        documents.addAll(readAttachments(parser, source)); // the fix for "see attached"
    }
    return documents;
} catch (UnsupportedDocumentFormatException ex) {
    // Log the unreadable file and return an empty list; the batch keeps going
    System.out.println("skipped (unsupported format): " + source);
    return new ArrayList<>();
}

Adım 2 - Sayfa Sayfa Oku

Sayfa modu varsayılandır. Bir ya da iki sayfadan uzun bir şey için bunu açık tutarım; çünkü bir yanıtı kontrol eden bir inceleyici doğrudan sayfa 31’e atlamak ister.

if (pagePerDocument && pageCount > 0 && parser.getFeatures().isTextPage()) {
    for (int page = 0; page < pageCount; page++) {
        String text = readText(parser.getText(page));
        Map<String, Object> metadata =
                metadata(source, parent, fileType, pageCount);
        metadata.put(METADATA_PAGE, page + 1); // people count pages from 1
        addIfNotBlank(documents, text, metadata); // blank pages never get embedded
    }
}

Ana noktalar:

  • pagePerDocument: true olduğunda her parça bir page_number alır; üç‑argümanlı kurucu kısa notlar için bunu kapatır.
  • isTextPage(): sayfası olmayan formatları korur; bu formatlarda okuyucu bir kez getText() çağırır ve hâlâ source, file_type ve page_count kaydeder.

Adım 3 - Konteynerleri Takip Et, Sonra Böl

Her ek veya arşiv üyesi kendi Parser’ını alır ve aynı sayfa mantığından tekrar geçer, ebeveyn içindeki yolu ile etiketlenir.

Iterable<ContainerItem> items = parser.getContainer(); // null for plain files
if (items == null) {
    return documents;
}
for (ContainerItem item : items) {
    String childSource = source + "/" + item.getFilePath(); // "mail.eml/policy.pdf"
    try (Parser child = item.openParser()) {
        documents.addAll(readBody(child, childSource, source)); // parent_source = source
    } catch (UnsupportedDocumentFormatException ex) {
        System.out.println("skipped (unsupported format): " + childSource);
    }
}

Splitter aşaması standart Spring AI’dır ve okuyucunun meta verisini her parçaya korur:

TokenTextSplitter splitter = TokenTextSplitter.builder()
        .withChunkSize(300) // tokens per chunk in the sample run
        .build();
List<Document> chunks = splitter.apply(documents);

4 örnek dosyada okuyucu 12 Document döndürür ve splitter 20 parça üretir. Demo, vectorStore.add(chunks) yerine output/spring-ai-documents.jsonl dosyasına yazar; böylece gerçek bir indeks oluşturmadan önce her satırda source, parent_source, page_number ve splitter’ın chunk_index değerlerini okuyabilirsiniz.

Bu dosya beş dakikalık zamanınıza değerdir. parent_source için arama yapın ve hangi parçaların e‑posta ve ZIP içinde geldiğini tam olarak görün; page_number için arama yapın ve her sayfa sınırının nerede olduğunu görün. İlk kez gerçek bir posta kutusu dışa aktarımıyla çalıştırdığımda, bu iki arama, bir gün süren prompt ayarlamaları kadar çok geri getirme kalitesi sorusunu yanıtladı.

Yan Yana: Önce vs. Sonra

İşte yardımcı‑format‑başına sürümüm ile tek okuyucu arasındaki değişiklikler:

Önce (yalnızca gövde okuma ve format başına yardımcılar) Sonra (GroupDocs.Parser DocumentReader)
Email attachments kayıp, ya da ayrı bir yardımcı tarafından çıkarıldı parent_source ile Belgeler olarak okundu
ZIP members önce diske açıldı yerinde okundu, source = sample.zip/sample.pdf
Citations yalnızca dosya adı dosya adı ve page_number
Code to maintain format başına bir yardımcı bir DocumentReader sınıfı
Unreadable file yardımcıya bağlı günlüğe kaydedildi, atlandı, toplu işlem devam ediyor

Okuyucudan sonraki boru hattı hiç değişmedi; sadece ona akan şey değişti.

Gerçek Dünya Örneği: Gece Boyu Destek İndeksi

Bir destek ekibi, paylaşılan posta kutusunu her gece dışa aktarır ve her ürün sürümüyle güncellenen kılavuzların ZIP’ini alır. Spring Boot servislerindeki zamanlanmış bir iş, yeni dosyaları listeler, GroupDocsParserDocumentReader ile okur, bölüştürür ve ajan asistanının arkasındaki vektör deposuna yazar. Bir ajan iade hakkında sorduğunda, yanıt artık manuals-2026-10.zip/refunds.pdf, sayfa 4, ve parent_source üzerindeki bir filtre en son sürüm paketine sınırlı bir arama yapabilir.

Üç adımın kodu bu işe bire bir eşlenir. Adım 1, gece dışa aktarımının ürettiği her şeyi kabul eder; uzantısı kaybolmuş dosyalar dahil. Adım 2, kılavuz sayfalarını ayrı tutar, böylece ajan müşteriye yanıt vermeden önce sayfa 4’ü açabilir. Adım 3, ZIP ve posta kutusunu opak bloblar yerine görünür ebeveynler yapar ve splitter çağrısı, ekibin zaten çalıştırdığı diğer Spring AI boru hatlarından farklı değildir.

Kısa iç notlar sayfa bölünmesine ihtiyaç duymaz, bu yüzden ekip bunları bütün‑dosya modunda okur ve ekleri hâlâ tutar:

// One Document per note; attachments still become their own Documents
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();

GroupDocs.Parser ile Başka Ne Yapabilirsiniz?

Ekler artık Spring AI’ya aktığına göre, birkaç ilgili yetenek tek bir çağrı uzakta:

Sonuç

“Ekli bak” sorununun çözümü okuyucu aşamasında ortaya çıktı. Spring AI’nin DocumentReader’ının arkasına GroupDocs.Parser konulduğunda, ekler, arşiv üyeleri ve tek tek sayfalar asistanın alıntı yapabileceği kaynaklar hâline gelir ve bunu takas etmek tek bir kurucu demektir.

Kendi korpusunuzda denemeye hazır mısınız?

Ek Kaynaklar