💡 完全に動作する例は GitHub で入手できます: spring-ai-document-reader-using-groupdocs-parser-java

従来の方法: インデックスに届かない添付ファイル

Spring AI DocumentReader は Spring AI ETL パイプラインの最初の段階です。ファイルを Document オブジェクトに変換し、スプリッターとベクトルストアが消費できるようにします。そのため、Reader が落としたものは二度と戻りません。サポートチームが求めたアシスタントを想像してください。共有メールボックスをエクスポートし、.eml ファイルのメッセージ本文をインデックス化し、分割、埋め込み、配信します。最初の本格的な質問が来て、回答が「添付の返金ポリシーをご覧ください」と全文がそのメール本文だけのものを引用したとします。ポリシー自体は PDF の添付ファイルですが、決して読まれませんでした。

下流の何もそれを修復できません。TokenTextSplitter は与えられた 1 文をチャンクに分け、ベクトルストアはそれを「返金期間はどれくらいですか?」という質問への最も近い一致として返します。すべてのコンポーネントは役割を果たしましたが、ギャップはパイプラインの最初、何をドキュメントとみなすかという判断にあります。メールボックスのエクスポートはツリー構造です:メッセージ、添付ファイル、そして時には添付ファイル内のアーカイブ。トップレベルだけを見るリーダーは包装紙だけをインデックスします。

私の最初のバージョンはまさにこれを行っていました。明らかな方法で修正しました:リリースバンドルを 1 つのヘルパーで解凍し、メッセージから添付ファイルを別のヘルパーで抽出し、PDF を 3 番目のヘルパーでページ単位で処理し、各ヘルパーでメタデータ名を作り出しました。うまくいきましたが、4 番目のフォーマットが登場し、ヘルパー間で「ページ」を何と呼ぶかで意見が合わなくなったのです。

実際のコスト: サンプルプロジェクトでは、4 つの入力ファイルのうち 2 つ(メールと ZIP)が本文だけの読み取りではインデックス化されないテキストを含んでいます。サポートコーパスにおいてこの比率がアシスタントの有用性を左右します。

より良い方法: すべての入力に対して 1 つのリーダー

GroupDocs.Parser for Java は 65 以上のファイル形式 をインプロセスで読み取ります。ストリームの内容から形式を検出し、ページ単位のテキストを返し、メールの添付ファイルや ZIP のエントリを 1 つのコンテナ API で列挙します。これらの呼び出しを Spring AI の DocumentReader インターフェイスでラップすれば、パイプラインの残りは違いを認識できません。ただし、Document にはページ番号と系統情報が付与されます。

Spring AI の独自リーダーもそれぞれのケースでは有力な選択肢です: ETL パイプラインリファレンス では Apache Tika の TikaDocumentReader と Apache PDFBox の PagePdfDocumentReader が紹介されています。ここでのポイントはより限定的です。添付ファイルやアーカイブメンバーを第一級のソースとして扱う必要があるとき、コンテナを歩くリーダーはヘルパーの群れを省くことができます。

開始する前に必要なもの:

  • Java 17 以上(Spring AI 2.x のベースライン)
  • GroupDocs.Parser for Java 26.10(一時ライセンスを取得)と org.springframework.ai:spring-ai-commons 2.0.2
  • 添付ファイル付きのエクスポートメール、またはリポジトリから取得できる 4 つのサンプルファイル

サンプルをクローンし、次のコマンドで実行します:

mvn compile exec:java

Spring AI DocumentReader が RAG 引用のために返すべきものは?

List<Document> が必要です。各エントリは引用に十分小さく、追跡できるようにラベル付けされている必要があります。実務上は、ページごとに 1 つの Document を作り、page_number、ファイル名を示す source、そしてメールやアーカイブ内に入ってきたものについてはコンテナ名を示す parent_source を付与します。TokenTextSplitter はこのメタデータをすべてのチャンクにコピーするため、引用はベクトルストアまで生き残ります。

新しい方法: 添付ファイルを第一級ドキュメントとして扱う

同じ処理を単一クラス GroupDocsParserDocumentReader で実装した例です。3 つのステップで完了します: リソースを開く、ページを読み取る、コンテナをたどってすべてをスプリッターに渡す。

Step 1 - 任意のリソースをストリームとして開く

リーダーは Spring の Resource を受け取ります。ファイル、クラスパスエントリ、アップロードのいずれも同じ経路で処理できます。Parser はバイトを読み取り、拡張子がなくても解析できます。

// フォーマット検出はコンテンツで行われるため、ファイル名はラベルにすぎません
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)); // 「添付をご覧ください」の修正
    }
    return documents;
} catch (UnsupportedDocumentFormatException ex) {
    // 読めないファイルをログに残し、空リストを返す; バッチは継続
    System.out.println("skipped (unsupported format): " + source);
    return new ArrayList<>();
}

Step 2 - ページ単位で読み取る

ページモードがデフォルトです。1〜2 ページ程度の短い文書でなければオンにしておくことをおすすめします。回答をチェックするレビュアーはページ 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); // 人は 1 から数える
        addIfNotBlank(documents, text, metadata); // 空白ページは埋め込まれない
    }
}

