コンテンツにスキップ
LinkedInX

チャンク戦略

約10分

対象読者: RAGのデータ前処理を設計したい方、チャンクサイズで迷っている方
前提知識: RAGとは の基本フローを把握していること

チャンキング(Chunking)とは、大きな文書を検索しやすい小さな単位(チャンク)に分割する処理です。RAGでは、100ページのPDFをそのまま検索対象にすることはできません。適切な単位に分割することで、「この質問に答えるのはこの部分」という精度の高い検索が可能になります。

100ページの仕様書があるとします。ユーザーが「ログイン機能の仕様は?」と質問した場合、仕様書全体をLLMに渡すことは現実的ではありません。理由は2つあります。

  1. コンテキスト長の制限: LLMが一度に処理できるテキスト量に上限がある
  2. ノイズの増加: 関係ない情報が大量に入ると、LLMが本当に必要な部分に集中できなくなる

チャンキングは、仕様書を「ログイン」「検索機能」「エラー処理」のような意味のある単位に分割し、それぞれを独立して検索・参照できるようにします。

戦略仕組み実装の複雑さ文脈品質向いている用途
固定長チャンク一定のトークン数で分割低(文中で切れる)プロトタイプ・均質なテキスト
段落・文境界チャンク改行・句点で分割低〜中一般的な文書、ブログ、マニュアル
セマンティックチャンクトピック変化で分割学術論文、複雑な技術文書
階層型チャンク(親子)セクション+文の2階層中〜高長文書・複数粒度の検索が必要な場合

固定長チャンクは、テキストを一定のトークン数(文字数)で機械的に分割する最もシンプルな方法です。LangChain の text splitters は、文字数・トークン数・言語構造などに基づく分割手段を提供しています。[1]

# pip install langchain-text-splitters tiktoken
from langchain_text_splitters import TokenTextSplitter

splitter = TokenTextSplitter(
    chunk_size=512,      # チャンクの最大トークン数
    chunk_overlap=50,    # オーバーラップ(前後の文脈を少し残す)
)

text = "長い文書のテキスト..."
chunks = splitter.split_text(text)
print(f"チャンク数: {len(chunks)}")

利点: 実装が単純で高速 欠点: 文の途中で切れることがある。「この機能は—」という文が前のチャンクの末尾と後のチャンクの先頭に分断されると、意味が失われる

改行(段落)や句点(文末)を基準に分割することで、文の途中での分断を避ける方法です。

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,          # 目標のチャンクサイズ(文字数)
    chunk_overlap=100,        # オーバーラップ
    separators=["\n\n", "\n", "。", ".", " ", ""],  # 分割の優先順位
)

with open("document.txt", "r", encoding="utf-8") as f:
    text = f.read()

chunks = splitter.split_text(text)

RecursiveCharacterTextSplitter は、まず段落(\n\n)で分割を試み、チャンクが大きすぎれば行(\n)で、それでも大きければ文末()で分割します。この「なるべく大きな単位で分割する」方針が、文脈の保持に役立ちます。[1]

文の意味の変化(トピックの転換)を検出して分割する手法です。隣接する文の埋め込みベクトルの距離を計算し、大きく変化した箇所を分割点とします。

# pip install langchain-experimental
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# breakpoint_threshold_type="percentile" で閾値を自動決定
splitter = SemanticChunker(
    embeddings,
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=95,  # 上位5%の変化点を分割点とする
)

chunks = splitter.split_text(long_document_text)

利点: トピックごとに意味のあるチャンクになる 欠点: 埋め込みモデルを使うため、前処理のコストが増加する。チャンクサイズが不均一になることがある

階層型チャンク(親子チャンク)

Section titled “階層型チャンク(親子チャンク)”

文書を「大きな単位(親チャンク)」と「小さな単位(子チャンク)」の2層で管理する手法です。LlamaIndex では、文書を Node として扱い、用途に応じた Node Parser で分割・構造化できます。[2]

