RAG 用の Office ドキュメント
Retrieval-Augmented Generation のパイプラインは取り込み品質次第で生死が決まります。Office Oxide はテキスト、Markdown、構造化 IR の 3 つの出力を提供し、これらは RAG パイプラインが必要とする 3 つのもの — 埋め込み用の本文、チャンク化用の構造、引用用のメタデータ — に綺麗にマップされます。
出力を選ぶ
| ゴール | 使用 |
|---|---|
| 最も安価な埋め込み、最低トークンコスト | plain_text() |
| 構造保存チャンク(最良の検索品質) | to_markdown() |
| セクション/スライド/セルでチャンク化と引用 | to_ir() |
ほとんどのプロジェクトでは to_markdown() が落としどころ: 見出しを保持(自然なチャンク境界)、テーブルを検索可能なまま、トークン爆発なしに埋め込めるサイズ。
Markdown からの見出し対応チャンク化
Markdown 出力はソース見出しに # / ## / ### を使います。そこで分割すると、意味的に一貫したチャンクが「無料で」得られます。
from office_oxide import Document
def chunk_by_heading(md: str, level: int = 2):
chunks, current = [], []
for line in md.splitlines():
if line.startswith("#" * level + " "):
if current:
chunks.append("\n".join(current))
current = [line]
else:
current.append(line)
if current:
chunks.append("\n".join(current))
return chunks
with Document.open("report.docx") as doc:
md = doc.to_markdown()
chunks = chunk_by_heading(md, level=2)
for c in chunks:
print(len(c), c[:60].replace("\n", " "))
引用精度のための IR ベースチャンク化
検索済みコンテキストで スライド 3 や シート “Q4 Forecast” を引用する必要があるなら、IR を歩いてください。各セクションは自然なロケーターを保持しています:
from office_oxide import Document
with Document.open("deck.pptx") as doc:
ir = doc.to_ir()
chunks = []
for i, section in enumerate(ir["sections"], 1):
title = section.get("title") or f"スライド {i}"
body = []
for el in section["elements"]:
if el["kind"] == "Heading":
body.append("# " + el["text"])
elif el["kind"] == "Paragraph":
body.append(" ".join(r["text"] for r in el["runs"]))
elif el["kind"] == "Table":
for row in el["rows"]:
body.append(" | ".join(row))
chunks.append({
"source": "deck.pptx",
"locator": f"slide:{i}",
"title": title,
"text": "\n".join(body),
})
これで取得したチャンクが引用用の正確なロケーター(slide:3 / sheet:Q4 Forecast / section:2)を持ちます。
LangChain 統合
from langchain_core.documents import Document as LCDoc
from office_oxide import Document
def load_office(path: str) -> list[LCDoc]:
with Document.open(path) as doc:
ir = doc.to_ir()
out = []
for i, section in enumerate(ir["sections"], 1):
body_lines = []
for el in section["elements"]:
if el["kind"] == "Paragraph":
body_lines.append(" ".join(r["text"] for r in el["runs"]))
elif el["kind"] == "Heading":
body_lines.append(el["text"])
if not body_lines:
continue
out.append(LCDoc(
page_content="\n".join(body_lines),
metadata={
"source": path,
"section_index": i,
"section_title": section.get("title"),
},
))
return out
docs = load_office("report.docx")
Chroma.from_documents(docs, embedder)(あるいは任意のベクターストア)に通常通り渡します。
LlamaIndex 統合
from llama_index.core import Document as LIDoc
from office_oxide import Document
def load_office(path: str) -> list[LIDoc]:
with Document.open(path) as doc:
md = doc.to_markdown()
return [LIDoc(text=md, metadata={"source": path})]
セクションごとのノードには上記の IR ベースのパターンを使い、各チャンクを別々の LIDoc として渡します。
テーブル — 難しい部分
LLM は Markdown 形式の小さなテーブルを上手く扱えます。大きなテーブル(50+ 行)は要約またはページネーションが好ましい:
def summarize_table(rows: list[list[str]]) -> str:
headers = rows[0]
body = rows[1:]
return f"列 {headers} と {len(body)} 行のテーブル。サンプル: {body[:3]}"
ダッシュボード(XLSX)には、フルセルダンプではなく シートごとの要約 の抽出を検討してください — LLM は「シート ‘Q4’ は 12 地域で売上 $4.2M を計上」のほうが 5,000 セル値より恩恵を受けます。
パフォーマンスとコスト
| Op | DOCX 1 ファイルあたり時間(中央値) | 注 |
|---|---|---|
plain_text() |
0.8 ms | 最安価 |
to_markdown() |
~1.5 ms | RAG に推奨 |
to_ir() |
~1.2 ms | 構造が必要なとき |
100 万ドキュメントのコーパスはシングルスレッドで ~25 分、8 コアで ~3 分 で抽出。RAG パイプラインの主要コストは Office 解析ではなく、埋め込み API 呼び出しです。
関連項目
- Markdown 抽出 — 完全な出力仕様
- 構造化 IR — 引用対応チャンク化のスキーマ
- バッチ処理 — 並列化パターン