重要ポイント:

  • pagePerDocument: true にすると各チャンクに page_number が付与されます。短いメモ向けに 3 引数コンストラクタでオフにできます。
  • isTextPage(): ページ概念のない形式をガードします。該当形式では getText() を一度だけ呼び、source、file_type、page_count を記録します。

Step 3 - コンテナをたどり、最後に分割

各添付ファイルやアーカイブメンバーは独自の Parser を取得し、同じページロジックを通して処理され、親内でのパスがラベル付けされます。

Iterable<ContainerItem> items = parser.getContainer(); // プレーンファイルは null
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);
    }
}

スプリッター段階は標準の Spring AI で、リーダーのメタデータはすべてのチャンクに保持されます:

TokenTextSplitter splitter = TokenTextSplitter.builder()
        .withChunkSize(300) // サンプル実行時のトークン数
        .build();
List<Document> chunks = splitter.apply(documents);

4 つのサンプルファイルに対してリーダーは 12 件の Document を返し、スプリッターは 20 のチャンクを生成します。デモは vectorStore.add(chunks) の代わりに output/spring-ai-documents.jsonl に書き出すので、実際のインデックスを作る前に各行で source、parent_source、page_number、そしてスプリッターの chunk_index を確認できます。

このファイルは 5 分程度の価値があります。parent_source で検索すれば、メールや ZIP 内から来たチャンクが正確に分かります。page_number で検索すれば、各ページ境界がどこにあったかが分かります。実際のメールボックスエクスポートで最初に実行したとき、これら 2 つの検索が、プロンプト調整に 1 日費やすよりも多くの検索品質に関する疑問に答えてくれました。

並列比較: Before vs. After

以下はヘルパー別フォーマット版と単一リーダー版で変わった点です:

Before (本文のみの読み取り+フォーマット別ヘルパー) After (GroupDocs.Parser DocumentReader)
Email attachments 失われるか、別ヘルパーで抽出される parent_source 付き Document として読み取られる
ZIP members まずディスクに解凍 インプレースで読み取り、source = sample.zip/sample.pdf
Citations ファイル名のみ ファイル名 + page_number
Code to maintain フォーマットごとにヘルパーが必要 DocumentReader クラス 1 つ
Unreadable file ヘルパー次第 ログに記録、スキップ、バッチは継続

リーダー以降のパイプラインは全く変わっていません。流入するデータだけが変わったのです。

実務例: 夜間サポートインデックス

サポートチームは毎晩共有メールボックスをエクスポートし、各製品リリースごとに更新されたマニュアルの ZIP を受け取ります。Spring Boot サービス内のスケジュールジョブが新規ファイルを列挙し、GroupDocsParserDocumentReader で読み取り、分割し、エージェントアシスタントの背後にあるベクトルストアへ書き込みます。エージェントが返金について質問すると、回答は manuals-2026-10.zip/refunds.pdf のページ 4 を引用し、parent_source フィルタで最新リリースバンドルに検索を限定できます。

3 つのステップのコードはこのジョブに 1 対 1 でマッピングされます。Step 1 は夜間エクスポートが生成するすべてのファイル(拡張子が失われたものも含む)を受け入れます。Step 2 はマニュアルページを個別に保持し、エージェントは顧客に返信する前にページ 4 を開くことができます。Step 3 は ZIP とメールボックスを不透明なバイナリではなく、可視的な親として扱い、スプリッター呼び出しはチームがすでに運用している他の Spring AI パイプラインと同様に変更なしです。

短い社内メモはページ分割を必要としないため、チームは全ファイルモードで読み取り、添付ファイルはそのまま保持します:

// メモ 1 件につき 1 Document; 添付ファイルは依然として独自 Document になる
DocumentReader reader = new GroupDocsParserDocumentReader(resource, false, true);
List<Document> documents = reader.read();

GroupDocs.Parser でさらにできることは?

添付ファイルが Spring AI に流れ込んだ今、関連機能がワンクリックで利用可能です:

  • 任意のオリジンからのストリーム読み取り: [ストリームからドキュメントをロード] ガイドはリーダーが使用する Parser(InputStream) コンストラクタをカバーしています。
  • コンテナを直接操作: [添付ファイルと ZIP アーカイブからデータを抽出] は getContainer と openParser の使い方を解説しています。
  • Spring AI 以外で PDF ページを読む: [PDF ドキュメントからテキストを抽出] は単体でのページレベル抽出方法を示します。
  • メールを直接処理: [メールから添付ファイルを抽出] は Spring AI 層を介さずにメールメッセージから添付ファイルを取得する手順を示します。

結論

「添付をご覧ください」の修正はリーダー段階にありました。GroupDocs.Parser を Spring AI の DocumentReader の背後に置くことで、添付ファイル、アーカイブメンバー、個別ページがアシスタントが引用できるソースとなり、差し替えはコンストラクタ 1 つで完了します。

自分のコーパスで試してみませんか?

  • 一時ライセンスを取得 して評価制限なしで実行
  • [ドキュメント内の統合ガイド] に従い、パターン、設定、運用チェックリストを確認
  • [GitHub のサンプルプロジェクト] をクローンし、メールボックスエクスポートのコピーを指すように設定

追加リソース