💡 Ejemplo completo funcionando disponible en GitHub: document-to-markdown-rag-chunks-java

El problema del escritorio de reclamaciones: Por qué la ingestión RAG falla con archivos reales

Una canalización de ingestión RAG local es una capa de análisis de documentos que convierte los archivos que ya tiene un equipo en texto buscable y citable sin enviarlos a un tercero. Las operaciones de seguros muestran por qué eso importa. Un gestor de reclamaciones le pregunta al asistente interno “¿qué dijo el ajustador sobre los daños por agua en esta póliza?”, y la respuesta está en un correo electrónico de marzo pasado, dentro de un informe PDF adjunto, bajo un encabezado llamado “Findings”. El asistente solo puede encontrarlo si la ingestión lee el adjunto, conserva el encabezado y recuerda a qué correo pertenece el informe.

La mayoría de los trabajos de ingestión fallan en uno de esos tres aspectos. Un extractor de texto plano devuelve el cuerpo del correo y ignora el adjunto. Un divisor fijo de 512 tokens corta el informe a la mitad del encabezado “Findings”. Y un analizador alojado resuelve ambos, pero solo después de que el archivo de la reclamación, con sus datos personales y médicos, se haya subido a los servidores de otro y se facture por página.

Lo que el equipo necesita es estrecho y concreto: leer archivos Word, PDF, EML y ZIP dentro del servicio Java que ya posee los datos, mantener su estructura como Markdown, dividir en los encabezados del autor y adjuntar la fuente y el padre a cada fragmento.

Instantánea del mercado: las guías de 2026 sobre “análisis de documentos para RAG” que revisamos comparan analizadores de Python de código abierto con APIs en la nube facturadas por página. Una biblioteca Java que se ejecuta en tu propio hardware falta casi por completo en esa conversación.

¿Por qué los pipelines RAG necesitan Markdown en lugar de texto plano?

Markdown conserva las partes de un documento que le indican a un recuperador dónde termina una idea y comienza la siguiente. Los encabezados se convierten en líneas #, las tablas permanecen como filas y columnas, y las listas conservan sus elementos. Un fragmentador puede dividir en esos encabezados, y un LLM que lee el fragmento ve el mismo esquema que una persona. El texto plano elimina ese esquema, dejando el tamaño como la única señal para dividir.

Por qué las soluciones genéricas no funcionan aquí

Los equipos suelen intentar una de tres cosas antes de construir una capa de ingestión adecuada:

  • Extracción de texto plano más un divisor de tokens: rápido de configurar, pero los fragmentos mezclan secciones y pierden el encabezado que citarías.
  • Una API de análisis en la nube: buena estructura, pero los archivos salen de la red y la factura crece con cada página reindexada.
  • Un analizador casero por formato: una biblioteca para DOCX, otra para PDF, un analizador MIME para correos y java.util.zip para archivos, cada una con sus propios casos límite.

GroupDocs.Parser for Java cubre los cuatro formatos detrás de una única clase Parser y se ejecuta en proceso.

La solución: GroupDocs.Parser en un flujo de trabajo de ingestión de reclamaciones

GroupDocs.Parser for Java lee un documento y puede devolver su texto como Markdown mediante getFormattedText. Los correos y los archivos comprimidos se exponen como contenedores cuyos elementos se abren como nuevas instancias Parser, de modo que un PDF adjunto a un correo se procesa exactamente como un PDF en disco. El proyecto de ejemplo envuelve eso en seis métodos estáticos y escribe el resultado como JSON Lines, que cualquier trabajo de incrustación o almacén vectorial puede cargar. No se involucra ningún framework RAG; el formato de salida es el punto de integración.

Para seguir el tutorial necesitas:

  • JDK 11 o posterior para el pom.xml incluido (el código en sí se mantiene en características de Java 8)
  • GroupDocs.Parser for Java 26.10 (obtener una licencia temporal)
  • Un puñado de archivos DOCX, PDF, EML o ZIP

El pom.xml del proyecto declara el repositorio https://releases.groupdocs.com/java/repo/ y la única dependencia, por lo que una compilación es un solo comando:

mvn compile exec:java

Implementando la capa de ingestión paso a paso

Los pasos a continuación siguen un archivo de reclamación desde la bandeja de entrada hasta el índice. Cada paso se corresponde con uno o dos métodos en RagChunksDemo.java. Los mantuvimos como métodos estáticos simples a propósito, para que puedas levantar cualquiera de ellos en un trabajador existente sin adoptar el resto.

Paso 1 - Abrir cada archivo y enrutarlo

Cada archivo subido obtiene su propio Parser. El nombre del archivo se convierte en el id de fuente, y un archivo de nivel superior no tiene padre.

