💡 Vollständiges funktionierendes Beispiel auf GitHub verfügbar:
spring-ai-dokument-leser-mit-groupdocs-parser-java
Der alte Weg: Anhänge, die nie den Index erreichen
Ein Spring AI DocumentReader ist die erste Stufe der Spring AI ETL‑Pipeline: Er wandelt eine Datei in die Document‑Objekte um, die ein Splitter und ein Vektor‑Store konsumieren, sodass alles, was er fallen lässt, endgültig verloren ist. Stellen Sie sich den Assistenten vor, den Ihr Support‑Team angefordert hat. Sie exportieren das freigegebene Postfach, indexieren die Nachrichten‑Bodies der .eml‑Dateien, splitten, betten ein und versenden. Die erste echte Frage kommt herein, und die Antwort zitiert eine E‑Mail, deren gesamter Body lautet: „Bitte sehen Sie die beigefügte Rückerstattungsrichtlinie.“ Die Richtlinie selbst, ein PDF‑Anhang, wurde nie gelesen.
Nichts nachgelagertes kann das reparieren. TokenTextSplitter zerlegt den einen Satz, den er erhalten hat, und der Vektor‑Store gibt ihn als die engste Übereinstimmung zu „Wie lange ist unser Rückgabefenster?“ zurück. Jede Komponente hat ihre Aufgabe erledigt. Die Lücke liegt ganz am Anfang der Pipeline, in der Entscheidung, was als Dokument zählt. Ein Postfach‑Export ist ein Baum: Nachrichten, Anhänge und manchmal Archive in Anhängen. Ein Reader, der nur die oberste Ebene sieht, indexiert das Umwickelpapier.
Meine eigene erste Version hat genau das getan. Ich habe es auf offensichtliche Weise behoben: Die Release‑Bundles in einem Helfer entzippen, Anhänge aus Nachrichten in einem anderen ziehen, PDFs mit einem dritten durchblättern und in jedem Metadaten‑Namen erfinden. Es funktionierte, bis das vierte Format auftauchte und die Helfer sich uneinig waren, wie eine Seite genannt werden soll.
Die wirklichen Kosten: Im Beispielprojekt enthalten 2 von 4 Eingabedateien, die E‑Mail und das ZIP, Text, den ein reines Body‑Lesen nie indexiert. In einem Support‑Korpus entscheidet dieses Verhältnis, ob der Assistent nützlich ist.
Ein besserer Weg: Ein Reader für jede Eingabe
GroupDocs.Parser für Java liest über 65 Dateiformate im Prozess. Es erkennt das Format anhand des Inhalts eines Streams, liefert Text pro Seite und listet die Anhänge einer E‑Mail oder die Einträge eines ZIP‑Archivs über eine einheitliche Container‑API auf. Wickeln Sie diese Aufrufe in Spring AI’s DocumentReader‑Schnittstelle ein und der Rest der Pipeline kann keinen Unterschied erkennen, außer dass die Documents jetzt Seitenzahlen und Herkunft mitführen.
Spring AI’s eigene Reader bleiben gute Optionen für ihre Anwendungsfälle: Die ETL‑Pipeline‑Referenz dokumentiert TikaDocumentReader auf Apache Tika und PagePdfDocumentReader auf Apache PDFBox. Der Punkt hier ist enger gefasst. Wenn Anhänge und Archiv‑Mitglieder erstklassige Quellen sein müssen, spart ein Reader, der Container durchläuft, Ihnen das Helfer‑Zoo.
Bevor wir starten, benötigen Sie:
- Java 17 oder höher, die Basis für Spring AI 2.x
- GroupDocs.Parser für Java 26.10 (temporäre Lizenz erhalten) und
org.springframework.ai:spring-ai-commons2.0.2 - Eine exportierte E‑Mail mit Anhang oder die vier Beispieldateien aus dem Repository
Klonen Sie das Beispiel und führen Sie es mit einem Befehl aus:
mvn compile exec:java
Was muss ein Spring AI DocumentReader zurückgeben für RAG‑Zitate?
Er benötigt eine List<Document>, bei der jeder Eintrag klein genug ist, um zitiert zu werden, und gut genug beschriftet ist, um nachverfolgt zu werden. In der Praxis bedeutet das ein Document pro Seite mit einem page_number, einem source, das die Datei benennt, und – für alles, was innerhalb einer E‑Mail oder eines Archivs angekommen ist – einem parent_source, das den Container benennt. TokenTextSplitter kopiert diese Metadaten auf jedes Chunk, sodass das Zitat bis in den Vektor‑Store überlebt.
Der neue Weg: Anhänge als erstklassige Dokumente
Hier ist dieselbe Aufgabe mit einer einzigen Klasse, GroupDocsParserDocumentReader. Drei Schritte decken sie ab: Ressource öffnen, Seiten lesen, dann den Containern folgen und alles an den Splitter übergeben.
Schritt 1 – Öffne jede Ressource als Stream
Der Reader akzeptiert ein Spring Resource, sodass eine Datei, ein Klassenpfad‑Eintrag und ein Upload denselben Weg nehmen. Der Parser liest Bytes, was bedeutet, dass ein Blob ohne Erweiterung trotzdem geparst wird.
// 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<>();
}
Schritt 2 – Seite für Seite lesen
Der Seiten‑Modus ist der Standard. Ich würde ihn für alles länger als ein oder zwei Seiten aktiviert lassen, da ein Prüfer, der eine Antwort überprüft, sofort zu Seite 31 springen möchte.
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
}
}
Wichtige Punkte:
pagePerDocument:truegibt jedem Chunk einepage_number; der Konstruktor mit drei Argumenten schaltet es für kurze Notizen aus.isTextPage(): schützt Formate ohne Seiten; für diese ruft der ReadergetText()einmal auf und zeichnet trotzdemsource,file_typeundpage_countauf.
Schritt 3 – Den Containern folgen, dann teilen
Jeder Anhang oder Archiv‑Eintrag erhält seinen eigenen Parser und durchläuft dieselbe Seiten‑Logik, gekennzeichnet mit seinem Pfad innerhalb des Elternteils.
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);
}
}
Die Splitter‑Stufe ist Standard‑Spring AI und bewahrt die Metadaten des Readers auf jedem Chunk:
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(300) // tokens per chunk in the sample run
.build();
List<Document> chunks = splitter.apply(documents);
Bei den 4 Beispieldateien liefert der Reader 12 Documents und der Splitter erzeugt 20 Chunks. Die Demo schreibt sie nach output/spring-ai-documents.jsonl anstelle von vectorStore.add(chunks), sodass Sie source, parent_source, page_number und den chunk_index des Splitters in jeder Zeile sehen können, bevor Sie einen echten Index bauen.
Diese Datei ist fünf Minuten Ihrer Zeit wert. Suchen Sie nach parent_source und Sie sehen genau, welche Chunks aus der E‑Mail bzw. dem ZIP stammen; suchen Sie nach page_number und Sie sehen, wo jede Seiten‑Grenze lag. Als ich das erste Mal gegen einen echten Postfach‑Export laufen ließ, beantworteten diese beiden Suchen mehr Fragen zur Retrieval‑Qualität als ein ganzer Tag Prompt‑Feinabstimmung.
Vergleich: Vorher vs. Nachher
Hier ist, was sich zwischen meiner Helfer‑pro‑Format‑Version und dem einzelnen Reader geändert hat:
| Vorher (Nur-Body-Lesen plus pro-Format-Helfer) | Nachher (GroupDocs.Parser DocumentReader) | |
|---|---|---|
| Email attachments | verloren, oder von einem separaten Helfer extrahiert | als Dokumente mit parent_source gelesen |
| ZIP members | zuerst auf die Festplatte entpackt | vor Ort gelesen, source = sample.zip/sample.pdf |
| Citations | nur Dateiname | Dateiname plus page_number |
| Code to maintain | ein Helfer pro Format | eine DocumentReader‑Klasse |
| Unreadable file | abhängig vom Helfer | geloggt, übersprungen, Batch läuft weiter |
Die Pipeline nach dem Reader hat sich überhaupt nicht geändert; nur das, was in sie fließt, hat sich geändert.
Praxisbeispiel: Ein nächtlicher Support‑Index
Ein Support‑Team exportiert sein freigegebenes Postfach jede Nacht und erhält ein ZIP mit aktualisierten Handbüchern zu jedem Produkt‑Release. Ein geplanter Job in ihrem Spring Boot‑Service listet neue Dateien, liest sie mit GroupDocsParserDocumentReader, splittet und schreibt sie in den Vektor‑Store hinter dem Agent‑Assistenten. Wenn ein Agent nach Rückerstattungen fragt, zitiert die Antwort jetzt manuals-2026-10.zip/refunds.pdf, Seite 4, und ein Filter auf parent_source kann die Suche auf das neueste Release‑Bundle beschränken.
Der Code aus den drei Schritten lässt sich eins‑zu‑eins auf diesen Job übertragen. Schritt 1 akzeptiert alles, was der nächtliche Export produziert, inklusive Dateien, deren Erweiterung unterwegs verloren ging. Schritt 2 hält Handbuchseiten getrennt, sodass der Agent Seite 4 öffnen kann, bevor er dem Kunden antwortet. Schritt 3 macht das ZIP und das Postfach zu sichtbaren Eltern statt zu undurchsichtigen Blobs, und der Splitter‑Aufruf bleibt unverändert gegenüber jeder anderen Spring AI‑Pipeline, die das Team bereits nutzt.
Kurze interne Notizen benötigen keinen Seitensplit, daher liest das Team sie im Ganzdatei‑Modus und behält Anhänge bei:
// One Document per note; attachments still become their own Documents
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();
Was kann man sonst noch mit GroupDocs.Parser tun?
Jetzt, wo Anhänge in Spring AI fließen, sind ein paar verwandte Fähigkeiten nur einen Aufruf entfernt:
- Read from streams of any origin: the load document from stream guide covers the
Parser(InputStream)constructor the reader uses. → Aus Streams beliebigen Ursprungs lesen: Der Leitfaden [load document from stream] erklärt denParser(InputStream)‑Konstruktor, den der Reader verwendet. - Work with containers directly: extract data from attachments and ZIP archives explains
getContainerandopenParser. → Direkt mit Containern arbeiten: [extract data from attachments and ZIP archives] erläutertgetContainerundopenParser. - Read PDF pages outside Spring AI: extract text from PDF documents shows page-level text extraction on its own. → PDF‑Seiten außerhalb von Spring AI lesen: [extract text from PDF documents] zeigt die extraktion von Text auf Seitenebene eigenständig.
- Handle email directly: extract attachments from emails walks through attachment extraction from email messages without the Spring AI layer. → E‑Mails direkt verarbeiten: [extract attachments from emails] führt durch die Extraktion von Anhängen aus E‑Mail‑Nachrichten ohne die Spring AI‑Schicht.
Fazit
Die Lösung für „siehe Anhang“ lag letztlich in der Reader‑Stufe. Mit GroupDocs.Parser hinter Spring AI’s DocumentReader werden Anhänge, Archiv‑Mitglieder und einzelne Seiten zu Quellen, die der Assistent zitieren kann, und der Austausch erfolgt über einen einzigen Konstruktor.
Bereit, es mit Ihrem eigenen Korpus auszuprobieren?
- Temporäre Lizenz erhalten um ohne Evaluations‑Limits zu laufen
- Folgen Sie dem Integrationsleitfaden in der Dokumentation für Muster, Einstellungen und eine Produktions‑Checkliste
- Klonen Sie das Beispielprojekt auf GitHub und zeigen Sie es auf eine Kopie Ihres Postfach‑Exports