💡 Exemple complet fonctionnel disponible sur GitHub : document-to-markdown-rag-chunks-java
Le problème du bureau des sinistres : pourquoi l’ingestion RAG échoue avec les fichiers réels
Un pipeline d’ingestion RAG sur site est une couche d’analyse de documents qui transforme les fichiers déjà en possession d’une équipe en texte consultable et citable, sans les envoyer à un tiers. Les opérations d’assurance montrent pourquoi c’est important. Un gestionnaire de sinistres demande à l’assistant interne « que dit l’expert au sujet des dégâts des eaux sur cette police ?», et la réponse se trouve dans un e‑mail de mars dernier, à l’intérieur d’un rapport PDF joint, sous un titre appelé « Findings ». L’assistant ne peut la trouver que si l’ingestion a lu la pièce jointe, conservé le titre et retenu à quel e‑mail le rapport était associé.
La plupart des jobs d’ingestion se trompent sur l’un de ces trois points. Un extracteur de texte brut renvoie le corps du e‑mail et ignore la pièce jointe. Un séparateur fixe à 512 tokens coupe le rapport au milieu de « Findings ». Et un parseur hébergé résout les deux, mais seulement après que le fichier de sinistre, avec ses données personnelles et médicales, a été téléchargé sur les serveurs d’un tiers et facturé à la page.
Ce dont l’équipe a besoin est précis et concret : lire les fichiers Word, PDF, EML et ZIP dans le service Java qui possède déjà les données, conserver leur structure en Markdown, découper aux titres de l’auteur, et associer la source et le parent à chaque fragment.
Instantané du marché : les guides 2026 sur « l’analyse de documents pour RAG » que nous avons examinés comparent les parseurs Python open‑source aux API cloud facturées à la page. Une bibliothèque Java fonctionnant sur votre propre matériel manque presque totalement à cette conversation.
Pourquoi les pipelines RAG ont besoin de Markdown plutôt que de texte brut ?
Le Markdown conserve les parties d’un document qui indiquent à un récupérateur où une idée se termine et où la suivante commence. Les titres deviennent des lignes #, les tableaux restent des lignes et colonnes, et les listes conservent leurs éléments. Un découpeur peut se séparer à ces titres, et un LLM lisant le fragment voit la même structure qu’une personne. Le texte brut supprime cette structure, ne laissant que la taille comme seul signal de découpage.
Pourquoi les solutions génériques ne fonctionnent pas ici
Les équipes essaient généralement l’une des trois approches avant de construire une vraie couche d’ingestion :
- Extraction de texte brut + séparateur de tokens : rapide à mettre en place, mais les fragments mélangent les sections et perdent le titre que vous voudriez citer.
- API d’analyse cloud : bonne structure, mais les fichiers quittent le réseau et la facture augmente à chaque page ré‑indexée.
- Parseur maison par format : une bibliothèque pour DOCX, une autre pour PDF, un parseur MIME pour les e‑mails et
java.util.zippour les archives, chacun avec ses propres cas limites.
GroupDocs.Parser pour Java couvre les quatre formats derrière une seule classe Parser et s’exécute en‑processus.
La solution : GroupDocs.Parser dans un flux d’ingestion de sinistres
GroupDocs.Parser pour Java lit un document et peut renvoyer son texte en Markdown via getFormattedText. Les e‑mails et les archives sont exposés comme des conteneurs dont les éléments s’ouvrent comme de nouvelles instances Parser, de sorte qu’un PDF joint à un e‑mail est traité exactement comme un PDF sur le disque. Le projet d’exemple encapsule cela dans six méthodes statiques et écrit le résultat en JSON Lines, que n’importe quel job d’embeddding ou magasin de vecteurs peut charger. Aucun framework RAG n’est impliqué ; le format de sortie est le point d’intégration.
Pour suivre le guide, vous avez besoin de :
- JDK 11 ou supérieur pour le
pom.xmlfourni (le code lui‑même se limite aux fonctionnalités Java 8) - GroupDocs.Parser pour Java 26.10 (obtenir une licence temporaire)
- Une poignée de fichiers DOCX, PDF, EML ou ZIP
Le pom.xml du projet déclare le dépôt https://releases.groupdocs.com/java/repo/ et la dépendance unique, de sorte qu’une construction ne nécessite qu’une commande :
mvn compile exec:java
Implémentation de la couche d’ingestion, étape par étape
Les étapes ci‑dessous suivent un fichier de sinistre depuis la boîte de réception jusqu’à l’index. Chaque étape correspond à une ou deux méthodes dans RagChunksDemo.java. Nous les avons laissées sous forme de méthodes statiques simples, afin que vous puissiez en extraire n’importe laquelle dans un worker existant sans adopter le reste.
Étape 1 - Ouvrir chaque fichier et le router
Chaque fichier téléchargé obtient son propre Parser. Le nom du fichier devient l’identifiant source, et un fichier de niveau supérieur n’a pas de parent.
// One call per uploaded claim document
String sourceId = new File(path).getName();
try (Parser parser = new Parser(path)) {
return chunkParser(parser, sourceId, null, 0); // null parent: top-level file
} catch (UnsupportedDocumentFormatException ex) {
// An unknown format is logged and skipped, the batch keeps going
System.out.println("skipped (unsupported format): " + sourceId);
return new ArrayList<>();
}
chunkParser extrait ensuite le corps, le découpe, et descend dans les pièces jointes tant que la profondeur est inférieure à 2.
Étape 2 - Demander du Markdown, accepter le texte brut
L’étape d’extraction interroge les capacités du format au lieu de coder en dur une liste d’extensions.
if (parser.getFeatures().isFormattedText()) {
// Headings, lists and tables survive as Markdown
FormattedTextOptions options =
new FormattedTextOptions(FormattedTextMode.Markdown);
try (TextReader reader = parser.getFormattedText(options)) {
if (reader != null) {
return new ExtractedText("markdown", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
try (TextReader reader = parser.getText()) {
if (reader != null) { // fallback for formats without formatted text
return new ExtractedText("plain", reader.readToEnd());
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return new ExtractedText("none", ""); // e.g. a photo with no OCR
Pourquoi ces paramètres sont importants pour les données de sinistres :
FormattedTextMode.Markdown: le titre « Findings » de l’expert arrive au découpeur sous la forme## Findings, ce qui peut devenir un point de séparation et une citation.isFormattedText(): dans l’exécution d’exemple, le DOCX et le e‑mail reviennent en Markdown tandis que le PDF retombe en texte brut, et le champformatenregistre ce qui s’est produit.- Le résultat
none: les photos de sinistres dans un ZIP ne produisent aucun fragment sans OCR, ce qui est honnête et facile à surveiller.
Étape 3 - Découper aux titres de l’expert
Le découpeur conserve une pile du titre courant à chacun des six niveaux Markdown. Un nouveau titre ferme la section en cours et réinitialise tout ce qui se trouve en dessous de son niveau.
for (String line : markdown.split("\\r?\\n", -1)) {
Matcher heading = HEADING.matcher(line); // "#" to "######" + title
if (heading.matches()) {
// Close the previous section under the old path
addSection(chunks, section.toString(), sourceId, parentId, path, format);
section.setLength(0);
int level = heading.group(1).length();
stack[level - 1] = heading.group(2).trim();
for (int i = level; i < stack.length; i++) {
stack[i] = null; // forget deeper headings
}
path = joinPath(stack); // "Heading 1 > Heading 1.2"
}
section.append(line).append('\n');
}
Les sections de plus de 1500 caractères sont coupées aux sauts de paragraphe, de sorte qu’une longue section de conclusions devienne plusieurs fragments partageant le même chemin de titre. Sur le fichier Word fourni, cela produit des chemins tels que Heading 1 > Heading 1.2 > Heading 1.2.1.
Étape 4 - Suivre les pièces jointes et écrire les JSON Lines
C’est l’étape qui intéresse le plus le bureau des sinistres. getContainer() répertorie les pièces jointes d’e‑mail et les entrées ZIP ; chaque élément s’ouvre comme un document enfant et hérite de l’identifiant du parent.
Iterable<ContainerItem> items = parser.getContainer();
if (items == null) {
return chunks; // not an email or archive
}
for (ContainerItem item : items) {
// "claim-0412.eml/report.pdf" style ids keep the lineage readable
String childId = sourceId + "/" + item.getFilePath();
try (Parser child = item.openParser()) {
chunks.addAll(chunkParser(child, childId, sourceId, depth + 1));
} catch (UnsupportedDocumentFormatException ex) {
System.out.println("skipped (unsupported format): " + childId);
}
}
writeJsonl écrit ensuite une ligne UTF‑8 par fragment avec id, source, parent, heading_path, format et text. Avec les quatre entrées d’exemple, l’exécution produit 22 lignes. Le fragment provenant de la pièce jointe de l’e‑mail d’exemple est email-with-attachment.eml/attachment.txt#0, et son parent est email-with-attachment.eml. Cette paire permet à l’assistant de dire « à partir du rapport joint à l’e‑mail du 14 mars » plutôt que « quelque part dans l’archive ».
Flux réel : ré‑indexation hebdomadaire des sinistres
Imaginez une équipe d’opérations de sinistres qui exporte chaque vendredi les boîtes aux lettres des sinistres clôturés sous forme de fichiers EML et de bundles ZIP de documents de support. Un worker Java déplace déjà ces exportations dans le magasin de documents. Ajouter l’ingestion signifie appeler chunkDocument pour chaque fichier et ajouter la sortie JSON Lines à la file d’attente d’embeddding.
L’étape 4 est celle qui conserve un rapport de réserve attaché à l’e‑mail qui l’a livré. L’étape 3 garantit que lorsque l’assistant cite « Findings », la citation indique le fichier et le titre au lieu d’un simple numéro de fragment. Ma règle de base pour ce type d’index : si un fragment ne peut pas être cité avec un fichier et un titre, il ne doit pas être présent dans l’index.
Impact métier : ce qui change
| Aspect | Configuration typique avant | Avec le pipeline GroupDocs.Parser |
|---|---|---|
| Où les fichiers sont analysés | Point d’accès tiers | À l’intérieur de votre propre JVM |
| Modèle de coût d’analyse | Par page, à chaque ré‑indexation | Aucun frais cloud par page |
| Limites des fragments | Fenêtres de tokens fixes | Titres de l’auteur, plafond de 1500 caractères |
| Pièces jointes d’e‑mail | Souvent ignorées ou aplaties | Analysées comme documents enfants |
| Données de citation | Nom de fichier au mieux | source, parent et heading_path |
| Dépendances d’exécution | SDK + accès réseau | Un seul artefact Maven |
L’effet combiné est un index que l’équipe conformité peut valider et un assistant dont les réponses pointent vers un document réel et une section précise.
Autres scénarios d’assurance où GroupDocs.Parser s’applique
Une fois la couche d’ingestion en place, le même code sert d’autres travaux lourds en documents :
- Recherche dans les libellés de police : documents Word de police découpés par titres de clause, de sorte que les réponses citent la section exacte.
- Soumissions de courtiers : téléchargements ZIP contenant PDFs et feuilles de calcul indexés membre par membre, chaque fragment lié à la soumission.
- Gestion des réclamations : e‑mails clients et leurs pièces jointes indexés ensemble, l’e‑mail étant le parent de chaque fragment de pièce jointe.
Démarrer dans votre environnement de sinistres
Clonez l’exemple, exécutez‑le sur quelques‑uns de vos propres e‑mails exportés, et ouvrez output/chunks.jsonl pour vérifier les chemins de titres et les identifiants parents avant de l’intégrer à votre job d’embeddding. Les notes d’architecture étape par étape se trouvent dans le cas d’utilisation de la documentation pour ce pipeline, et la référence de sortie Markdown répertorie les formats conservés par le mode.