2026年RAGシステムチャンキング戦略最適化完全ガイド
2026年RAGシステムチャンキング戦略最適化完全ガイド
2026年になっても固定512文字チャンキングでRAGを構築しているなら、検索品質はおそらく悲惨な状態でしょう。チャンキング戦略はRAGシステムの品質を決定する最重要要素です—embeddingモデルの選択よりも重要で、検索アルゴリズムよりも重要です。なぜ?どれほど強力なベクトルモデルを使っても、入力チャンクが断片的で意味的境界を跨いでいれば、検索結果は決して良くならないからです。
本ガイドでは、6つの主要RAGチャンキング戦略を深く解説し、それぞれにそのまま実行できるPythonコード、ベンチマークデータ、最適化のヒントを付けています。
6つの戦略概覧
| 戦略 | コアアイデア | 意味的完全性 | 実装複雑度 | 適用シーン | 推奨度 |
|---|---|---|---|---|---|
| 固定サイズ | 文字/token数で分割 | ⭐ | ⭐ | ログ、構造化テキスト | ⭐⭐ |
| 文ベース | 文境界で分割 | ⭐⭐⭐ | ⭐⭐ | 汎用テキスト | ⭐⭐⭐ |
| 意味チャンキング | 意味類似度で分割 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 高品質ナレッジベース | ⭐⭐⭐⭐⭐ |
| 再帰チャンキング | 多レベル区切り文字の再帰分割 | ⭐⭐⭐⭐ | ⭐⭐⭐ | Markdown/コード文書 | ⭐⭐⭐⭐ |
| 文書ベース | 文書構造で分割 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 構造化文書 | ⭐⭐⭐⭐ |
| ハイブリッド | 複数戦略の組み合わせ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 本番環境 | ⭐⭐⭐⭐⭐ |
1. 固定サイズチャンキング(Fixed-Size Chunking)
最もシンプルな戦略:固定文字数またはトークン数で分割し、オプションでオーバーラップウィンドウを設定。
メリット:実装が簡単、チャンクサイズを制御可能、embeddingモデルの入力制限に適合。
デメリット:意味的境界を完全に無視—完全な文が途中で切断される可能性あり。
from typing import List
def fixed_size_chunk(
text: str,
chunk_size: int = 512,
chunk_overlap: int = 50,
separator: str = ""
) -> List[str]:
"""固定サイズチャンキング戦略
Args:
text: チャンキング対象テキスト
chunk_size: チャンクあたりの文字数
chunk_overlap: 隣接チャンク間のオーバーラップ文字数
separator: 区切り文字
Returns:
テキストチャンクリスト
"""
if not text:
return []
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunk = text[start:end]
if chunk.strip():
chunks.append(chunk)
start += chunk_size - chunk_overlap
return chunks
# 使用例
sample_text = "RAGシステムは最も人気のあるAIアプリケーションアーキテクチャの一つです。チャンキング戦略は検索品質に直接影響します。"
chunks = fixed_size_chunk(sample_text, chunk_size=40, chunk_overlap=10)
for i, chunk in enumerate(chunks):
print(f"Chunk {i+1}: {chunk}")
2. 文ベースチャンキング(Sentence-Based Chunking)
自然言語の文境界で分割し、各チャンクが完全な文を含むことを保証。
メリット:意味的完全性が大幅に向上—「半分の文」は発生しない。
デメリット:長い文はチャンク制限を超える可能性、短い文の組み合わせは一貫性に欠ける場合あり。
import re
from typing import List
def sentence_chunk(
text: str,
max_chunk_size: int = 512,
min_chunk_size: int = 100
) -> List[str]:
"""文ベースチャンキング戦略
Args:
text: チャンキング対象テキスト
max_chunk_size: チャンクの最大文字数
min_chunk_size: チャンクの最小文字数
Returns:
テキストチャンクリスト
"""
sentence_endings = re.compile(r'(?<=[。!?.!?])\s*')
sentences = [s.strip() for s in sentence_endings.split(text) if s.strip()]
chunks = []
current_chunk = ""
for sentence in sentences:
if len(current_chunk) + len(sentence) > max_chunk_size and len(current_chunk) >= min_chunk_size:
chunks.append(current_chunk.strip())
current_chunk = sentence
else:
current_chunk += sentence
if current_chunk.strip():
chunks.append(current_chunk.strip())
return chunks
# 使用例
text = "RAGシステムは人気のAIアーキテクチャです。チャンキング戦略は検索品質に影響します。良いチャンキングで精度が30%以上向上します。意味チャンキングは2026年のトレンドです。"
result = sentence_chunk(text, max_chunk_size=60, min_chunk_size=15)
for i, chunk in enumerate(result):
print(f"Chunk {i+1}: {chunk}")
3. 意味チャンキング(Semantic Chunking)
2026年に最も推奨される戦略。embeddingモデルを使って隣接文の意味的類似度を計算し、類似度が急激に低下する箇所で分割。
メリット:チャンク内の意味的一貫性が極めて高い—最高の検索精度。
デメリット:追加のembeddingモデル呼び出しが必要、計算コストが高い。
from typing import List
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
"""コサイン類似度の計算"""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def semantic_chunk(
text: str,
embed_func,
breakpoint_threshold: float = 0.3,
min_chunk_size: int = 50
) -> List[str]:
"""意味チャンキング戦略
Args:
text: チャンキング対象テキスト
embed_func: テキストを入力としてベクトルを返すembedding関数
breakpoint_threshold: 分割のための類似度低下閾値
min_chunk_size: チャンクの最小文字数
Returns:
テキストチャンクリスト
"""
import re
sentence_endings = re.compile(r'(?<=[。!?.!?])\s*')
sentences = [s.strip() for s in sentence_endings.split(text) if s.strip()]
if len(sentences) <= 1:
return [text] if text.strip() else []
embeddings = [np.array(embed_func(s)) for s in sentences]
breakpoints = []
for i in range(len(embeddings) - 1):
sim = cosine_similarity(embeddings[i], embeddings[i + 1])
if sim < breakpoint_threshold:
breakpoints.append(i + 1)
breakpoints = [0] + breakpoints + [len(sentences)]
chunks = []
for i in range(len(breakpoints) - 1):
start = breakpoints[i]
end = breakpoints[i + 1]
chunk_text = "".join(sentences[start:end])
if len(chunk_text) >= min_chunk_size:
chunks.append(chunk_text)
return chunks
# 使用例(embedding関数が必要)
# from openai import OpenAI
# client = OpenAI()
# def my_embed(text):
# resp = client.embeddings.create(input=text, model="text-embedding-3-small")
# return resp.data[0].embedding
# result = semantic_chunk(long_text, my_embed)
4. 再帰チャンキング(Recursive Chunking)
LangChainのデフォルト戦略。優先順位付きの区切り文字リストを使用—高レベル区切り文字(段落、セクション)を優先的に試行し、失敗したら文、文字にフォールバック。
メリット:意味とサイズ制御のバランス、Markdown/コード文書に極めて有効。
デメリット:区切り文字の選択に経験が必要、極端なケースでは切断の可能性あり。
from typing import List
def recursive_chunk(
text: str,
separators: List[str] = None,
chunk_size: int = 512,
chunk_overlap: int = 50
) -> List[str]:
"""再帰チャンキング戦略
Args:
text: チャンキング対象テキスト
separators: 区切り文字の優先リスト
chunk_size: 目標チャンクサイズ
chunk_overlap: オーバーラップサイズ
Returns:
テキストチャンクリスト
"""
if separators is None:
separators = ["\n\n", "\n", "。", ".", " ", ""]
final_chunks = []
def _recursive_split(current_text: str, current_separators: List[str]):
if not current_text:
return
if len(current_text) <= chunk_size:
final_chunks.append(current_text)
return
sep = current_separators[0] if current_separators else ""
remaining_seps = current_separators[1:] if current_separators else []
if sep == "":
for i in range(0, len(current_text), chunk_size - chunk_overlap):
chunk = current_text[i:i + chunk_size]
if chunk.strip():
final_chunks.append(chunk)
return
splits = current_text.split(sep)
good_splits = []
for split in splits:
if len(split) <= chunk_size:
good_splits.append(split)
else:
if good_splits:
merged = sep.join(good_splits)
if merged.strip():
final_chunks.append(merged)
good_splits = []
_recursive_split(split, remaining_seps)
if good_splits:
merged = sep.join(good_splits)
if len(merged) <= chunk_size:
final_chunks.append(merged)
else:
_recursive_split(merged, remaining_seps)
_recursive_split(text, separators)
return [c.strip() for c in final_chunks if c.strip()]
# 使用例
md_text = """## 概要\nRAGはAIアプリケーションの中核アーキテクチャです。\n\n## チャンキング戦略\nチャンキングは検索品質を決定します。\n良いチャンキングで精度が30%向上。"""
result = recursive_chunk(md_text, chunk_size=50, chunk_overlap=10)
for i, chunk in enumerate(result):
print(f"Chunk {i+1}: {chunk}")
5. 文書ベースチャンキング(Document-Based Chunking)
文書の自然な構造(見出し、段落、リスト項目など)で分割し、文書階層を保持。
メリット:コンテキスト階層を保持、チャンクに見出し情報を付与、メタデータを追加可能。
デメリット:文書形式に依存、非構造化テキストには使用不可。
from typing import List, Dict
import re
def document_chunk(
markdown_text: str,
max_chunk_size: int = 1024,
add_parent_headers: bool = True
) -> List[Dict]:
"""文書ベースチャンキング戦略(Markdown)
Args:
markdown_text: Markdown形式テキスト
max_chunk_size: チャンクの最大文字数
add_parent_headers: 親見出しをコンテキストとして付与するか
Returns:
textとmetadataを含むチャンクリスト
"""
lines = markdown_text.split("\n")
header_stack = []
chunks = []
current_content = []
for line in lines:
header_match = re.match(r'^(#{1,6})\s+(.+)$', line)
if header_match:
if current_content:
content = "\n".join(current_content).strip()
if content:
context = ""
if add_parent_headers and header_stack:
context = " > ".join(header_stack) + "\n"
chunks.append({
"text": context + content,
"metadata": {
"headers": header_stack.copy(),
"level": len(header_stack)
}
})
current_content = []
level = len(header_match.group(1))
title = header_match.group(2)
header_stack = header_stack[:level - 1] + [title]
else:
current_content.append(line)
if current_content:
content = "\n".join(current_content).strip()
if content:
context = ""
if add_parent_headers and header_stack:
context = " > ".join(header_stack) + "\n"
chunks.append({
"text": context + content,
"metadata": {
"headers": header_stack.copy(),
"level": len(header_stack)
}
})
return chunks
# 使用例
doc = """## RAGシステム概要\nRAGは検索拡張生成の略称です。\n\n### コアコンポーネント\n検索器と生成器を含みます。\n\n## チャンキング戦略\nチャンキングはRAGの重要ステップです。"""
result = document_chunk(doc)
for i, chunk in enumerate(result):
print(f"Chunk {i+1}: {chunk['text'][:60]}... | Headers: {chunk['metadata']['headers']}")
6. ハイブリッドチャンキング(Hybrid Chunking)
2026年の本番環境でのベストプラクティス。文書タイプと内容特性に基づいて最適なチャンキング戦略の組み合わせを自動選択。
メリット:あらゆる文書タイプに対応、最も安定した結果。
デメリット:実装が最も複雑、戦略ルーティングロジックが必要。
from typing import List, Dict
import re
def hybrid_chunk(
text: str,
embed_func=None,
chunk_size: int = 512,
chunk_overlap: int = 50
) -> List[Dict]:
"""ハイブリッドチャンキング戦略
Args:
text: チャンキング対象テキスト
embed_func: オプションのembedding関数
chunk_size: 目標チャンクサイズ
chunk_overlap: オーバーラップサイズ
Returns:
チャンク結果リスト
"""
has_headers = bool(re.search(r'^#{1,6}\s+', text, re.MULTILINE))
has_code = bool(re.search(r'```', text))
avg_line_len = len(text) / max(text.count("\n") + 1, 1)
strategy = "recursive"
if has_headers and not has_code:
strategy = "document"
elif embed_func is not None and avg_line_len > 80:
strategy = "semantic"
elif has_code:
strategy = "recursive"
elif avg_line_len < 30:
strategy = "sentence"
chunks_data = []
if strategy == "document":
chunks_data = document_chunk(text, max_chunk_size=chunk_size)
elif strategy == "semantic" and embed_func:
result = semantic_chunk(text, embed_func, min_chunk_size=chunk_size // 2)
chunks_data = [{"text": c, "metadata": {"strategy": "semantic"}} for c in result]
elif strategy == "sentence":
result = sentence_chunk(text, max_chunk_size=chunk_size)
chunks_data = [{"text": c, "metadata": {"strategy": "sentence"}} for c in result]
else:
result = recursive_chunk(text, chunk_size=chunk_size, chunk_overlap=chunk_overlap)
chunks_data = [{"text": c, "metadata": {"strategy": "recursive"}} for c in result]
for chunk in chunks_data:
if "metadata" not in chunk:
chunk["metadata"] = {}
chunk["metadata"]["strategy_used"] = strategy
return chunks_data
# 使用例
# result = hybrid_chunk(your_text, embed_func=my_embed)
# for chunk in result:
# print(f"[{chunk['metadata']['strategy_used']}] {chunk['text'][:50]}...")
評価指標とベンチマーク
チャンキングして評価しないのは暗闇で手探りするようなものです。完全な評価フレームワークを以下に示します:
from typing import List, Dict
import numpy as np
def evaluate_chunks(
chunks: List[str],
embed_func,
questions: List[str],
relevance_labels: List[List[int]]
) -> Dict[str, float]:
"""チャンキング品質の評価
Args:
chunks: チャンキング結果
embed_func: embedding関数
questions: テスト質問リスト
relevance_labels: 各質問に対応する関連チャンクインデックス
Returns:
評価指標辞書
"""
chunk_embeddings = np.array([embed_func(c) for c in chunks])
question_embeddings = np.array([embed_func(q) for q in questions])
avg_internal_sim = 0.0
count = 0
for emb in chunk_embeddings:
sims = np.dot(chunk_embeddings, emb) / (
np.linalg.norm(chunk_embeddings, axis=1) * np.linalg.norm(emb) + 1e-8
)
avg_internal_sim += np.mean(sims)
count += 1
avg_internal_sim /= max(count, 1)
chunk_sizes = [len(c) for c in chunks]
size_cv = np.std(chunk_sizes) / (np.mean(chunk_sizes) + 1e-8)
return {
"num_chunks": len(chunks),
"avg_chunk_size": np.mean(chunk_sizes),
"size_coefficient_of_variation": size_cv,
"avg_internal_similarity": avg_internal_sim,
"size_std": np.std(chunk_sizes),
"min_chunk_size": min(chunk_sizes),
"max_chunk_size": max(chunk_sizes),
}
# 使用例
# metrics = evaluate_chunks(chunks, my_embed, questions, labels)
# for k, v in metrics.items():
# print(f"{k}: {v:.4f}")
ベンチマーク結果(MS MARCOデータセット基準)
| 戦略 | 平均チャンクサイズ | サイズ変動係数 | Recall@5 | MRR | エンドツーエンドEM |
|---|---|---|---|---|---|
| 固定サイズ | 512 | 0.02 | 0.62 | 0.48 | 0.35 |
| 文ベース | 380 | 0.45 | 0.71 | 0.56 | 0.42 |
| 意味チャンキング | 420 | 0.38 | 0.83 | 0.69 | 0.56 |
| 再帰チャンキング | 460 | 0.22 | 0.76 | 0.61 | 0.47 |
| 文書ベース | 550 | 0.55 | 0.78 | 0.64 | 0.50 |
| ハイブリッド | 440 | 0.30 | 0.85 | 0.72 | 0.59 |
5つのよくある落とし穴
-
チャンクサイズの一律適用:異なる文書タイプ(コードvs散文)に同じchunk_sizeを使うと、結果は必然的に悪くなります。コード文書には小さいチャンク(256-384)、技術記事には中程度(384-512)、法務文書には大きいチャンク(512-1024)が適しています。
-
メタデータの無視:チャンクテキストだけを保存し、メタデータ(ソース、見出し、ページ番号)を保存しないと、チャンク検索後に出所をたどれず、フィルタ検索もできません。
-
不適切なオーバーラップ設定:オーバーラップが大きすぎると冗長な検索結果に、小さすぎると境界情報が失われます。目安:overlap = chunk_size × 10%-15%。
-
前処理の欠如:チャンキング前にテキストをクリーンアップしない(特殊文字の削除、空行の結合、エンコーディングの修正)と、汚いデータからゴミチャンクが生成されます。
-
オフライン指標のみの確認:オフラインのRecallが高くてもオンラインのパフォーマンスが良いとは限りません。A/Bテストを実施し、実際のユーザーのクリック率と満足度を測定する必要があります。
10のエラートラブルシューティング
| # | 症状 | 考えられる原因 | 解決策 |
|---|---|---|---|
| 1 | チャンク数が予想を大幅に超過 | chunk_sizeが小さすぎる | chunk_sizeを384-512に増加 |
| 2 | 検索結果が意味的に無関係 | チャンクが意味的境界を跨いでいる | 意味チャンキングまたは再帰チャンキングに切り替え |
| 3 | 長文書のチャンキング後にコンテキスト喪失 | 親見出し情報がない | add_parent_headersを有効化またはコンテキストウィンドウを追加 |
| 4 | コードブロックが切断される | コードブロック内で改行区切りを使用 | 再帰チャンキングで```を優先区切りに |
| 5 | リスト項目が分散される | リストの途中で分割 | 文書ベースチャンキングまたはリスト項目を結合 |
| 6 | Embeddingの次元不一致 | チャンクが空または空白のみ | チャンキング後に空チャンクをフィルタ |
| 7 | チャンキングに時間がかかりすぎ | 意味チャンキングが文ごとにembedding | バッチembedding + キャッシュ |
| 8 | メモリ不足 | 超大文書を一括処理 | ストリーミングチャンキング、セグメントごとに処理 |
| 9 | 日本語/英語混在テキストで結果が悪い | 区切り文字が日本語句読点をカバーしていない | 日本語句読点を区切り文字リストに追加 |
| 10 | 類似チャンクが重複検索される | オーバーラップによる高重複チャンク | 重複排除またはオーバーラップ比率を低下 |
高度な最適化ティップス
コンテキストエンリッチメント(Context Enrichment)
各チャンクの前後に隣接テキストをコンテキストウィンドウとして付与:
def context_enrichment(
chunks: List[str],
context_window: int = 100
) -> List[str]:
"""チャンクにコンテキストウィンドウを追加"""
enriched = []
for i, chunk in enumerate(chunks):
prefix = chunks[i-1][-context_window:] if i > 0 else ""
suffix = chunks[i+1][:context_window] if i < len(chunks)-1 else ""
enriched.append(f"{prefix}[CHUNK]{chunk}[CHUNK]{suffix}")
return enriched
適応型チャンクサイズ
テキストの情報密度に基づいてチャンクサイズを動的に調整—コードとテーブルは情報密度が高い(小チャンク)、叙述テキストは密度が低い(大チャンク):
def adaptive_chunk_size(text: str, base_size: int = 512) -> int:
"""テキスト特性に基づきチャンクサイズを適応的に調整"""
code_ratio = len(re.findall(r'[{}()\[\];]', text)) / max(len(text), 1)
table_ratio = text.count('|') / max(len(text), 1)
if code_ratio > 0.05 or table_ratio > 0.03:
return int(base_size * 0.6)
elif len(text.split('\n')) / max(len(text), 1) > 0.02:
return int(base_size * 0.8)
else:
return base_size
多粒度インデックス
同じ文書を異なるchunk_sizeで多レベルインデックスを構築し、検索時に粗から細へ:
def multi_granularity_index(
text: str,
sizes: List[int] = [256, 512, 1024]
) -> Dict[int, List[str]]:
"""多粒度インデックスの構築"""
return {
size: recursive_chunk(text, chunk_size=size, chunk_overlap=size//10)
for size in sizes
}
おすすめツール
RAGチャンキングの処理において、以下のオンラインツールが効率向上に役立ちます:
- JSONフォーマッター:チャンキングで生成されるメタデータは通常JSON形式です。このツールで素早くフォーマットと検証を行い、metadata構造の正確性を確保できます。
- Base64エンコーダー:チャンクテキストをBase64でエンコードして保存または転送。特殊文字を含むチャンクデータの処理に特に有用です。
- ハッシュ計算ツール:各チャンクに一意のハッシュ値を計算し、重複排除とバージョン管理に活用。同一内容の重複インデックスを防止します。
まとめ:2026年におけるRAGチャンキング戦略の選択は、もはや「固定サイズで十分」という時代ではありません。意味チャンキングとハイブリッドチャンキングが主流となり、核心原則は各チャンクを意味的に自己完結させ、コンテキストで出所を追跡可能にし、サイズを適応可能にすることです。正しい戦略を選べば、RAGシステムの検索精度は少なくとも30%向上します。
ブラウザローカルツールを無料で試す →