💡 Exemplo completo funcionando disponível no GitHub:
spring-ai-document-reader-using-groupdocs-parser-java

O Modo Antigo: Anexos Que Nunca Chegam ao Índice

Um Spring AI DocumentReader é a primeira etapa do pipeline ETL do Spring AI: ele transforma um arquivo em objetos Document que um splitter e um vector store consomem, de modo que tudo o que ele descarta se perde para sempre. Imagine o assistente que sua equipe de suporte pediu. Você exporta a caixa de correio compartilhada, indexa os corpos das mensagens dos arquivos .eml, divide, incorpora e envia. A primeira pergunta real chega, e a resposta cita um e‑mail cujo corpo inteiro é “Please see the attached refund policy.” A própria política, um anexo PDF, nunca foi lida.

Nada a jusante pode reparar isso. TokenTextSplitter fragmenta a única frase que recebeu, e o vector store a devolve como a correspondência mais próxima para “what is our refund window?”. Cada componente fez seu trabalho. A lacuna está logo no início do pipeline, na decisão sobre o que conta como documento. Uma exportação de caixa de correio é uma árvore: mensagens, anexos e, às vezes, arquivos compactados dentro de anexos. Um leitor que vê apenas o nível superior indexa o papel de embrulho.

Minha própria primeira versão fez exatamente isso. Corrigi da maneira óbvia: descompactar os pacotes de liberação em um helper, extrair anexos das mensagens em outro, percorrer PDFs página a página com um terceiro, e inventar nomes de metadados em cada um. Funcionou, até que o quarto formato chegou e os helpers discordaram sobre como chamar uma página.

O custo real: no projeto de exemplo, 2 de 4 arquivos de entrada, o e‑mail e o ZIP, contêm texto que uma leitura apenas do corpo nunca indexa. Em um corpus de suporte, essa proporção decide se o assistente é útil.

Uma Forma Melhor: Um Leitor para Cada Entrada

GroupDocs.Parser for Java lê mais de 65 formatos de arquivo em processo. Detecta o formato a partir do conteúdo do fluxo, devolve texto por página e lista os anexos de um e‑mail ou as entradas de um ZIP através de uma única API de contêiner. Envolva essas chamadas na interface DocumentReader do Spring AI e o resto do pipeline não perceberá a diferença, exceto que os Documents agora carregam números de página e linhagem.

Os leitores nativos do Spring AI continuam boas opções para seus casos: a referência do pipeline ETL documenta TikaDocumentReader baseado no Apache Tika e PagePdfDocumentReader baseado no Apache PDFBox. O ponto aqui é mais restrito. Quando anexos e membros de arquivos compactados precisam ser fontes de primeira classe, um leitor que percorre contêineres elimina a necessidade de múltiplos helpers.

Antes de começarmos, você precisará:

  • Java 17 ou superior, base para Spring AI 2.x
  • GroupDocs.Parser for Java 26.10 (obter uma licença temporária) e org.springframework.ai:spring-ai-commons 2.0.2
  • Um e‑mail exportado com anexo, ou os quatro arquivos de exemplo do repositório

Clone o exemplo e execute-o com um único comando:

mvn compile exec:java

O que um Spring AI DocumentReader precisa retornar para citações RAG?

Ele precisa de um List<Document> onde cada entrada seja pequena o suficiente para ser citada e rotulada o bastante para ser rastreada. Na prática, isso significa um Document por página com um page_number, um source nomeando o arquivo e, para tudo que chegou dentro de um e‑mail ou arquivo compactado, um parent_source nomeando o contêiner. TokenTextSplitter copia esses metadados para cada fragmento, de modo que a citação sobreviva até o vector store.

O Novo Modo: Anexos como Documentos de Primeira Classe

Aqui está o mesmo trabalho com uma única classe, GroupDocsParserDocumentReader. Três etapas o cobrem: abrir o recurso, ler as páginas e, em seguida, percorrer os contêineres e entregar tudo ao splitter.

Etapa 1 - Abrir Qualquer Recurso como Fluxo

O leitor aceita um Resource do Spring, portanto um arquivo, uma entrada no classpath e um upload seguem o mesmo caminho. O Parser lê bytes, o que significa que um blob sem extensão ainda é analisado.