// 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 luego extrae el cuerpo, lo fragmenta y desciende a los adjuntos mientras la profundidad sea inferior a 2.

Paso 2 - Solicitar Markdown, aceptar texto plano

El paso de extracción sondea lo que el formato soporta en lugar de codificar una lista de extensiones.

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

Por qué estos ajustes son importantes para los datos de reclamaciones:

  • FormattedTextMode.Markdown: el encabezado “Findings” del ajustador llega al fragmentador como ## Findings, por lo que puede convertirse en un punto de división y una cita.
  • isFormattedText(): en la ejecución de ejemplo el DOCX y el correo vuelven como Markdown mientras que el PDF recurre a texto plano, y el campo format registra lo ocurrido.
  • El resultado none: fotos de reclamaciones dentro de un ZIP no generan fragmentos sin OCR, lo cual es honesto y fácil de monitorizar.

Paso 3 - Dividir en los encabezados del ajustador

El fragmentador mantiene una pila del encabezado actual en cada uno de los seis niveles de Markdown. Un nuevo encabezado cierra la sección en curso y restablece todo lo que está bajo su nivel.

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');
}

Las secciones de más de 1500 caracteres se recortan en saltos de párrafo, de modo que una sección larga de hallazgos se convierta en varios fragmentos que comparten una ruta de encabezado. En el archivo Word incluido esto produce rutas como Heading 1 > Heading 1.2 > Heading 1.2.1.

Paso 4 - Seguir los adjuntos y escribir JSON Lines

Este es el paso que más le importa a un escritorio de reclamaciones. getContainer() enumera los adjuntos de correos y las entradas ZIP; cada elemento se abre como un documento hijo y hereda el id del padre.

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 luego escribe una línea UTF-8 por fragmento con id, source, parent, heading_path, format y text. Con los cuatro inputs de ejemplo la ejecución produce 22 líneas. El fragmento del adjunto del correo de ejemplo es email-with-attachment.eml/attachment.txt#0, y su parent es email-with-attachment.eml. Ese par es lo que permite al asistente decir “del informe adjunto al correo del 14 de marzo” en lugar de “de algún lugar del archivo”.

Flujo de trabajo real: Reindexado semanal de reclamaciones

Imagina un equipo de operaciones de reclamaciones que exporta los buzones de reclamaciones cerradas de la semana cada viernes como archivos EML y paquetes ZIP de documentos de soporte. Un trabajador Java ya mueve esas exportaciones al almacén de documentos. Añadir la ingestión significa llamar a chunkDocument para cada archivo y añadir la salida JSON Lines a la cola de incrustación.

El Paso 4 es lo que mantiene un informe de reserva adjunto al correo que lo entregó. El Paso 3 significa que cuando el asistente cita “Findings”, la cita nombra el archivo y el encabezado en lugar de un número de fragmento desnudo. Mi regla práctica para este tipo de índice: si un fragmento no puede citarse de vuelta a un archivo y un encabezado, no debería estar en el índice en absoluto.

Impacto empresarial: Qué cambia esto

Aspecto Configuración típica antes Con el pipeline GroupDocs.Parser
Dónde se analizan los archivos Punto final de terceros Dentro de tu propia JVM
Modelo de coste de análisis Por página, en cada reindexado Sin tarifa por página en la nube
Límites de fragmentos Ventanas fijas de tokens Encabezados del autor, límite de 1500 caracteres
Adjuntos de correo A menudo omitidos o aplanados Analizados como documentos hijos
Datos de citación Nombre de archivo como máximo source, parent y heading_path
Dependencias en tiempo de ejecución SDK más acceso a red Un artefacto Maven

El efecto compuesto es un índice que el equipo de cumplimiento puede aprobar y un asistente cuyas respuestas apuntan a un documento y sección reales.

Otros escenarios de seguros donde GroupDocs.Parser encaja

Una vez que la capa de ingestión existe, el mismo código sirve a otros trabajos intensivos en documentos:

  • Búsqueda de redacción de pólizas: documentos de póliza en Word divididos por encabezados de cláusulas, de modo que las respuestas citan la sección exacta.
  • Envíos de corredores: cargas ZIP con PDFs y hojas de cálculo mixtas indexadas miembro por miembro, cada una vinculada al envío.
  • Gestión de quejas: correos electrónicos de clientes y sus adjuntos indexados juntos, con el correo como padre de cada fragmento adjunto.

Comenzando en su entorno de reclamaciones

Clona el ejemplo, ejecútalo contra algunos de tus propios correos exportados y abre output/chunks.jsonl para comprobar las rutas de encabezado y los ids de padre antes de integrarlo en tu trabajo de incrustación. Las notas de arquitectura paso a paso están en el caso de uso de documentación para este pipeline, y la referencia de salida Markdown enumera qué formato conserva el modo.

Recursos