💡 Exemple complet fonctionnel disponible sur GitHub :
spring-ai-document-reader-using-groupdocs-parser-java
L’ancienne méthode : pièces jointes qui n’atteignent jamais l’index
Un Spring AI DocumentReader est la première étape du pipeline ETL de Spring AI : il transforme un fichier en objets Document qu’un séparateur et un magasin vectoriel consomment, donc tout ce qu’il laisse tombe définitivement. Imaginez l’assistant que votre équipe de support a demandé. Vous exportez la boîte aux lettres partagée, indexez le corps des messages des fichiers .eml, découpez, vectorisez et stockez. La première vraie question arrive, et la réponse cite un e‑mail dont le corps complet est « Please see the attached refund policy. ». La politique elle‑même, une pièce jointe PDF, n’a jamais été lue.
Rien en aval ne peut réparer cela. TokenTextSplitter découpe la seule phrase qui lui a été donnée, et le magasin vectoriel la renvoie comme la correspondance la plus proche de « what is our refund window? ». Chaque composant a fait son travail. L’écart se situe au tout début du pipeline, dans la décision de ce qui compte comme document. Une exportation de boîte aux lettres est un arbre : messages, pièces jointes, et parfois des archives à l’intérieur des pièces jointes. Un lecteur qui ne voit que le niveau supérieur indexe le papier d’emballage.
Ma première version faisait exactement cela. Je l’ai corrigée de la façon évidente : décompresser les paquets de libération dans un helper, extraire les pièces jointes des messages dans un autre, parcourir les PDF avec un troisième, et inventer des noms de métadonnées dans chacun. Ça fonctionnait, jusqu’à ce que le quatrième format arrive et que les helpers ne s’accordent plus sur le nom à donner à une page.
Le vrai coût : dans le projet d’exemple, 2 des 4 fichiers d’entrée, l’e‑mail et le ZIP, contiennent du texte qu’une lecture uniquement du corps n’indexe jamais. Dans un corpus de support, ce ratio détermine si l’assistant est utile.
Une meilleure façon : un lecteur pour chaque entrée
GroupDocs.Parser pour Java lit plus de 65 + formats de fichiers (liste des formats pris en charge) en‑processus. Il détecte le format à partir du contenu d’un flux, renvoie le texte page par page, et liste les pièces jointes d’un e‑mail ou les entrées d’un ZIP via une API de conteneur unique. Enveloppez ces appels dans l’interface DocumentReader de Spring AI et le reste du pipeline ne pourra pas faire la différence, sauf que les Document portent désormais les numéros de page et la lignée.
Les lecteurs natifs de Spring AI restent de bons choix pour leurs cas d’usage : le pipeline ETL de référence documente TikaDocumentReader basé sur Apache Tika et PagePdfDocumentReader basé sur Apache PDFBox. Le point ici est plus restreint. Quand les pièces jointes et les membres d’archives doivent être des sources de première classe, un lecteur qui parcourt les conteneurs vous évite la ménagerie de helpers.
Avant de commencer, vous aurez besoin de :
- Java 17 ou supérieur, la base pour Spring AI 2.x
- GroupDocs.Parser pour Java 26.10 (obtenir une licence temporaire) et
org.springframework.ai:spring-ai-commons2.0.2 - Un e‑mail exporté avec une pièce jointe, ou les quatre fichiers d’exemple du dépôt
Clonez l’exemple et exécutez‑le en une seule commande :
mvn compile exec:java
Que doit renvoyer un Spring AI DocumentReader pour les citations RAG ?
Il doit renvoyer une List<Document> où chaque entrée est suffisamment petite pour être citée et suffisamment étiquetée pour être tracée. En pratique, cela signifie un Document par page avec un page_number, un source nommant le fichier, et, pour tout ce qui provient d’un e‑mail ou d’une archive, un parent_source nommant le conteneur. TokenTextSplitter copie ces métadonnées sur chaque fragment, de sorte que la citation survive jusqu’au magasin vectoriel.
La nouvelle méthode : pièces jointes comme documents de première classe
Voici le même travail avec une seule classe, GroupDocsParserDocumentReader. Trois étapes le couvrent : ouvrir la ressource, lire les pages, puis parcourir les conteneurs et transmettre le tout au séparateur.
Étape 1 – Ouvrir n’importe quelle ressource en tant que flux
Le lecteur accepte un Resource Spring, donc un fichier, une entrée du classpath ou un téléchargement passent tous par le même chemin. Le Parser lit les octets, ce qui signifie qu’un blob sans extension est quand même analysé.
// 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<>();
}
Étape 2 – Lire page par page
Le mode page est le défaut. Je le laisserais activé pour tout ce qui dépasse une ou deux pages, car un relecteur qui vérifie une réponse veut pouvoir sauter directement à la page 31.
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
}
}
Points clés :
pagePerDocument:truedonne à chaque fragment unpage_number; le constructeur à trois arguments le désactive pour les notes courtes.isTextPage(): protège les formats sans pages ; pour ceux‑ci le lecteur appellegetText()une fois et enregistre quand mêmesource,file_typeetpage_count.
Étape 3 – Parcourir les conteneurs, puis découper
Chaque pièce jointe ou membre d’archive obtient son propre Parser et repasse par la même logique de page, étiqueté avec son chemin à l’intérieur du parent.
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);
}
}
L’étape de découpage est le Spring AI standard, et elle conserve les métadonnées du lecteur sur chaque fragment :
TokenTextSplitter splitter = TokenTextSplitter.builder()
.withChunkSize(300) // tokens per chunk in the sample run
.build();
List<Document> chunks = splitter.apply(documents);
Sur les 4 fichiers d’exemple, le lecteur renvoie 12 Documents et le séparateur crée 20 fragments. La démo les écrit dans output/spring-ai-documents.jsonl à la place de vectorStore.add(chunks), de sorte que vous puissiez lire source, parent_source, page_number et le chunk_index du séparateur sur chaque ligne avant de construire un vrai index.
Ce fichier vaut bien cinq minutes de votre temps. Recherchez parent_source et vous verrez exactement quels fragments proviennent de l’e‑mail et du ZIP ; recherchez page_number et vous verrez où chaque frontière de page se situe. Lorsque je l’ai d’abord exécuté sur une vraie exportation de boîte aux lettres, ces deux recherches ont répondu à plus de questions sur la qualité de récupération qu’une journée de réglage de prompts.
Comparaison côte à côte : avant vs. après
Voici ce qui a changé entre ma version « helper‑par‑format » et le lecteur unique :
| Avant (lecture du corps uniquement + helpers par format) | Après (GroupDocs.Parser DocumentReader) | |
|---|---|---|
| Pièces jointes d’e‑mail | perdues, ou extraites par un helper séparé | lues comme Documents avec parent_source |
| Membres ZIP | décompressés sur disque d’abord | lus sur place, source = sample.zip/sample.pdf |
| Citations | uniquement le nom du fichier | nom du fichier + page_number |
| Code à maintenir | un helper par format | une classe DocumentReader |
| Fichier illisible | dépend du helper | journalisé, ignoré, le lot continue |
Le pipeline après le lecteur n’a pas changé du tout ; seul ce qui y circule a changé.
Exemple réel : index de support nocturne
Une équipe de support exporte sa boîte aux lettres partagée chaque nuit et reçoit un ZIP de manuels mis à jour à chaque version produit. Un job planifié dans leur service Spring Boot liste les nouveaux fichiers, les lit avec GroupDocsParserDocumentReader, les découpe et les écrit dans le magasin vectoriel derrière l’assistant. Quand un agent demande des informations sur les remboursements, la réponse cite maintenant manuals-2026-10.zip/refunds.pdf, page 4, et un filtre sur parent_source peut restreindre la recherche au dernier bundle de version.
Le code des trois étapes se mappe un à un sur ce job. L’étape 1 accepte tout ce que l’exportation nocturne produit, y compris les fichiers dont l’extension a été perdue en cours de route. L’étape 2 garde les pages du manuel séparées, de sorte que l’agent puisse ouvrir la page 4 avant de répondre au client. L’étape 3 rend le ZIP et la boîte aux lettres des parents visibles au lieu de blobs opaques, et l’appel au séparateur reste identique à tout autre pipeline Spring AI déjà en place.
Les notes internes courtes n’ont pas besoin de découpage de page, donc l’équipe les lit en mode fichier complet et conserve les pièces jointes :
// One Document per note; attachments still become their own Documents
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();
Que pouvez‑vous encore faire avec GroupDocs.Parser ?
Maintenant que les pièces jointes circulent dans Spring AI, quelques capacités connexes sont à un appel :
- Lire depuis des flux de n’importe quelle origine : le guide charger un document depuis un flux couvre le constructeur
Parser(InputStream)utilisé par le lecteur. - Travailler directement avec les conteneurs : extraire des données des pièces jointes et des archives ZIP explique
getContaineretopenParser. - Lire les pages PDF hors Spring AI : extraire du texte de documents PDF montre l’extraction de texte au niveau page de façon autonome.
- Gérer les e‑mails directement : extraire les pièces jointes des e‑mails parcourt l’extraction des pièces jointes des messages sans la couche Spring AI.
Conclusion
La correction du « see attached » s’est avérée se situer à l’étape du lecteur. Avec GroupDocs.Parser derrière le DocumentReader de Spring AI, les pièces jointes, les membres d’archive et les pages individuelles deviennent des sources que l’assistant peut citer, et le remplacer ne nécessite qu’un constructeur.
Prêt à l’essayer sur votre propre corpus ?
- Obtenir une licence temporaire pour exécuter sans limites d’évaluation
- Suivez le guide d’intégration dans la documentation pour les modèles, les paramètres et une checklist de production
- Clonez le projet d’exemple sur GitHub et pointez‑le vers une copie de votre exportation de boîte aux lettres