- AI ⚙️
RAGのチャンク分割の決め方|サイズ・overlapを247記事で実測
結論
チャンクの大きさは「件数(k)」ではなく「LLM に渡せる文字数(文脈の予算)」を揃えて比べると、答えが変わります。RJC の技術記事247本・800問で測ると、2,000字の予算では BM25 は200〜400字の小さなチャンクが、LSA(密ベクトルの代わり)は400字以上や見出し単位が上でした。検索方式と文書の構造で最適値が動くので、自分の評価セットで1度測って決めるのが近道です。
この記事でわかること
- 固定長・文・再帰・見出し・構造・親子の17通りを同じ評価で比べた表
- overlap の効き方と、指定しても重ならない LangChain の挙動
- 表やコードを切る割合と、見出しを付け足したときの差
この記事で実際に動かしたもの:RJC の技術記事247本(約639万字)を17通りに分割し、BM25 と LSA の2つの検索で、FAQ から作った800問の評価セットを検索して数えました。LLM 本体と学習済みの埋め込みモデルは使っていません。
flowchart TD
A[記事247本のHTML] --> B[見出し・段落・表・preに分ける]
B --> C[17通りに分割]
C --> D[BM25の索引]
C --> E[LSAの密ベクトル]
Q[FAQの質問800問] --> D
Q --> E
D --> F[上位5件と2000字の予算で数える]
E --> F
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| チャンクサイズの決め方 | 渡せる文字数を先に決め、その予算で Recall を比べる | チャンクサイズの決め方 |
| チャンクサイズの目安 | BM25 は200〜400字、LSA は400字以上が上だった(この文書で) | 検索方式で最適値が変わる |
| chunk overlap は何%か | 10%は差が誤差の範囲、50%は下がった。0〜10%から始める | chunk overlap は効くのか |
| RAG チャンク分割の方法 | 固定長・文・再帰・見出し・構造・親子・意味(埋め込み) | チャンク分割とは |
| 表やコードの扱い | 固定長400字は表と pre の71%を途中で切った。構造に沿うと7% | 表やコードを途中で切ると |
| 見出しを付けるべきか | 付けて損は小さい。BM25 で +2.0ポイント、LSA で記事の当たりが +5ポイント | 見出しを付け足す効果 |
| 親子チャンクは効くか | この文書と BM25 では、親の大きさで直接切るほうが上だった | 親子チャンク |
| LangChain の日本語の区切り | 既定は空白で切るので、「。」「、」を separators に足す | RecursiveCharacterTextSplitter |
確認環境:Ubuntu 24.04.5 LTS(2 CPU・メモリ 8GB を10件の作業で共有)の上で、Python 3.13.18 の仮想環境に numpy 2.5.4・scipy 1.18.1・scikit-learn 1.9.1・beautifulsoup4 4.15.0・lxml 6.1.3・langchain-text-splitters 1.1.3(langchain-core 1.7.4)を PyPI から入れて動かしました。numpy は OPENBLAS_NUM_THREADS=1、1回の計算は60秒以内、1プロセスのメモリは最大 963MB でした。コーパスは RJC の技術記事247本の本文 HTML(読み取り専用)、検索は自作の BM25(文字2-gram)と、TF-IDF+SVD(128次元)の LSA です。学習済みの埋め込みモデルと LLM の API には接続していません。LSA は埋め込みの代わりで、実際の埋め込みモデルでは数字が変わります。意味(セマンティック)チャンキングと LLM によるチャンキングは動かさず、Chroma の技術レポートと Microsoft・LangChain・LlamaIndex の公式ドキュメントで確かめた範囲で書きました。図は mermaid-cli 11.16.0 で描画を確かめています(確認日 2026年11月4日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「RAGのチャンク分割の決め方|サイズ・overlapを247記事で実測」
RAGのチャンク分割とは(何を決めるのか)
この節では、チャンク分割で決めることと、よく使われる分割方法の違いが分かります。
チャンク分割(チャンキング)は、検索の索引に入れる単位を決める作業です。RAG は質問に近いチャンクを探して LLM に渡すので、チャンクの切り方が「探せるか」と「渡した中に答えが入っているか」の両方を決めます。文書を丸ごと1件にすると LLM の入力に入りきらず、細かくしすぎると答えが複数のチャンクに散ります。
決めることは、主に次の4つです。
1大きさ(chunk_size)
1チャンクの最大の文字数かトークン数。小さいと探す精度が上がり、大きいと文脈が残る。
2重なり(chunk_overlap)
前のチャンクの末尾を次のチャンクの先頭に重ねる量。境界で切れた文を救うが、件数と重複が増える。
3切る場所(区切り・構造)
文字数だけで切るか、段落・文・見出し・表の境目で切るか。日本語は空白で単語が分かれない。
4何を足して索引するか
記事の題名・見出しを付け足すか、小さく探して大きく渡す(親子)か。
分割の方法は、上位の解説や公式ドキュメントでおおむね次のように分けられています。
| 方法 | 切り方 | この記事で |
|---|---|---|
| 固定長 | 文字数(トークン数)で機械的に切る。overlap を付けることが多い | 200〜1600字・overlap 0〜50% |
| 文の区切り | 句点で文に分け、上限まで詰める | 400・800字 |
| 再帰的分割 | 段落→行→文→読点の順に、上限に収まるまで区切りを下げる | LangChain で200〜1600字 |
| 見出し単位・構造に沿った分割 | h2/h3 の節、表・コードを1つの塊として扱う | 自作と LangChain の HTML 用 |
| 親子(階層的) | 小さいチャンクで探し、それを含む大きいチャンクを渡す | 5通り |
| 意味(セマンティック) | 隣り合う文の埋め込みが離れたところで切る | 動かしていない(公式・レポートの範囲) |
| LLM・エージェント | LLM に切る場所を決めさせる | 動かしていない |
Microsoft の Azure Architecture Center は、チャンクの方法を「ソリューション設計全体の中で半恒久的な選択」とし、変えると後ろの処理すべてに響くため、本番の前に実験で比べることを勧めています。RAG 全体の作り方は、関連記事「RAGの作り方|Pythonで動かす最小構成と精度の上げ方」で扱っています。
実験の作り方:コーパス・評価セット・検索(BM25とLSA)
この節では、どの文書を、どの質問で、どう数えたのかが分かります。数字を読む前の前提です。
コーパス:HTML をブロックに分ける
各記事の HTML を BeautifulSoup で読み、上から順に「h2・h3・段落・リスト・表・pre・部品」のブロックにしました。表は1行を「セル | セル」の1行に、pre は中身をそのまま、Mermaid の図と全記事に共通する出典の枠は外しています。ブロックごとに本文の中の開始位置・終了位置と、その時点の h2・h3 を記録しました。構造に沿った分割と、表を切ったかどうかの判定は、この位置を使います。
| ブロック | 件数 | 扱い |
|---|---|---|
| 段落 | 15,809 | 空白を詰めて1行 |
| pre(コード・出力) | 4,794 | 改行を残す |
| h2・h3 | 3,917・2,408 | 見出しの文字を残し、位置を記録 |
| 表 | 2,861 | 1行を「 | 」でつなぐ |
| FAQ の答え | 2,588 | 質問の文は外し、答えだけ残す |
| リスト・部品 | 1,635・1,838 | 項目ごとに改行 |
評価セット:FAQ の質問をクエリ、答えを正解の箇所にする
RJC の記事には「Q. …」「A. …」の形のよくある質問が8〜12問ずつあります。質問の文を本文から外してクエリにし、答えの文(40字以上)を「正解の箇所」にしました。2,439問から乱数の種0で800問を選んでいます。人手でも LLM でも作っていないので、作り方は再現できますが、次の弱点があります。
- 同じ内容が本文の別の節やまとめにも書かれていることが多く、そちらを取っても「正解の箇所」には数えません。Recall の絶対値は低めに出ます。方法どうしの比較に使います。
- 質問は記事を書いた側の言い回しです。利用者の実際の質問とは語の重なり方が違います。
数え方:答えの文が取り出したチャンクにどれだけ入ったか
正解の箇所を空白を除いて8文字ずつの断片にし、取り出したチャンク(同じ記事のものだけ)に含まれた断片の割合を「網羅率」としました。Recall@5 は上位5件での網羅率の平均、完全率は95%以上入った問の割合、予算 Recall は上から2,000字に収まるまで取ったときの網羅率です。文字の断片で照合するので、見出しを付け足したチャンクや LangChain が作った文字列でも同じ物差しで数えられます。Chroma の技術レポートも、文書単位ではなくトークン単位の再現率・適合率・IoU で分割方法を比べています。指標そのものの詳しい解説と MRR・nDCG は、別記事「RAGの評価|検索をRecall@k・MRR・nDCGで測り、件数のぶれを実測」(公開準備中)で扱います。
WS = re.compile(r'\s+')
def shingles(t, n=8):
t = WS.sub('', t)
return {t[i:i + n] for i in range(max(1, len(t) - n + 1))}
def coverage(gold_text, retrieved, gold_doc):
gold = shingles(gold_text)
got = set()
for doc, text in retrieved: # 上位から取り出したチャンク
if doc == gold_doc:
got |= shingles(text)
return len(gold & got) / len(gold) # 0〜1。0.95以上を「完全」と数えた
検索:BM25(文字2-gram)と LSA
BM25 は scikit-learn の CountVectorizer で文字2-gram を数え、k1=1.2・b=0.75 の重みを疎行列で持つ自作です。LSA は文字2-gram の TF-IDF(6万語まで)を、6,000件の標本で学習した128次元の SVD で縮めて正規化し、コサイン類似度で並べます。LSA は学習済みの埋め込みモデルの代わりで、意味の近さを捉える力はずっと弱いため、「密ベクトルの検索では傾向がこう変わりうる」という目安として読んでください。BM25 とベクトル検索を組み合わせる方法は、別記事「ハイブリッド検索(BM25+ベクトル検索)とRRFでRAGの検索精度を上げる」(公開準備中)で扱います。
チャンクサイズの決め方:k件固定と文脈の予算で答えが逆になる
この節では、チャンクサイズを比べるときに何を揃えるべきかが、固定長200〜1600字の実測で分かります。
固定長で切って、BM25 で上位5件を取りました。
| 固定長 | チャンク数 | Recall@5 | 渡す文字数(5件) | 予算2,000字の Recall |
|---|---|---|---|---|
| 200字 | 32,075 | 26.3% | 1,000字 | 37.3% |
| 400字 | 16,104 | 35.3% | 1,984字 | 35.3% |
| 800字 | 8,109 | 44.1% | 3,971字 | 25.7% |
| 1600字 | 4,117 | 53.4% | 7,924字 | 18.0% |
上位5件で比べると1600字がいちばん上ですが、LLM に渡す文字数は200字の8倍です。同じ2,000字までしか渡さないと決めて比べると、順番が逆になり、200字が37.3%、1600字は18.0%でした(1600字は1件で予算を超えるので、1件だけ渡した値です)。
チャンクサイズの比べ方
見分ける問い:「比べている2つは、同じ量の文字を LLM に渡しているか」
LlamaIndex の公式ドキュメントも、チャンクサイズを半分にした例で similarity_top_k を2から4に倍にしており、大きさと件数は組で決めるものとして書かれています。予算の大きさは、モデルの入力の上限・料金・応答時間・プロンプトの他の部分から逆算します。
上位の記事にある「目安の数字」との関係
解説記事では「FAQ は200〜400文字、一般文書は500〜800文字、技術文書は800〜1500文字」「200〜1000トークン」、Azure AI Search の公式ドキュメントでは「まず512トークン(約2,000文字)・25%の重なりから」という出発点が示されています。どれも出発点で、この記事の数字もこの文書・この検索・この予算のときの値です。文字とトークンの換算は言語とトークナイザで変わり、Azure の「512トークン≒2,000文字」は英語の目安です。この記事は大きさを文字数で数えています。
検索方式でチャンクサイズの最適値が変わる(BM25とLSA)
この節では、同じ分割でも BM25 と密ベクトル(LSA)で向く大きさが違うことが、予算2,000字の比較で分かります。
| 分割 | 平均の長さ | BM25 予算 Recall | LSA 予算 Recall | LSA 記事の当たり@5 |
|---|---|---|---|---|
| 再帰200字 | 135字 | 41.4% | 24.6% | 60.4% |
| 構造400字+見出し | 255字 | 40.0% | 29.5% | 77.4% |
| 固定200字 | 199字 | 37.3% | 25.7% | 70.5% |
| 固定400字 | 397字 | 35.3% | 33.4% | 75.2% |
| 固定400字+見出し | 397字 | 36.8% | 34.7% | 80.5% |
| 固定800字・overlap 80 | 790字 | 25.2% | 32.9% | 79.5% |
| 見出し(h2)単位 | 1,533字 | 19.9% | 35.3% | 81.8% |
BM25 は小さいチャンクほど予算 Recall が高く、LSA は小さいチャンクで大きく崩れました。LSA で再帰200字を使うと、正しい記事が上位5件に1つも入らない問が約4割ありました。短い文字列は TF-IDF の語が少なく、128次元に縮めたベクトルが不安定になるためと考えられます。逆に LSA は h2 の節を丸ごと1件にしても予算 Recall が下がりませんでした。
学習済みの埋め込みモデルは LSA より短い文でも意味を捉えますが、モデルごとに得意な長さと入力の上限があります。Chroma の技術レポート(2024年7月)は、OpenAI の text-embedding-3-large で、再帰的分割の200トークン・overlap なしが各指標で安定して高く、800トークン・overlap 400(当時の OpenAI Assistants の既定)は再現率はほぼ同じでも適合率と IoU が最も低かった、と報告しています。また all-MiniLM-L6-v2 では、小さいチャンクで overlap を付けると再現率が上がった例も載っています。検索方式(BM25・埋め込みのモデル)を替えたら、チャンクの大きさも測り直すのが安全です。埋め込みとコサイン類似度の仕組みは、関連記事「エンベディングとは?ベクトル検索とコサイン類似度を計算例で解説」で扱っています。
差はどこからが本物か
800問の Recall の標準誤差は、どの方法でもおよそ1.4ポイントでした。同じ800問で2つの方法を比べた差を、ブートストラップ(2,000回)で95%区間にしています。
| 比べたもの(BM25) | 指標 | 差 | 95%区間 |
|---|---|---|---|
| 固定400 → 再帰200 | 予算 Recall | +6.0 | +3.3〜+8.8 |
| 固定400 → 構造400 | 予算 Recall | +2.7 | +0.3〜+5.0 |
| 固定400 → overlap 40 | Recall@5 | +0.7 | −1.4〜+2.9 |
| 固定400 → overlap 200 | Recall@5 | −3.1 | −5.1〜−1.0 |
| 構造400 → 見出し付き | 予算 Recall | +2.0 | +0.3〜+3.7 |
区間が0をまたぐ差(overlap 40)は、この評価セットでは「差があるとは言えない」と読みます。2ポイント前後の差を見分けたいなら、評価セットは数百問より少なくしないほうがよい、ということでもあります。
chunk overlapは効くのか(10%・20%・50%の実測)
この節では、overlap の量でチャンク数・Recall・表の切断がどう変わるかが分かります。
| 固定400字の overlap | チャンク数 | BM25 Recall@5 | 完全率@5 | LSA Recall@5 |
|---|---|---|---|---|
| 0 | 16,104 | 35.3% | 23.0% | 33.1% |
| 40字(10%) | 17,850 | 36.0% | 25.9% | 30.2% |
| 80字(20%) | 20,031 | 35.6% | 24.7% | 32.2% |
| 200字(50%) | 31,828 | 32.3% | 21.1% | 31.5% |
10〜20%の overlap は、BM25 の Recall を0〜1ポイント動かしただけで、誤差の範囲でした。完全率(答えがまるごと入った問)は 23.0%→25.9% と少し上がっており、境界で切れた答えを救う働きは見えます。一方、50%にするとチャンク数がほぼ倍になり、上位5件に同じ文の重複が入るぶん Recall が3.1ポイント下がりました。800字では overlap 0・80・160字の BM25 Recall@5 が 44.1%・43.2%・41.6%、LSA は 49.6%・52.8%・50.0%で、LSA の80字だけが上でした。
overlap で得るもの
境界で切れた文・答えがどちらかのチャンクに丸ごと入る
固定長のように文の途中で切る方法ほど効く
密ベクトルで、チャンクの冒頭の文脈が残る
overlap で払うもの
チャンク数(埋め込みの計算・保存)が 1/(1−率) 倍に増える
上位 k 件に同じ文が重複して入り、文脈の予算を食う
境界が増え、表や pre を切る割合も上がる(71%→90%)
Chroma の技術レポートも、overlap を外すと重複が減るぶん IoU が上がると書いています。段落や文の境目で切る方法なら、まず overlap 0〜10%で始め、完全率を見て増やすのが、件数を増やさずに済む順番です。
LangChainのRecursiveCharacterTextSplitterを日本語で使う
この節では、LangChain の再帰的分割を日本語に使うときの区切り文字と、chunk_overlap の実際の挙動が分かります。
langchain-text-splitters 1.1.3 の RecursiveCharacterTextSplitter の既定の区切りは ["\n\n", "\n", " ", ""] です。公式ドキュメントは、中国語・日本語・タイ語のように単語の境目に空白が無い言語では語の途中で切れることがあるとして、句点・読点・全角のピリオド・ゼロ幅スペースなどを区切りに足す例を載せています。Prophet の記事の167字の段落(途中に空白が1つ)を chunk_size=80 で切ると、次のようになりました(各片の末尾12字)。
-- 既定: 3片 [67, 79, 20]
[性が吸い込んでいました。]
[文字化け対策|OS別・C]
[2対応」で扱っています。]
-- 日本語: 3片 [67, 24, 75]
[性が吸い込んでいました。]
[この山は小さくなります。]
[2対応」で扱っています。]
既定の区切りでは、空白の次に「1文字ずつ」まで下がり、記事名の途中(「OS別・C」)で切れました。「。」「、」を足すと、どの片も文の終わりで切れています。使った設定は次のとおりです。
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=40,
separators=["\n\n", "\n", "。", "、", ""],
keep_separator="end", # 「。」を前の片の末尾に残す(既定 True は次の片の先頭に付く)
add_start_index=True, # metadata["start_index"] に元の位置が入る
)
docs = splitter.create_documents([text])
chunk_overlap を指定しても、ほとんど重ならない
同じ記事を chunk_size=400・chunk_overlap=40 で切り、隣り合うチャンクの重なりを start_index から数えました。
== overlap 指定40 → 実際の重なり 件数 84 0文字 71 平均 3.6 最大 37
84か所の境目のうち71か所は、重なりが0文字でした。再帰的分割は、区切りで分けた片を上限まで詰め、次のチャンクの先頭に「前の片のうち chunk_overlap に収まるもの」だけを持ち越します。段落が40字より長いと持ち越せる片が無く、重なりは0になります。chunk_overlap は「最大でこれだけ重ねる」という目安で、必ず重なる量ではありません。全記事で数えると、再帰400字・overlap 40のチャンク数は22,540件で、固定400字・overlap 40の17,850件より多く、平均の長さは284字でした。段落の境目を優先したぶん、上限より短いチャンクが増えています。
chunk_overlap を chunk_size より大きくすると、作る時点で次のエラーになります。
ValueError: Got a larger chunk overlap (200) than chunk size (100), should be smaller.
なお、基底クラス TextSplitter の既定は chunk_size=4000・chunk_overlap=200 で、大きさは len(文字数)で数えます。トークン数で揃えたいときは length_function にトークナイザの数え方を渡すか、from_tiktoken_encoder などを使います。LangChain の基本は、関連記事「LangChain入門|PythonでLLMアプリを作る基本とコピペ集」で扱っています。
見出し単位・HTML構造に沿った分割(h2/h3・表・pre)
この節では、見出しで切る方法と、見出し・表・pre を壊さずに詰める方法の大きさと結果が分かります。
見出しで切るだけだと、大きさがばらつく
h2 が変わるところだけで切ると4,164件、h3 でも切ると6,572件になりました。
| 分割 | 件数 | 平均 | 95%点 | 最大 |
|---|---|---|---|---|
| h2 単位 | 4,164 | 1,533字 | 3,144字 | 10,824字 |
| h2・h3 単位 | 6,572 | 970字 | 2,096字 | 10,824字 |
| 構造に沿って800字まで | 12,758 | 499字 | 782字 | 800字 |
最大の節は10,824字で、上限の無い見出し単位は、埋め込みモデルの入力の上限を超えるチャンクを作ります。多くの埋め込みモデルは上限を超えた部分を切り捨てるか、エラーにします。見出し単位は「上限つき」で使うのが前提です。
構造に沿った分割(自作)
自作の分割は、h2/h3 で必ず切り、その中でブロック(段落・リスト・表・pre)を切らずに上限まで詰めます。上限より大きいブロックだけ、表は行の境目、pre は改行、それ以外は再帰的分割で切り、表の2片目以降には見出し行を先頭に付けて索引しました。要点は次の形です。
def structure(doc, size):
out, cur = [], None
for b in doc["blocks"]: # 上から順のブロック(位置つき)
if b["kind"] in ("h2", "h3"): # 見出しでは必ず切る
if cur: out.append(cur)
cur = [b["s"], b["e"]]
continue
if b["e"] - b["s"] > size: # 大きすぎるブロックだけ中で切る
if cur: out.append(cur); cur = None
out += split_big_block(doc, b, size) # 表は行、pre は改行で
continue
if cur and b["e"] - cur[0] <= size: # 入るなら同じチャンクに詰める
cur[1] = b["e"]
else:
if cur: out.append(cur)
cur = [b["s"], b["e"]]
if cur: out.append(cur)
return out
BM25 の予算 Recall は、構造400字が38.0%(固定400字は35.3%)、見出しを付けると40.0%でした。上限800字では、表と pre を途中で切る割合が7%まで下がりました(次の節)。
LangChain の HTML 用の分割器を動かした結果
同じ HTML(FAQ の質問文・出典の枠・Mermaid を外したもの)を、LangChain の HTMLHeaderTextSplitter(h2・h3)と HTMLSemanticPreservingSplitter(表・リスト・pre を保つ。1.1.3 では beta の警告が出ます)に通しました。
| 分割器 | 件数・平均 | 最大 | BM25 Recall@5 | LSA Recall@5 |
|---|---|---|---|---|
| HTMLHeaderTextSplitter | 12,897件・509字 | 10,830字 | 43.6% | 8.6% |
| SemanticPreserving 800 | 7,496件・818字 | 10,734字 | 42.7% | 42.2% |
| SemanticPreserving 1600 | 6,589件・931字 | 10,734字 | 47.7% | 45.4% |
手元で分かったことは3つです。
1見出しだけのチャンクが多い
HTMLHeaderTextSplitter の出力(先頭60記事・2,978件)のうち1,054件が30字未満で、ほとんどが見出しの文字だけだった。LSA ではこれが上位を占め、Recall@5 が8.6%まで落ちた。
2文中のコードが文の外へ移る
表のセル「python -m pip install prophet。Python 3.10 以上…」が、「。Python 3.10 以上…」と、コードだけの行に分かれて出力された。
3max_chunk_size を超える
SemanticPreserving は表を保つため、800字指定でも最大10,734字のチャンクができた。公式ドキュメントも「厳密な上限ではない」と書いている。
どちらも構造を保つという目的には合っていますが、出力の長さの分布と、文中のコードの扱いを一度見てから使うのが安全です。短すぎるチャンクは除くか前後と結合し、長すぎるものは再帰的分割にかけ直します。
表やコードを途中で切ると何が起きるか
この節では、分割の方法ごとに表と pre(コード・出力)を途中で切った割合と、切れたチャンクの中身が分かります。
表と pre のブロック7,655個のうち、チャンクの境目が中に入ったものの割合です。
| 分割 | 表・pre を切った割合 | BM25 Recall@5 |
|---|---|---|
| 固定200字 | 90% | 26.3% |
| 固定400字 | 71% | 35.3% |
| 固定800字 | 44% | 44.1% |
| 文で800字まで | 42% | 42.5% |
| 構造に沿って400字まで | 34% | 33.4% |
| 再帰800字・overlap 80 | 14% | 39.8% |
| 構造に沿って800字まで | 7% | 39.4% |
| 構造に沿って1600字まで | 1% | 48.0% |
固定長400字で切ったチャンクのうち、早見表の途中から始まるものの先頭は次のとおりでした。
入れる
外れ値の扱い | y を NaN にする(公式の推奨)。予定のあるイベントは休日として登録 | 外れ値の扱い
精度の測り方 | cross_validation と performance_metrics で期間ごとの MAE・MAPE | 交差検証
ARIMA とどちらがよいか | 季節性と祝日のある売上では
表の見出し行(「知りたいこと | 答え | 詳しい節」)が前のチャンクに残り、どの列が何を表すか分からない行だけが渡ります。先頭の「入れる」は前の行の切れ端です。pre を切ると、式の説明の途中から始まるチャンクができ、後半の変数の定義だけが残りました。
数=sin と cos の重ね合わせで表す)
h(t) : 祝日・イベントの効果(指定した日だけ足し引きされる)
ε(t) : 説明できない誤差
構造に沿った分割では、400字に入らない表だけを行で切り、2片目以降に見出し行を付けて索引しました。
知りたいこと | 答え | 詳しい節
外れ値の扱い | y を NaN にする(公式の推奨)。予定のあるイベントは休日として登録 | 外れ値の扱い
精度の測り方 | cross_validation と performance_metrics で期間ごとの MAE・MAPE | 交差検証
この評価セットの答えは段落なので、切断率の差は Recall に直接は出ていません。表の値やコードを答えにする質問(「この設定の既定値は」「このエラーの直し方のコマンドは」)が多い文書ほど、切断率を下げる効き目が大きくなります。Azure Architecture Center も、表は読み込みの段階で扱いやすい形に整えるか、表の要約を作ってチャンクにする方法を挙げています。
- 1表は行の境目で切るセルの途中で切らない。1行が上限を超えるなら、その行だけ1チャンクにする
- 2切った表の片に見出し行を付ける列の意味が分からない行だけのチャンクを作らない
- 3pre は改行で切り、言語と見出しを付けるコードの途中から始まるチャンクは、何のコードか分からない
- 4切断率を数えるブロックの位置とチャンクの境目を比べれば、数行で数えられる
見出しをチャンクに付け足す効果
この節では、記事の題名と h2・h3 をチャンクの先頭に足して索引したときの差が分かります。
索引する文字列だけを「題名 > h2 > h3」+本文にし、LLM に渡す本文と網羅率の計算は変えていません。
Prophetの使い方|Pythonの時系列予測を実測、ARIMAとも比較 > Prophetの使い方:fitとpredictの最小コード
Prophetの使い方:fitとpredictの最小コード
この節では、CSV を読み込んで90日先を予測し、図を保存するまでの最小のコードと、その実行結果が
| 分割 | BM25 Recall@5 | LSA Recall@5 | LSA 記事の当たり@5 |
|---|---|---|---|
| 固定400字 → 見出し付き | 35.3% → 36.8% | 33.1% → 34.7% | 75.2% → 80.5% |
| 再帰800字 → 見出し付き | 39.8% → 40.4% | 42.1% → 43.1% | 77.9% → 80.6% |
| 構造400字 → 見出し付き | 33.4% → 34.1% | 15.8% → 19.8% | 67.9% → 77.4% |
| 構造800字 → 見出し付き | 39.4% → 39.7% | 33.1% → 28.0% | 77.1% → 80.6% |
いちばんはっきり効いたのは、LSA で「正しい記事のチャンクが上位に入るか」でした(+3〜+10ポイント)。題名が付くと、どの記事の話かという手がかりがすべてのチャンクに入るためです。BM25 の予算 Recall も構造400字で +2.0ポイント(95%区間 +0.3〜+3.7)でした。一方で構造800字の LSA Recall@5 は5ポイント下がっており、同じ記事の別の節が上位に並びやすくなった分、答えの節が押し出された例もあります。
Anthropic は、チャンクごとに文脈の説明を LLM で作って先頭に足す Contextual Retrieval で、上位20件の取りこぼし(1−Recall@20)が埋め込みだけで35%、BM25 と組み合わせて49%減ったと報告しています(2024年9月)。見出しの付け足しは、その LLM を使わない一番安い形と考えられます。
親子チャンク(小さく探して大きく渡す)の実装と結果
この節では、小さい子チャンクで探して、それを含む親チャンクを渡す方法の作り方と、この文書での結果が分かります。
子チャンクの開始位置が入る親チャンクを二分探索で引き、子の順位のまま親に置き換えて、同じ親は1回だけ数えました。
import bisect
def child_to_parent(children, parents):
starts = {}
for i, (doc, s, e) in enumerate(parents):
starts.setdefault(doc, []).append((s, e, i))
table = []
for doc, s, e in children:
lst = starts[doc]
j = bisect.bisect_right([x[0] for x in lst], s) - 1
table.append(lst[max(j, 0)][2]) # 子 → 親の番号
return table
def retrieve_parents(ranked_children, table, k=5):
seen, out = set(), []
for c in ranked_children: # 子の順位のまま親に置き換える
p = table[c]
if p not in seen:
seen.add(p); out.append(p)
if len(out) == k:
break
return out
| 探す → 渡す(BM25) | Recall@5 | 渡す文字数 | 親で直接探すと |
|---|---|---|---|
| 再帰200字 → 構造800字 | 36.4% | 2,821字 | 39.4%(2,708字) |
| 固定200字 → 固定800字 | 40.5% | 3,981字 | 44.1%(3,971字) |
| 再帰200字 → h2・h3 の節 | 44.2% | 6,014字 | 49.2%(5,458字) |
| 構造400字 → h2・h3 の節 | 46.8% | 5,927字 | 49.2%(5,458字) |
この文書と BM25 では、親子にすると、同じくらいの文字数を渡す「親の大きさで直接探す」より2〜5ポイント下がりました。LSA ではさらに差が開き、子が短すぎて探す段階で外れていました(再帰200字の子で記事の当たり@5 が60.4%)。
親子が効くのは、短い文のほうが探す精度が高い検索を使うときです。LlamaIndex の公式ドキュメントも「小さいチャンクほど埋め込みが正確になり、大きいと細部を取りこぼしうる」と書いています。BM25 は長さで正規化しているので、長い親を直接探しても精度が落ちにくく、親子の利点が出にくかったと考えられます。埋め込みモデルを使うなら、子の大きさを100〜400字で振って、親で直接探す場合と同じ文字数で比べてから採用を決めます。LlamaIndex での RAG の組み立ては、関連記事「LlamaIndex入門|PythonでRAGを作るコピペ集」で扱っています。
自分の文書でチャンキング戦略を決める手順
この節では、自分の文書・自分の検索で、チャンク分割を数字で決める順番が分かります。
渡せる文字数(予算)を決める
LLM の入力の上限から、システムプロンプト・会話の履歴・出力の分を引き、検索結果に使える量を決める。料金と応答時間もここで効く。
評価セットを作る
質問と「答えが書かれた箇所」の組を数百問。FAQ・問い合わせの履歴・手で書いた質問から作る。答えの箇所は文字の位置か本文の抜き出しで持つ。
文書をブロックに分けて位置を持つ
見出し・段落・表・コードを、本文の中の位置つきで取り出す。PDF なら読み込みの段階でレイアウトを解析する。
大きさを3〜4通りに振る
本番と同じ検索(BM25・埋め込みモデル)で、200・400・800字などを比べる。予算 Recall と完全率を見る。
構造・見出し・overlap を足して比べる
いちばん良かった大きさで、構造に沿った分割・見出しの付け足し・overlap 10%を1つずつ足す。表と pre の切断率も数える。
出力の分布と中身を目で見る
長さの最大・95%点、30字未満の件数、表の途中から始まるチャンクを数件読む。
手元で計った時間は、247本(639万字)の分割が固定長で0.1秒以内、再帰的分割で0.2〜1.2秒、LangChain の HTML 用の分割器で14〜16秒(HTML の解析を含む)、BM25 の索引作りと800問の検索が3〜21秒でした。分割そのものより、評価の仕組みを一度作ることのほうが手間がかかります。評価の仕組みがあれば、大きさや区切りを変えた比較は数分で回せます。RAG の回答全体の評価(忠実さ・関連性)は、関連記事「RagasでRAGを評価する|指標の意味とPythonコード」で扱っています。
自分で確かめる(2つ)
手元の分割器の出力を、次の1行ずつで確かめられます(docs は分割後の Document のリスト)。
# 長さの分布:最大と30字未満の件数
lens = sorted(len(d.page_content) for d in docs); print(lens[-1], sum(l < 30 for l in lens))
# overlap が実際に何文字重なったか(add_start_index=True のとき)
print([max(0, a.metadata["start_index"] + len(a.page_content) - b.metadata["start_index"]) for a, b in zip(docs, docs[1:])][:20])
よくあるエラー・つまずき
この節では、チャンク分割で手元で出たエラーと、数字がおかしく見えるときの原因が分かります。
| 症状・原文 | 原因 | 直し方 |
|---|---|---|
ValueError: Got a larger chunk overlap (200) than chunk size (100), should be smaller. |
chunk_overlap が chunk_size より大きい | overlap は size の0〜20%程度にする |
LangChainBetaWarning: The class `HTMLSemanticPreservingSplitter` is in beta. |
1.1.3 では beta の扱い | 版を固定し、上げたら出力の分布を見直す |
| 日本語が語の途中で切れる | 既定の区切りが空白の次に1文字ずつへ下がる | separators に「。」「、」を足す |
| overlap を付けたのに重なっていない | 再帰的分割は区切りの片ごとにしか持ち越さない | start_index で実際の重なりを数える。必要なら文単位の窓にする |
| 「。」が次のチャンクの先頭に付く | keep_separator の既定(True)は区切りを次の片の先頭に付ける | keep_separator="end" |
| 大きいチャンクほど良い結果に見える | 上位 k 件を固定して比べている | 渡す文字数の予算を揃えて比べる |
| 密ベクトルの検索で見出しだけのチャンクが上位に来る | 見出し単位の分割が30字未満のチャンクを大量に作った | 短いチャンクを除くか、次のブロックと結合する |
| 上限を指定したのに巨大なチャンクがある | 表・リストを保つ分割器が上限を超えて残した | 大きすぎる表だけ行で切り、見出し行を付ける |
チャンク分割の判断表
この節では、状況ごとにどの分割から始めるかが分かります。いずれも出発点で、評価セットで確かめて決めます。
| 状況 | 最初に試す分割 | 理由(この記事の実測) |
|---|---|---|
| BM25 が主、予算が小さい | 構造に沿って200〜400字+見出し | 予算 Recall 40.0%で上位。表の切断も固定長より少ない |
| 埋め込み(密ベクトル)が主 | 構造・再帰で400〜800字+見出し | LSA は短いと崩れた。モデルで測り直す |
| HTML・Markdown で見出しが整っている | 見出しで切り、上限つきで詰める | 上限なしの見出し単位は最大10,824字 |
| 表・コードが答えになる | 構造に沿った分割、表は行で切って見出し行 | 固定400字は表と pre の71%を切った |
| 文の途中で切る方法しか使えない | 固定長+overlap 10〜20% | 完全率が 23.0%→25.9% |
| 件数・保存の費用を抑えたい | overlap 0〜10% | 50%で件数がほぼ倍、Recall は −3.1 |
| 短い文で精度が上がる埋め込みを使う | 親子(子200字前後→親800字・節) | BM25 では親を直接探すほうが上だった |
| 文書の種類が混ざる | 種類ごとに分割を分け、メタデータで区別 | Azure は分割の実装の数自体を費用に数えている |
- 1予算を決める → 渡せる文字数比べる物差しを先に固定する
- 2大きさを振る → 200・400・800字本番と同じ検索で
- 3切る場所を構造に合わせる → 見出し・表・pre切断率を数える
- 4見出しを足す → 題名 > h2 > h3安く効きやすい
- 5overlap・親子・意味で切る → 必要なら費用が増える順。差が区間を超えたときだけ採用
RAG で足りないのが「検索」なのか「知識の持たせ方」なのかの切り分けは、関連記事「RAGとファインチューニングの違いと使い分け|判断基準の早見表」で扱っています。
よくある質問
この節では、チャンク分割について検索されやすい疑問に短く答えます。
Q. RAG のチャンクサイズは何文字がよいですか?
A. 文書と検索方式で変わります。この記事の247記事では、2,000字の予算で BM25 は200〜400字、LSA は400字以上と見出し単位が上でした。渡せる文字数を決め、200・400・800字の3通りを自分の評価セットで比べるのが確実です。
Q. chunk overlap は何%にすればよいですか?
A. 段落や文の境目で切るなら0〜10%から始めます。固定400字で10%・20%は Recall の差が誤差の範囲、50%はチャンク数がほぼ倍になり Recall が3.1ポイント下がりました。答えがまるごと入る割合(完全率)は10%で少し上がりました。
Q. 文字数とトークン数のどちらで数えるべきですか?
A. 埋め込みモデルや LLM の上限はトークンで決まるので、上限に近い大きさならトークンで数えます。LangChain は length_function で数え方を替えられます。日本語の文字とトークンの比はトークナイザで変わるため、英語の「512トークン≒2,000文字」は当てはまりません。
Q. セマンティックチャンキングは使うべきですか?
A. 埋め込みの計算が分割の段階でも要り、費用が増えます。Chroma の技術レポートでは、意味で切る方法の一部が再帰的分割より再現率で数ポイント上でしたが、既定の設定のままでは下回った方法もありました。まず構造に沿った分割と見出しの付け足しを試し、それでも足りないときに比べるのが順番です。
Q. 見出し(Markdown の # や HTML の h2)で切るだけではいけませんか?
A. 見出しで切るのは有効ですが、上限が無いと大きさがばらつきます。手元では最大10,824字の節がありました。見出しで切ったうえで、上限を超える節だけ段落や表の行で分けます。
Q. 親子チャンク(ParentDocumentRetriever など)はいつ効きますか?
A. 短い文で探したほうが精度が上がる検索を使い、答えの前後の文脈も渡したいときです。この記事の BM25 では、同じくらいの文字数なら親の大きさで直接探すほうが2〜5ポイント上でした。採用する前に、同じ文字数で比べます。
Q. 表は Markdown の表のまま入れてよいですか?
A. 1つのチャンクに収まるならそのまま入れて問題ありません。ただし途中で切るなら行の境目で切り、各片に見出し行を付けます。見出し行が無い片は列の意味が分からなくなります。
Q. 日本語は形態素解析をしないと分割できませんか?
A. 分割の段階では、句点・読点・改行を区切りにすれば語の途中で切れることはほぼ避けられます。この記事は形態素解析を使わずに分割しました。検索の分かち書き(BM25 の語の単位)は別の問題で、この記事は文字2-gram を使っています。
Q. 評価セットは何問あればよいですか?
A. 見分けたい差によります。800問で Recall の標準誤差は約1.4ポイントで、2ポイント前後の差は95%区間が0をまたぐことがありました。数十問だと、分割の違いより問の選び方のぶれのほうが大きく出ます。
Q. 分割の方法はあとから変えられますか?
A. 変えられますが、索引(埋め込み)を全件作り直すことになります。Azure Architecture Center も分割の方法を半恒久的な選択としています。読み込みの結果(ブロックと位置)を保存しておくと、分割だけやり直せます。
まとめ
- チャンクサイズは、上位 k 件ではなく「LLM に渡せる文字数」を揃えて比べる。k 固定だと大きいチャンクが必ず有利に見え、予算2,000字では順番が逆になった。
- 最適な大きさは検索方式で変わる。BM25 は200〜400字、LSA(密ベクトルの代わり)は400字以上と見出し単位が上だった。検索を替えたら測り直す。
- overlap は10〜20%で誤差の範囲、50%で件数がほぼ倍・Recall −3.1。LangChain の再帰的分割は、指定しても Prophet の記事の境目84か所中71か所で重ならなかった。
- 固定長400字は表と pre の71%を途中で切った。見出しで切り、ブロックを切らずに詰め、大きい表は行で切って見出し行を付けると7%まで下がる。
- 見出しの付け足しは安く効きやすい(LSA の記事の当たり +3〜+10ポイント)。親子チャンクはこの文書と BM25 では効かず、採用は同じ文字数での比較で決める。
参考・出典
確認日はいずれも 2026年11月4日です。
- Microsoft Learn「RAG チャンキング フェーズ」(Azure Architecture Center):https://learn.microsoft.com/ja-jp/azure/architecture/ai-ml/guide/rag/rag-chunking-phase
- Microsoft Learn「Chunk large documents for RAG and vector search in Azure AI Search」:https://learn.microsoft.com/azure/search/vector-search-how-to-chunk-documents
- LangChain「Splitting recursively」:https://docs.langchain.com/oss/python/integrations/splitters/recursive_text_splitter
- LangChain「Split HTML」:https://docs.langchain.com/oss/python/integrations/splitters/split_html
- LlamaIndex「Basic Strategies」:https://developers.llamaindex.ai/python/framework/optimizing/basic_strategies/basic_strategies/
- Chroma「Evaluating Chunking Strategies for Retrieval」(Brandon Smith・Anton Troynikov、2024年7月3日):https://www.trychroma.com/research/evaluating-chunking
- Anthropic「Introducing Contextual Retrieval」(2024年9月19日):https://www.anthropic.com/news/contextual-retrieval
- PyPI「langchain-text-splitters」:https://pypi.org/project/langchain-text-splitters/
- scikit-learn「TruncatedSVD」:https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.TruncatedSVD.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「RAGのチャンク分割の決め方|サイズ・overlapを247記事で実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。