仕組み:

  1. 文書をセクション単位(親チャンク)に分割する
  2. 各セクションをさらに小さな文単位(子チャンク)に分割する
  3. 検索は子チャンクで行い(精度が高い)、LLMには親チャンクを渡す(文脈が豊富)
graph TD
    D["文書"] --> P1["親チャンク: セクション1(1000トークン)"]
    D --> P2["親チャンク: セクション2(1000トークン)"]
    P1 --> C1["子チャンク: 文1(200トークン)"]
    P1 --> C2["子チャンク: 文2(200トークン)"]
    P1 --> C3["子チャンク: 文3(200トークン)"]
    C1 -->|"検索でヒット"| R["子チャンクのIDを返す"]
    R --> P1_RETURN["対応する親チャンクをLLMへ渡す"]
from langchain.retrievers import ParentDocumentRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 親チャンク: 大きめ(文脈が豊富)
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000)
# 子チャンク: 小さめ(検索精度が高い)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=400)

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,     # 子チャンクのベクトルDB
    docstore=docstore,           # 親チャンクの保存場所
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)

オーバーラップとは、前のチャンクの末尾部分を次のチャンクの先頭に重複して含める設定です。

例えば chunk_size=512, chunk_overlap=50 の場合:

  • チャンク1: トークン 1〜512
  • チャンク2: トークン 463〜974(前のチャンクの末尾50トークンを引き継ぐ)
  • チャンク3: トークン 925〜1436(同様)
チャンク1: [... テキスト ... オーバーラップ部分]
チャンク2:                  [オーバーラップ部分 ... テキスト ... オーバーラップ部分]
チャンク3:                                          [オーバーラップ部分 ... テキスト]

オーバーラップがないと、ちょうど分割点にある重要な情報が「どのチャンクにも含まれない」状態になるリスクがあります。10〜20%のオーバーラップ(例: チャンクサイズ512に対して50〜100トークン)は、RAG向けチャンキングの実務上の出発点としてよく使われます。[3]

文書の種類と用途によって、適切なチャンクサイズは異なります。Pinecone のチャンキング解説でも、短すぎるチャンクは文脈を失い、長すぎるチャンクはノイズを増やすため、ユースケースに合わせた評価が必要だと説明されています。[3]

文書の種類推奨チャンクサイズ理由
FAQ、短い回答形式256〜512トークン1問1答が自己完結するサイズ
製品マニュアル、技術ドキュメント512〜1024トークン手順の流れを1チャンクで保持
法律・契約書1024〜2048トークン条項の前後関係が重要
学術論文512〜1024トークン(+ 階層型)段落単位で意味が完結
コードファイル関数・クラス単位構文的な区切りを優先

トークン数の目安: 英語では1トークン ≈ 4文字(0.75単語)、日本語では1トークン ≈ 1〜2文字(モデルによる)

チャンクには本文だけでなく、メタデータを必ず付けます。メタデータは回答の引用・フィルタリング・デバッグに不可欠です。

from langchain_core.documents import Document

chunk = Document(
    page_content="ログイン機能はOAuth 2.0を使用します...",
    metadata={
        "source": "product-spec-v2.pdf",      # ファイル名
        "page": 15,                            # ページ番号
        "section": "認証・セキュリティ",        # セクションタイトル
        "doc_type": "仕様書",                  # 文書の種類
        "last_updated": "2026-04-01",          # 文書の更新日
        "visibility": "internal",              # 公開範囲
    }
)

メタデータが充実していると:

  • LLMへの回答に「参照元: product-spec-v2.pdf 15ページ」のような引用を付けられる
  • 「2026年以降の仕様書のみ検索する」のようなフィルタリングができる
  • 「どの文書のどの部分が取得されたか」のデバッグが容易になる

表をテキストに変換するとき、行と列の関係が崩れることがあります。表は可能な限り1つのチャンクに収め、前後の見出しをメタデータとして付与します。

