1. RAGパイプライン構築におけるドキュメント処理の悪夢
先月、私たちのチームはある典型的な案件に取り組みました。オペレーション部門および財務部門向けの社内QAチャットボットの構築です。クライアントから共有されたデータソースは、約15GBもの容量を持つGoogle Driveフォルダで、あらゆる形式のファイルが400件以上混在していました。
- 15〜20個のシートと複雑な表組みを含む
.xlsx形式の財務レポート - 参照用テーブルや4〜5階層に及ぶ入れ子の箇条書きを含む、Word(
.docx)形式の標準業務手順書(SOP) - PowerPoint(
.pptx)形式の人事研修スライド - ネイティブテキストとスキャン画像が混在した
.pdf形式の社内規程文書
当初、チームは「自前での実装」を選択しました。pypdfやpython-docx、openpyxlといったおなじみのライブラリを組み合わせて生テキスト(raw text)を抽出し、そのままテキストスプリッターに流し込む構成です。しかし、テストの結果は全員を失望させるものでした。
例えば「ハノイ出張時におけるTech Leadの宿泊費上限はいくらですか?」と質問したところ、元の規程には明確に1泊あたり1,200,000 VNDと記載されているにもかかわらず、ボットは試用期間中の社員の上限である800,000 VNDと誤って引用して回答してしまったのです。
ベクターデータベース(Vector Database)を確認してリトリーブされたコンテキストを調査すると、原因はすぐに判明しました。Excel内の手当一覧表がヘッダー(列名・行名)をすべて失い、単なる平坦な連続テキストとして抽出されていたのです。さらにチャンキング処理によって、金額と役職の途中で行データが真っ二つに分断されていました。文脈を完全に失った結果、モデルがハルシネーション(情報の捏造)を引き起こしていました。
2. なぜ生テキストの抽出はRAGの精度を崩壊させるのか?
RAGの実装を始めたばかりの頃は、「ファイルから文字さえ抽出できれば十分」と思いがちです。しかし実際には、大規模言語モデル(LLM)が情報同士の関連性を正しく把握するためには、意味的な構造(セマンティック構造)が不可欠です。
生テキストのままでは、RAGパイプラインは次の3点で破綻します。
- 階層構造の欠落:見出し(H1, H2)や箇条書きは、規程の適用範囲をLLMに認識させる重要な役割を果たします。これらが平坦なテキストの塊になってしまうと、LLMは一般条項と例外規定を区別できなくなります。
- 表データのフラット化:テーブル内の数値は、列名や行名と紐付いて初めて意味を持ちます。安易な変換を行うと、2次元のマトリックスデータが無意味な数字の羅列へと成り下がってしまいます。
- チャンキングの分断:
RecursiveCharacterTextSplitterなどのテキスト分割器は、Markdown構文(#、##やテーブル行の|---|など)を目印にして分割することで真価を発揮します。これらの区切りを失うと、文字数だけで機械的に切断されるため、重要な情報が2つのチャンクにまたがって引き裂かれる原因となります。
3. 自前でのドキュメント処理で直面する「2つの袋小路」
MarkItDownに辿り着く前、私たちのチームもよくある2つのアプローチを試しました。
アプローチ1:Pythonでカスタムパーサーを自作する
PDFにはpdfplumber、Wordにはpython-docx、Excelにはpandasといった専用スクリプトを自作する手法です。一見手軽に見えましたが、コード量はあっという間に1,200行を超えて肥大化しました。Excelの結合セル、Word内の入れ子テーブル、PDFの行ズレといったエッジケースが次々と発生し、メンテナンスコストがプロジェクトの巨大な負担になってしまいました。
アプローチ2:商用パースAPI(Unstructured、LlamaParseなど)の呼び出し
これらのSaaSソリューションによるパース品質は非常に優れています。しかし、チームはコストとセキュリティという2つの大きな壁にぶつかりました。数万ページに及ぶドキュメント群では、毎月のAPI利用料が跳ね上がります。さらに深刻だったのはクライアントのセキュリティポリシーで、社内の機密財務データを外部サーバーへ送信することが厳格に禁じられていたのです。
4. Microsoft MarkItDownによるデータの標準化
2024年末、MicrosoftはMarkItDownをオープンソースとして公開しました。このPythonライブラリは、一般的な主要フォーマット(PDF、DOCX、XLSX、PPTX、HTML、CSV、JSON)を、唯一の標準フォーマットであるMarkdownへと統合・変換することに特化しています。
なぜMarkdownなのか?第一に、見出し階層や| Col1 | Col2 |形式の表構造を完全に保持できる点です。第二に、HTMLやJSONと比較してトークン消費量を大幅に削減でき、かつLLMのコンテキスト認識に最適化されているためです。
ライブラリのインストール
要件に応じて、標準版または拡張版をインストールできます。
# 一般的なオフィスファイル向けの基本構成
pip install markitdown
# 画像のOCRや音声抽出が必要な場合のフル構成
pip install "markitdown[all]"
基本的なドキュメント変換
初期化の構文は極めてシンプルです。MarkItDownクラスを呼び出し、convert()メソッドを実行するだけです。
from markitdown import MarkItDown
md = MarkItDown()
# Word規程ファイル(.docx)のパース
result_docx = md.convert("quy-che-cong-tac-phi.docx")
print(result_docx.text_content[:300])
# Excelシート(.xlsx)をMarkdownテーブルに変換
result_xlsx = md.convert("bang-gia-dich-vu.xlsx")
print(result_xlsx.text_content)
Excelワークシートの出力は、列ヘッダーと各行の値をそのまま維持した標準的なMarkdownテーブルへと変換されます。
| Ma_DV | Ten_Dich_Vu | Gia_Goi_Thang | Ghi_Chu |
| --- | --- | --- | --- |
| SRV01 | Cloud VPS Basic | 150000 | Ram 2GB, 1 Core |
| SRV02 | Cloud VPS Pro | 350000 | Ram 4GB, 2 Core |
複数ファイルの一括処理パイプラインの構築
実際の案件では、何百ものドキュメントが含まれるディレクトリ全体を走査し、一括で.mdに変換した上でVector Databaseに格納することになります。
from pathlib import Path
from markitdown import MarkItDown
def batch_convert_to_markdown(source_dir: str, target_dir: str):
md = MarkItDown()
valid_extensions = {".pdf", ".docx", ".xlsx", ".pptx", ".csv", ".html"}
src = Path(source_dir)
dst = Path(target_dir)
dst.mkdir(parents=True, exist_ok=True)
for file_path in src.iterdir():
if file_path.suffix.lower() not in valid_extensions:
continue
try:
print(f"[+] 処理中: {file_path.name}")
parsed = md.convert(str(file_path))
output_file = dst / f"{file_path.stem}.md"
output_file.write_text(parsed.text_content, encoding="utf-8")
print(f"[OK] 保存完了: {output_file.name}")
except Exception as err:
print(f"[!] {file_path.name} の処理中にエラーが発生しました: {err}")
if __name__ == "__main__":
batch_convert_to_markdown("./raw_docs", "./cleaned_markdown")
Visionモデルを統合した図や画像の解説テキスト生成
MarkItDownの大きな強みの1つが、OpenAIやAzure OpenAIのクライアントを連携できる点です。インフラ構成図などを含むPowerPointスライドを処理する際、ライブラリが自動的にVisionモデルを呼び出し、画像の内容をテキストで要約してMarkdownファイル内に直接埋め込んでくれます。
import os
from openai import OpenAI
from markitdown import MarkItDown
# LLMクライアントの設定
ai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# 画像読み取り用にGPT-4oモデルを指定してMarkItDownを初期化
md_vision = MarkItDown(llm_client=ai_client, llm_model="gpt-4o")
# 技術構成図を含むPowerPointスライドの変換
result = md_vision.convert("so-do-ha-tang.pptx")
output_path = Path("so-do-ha-tang.md")
output_path.write_text(result.text_content, encoding="utf-8")
本番環境における実践的なTips
- 巨大なExcelファイルの分割:2,000行を超えるスプレッドシートの場合、生成されるMarkdownテーブルがコンテキストウィンドウを圧迫・超過(8k〜16kトークン越え)する恐れがあります。変換前にシート単位で絞り込むか、データをあらかじめ小分けにしておくことを推奨します。
- スキャンPDFをパーサー任せにしない:MarkItDownはネイティブなPDFテキストを非常に高精度に抽出できます。しかし、不鮮明なスキャン文書や傾いて撮影された領収書などに対しては、事前に専用のOCRパイプライン(PaddleOCRやTesseractなど)を組み合わせて画像処理を行う必要があります。
- YAML Frontmatterの付与:
.mdファイルを保存する前に、ファイル先頭へメタデータ(元のファイルパス、発行部署、更新日など)を挿入しておきましょう。検索(Retriever)時にベクター検索の前段階としてメタデータフィルタリングを適用できるため、検索速度と精度が大幅に向上します。
データ前処理の全工程をMarkItDownに移行した結果、私たちのチームのプロジェクトでは詳細な数値データに対する正答率が68%から92%以上へと跳ね上がりました。ローカル環境で軽量に動作するシンプルなツールでありながら、モデルに投入する前のデータノイズ問題を根本から解決してくれます。
