Skip to content

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 呼び出しです。

関連項目