// 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<>();
}

Etapa 2 - Ler Página por Página

O modo página é o padrão. Eu deixaria ativado para qualquer coisa maior que uma ou duas páginas, já que quem revisa uma resposta quer pular direto para a página 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
    }
}

Pontos principais:

  • pagePerDocument: true atribui a cada fragmento um page_number; o construtor de três argumentos desliga isso para notas curtas.
  • isTextPage(): protege formatos sem páginas; nesses casos o leitor chama getText() uma única vez e ainda registra source, file_type e page_count.

Etapa 3 - Seguir os Contêineres, Então Dividir

Cada anexo ou membro de arquivo compactado recebe seu próprio Parser e volta a passar pela mesma lógica de página, rotulado com seu caminho dentro do pai.

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

A fase de splitter é o Spring AI padrão, e ele mantém os metadados do leitor em cada fragmento:

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

Nos 4 arquivos de exemplo, o leitor devolve 12 Documents e o splitter gera 20 fragmentos. A demonstração grava-os em output/spring-ai-documents.jsonl no lugar de vectorStore.add(chunks), para que você possa ler source, parent_source, page_number e o chunk_index do splitter em cada linha antes de construir um índice real.

Esse arquivo vale cinco minutos do seu tempo. Pesquise por parent_source e você verá exatamente quais fragmentos vieram de dentro do e‑mail e do ZIP; pesquise por page_number e verá onde cada limite de página ocorreu. Quando executei pela primeira vez em uma exportação real de caixa de correio, essas duas buscas responderam mais perguntas sobre a qualidade da recuperação do que um dia inteiro ajustando prompts.

Lado a Lado: Antes vs. Depois

Aqui está o que mudou entre a versão com helpers por formato e o leitor único:

Antes (leitura apenas do corpo mais auxiliares por formato) Depois (GroupDocs.Parser DocumentReader)
Anexos de e‑mail perdidos, ou extraídos por um auxiliar separado lidos como Documents com parent_source
Membros de ZIP descompactados para disco primeiro lidos no local, source = sample.zip/sample.pdf
Citações apenas nome do arquivo nome do arquivo mais page_number
Código a manter um auxiliar por formato uma classe DocumentReader
Arquivo ilegível depende do auxiliar registrado, ignorado, lote continua

O pipeline após o leitor não mudou nada; apenas o que flui para ele mudou.

Exemplo do Mundo Real: Um Índice de Suporte Noturno

Uma equipe de suporte exporta sua caixa de correio compartilhada todas as noites e recebe um ZIP com manuais atualizados a cada lançamento de produto. Um job agendado no serviço Spring Boot deles lista novos arquivos, lê-os com GroupDocsParserDocumentReader, divide e grava no vector store por trás do assistente. Quando um agente pergunta sobre reembolsos, a resposta agora cita manuals-2026-10.zip/refunds.pdf, página 4, e um filtro em parent_source pode restringir a busca ao pacote de lançamento mais recente.

O código das três etapas mapeia exatamente para esse job. A Etapa 1 aceita qualquer coisa que a exportação noturna produza, inclusive arquivos cuja extensão se perdeu no caminho. A Etapa 2 mantém as páginas dos manuais separadas, permitindo que o agente abra a página 4 antes de responder ao cliente. A Etapa 3 torna o ZIP e a caixa de correio pais visíveis em vez de blobs opacos, e a chamada ao splitter permanece inalterada em relação a qualquer outro pipeline Spring AI que a equipe já utiliza.

Notas internas curtas não precisam de divisão por página, então a equipe as lê no modo de arquivo inteiro e mantém os anexos ativados:

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

O Que Mais Você Pode Fazer com GroupDocs.Parser?

Agora que os anexos fluem para o Spring AI, algumas capacidades relacionadas estão a um chamado de distância:

Conclusão

A correção para “see attached” acabou ficando na fase do leitor. Com o GroupDocs.Parser por trás do DocumentReader do Spring AI, anexos, membros de arquivos compactados e páginas individuais tornam‑se fontes que o assistente pode citar, e substituí‑lo requer apenas um construtor.

Pronto para experimentar no seu próprio corpus?

Recursos Adicionais