# Markdownの表として保持する
table_chunk = Document(
    page_content="""
## 料金プラン比較

| プラン | 月額 | ユーザー数 | ストレージ |
|--------|------|----------|-----------|
| Basic  | $10  | 1        | 5GB       |
| Pro    | $30  | 10       | 50GB      |
| Enterprise | 要相談 | 無制限 | 1TB  |
""",
    metadata={"section": "料金プラン", "content_type": "table"}
)

コードは関数・クラス単位で分割します。関数の途中での分割は意味を失います。

# pip install tree-sitter
# AST(抽象構文木)を使って関数単位で分割する
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter

python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1000,
    chunk_overlap=0  # コードはオーバーラップなしが一般的
)

チャンクが小さすぎる(文脈の消失)

Section titled “チャンクが小さすぎる(文脈の消失)”

1チャンク = 1〜2文のような細かすぎる分割は、前後の文脈が失われます。「この機能は前のセクションで説明した方法に基づいています」という文があっても、前のセクションがないチャンクでは意味が通じません。

チャンクが大きすぎる(ノイズの増加)

Section titled “チャンクが大きすぎる(ノイズの増加)”

2000〜4000トークンの大きなチャンクは、関連情報と無関係な情報が混在します。LLMへのコンテキストが長くなるほど、重要な情報が埋もれるリスクが高まります。

オーバーラップなし(境界の情報損失)

Section titled “オーバーラップなし(境界の情報損失)”

オーバーラップを設定しないと、ちょうど分割点にある重要な情報(「以下の条件のとき——」という文の後半が次のチャンクに移る場合など)が取得されないことがあります。

  • チャンキングは「文書を検索可能な単位に分割する」処理で、RAGの品質に大きく影響する
  • 基本は段落境界チャンク(RecursiveCharacterTextSplitter)から始め、精度要件に応じて改善する
  • オーバーラップ(10〜20%)は文脈の連続性を保つために設定する
  • メタデータ(ソース、ページ番号、セクション名)は引用・フィルタリングに不可欠
  • テーブルとコードは特別な処理が必要(表は1チャンクに、コードは構文単位で分割)

Q: どのチャンクサイズから始めるべきですか?

A: まず chunk_size=512, chunk_overlap=50 から試すことをお勧めします。これはバランスの良い出発点で、多くの文書タイプに対応します。評価セットを用意し、小さくしたときと大きくしたときの検索精度を比較して調整してください。「512トークンが絶対正解」ではなく、文書の種類と質問のパターンによって最適値が変わります。

Q: テーブルやコードブロックはどう扱えばいいですか?

A: テーブルは1つのチャンクに収めます。分割するとヘッダーと行の対応関係が崩れるためです。コードは関数・クラスを単位として RecursiveCharacterTextSplitter.from_language() を使います。PDF からテーブルを正確に抽出するには、pdfplumbercamelot などの専用ライブラリが役立ちます。

Q: チャンク戦略を変更した場合、再インデックスが必要ですか?

A: はい。チャンクサイズ、分割戦略、オーバーラップを変更した場合は、全文書を再分割・再ベクトル化(再インデックス)する必要があります。本番環境でチャンク戦略を変更するときは、事前に評価セットで精度を確認し、計画的に移行してください。

Q: 日本語文書の場合、トークン数の計算はどうすればいいですか?

A: LangChainや LlamaIndex のスプリッターは文字数(len())またはトークン数(tiktoken など)でカウントできます。日本語の場合、text-embedding-3-small などのトークナイザーでは1文字が1〜2トークンになることが多く、英語よりも少ないトークン数で同じ情報量になります。実装では tiktoken を使ったトークンカウントが正確です。

  1. LangChain Text Splitters — Documentation
  2. LlamaIndex Node Parsers — Documentation
  3. Chunking Strategies for LLM Applications — Pinecone
クイズ