💡 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-commons2.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:trueatribui a cada fragmento umpage_number; o construtor de três argumentos desliga isso para notas curtas.isTextPage(): protege formatos sem páginas; nesses casos o leitor chamagetText()uma única vez e ainda registrasource,file_typeepage_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:
- Ler de fluxos de qualquer origem: o guia load document from stream cobre o construtor
Parser(InputStream)que o leitor usa. - Trabalhar com contêineres diretamente: extract data from attachments and ZIP archives explica
getContainereopenParser. - Ler páginas de PDF fora do Spring AI: extract text from PDF documents demonstra extração de texto nível página de forma independente.
- Manipular e‑mail diretamente: extract attachments from emails percorre a extração de anexos de mensagens de e‑mail sem a camada Spring AI.
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?
- Obter uma licença temporária para executar sem limites de avaliação
- Siga o guia de integração na documentação para padrões, configurações e uma checklist de produção
- Clone o projeto de exemplo no GitHub e aponte‑o para uma cópia da sua exportação de caixa de correio