RAG 上級講座 #2 検索品質を左右するチャンキング戦略

読了 6分

第1回で基準線を作りました。いよいよ検索失敗を直しに行きます。ところが検索失敗の根は、検索アルゴリズムではなくその前の段階にある場合が多いです。文書をどう切り分けたか、つまりチャンキング(chunking、文書を検索の単位となるかけらに分けること)です。どんなに良い検索でも、かけら自体が悪ければ良い結果を持って来られません。

固定サイズ分割の限界 #

LLM アプリ開発 第8回では、300文字ずつ切って50文字を重ねる固定サイズ分割を使いました。出発点としては十分ですが、限界がはっきりしています。文書の意味の単位と無関係に切るからです。

たとえば返金ポリシーの文書で、「返金手数料」の節の見出しと手数料率の表が別々のチャンクに分かれてしまうと、どちらのチャンクも単独では質問に答えられません。検索はその中途半端なチャンクを取ってきて、第1回の診断では「チャンクの中に正解がない」と判定されます。チャンキングの失敗が検索の失敗として現れる典型的な姿です。

構造ベースのチャンキング — 文書の単位に沿って切る #

改善の基本方針は、文字数ではなく文書の構造に沿って切ることです。Markdown なら見出しが、一般の文書なら段落が自然な境界です。

structural_chunking.py
import re

def chunk_by_heading(markdown: str, max_chars: int = 1500) -> list:
    """見出し単位で切り、大きすぎるセクションだけ段落単位で分け直す。"""
    sections = re.split(r'(?=^#{1,3} )', markdown, flags=re.M)
    chunks = []
    for sec in sections:
        sec = sec.strip()
        if not sec:
            continue
        if len(sec) <= max_chars:
            chunks.append(sec)
        else:
            for para in split_by_paragraph(sec, max_chars):
                chunks.append(para)
    return chunks

核心は「セクション一つがチャンク一つ」という対応です。見出しと本文と表が一つのチャンクに一緒にあるので、そのチャンク一つで質問に答えられます。チャンクのサイズは不揃いになりますが、構いません。均一なサイズより完全な意味のほうが、検索品質にはるかに重要です。

サイズの上限(max_chars)は二つを見て決めます。大きすぎると一つのチャンクに複数の主題が混ざって埋め込みがぼやけ、小さすぎると文脈が切れます。文書の中で「質問一つに答える単位」がふつうどの程度の分量かを基準に置き、第1回のゴールデンセットで前後を比較しながら調整します。

表とコードはまるごと #

固定サイズ分割の最大の被害者は表とコードブロックです。表が途中で切れると、ヘッダーと値が分離して両方のチャンクとも使い物にならなくなります。ルールは単純です。表とコードブロックは切らず、まるごと一つのチャンクに置きます。構造ベースのチャンキングで段落を分けるとき、表とコードフェンスの内側を分割の境界にしなければよいだけです。

表が大きすぎて上限を超えるなら、切って二つのチャンクを作る代わりに、各チャンクにヘッダー行を複製して入れます。どのチャンクを取ってきても、列名と値が一緒にあるようにするのです。

メタデータ — チャンクに出自を書き込む #

チャンクには本文のほかに出自の情報を一緒に保存します。どの文書のどのセクションから来たのか、です。

chunk_with_metadata.py
{
    "text": "返金手数料は決済金額の10%です。...",
    "metadata": {
        "source": "refund-policy.md",
        "section": "返金手数料",
        "updated": "2026-05-01",
    },
}

メタデータの使い道は三つです。第一に、埋め込みのとき本文の前にセクションのパスを付けると(「返金ポリシー > 返金手数料: …」)、短いチャンクの検索品質が上がります。第二に、検索の段階でフィルタとして使います。「最新の文書だけ」「人事規定だけ」のような条件です。第三に、答えに出典を表示するときそのまま使います。第5回の引用でまた出会います。

親子チャンキング — 小さく探して大きく入れる #

検索と生成では、チャンクのサイズに対する要求が互いに異なります。検索は主題が一つにくっきりした小さなチャンクで正確になり、生成は前後の文脈を含んだ大きなチャンクを受け取るとき良い答えを作ります。この緊張を解く方法が親子チャンキングです。

  • 文書を大きな単位(親、セクション全体)に分け、各親をさらに小さな単位(子、段落)に分けます。
  • 埋め込みと検索は子で行い、子が検索されたらその親をコンテキストに入れます
parent_child.py
def search_with_parent(question: str, top_k: int = 5) -> list:
    children = vector_search(question, top_k=top_k)   # 小さなチャンクで検索する
    parent_ids = {c.metadata["parent_id"] for c in children}
    return [parents[pid] for pid in parent_ids]        # 大きなチャンクを返す

実装の負担は少し増えますが、「検索は当たったのにチャンクが短すぎて答えを作れない」という型の失敗に直接の効果があります。第1回の診断でそのパターンが見えていたなら、優先度を上げる価値があります。

もう一度切って、もう一度測る #

チャンキングを変えると埋め込みを作り直す必要があるので、インデックス全体を再構築します。そして必ず第1回のゴールデンセットで前後を比較します。チャンキングの変更は効果が大きい分、方向も両側です。ある質問群は良くなり別の質問群は悪くなることがあるので、数字なしで変えると、良くなったという錯覚だけが残ります。

よくつまずくところ #

  • 均一なサイズにこだわる — きれいに N 文字ずつ切られたチャンクは見た目には良いですが、意味が途切れています。不揃いでも意味の単位が優先です。
  • 見出しを捨てる — 段落だけ切って入れると、「それ」「このポリシー」のような代名詞の指す先が消えます。セクションの見出しをチャンクに含めるか、メタデータとして前に付けます。
  • インデックスの再構築を後回しにする — チャンキングのコードだけ変えて既存のインデックスでテストしても、何も変わりません。チャンキングの変更は常に再インデックスとひとまとまりです。

まとめ #

今回は、検索品質の土台であるチャンキングを扱いました。

  • 固定サイズ分割は意味の単位を断ち切ります。見出しと段落のような文書の構造に沿って切り、表とコードはまるごと置きます。
  • チャンクには出典やセクションのようなメタデータを付け、埋め込みの補強、フィルタ、出典の表示に使います。
  • 検索は小さなチャンク、生成は大きなチャンクが有利です。親子チャンキングが両方を同時につかみます。

チャンクが良くなったので、次は探し方です。意味検索だけでは製品コードや固有名詞の質問を取りこぼします。次回の「RAG 上級講座 #3 ハイブリッド検索 — ベクトルとキーワードの組み合わせ」で、二つの検索を組み合わせます。

X