💡 完全に動作する例はGitHubで入手可能です: document-to-markdown-rag-chunks-java
クレームデスクの問題: なぜRAGインジェッションは実際のファイルで失敗するのか
オンプレミスのRAGインジェッションパイプラインは、チームがすでに所有しているファイルをサードパーティに送信せずに検索可能で引用可能なテキストに変換するドキュメント解析層です。保険業務がその重要性を示します。クレーム担当者が内部アシスタントに「このポリシーの水害について調整員は何と言っていたか?」と尋ねると、答えは昨年3月のメールに添付されたPDFレポートの「Findings」という見出しの下にあります。アシスタントは、インジェッションが添付ファイルを読み取り、見出しを保持し、どのメールにレポートが付いていたかを記憶している場合にのみそれを見つけられます。
ほとんどのインジェッションジョブは次の3つのうちのいずれかを間違えます。プレーンテキスト抽出器はメール本文だけを返し、添付ファイルを無視します。固定長512トークンのスプリッターは「Findings」の途中でレポートを切ってしまいます。ホスト型パーサーは両方を解決しますが、個人情報や医療情報を含むクレームファイルが他者のサーバーにアップロードされ、ページ単位で課金されます。
チームが必要としているのは、狭く具体的なことです:Javaサービス内でWord、PDF、EML、ZIPファイルを読み取り、構造をMarkdownとして保持し、著者の見出しで分割し、すべてのチャンクにソースと親を付与すること。
市場スナップショット: 私たちがレビューした2026年版「RAG向けドキュメントパーシング」ガイドは、ページ単位で課金されるクラウドAPIとオープンソースPythonパーサーを比較しています。自前のハードウェア上で動作するJavaライブラリはほとんど議論に登場しません。
なぜRAGパイプラインはプレーンテキストではなくMarkdownを必要とするのか?
Markdownは、リトリーバーに「ここで一つのアイデアが終わり、次が始まる」ことを示す文書の構造情報を保持します。見出しは # 行になり、表は行と列のまま、リストは項目を保持します。チャンク分割器はこれらの見出しで分割でき、LLMがチャンクを読むと人が読むときと同じアウトラインが得られます。プレーンテキストはそのアウトラインを失い、サイズだけが分割の唯一のシグナルになります。
なぜ汎用的な解決策はここで機能しないのか
チームは通常、適切なインジェッション層を構築する前に次の3つのうちのいずれかを試します:
- プレーンテキスト抽出+トークンスプリッター: 設定は速いが、チャンクがセクションを混在させ、引用したい見出しを失う。
- クラウドパーシングAPI: 構造は良好だが、ファイルがネットワークを離れ、再インデックスごとに請求が増える。
- フォーマットごとの自前パーサー: DOCX用のライブラリ、PDF用の別ライブラリ、メール用のMIMEパーサー、アーカイブ用の
java.util.zipなど、各々が独自のエッジケースを持つ。
GroupDocs.Parser for Java は4つのフォーマットすべてを単一の Parser クラスでカバーし、プロセス内で実行できます。
解決策: クレームインジェッションワークフローにおける GroupDocs.Parser
GroupDocs.Parser for Java はドキュメントを読み取り、getFormattedText を通じてテキストをMarkdownとして返すことができます。メールやアーカイブはコンテナとして公開され、項目は新しい Parser インスタンスとして開かれるため、メールに添付されたPDFもディスク上のPDFと同様に処理されます。サンプルプロジェクトはこれを6つの静的メソッドでラップし、結果をJSON Linesとして書き出します。これなら任意の埋め込みジョブやベクトルストアが読み込めます。RAGフレームワークは関与せず、出力形式が統合ポイントです。
必要なもの:
pom.xmlに記載された JDK 11 以上(コード自体は Java 8 の機能に留まります)- GroupDocs.Parser for Java 26.10(一時ライセンスを取得)
- DOCX、PDF、EML、ZIP ファイル数点
プロジェクトの pom.xml は https://releases.groupdocs.com/java/repo/ リポジトリと単一の依存関係を宣言しているので、ビルドは次のコマンド一つです:
mvn compile exec:java
インジェッション層をステップバイステップで実装する
以下の手順は、受信トレイからインデックスまでの 1 件のクレームファイルに沿って進みます。各ステップは RagChunksDemo.java の 1〜2 メソッドに対応しています。意図的にプレーンな静的メソッドとして残しているので、既存のワーカーに好きなものだけ取り込めます。
手順 1 - 各ファイルを開きルーティングする
アップロードされた各ファイルは独自の Parser を取得します。ファイル名がソース ID となり、トップレベルファイルには親がありません。
// 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 は本文を抽出し、チャンク化し、深さが 2 未満の間は添付ファイルへ降りていきます。
手順 2 - Markdown を要求し、プレーンテキストを受け入れる
抽出ステップは拡張子リストをハードコーディングする代わりに、フォーマットがサポートするものを問い合わせます。
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
クレームデータに対してこれらの設定が重要な理由:
FormattedTextMode.Markdown: 調整員の「Findings」見出しが## Findingsとしてチャンク分割器に届くため、分割ポイント兼引用になる。isFormattedText(): サンプル実行では DOCX とメールが Markdown で返り、PDF はプレーンテキストにフォールバックし、formatフィールドにその結果が記録されます。none結果: ZIP 内のクレーム写真は OCR が無ければチャンクが生成されず、正直で監視しやすい状態になります。
手順 3 - 調整員の見出しで分割する
チャンク分割器は 6 段階の Markdown レベルごとに現在の見出しスタックを保持します。新しい見出しが来ると、以前のセクションを閉じてそのレベル以下をリセットします。
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');
}
1500文字を超えるセクションは段落区切りでカットされるため、長い Findings セクションは同じ見出しパスを共有する複数のチャンクに分割されます。バンドルされた Word ファイルでは Heading 1 > Heading 1.2 > Heading 1.2.1 のようなパスが生成されます。
手順 4 - 添付ファイルをたどり JSON Lines に書き出す
クレームデスクが最も関心を持つステップです。getContainer() はメール添付や ZIP エントリを列挙し、各項目は子ドキュメントとして開かれ、親の ID を継承します。
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 は id、source、parent、heading_path、format、text を含む UTF‑8 行をチャンクごとに書き出します。4 つのサンプル入力で実行すると 22 行が生成されます。サンプルメールの添付からのチャンクは email-with-attachment.eml/attachment.txt#0 で、parent は email-with-attachment.eml です。このペアがアシスタントに「3月14日のメールに添付されたレポートから」と言わせる鍵になります。
実際のワークフロー: 週次クレーム再インデックス
想像してください。クレーム運用チームが毎金曜にクローズ済みクレームのメールボックスを EML ファイルとサポート文書の ZIP バンドルとしてエクスポートします。Java ワーカーはすでにそれらのエクスポートをドキュメントストアに移動しています。インジェッションを追加するだけで、各ファイルに対して chunkDocument を呼び出し、JSON Lines 出力を埋め込みキューに追加できます。
手順 4 はメールに添付されたレポートを保持し、手順 3 はアシスタントが「Findings」を引用するときにファイル名と見出しを示すようにします。私の経験則は次のとおりです。チャンクがファイルと見出しに遡って引用できなければ、インデックスに入れるべきではありません。
ビジネスインパクト: 何が変わるか
| 項目 | 従来の典型的な構成 | GroupDocs.Parser パイプライン導入後 |
|---|---|---|
| ファイルの解析場所 | サードパーティエンドポイント | 自社 JVM 内 |
| 解析コストモデル | ページ単位、再インデックスごとに課金 | ページ単位のクラウド料金なし |
| チャンク境界 | 固定トークンウィンドウ | 著者の見出し、1500文字上限 |
| メール添付 | 多くはスキップまたは平坦化 | 子ドキュメントとして解析 |
| 引用データ | ファイル名が上限 | source、parent、heading_path |
| ランタイム依存関係 | SDK + ネットワークアクセス | 1 つの Maven アーティファクト |
この複合効果により、コンプライアンスチームが承認できるインデックスができ、アシスタントの回答は実際の文書とセクションを指し示すようになります。
GroupDocs.Parser が適合する他の保険シナリオ
インジェッション層ができれば、同じコードが他の文書中心の業務にも活用できます:
- ポリシー文言検索: 条項見出しで分割された Word ポリシー文書。回答は正確なセクションを引用。
- ブローカー提出物: PDF とスプレッドシートが混在した ZIP アップロードをメンバー単位でインデックス化し、提出物に紐付け。
- 苦情処理: 顧客メールとその添付ファイルを一緒にインデックス化し、メールがすべての添付チャンクの親になる。
クレーム環境での開始手順
サンプルをクローンし、独自のエクスポートメール数件で実行し、output/chunks.jsonl を開いて見出しパスと親 ID を確認してから埋め込みジョブに組み込みます。段階的なアーキテクチャの詳細は このパイプラインのユースケースドキュメント に、Markdown 出力リファレンス にはモードが保持するフォーマットが一覧化されています。