chunking-strategies について
RAG の検索パイプラインを構築する開発者向けに、テキスト分割方式を比較し、Python の実装例、サイズの指針、評価方法を示すスキルです。
主な機能
- 01オーバーラップ付き固定サイズ分割
- 02セマンティック分割と親子分割
- 03文書コンテキストを保つレイトチャンク化
- 04Markdown・コード対応の分割
- 05分割サイズと評価の指針
活用シーン
- 01技術文書を検索用に分割
- 02RAGで関連する文章をまとめて保持
- 03内容に応じた分割サイズの選択
- 04テストクエリによる分割結果の評価
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ latestaiagents/ agent-skills --skill chunking-strategiesインストール後、Claude Code で/chunking-strategiesを入力して読み込まれたか確認します。
詳細
RAG のチャンク分割戦略
最適なチャンク分割は、RAG の性能を良好な水準から優れた水準へと引き上げます。
チャンク分割が重要な理由
不適切なチャンク分割は、次の問題を引き起こします。
- コンテキストの断片化(回答が複数のチャンクに分割される)
- 関連性の低い検索結果(チャンクにノイズが多すぎる)
- 関係性の喪失(親子関係にあるコンテンツが分離される)
- トークンの無駄(チャンクが大きすぎる、または小さすぎる)
チャンク分割方法の比較
| 方法 | 最適な用途 | チャンクの品質 | 実装 |
|---|---|---|---|
| 固定サイズ | シンプルなドキュメント、均一なコンテンツ | 中 | 簡単 |
| 再帰的 | 構造化されたドキュメント、マークダウン | 高 | 中程度 |
| セマンティック | 複雑なドキュメント、多様なコンテンツ | 最高 | 複雑 |
| 親子 | 階層構造のドキュメント | 高 | 中程度 |
| レイトチャンク分割 | コンテキストの保持 | 最高 | 複雑 |
パターン 1:オーバーラップを使った固定サイズ分割
基本的なアプローチです。シンプルですが効果的です。
from langchain.text_splitter import RecursiveCharacterTextSplitter
def create_fixed_chunks(
text: str,
chunk_size: int = 512,
chunk_overlap: int = 50
) -> list[str]:
"""
Split text into fixed-size chunks with overlap.
Guidelines:
- chunk_size: 256-1024 tokens (512 is solid default)
- overlap: 10-20% of chunk_size
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
return splitter.split_text(text)
パターン 2:セマンティック分割
任意の境界ではなく、意味に基づいてグループ化します。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
def create_semantic_chunks(text: str) -> list[str]:
"""
Split text based on semantic similarity between sentences.
Keeps related content together.
"""
embeddings = OpenAIEmbeddings()
splitter = SemanticChunker(
embeddings=embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95 # Higher = fewer, larger chunks
)
return splitter.split_text(text)
カスタムセマンティック分割
import numpy as np
from sentence_transformers import SentenceTransformer
def semantic_chunk(
sentences: list[str],
model_name: str = "all-MiniLM-L6-v2",
threshold: float = 0.5
) -> list[list[str]]:
"""
Group sentences by semantic similarity.
"""
model = SentenceTransformer(model_name)
embeddings = model.encode(sentences)
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
# Cosine similarity between consecutive sentences
sim = np.dot(embeddings[i-1], embeddings[i]) / (
np.linalg.norm(embeddings[i-1]) * np.linalg.norm(embeddings[i])
)
if sim >= threshold:
current_chunk.append(sentences[i])
else:
chunks.append(current_chunk)
current_chunk = [sentences[i]]
chunks.append(current_chunk)
return chunks
パターン 3:親子チャンク分割
小さい単位で検索し、コンテキストとともに返します。
from llama_index.core.node_parser import (
HierarchicalNodeParser,
SentenceSplitter,
get_leaf_nodes
)
from llama_index.core import StorageContext, VectorStoreIndex
from llama_index.core.retrievers import AutoMergingRetriever
def create_hierarchical_index(documents):
"""
Create parent-child chunk hierarchy.
Small chunks for retrieval, auto-merge to parents for context.
"""
# Define chunk sizes for each level
node_parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128] # Parent → Child → Leaf
)
nodes = node_parser.get_nodes_from_documents(documents)
leaf_nodes = get_leaf_nodes(nodes)
# Store all nodes
storage_context = StorageContext.from_defaults()
storage_context.docstore.add_documents(nodes)
# Index only leaf nodes
index = VectorStoreIndex(
leaf_nodes,
storage_context=storage_context
)
# Retriever auto-merges to parents when siblings retrieved
retriever = AutoMergingRetriever(
index.as_retriever(similarity_top_k=12),
storage_context=storage_context,
simple_ratio_thresh=0.3 # Merge if 30%+ siblings retrieved
)
return retriever
パターン 4:レイトチャンク分割(2026 年の手法)
まずドキュメント全体を埋め込み、その後チャンクに分割します。これにより、グローバルコンテキストが保持されます。
def late_chunking(
document: str,
model,
chunk_size: int = 512
) -> list[dict]:
"""
Late chunking: embed document, then split embeddings.
Preserves document-level context in chunk embeddings.
Reference: Jina AI Late Chunking (2024)
"""
# 1. Get token-level embeddings for full document
tokens = model.tokenize(document)
token_embeddings = model.encode_tokens(tokens)
# 2. Split into chunks
chunks = []
for i in range(0, len(tokens), chunk_size):
chunk_tokens = tokens[i:i + chunk_size]
chunk_embeddings = token_embeddings[i:i + chunk_size]
# 3. Pool chunk embeddings (mean pooling)
chunk_vector = np.mean(chunk_embeddings, axis=0)
chunks.append({
"text": model.decode(chunk_tokens),
"embedding": chunk_vector
})
return chunks
パターン 5:マークダウン/コードを考慮したチャンク分割
from langchain.text_splitter import (
MarkdownHeaderTextSplitter,
Language,
RecursiveCharacterTextSplitter
)
def chunk_markdown(text: str) -> list[dict]:
"""Split markdown by headers, preserving structure."""
headers_to_split_on = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on
)
return splitter.split_text(text)
def chunk_code(code: str, language: str = "python") -> list[str]:
"""Split code respecting language syntax."""
lang_map = {
"python": Language.PYTHON,
"javascript": Language.JS,
"typescript": Language.TS,
}
splitter = RecursiveCharacterTextSplitter.from_language(
language=lang_map.get(language, Language.PYTHON),
chunk_size=1000,
chunk_overlap=100
)
return splitter.split_text(code)
チャンクサイズのガイドライン
| コンテンツの種類 | 推奨サイズ | オーバーラップ |
|---|---|---|
| Q&A/FAQ | 256-512 | 25-50 |
| 技術ドキュメント | 512-1024 | 50-100 |
| 法的文書 | 1024-2048 | 100-200 |
| コード | 500-1000 | 50-100 |
| 会話 | 256-512 | 50-100 |
評価:チャンク分割の品質を確認する方法
def evaluate_chunking(chunks: list[str], test_queries: list[dict]):
"""
Evaluate chunk quality with test queries.
test_queries format:
[{"query": "What is X?", "expected_chunk_contains": "X is..."}]
"""
results = {
"avg_chunk_size": np.mean([len(c) for c in chunks]),
"chunk_size_std": np.std([len(c) for c in chunks]),
"total_chunks": len(chunks),
"retrieval_hits": 0
}
for tq in test_queries:
# Check if expected content is in a single chunk
for chunk in chunks:
if tq["expected_chunk_contains"] in chunk:
results["retrieval_hits"] += 1
break
results["hit_rate"] = results["retrieval_hits"] / len(test_queries)
return results
ベストプラクティス
- チャンクサイズをクエリの長さに合わせる - チャンクのサイズは、想定されるクエリと同程度にします
- 意味のまとまりを保つ - 文の途中や段落の途中で分割しないでください
- メタデータを含める - 各チャンクにソース、ページ、セクションの情報を追加します
- 実際のクエリでテストする - 実際のユースケースで評価します
- 検索モデルを考慮する - 特定の埋め込みモデルは、特定のチャンクサイズを好む場合があります
クイック判断ツリー
What type of content?
├─ Structured (headers, sections)
│ └─ Use: Markdown/recursive splitter + hierarchy
├─ Unstructured (prose, articles)
│ └─ Use: Semantic chunking
├─ Code
│ └─ Use: Language-aware splitter
└─ Mixed
└─ Use: Parent-child with semantic leaves
コメント
Markdown 対応