HN 日本語サマリー

← 一覧へ戻る
プログラミング

長文ドキュメントのためのより良いベクトル検索:Manticore Search内のチャンキング

Better Vector Search for Long Documents: Chunking Inside Manticore Search (manticoresearch.com)

65 pointsby GloriaVinogrado10 コメント

要約

Manticore Searchは、長文ドキュメントに対するベクトル検索の課題を解決するため、自動チャンキング機能を導入しました。この機能により、ユーザーはドキュメントを手動で分割・埋め込み・管理する必要がなくなり、テーブル定義に`chunk_strategy`を指定するだけで、ドキュメントが自動的に分割され、各チャンクが埋め込まれて検索されます。これにより、モデルの入力ウィンドウ制限を超えたドキュメントでも、検索精度が大幅に向上します。

全文翻訳

Manticore Searchにおける長文ドキュメントのためのベクトル検索:チャンキング戦略

チームの内部ドキュメント(ガイド、ランブック、ポストモーテムなど)で検索機能を構築しているとします。そこには自動埋め込みを備えたテーブルがあります。テキストを挿入すると、Manticoreがモデルを実行し、ベクトル列を自動的に埋めてくれます。(もしこれが初めてなら、Manticoreでのベクトル検索から始めてください。)4,000語のドキュメントをロードします。挿入は成功し、検索も機能します。すべて順調に見えます。

しかし、選択したモデルの入力ウィンドウが512トークンで、そのドキュメントは約5,000トークン長だとどうなるでしょうか?モデルは最初の380語を読み込み、残りの3,600語を破棄します。それ以降のドキュメントの部分からは何も取得できず、それを知らせるものは何もありませんでした。また、埋め込みがドキュメント全体を代表していない可能性もあります。

これまでは、通常、ドキュメントを自分でいくつかのピースに分割し、それぞれに埋め込みを作成し、チャンク検索ではなくドキュメント検索を行いたい場合には結果をどのように組み合わせるかを考える必要がありました。Manticoreは現在、これをテーブル定義で処理します。CREATE TABLEのベクトル列にchunk_strategyを追加するだけで、Manticoreは各ドキュメントをチャンクに分割し、各チャンクを埋め込み、それらすべてを検索します。

DROP TABLE IF EXISTS docs;

CREATE TABLE docs (
  title text,
  content text,
  chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,content'
    chunk_strategy='sentence' max_tokens='256' overlap_tokens='32'
);

これがこの機能のすべてです。インジェストパイプライン、スプリッターライブラリ、チャンク用の2番目のテーブル、チャンクヒットをドキュメントに折りたたむためのGROUP BYは不要です。

TL;DR

  • 5つの戦略: truncate(旧デフォルト)、mean、fixed、recursive、sentence。モデルバックのベクトル列のchunk_strategyで設定します。
  • truncateとmeanはドキュメントごとに1つのベクトルを生成し、float_vector列で動作します。fixed、recursive、sentenceは複数生成するため、float_vector_array列が必要です。
  • ドキュメントは依然として1つの検索結果です。チャンクは個別に競合し、Manticoreはドキュメントを1回だけ返します。knn_dist()は最も近いチャンクへの距離を示します。kはチャンクではなくドキュメント数をカウントします。
  • チューニングノブ: max_tokens(チャンクサイズ)、overlap_tokens(隣接チャンク間の共有トークン)、max_chunks(ドキュメントあたりの上限)。
  • Manticoreマニュアルで測定(189ページ、約298k語):モデルのウィンドウを超えて埋もれたコンテンツについて、recall@5は55.1% → 83.3%に、MRRは0.44 → 0.70に向上しました。RAMは約2.5倍、インジェスト時間は約4倍です。
  • クエリはチャンク化されません。クエリは全体として埋め込まれるのに十分短く、保存されるドキュメントのみが分割されます。

小さな例で示す問題

4つのドキュメントがあるとします。

  1. バックアップと復元ランブック — 約700語、約900トークン。バックアップスケジュール、保持期間、復元ドリル、認証情報、キャパシティプランニング。最後のセクションには、レプリケーションポートで使用されるTLS証明書をローテーションする方法が説明されています。
  2. 監視とアラートガイド — 無関係。
  3. CLIの開始方法 — 無関係。
  4. HTTP APIのTLSと証明書 — 証明書のみを扱う短いページで、ローテーションやレプリケーションについては言及していません。

以下のコマンドを使用して、テーブルを作成し、ドキュメントを追加できます。

したがって、1つのテーブルに、同じソーステキストを持つ3つのベクトル列があります。戦略ごとに1つです。単一のINSERTですべて3つが埋め込まれるため、比較条件はすべて同じです。

DROP TABLE IF EXISTS docs;
CREATE TABLE docs (
  title text,
  body text,
  v_truncate float_vector       knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,body',
  v_mean     float_vector       knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,body' chunk_strategy='mean',
  v_sentence float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
    chunk_strategy='sentence' max_tokens='128' overlap_tokens='32'
);

4つのドキュメントを挿入します。

INSERT INTO docs (id, title, body) VALUES
  (1, 'Backup and restore runbook',
   'Nightly backups run at 02:00 UTC from the standby node. The job snapshots every table directory, writes a manifest, and uploads the result to object storage. Retention is thirty daily copies, twelve monthly copies, and one yearly copy. A restore drill runs on the first Monday of each month against a scratch cluster. The drill counts as passed only when a full-text search over the restored data returns the same document count as production. Anything less is treated as a failed drill and investigated the same week. Before a restore, freeze the target cluster so that no writes land while files are being replaced. Copy the manifest first and verify its checksum. If the checksum does not match, stop: a partial restore is worse than no restore, because the cluster will start and silently serve half the corpus. After the files are in place, unfreeze and let replication catch up. Watch the queue depth. If it does not drain within ten minutes, the node is probably still reading from cold storage and needs a warm-up pass before it can serve traffic. Backup failures page the on-call engineer. The three most common causes are an expired object storage credential, a disk that filled up while the snapshot was being written, and a table left frozen by a previous failed run. All three are recoverable without data loss. Check the job log first, then the disk, then the freeze state of every table. Capacity planning for backups is boring but it matters. A daily copy of the search cluster is roughly the size of the data directory plus fifteen percent for the manifest and metadata. Multiply by the retention count, add the transfer cost, and you have the monthly bill. Most teams discover too late that the yearly copies dominate the storage line. Object storage lifecycle rules do most of the retention work. Daily copies move to infrequent access after seven days and expire after thirty. Monthly copies move to archive after sixty days. Yearly copies never expire automatically; deleting one is a manual action that requires a second approver. Credentials for the backup job live in the secret manager and are issued to a role, not to a person. The role can write new objects and list the bucket. It cannot delete, and it cannot read objects older than the current day. That last restriction is the cheapest defence against a compromised backup runner turning into a data exfiltration path. Documentation for each table lives next to its schema: what the table is for, who owns it, how large it is expected to get, and whether it can be rebuilt from an upstream source. A table that can be rebuilt does not need thirty daily copies. Roughly half of most clusters turns out to be derived data that nobody had marked as derived. Verification is not the same as the job exiting zero. The job can succeed while producing an unusable copy: an empty table, a truncated upload, a manifest that references a file that was never written. The verification step reads the manifest back, checks every referenced object exists and matches its recorded size, and compares row counts on three sampled tables against production. Rotating the replication TLS certificate is a separate procedure and the step people most often get wrong. The certificate that secures the replication port is not the same as the one the HTTP API uses, and replacing one does not replace the other. Generate the new key and signing request on the node that will be rotated first, sign them with the cluster certificate authority, and place the files next to the existing ones rather than on top of them. Then update the node configuration to point at the new paths and reload. Do one node at a time and confirm that the cluster reports every peer as synced before moving on. A half-rotated cluster where two nodes trust different authorities will keep accepting writes on both sides and diverge quietly. When every node has been rotated, remove the old key material and revoke the retired certificate at the authority.'),
  (2, 'Monitoring and alerting guide',
   'Every node exports metrics over an HTTP endpoint that a scraper collects once per fifteen seconds. The dashboards are grouped into four rows: traffic, latency, saturation, and errors. Traffic is queries per second broken down by table. Latency is the ninety-fifth and ninety-ninth percentile of query time, measured server side. Alerting is deliberately thin. Paging alerts fire on sustained error rate above one percent for five minutes, on ninety-ninth percentile latency above two seconds for ten minutes, and on a node dropping out of the cluster. Everything else is a ticket, not a page. Teams that page on every anomaly stop reading pages within a month. Log retention is fourteen days hot and ninety days cold. The query log records the query text, the table, the match count, and the elapsed time. Turning it on costs a few percent of throughput and is almost always worth it, because most performance investigations start with a slow query nobody knew was being issued.'),
  (3, 'Getting started with the CLI',
   'The command line client connects over the MySQL wire protocol, so any MySQL client works and you do not need to install anything special. Point it at port 9306 and you get an interactive shell. The shell understands the usual conveniences: history, tab completion of table names, and vertical output when a row is too wide for the terminal. Start by listing tables, then look at one with SHOW CREATE TABLE. The output is the exact statement that would recreate the table, including every option that was applied implicitly, which makes it the fastest way to find out what a table actually does rather than what someone documented two years ago. Bulk loading from the shell is possible but rarely what you want. For anything above a few thousand rows, use the HTTP bulk endpoint or one of the log shipper integrations, both of which batch and retry for you.'),
  (4, 'TLS and certificates for the HTTP API',
   'The HTTP API can be served over TLS. You supply a certificate, a private key, and optionally a chain file, and the listener starts speaking HTTPS instead of HTTP. Clients that present a certificate of their own can be authenticated by it, which is the usual way to lock an internal API down without putting a password in every config file. Certificates for the HTTP API come from wherever your organisation gets certificates: a public authority, an internal authority, or an automated issuer. The file format is PEM. Both the certificate and the key must be readable by the user the server runs as, and the key must not be world readable or the listener refuses to start. Debugging TLS problems is mostly about reading the handshake. A client that reports an unknown authority is missing the chain. A client that reports a hostname mismatch is connecting by an address that is not in the certificate. A client that hangs is usually talking TLS to a plaintext port.');

今度は、ランブックの最後のセクションにある質問を、戦略ごとに1回ずつ尋ねてみましょう。

SELECT title, knn_dist() FROM docs
WHERE knn(v_truncate, 4, 'how do I rotate the TLS certificate used for replication');

SELECT title, knn_dist() FROM docs
WHERE knn(v_mean, 4, 'how do I rotate the TLS certificate used for replication');

SELECT title, knn_dist() FROM docs
WHERE knn(v_sentence, 4, 'how do I rotate the TLS certificate used for replication');
戦略1番目の結果2番目の結果
truncate (デフォルト)TLS and certificates for the HTTP API — 0.762Backup and restore runbook — 0.936
meanBackup and restore runbook — 0.656TLS and certificates for the HTTP API — 0.762
sentence, 128トークン, 32オーバーラップBackup and restore runbook — 0.254TLS and certificates for the HTTP API — 0.700

truncateでは、実際に質問に答えるドキュメントが、証明書についてそれらしく見えるだけのデコイに負けてしまいます。ランブックの単一ベクトルは、モデルが読み取ることを許可された内容のすべてであるバックアップスケジュールと復元ドリルに関する最初の数ページから構築されたためです。

sentenceチャンキングでは、ランブックは1つのベクトルではなく9つのベクトルとして保存されます。

SELECT id, title, LENGTH(v_sentence) AS chunks FROM docs ORDER BY id ASC;
+------+---------------------------------------+--------+
| id   | title                                 | chunks |
+------+---------------------------------------+--------+
|    1 | Backup and restore runbook            |      9 |
|    2 | Monitoring and alerting guide         |      2 |
|    3 | Getting started with the CLI          |      2 |
|    4 | TLS and certificates for the HTTP API |      2 |
+------+---------------------------------------+--------+

その9つのうちの1つが証明書ローテーションの段落です。クエリとほぼ一致するため、ドキュメントは圧倒的な差で勝利します:0.254対0.700です。

チャンキング戦略についてさらに詳しく

戦略ドキュメントあたりのベクトル数列の型何をするか
truncate1float_vectorモデルのウィンドウに収まるだけ埋め込み、残りは破棄します。古いバージョンで唯一利用可能なモードであり、依然としてデフォルトです。
mean1float_vectorドキュメント全体を分割し、各部分を埋め込み、それらを1つのベクトルに平均化します。
fixedNfloat_vector_arraymax_tokensトークンの固定ウィンドウ。
recursiveNfloat_vector_array区切り文字の階層(段落、行、文、スペース)で分割し、各ピースをmax_tokens内に収めます。
sentenceNfloat_vector_array文の境界(Unicode UAX #29)、max_tokensまでパックします。

重要な区別は、テキストがどのようにカットされるかではありません。それは、一致が何を意味するかです。

ドキュメントごとに1つのベクトルがある場合、検索は次のように尋ねます:「このドキュメント全体は、クエリと類似していますか?」単一の関連段落は周囲のすべてによって薄められ、5つのトピックをカバーするドキュメントは、それらのいずれにも実際には一致しないことになります。

チャンクごとに1つのベクトルがある場合、検索は次のように尋ねます:「このドキュメントは類似したものを含んでいますか?」各チャンクは独自のメリットで競合し、Manticoreはドキュメントを1回だけ返します。そのスコアは、最も良いチャンクに基づきます。

truncate — ドキュメントが短い場合に保持
title text,
v float_vector knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='title'

これはすでに持っているものです。chunk_strategy='truncate'はデフォルトであり、書く必要はありません。製品タイトル、短い説明、タグ、チャットメッセージ、ログ行、検索クエリ、コミット件名など、テキストがモデルのウィンドウに収まる場合は、これが正しい選択であり、最も速く、最も小さいです。

どのくらい収まりますか?ほとんどの人が想定するよりも多く、期待するよりも少ないです。all-MiniLM-L6-v2は512トークン、約380英語単語を処理します。text-embedding-3-smallは8,192トークンです。95パーセンタイルのドキュメントが制限を十分に下回っている場合は、読み込みを停止し、truncateを維持してください。

いつ hurts か:長文のもの。ドキュメントページ、ナレッジベース記事、契約書、トランスクリプト、メールスレッド、Wikiページ、READMEファイル、インシデントポストモーテム。

mean — 1つのベクトル、しかしドキュメント全体
v float_vector knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='title,content'
  chunk_strategy='mean'

Manticoreはドキュメントを分割し、各チャンクを埋め込み、チャンクベクトルを平均化して単一の正規化ベクトルを作成します。ストレージと検索コストはtruncateと同じです(ドキュメントごとに1つのベクトル、1つのHNSWノード)が、何も破棄されません。

使用する場合:

  • テールの部分をカウントしたいが、より多くのベクトルを許容できない場合 — インデックスRAMが制約となっている非常に大規模なコーパス。
  • 列が通常のfloat_vectorであり、型を変更できない場合(例:マルチベクタ戦略がサポートしていない既存のテーブルに列を追加する場合)。
  • ドキュメントが1つのことについて書かれており、単に長い場合。単一製品の完全な説明、1つのレシピ、1つの求人情報。

使用しない場合:ドキュメントが複数の無関係なトピックをカバーしている場合。法的契約の免責条項と支払い条件を平均化すると、それらの間に位置し、どちらにも近くないベクトルが生成されます。以下のベンチマークでは、meanはチャンキングが解消するギャップの約3分の1を回復しました — これは実際の改善であり、明らかに同じものではありません。

fixedチャンキング — 予測可能で、推論しやすい
v float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='content'
  chunk_strategy='fixed' max_tokens='256' overlap_tokens='32'

テキストがその時点で何をしていても、max_tokensトークンごとにカットします。チャンク数はドキュメント長の直接的な関数であるため、何かをロードする前にインデックスサイズを予測できます。

使用する場合:テキストに信頼できる構造がない場合:OCR出力、段落を失ったスクレイピングされたHTML、句読点のない機械トランスクリプト、ログダンプ、ミニファイされたコンテンツ。また、単にトランケーションを停止する最も安価な方法が必要な場合の優れたデフォルトです。

コスト:境界が文の途中に着地する可能性があり、思考の途中で始まるチャンクは悪く埋め込まれます。これはまさにoverlap_tokensが役立つ理由です — 以下を参照してください。

recursiveチャンキング — プロセの最も優れた汎用デフォルト
v float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='title,content'
  chunk_strategy='recursive' max_tokens='256' overlap_tokens='32'

fixedと同じトークン予算ですが、各カットは自然な境界(まず空行、次に改行、次に文末、次にスペース)に引き戻されます。チャンクはテキストが停止する場所で停止し、カウンターが切れる場所ではありません。境界はチャンクの中間点 past に引き戻されることはないため、小さな断片のストリームを取得することはありません。

LangChainのRecursiveCharacterTextSplitterを使用したことがあるなら、これは同じ考え方ですが、データベース内で文字ではなくモデルの実際のトークン上で実行され、インストールするものは何もありません。

使用する場合:MarkdownとHTMLドキュメント、Wikiページ、ナレッジベース、ブログ投稿、READMEファイル、構造化レポート — 人間が段落で書いたものなら何でも。これは、ベンチマークで深いコンテンツに対して最も高いスコアを記録しました。

sentenceチャンキング — チャンクが完全な思考である必要がある場合
v float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='content'
  chunk_strategy='sentence' max_tokens='256' overlap_tokens='32'

Unicode UAX #29で文の境界を検出し、トークン予算に達するまで文全体を貪欲にパックします。チャンクが文の途中で開始または終了することはありません。予算より長い単一の文は、最後の手段としてトークンウィンドウによって分割されます。

使用する場合:サポートチケットとメールスレッド、チャットと会議のトランスクリプト、法律およびポリシーテキスト、ニュース、顧客レビュー、医学および科学の抄録 — 文の断片が意味を変更または破壊する可能性のあるもの。また、チャンクが後でLLMに供給される場合にも選択する戦略です。なぜなら、プロンプトで句の途中で終わるチャンクは読みにくいからです。

sentenceはrecursiveよりも少し保守的です。テストではより少なく、よりクリーンなチャンクを生成し、recall@5でほぼ同じスコアでした。

3つのノブ

chunk_strategy  = truncate | mean | fixed | recursive | sentence
max_tokens      = トークンあたりのチャンクサイズ; 0 (デフォルト) = モデル自身の制限
overlap_tokens  = 隣接するチャンク間で共有されるトークン; 非ゼロの`max_tokens`が必要
max_chunks      = ドキュメントあたりのベクトルの上限; 0 (デフォルト) = 無制限

max_tokensは、モデルが実際に受け入れられるものに制限されています — 512トークンモデルで4,096を要求しても、エラーではなく512が得られます。チャンクが小さいほど、マッチはシャープになり、ベクトルは多くなります。チャンクが大きいほど、ベクトルあたりのコンテキストは増え、ベクトルは少なくなります。英語のプロセでは、128〜512でほとんどのユースケースをカバーできます。ベンチマークでは256を使用しました。

overlap_tokensは、各チャンクの末尾を次のチャンクの先頭に繰り返すため、境界をまたぐ文も完全な形で表示されます。max_tokensの10〜20%が一般的な設定です。Manticoreは前進を保証します:fixedとrecursiveはオーバーラップをチャンクサイズの半分に制限し、sentenceは少なくとも1つの文を進みながら、最後の文のoverlap_tokensまでの量を再シードします。明示的な非ゼロのmax_tokensが必要です — 「モデルの制限が偶然何であれ」に対するオーバーラップは意味のある設定ではないため、Manticoreはそれを拒否します。

max_chunksは、異常に大きなドキュメントの影響を制限します。これがないと、1行に貼り付けられた400ページのPDFは数千のHNSWノードになります。これがあると、Manticoreはオーバーフローを最後の保持されたチャンクにマージし、埋め込む際にモデルのウィンドウに切り詰めます。

-- 約600トークンのドキュメント、64トークンでチャンク化
chunk_strategy='fixed' max_tokens='64'                  -- 22ベクトル
chunk_strategy='fixed' max_tokens='64' max_chunks='3'   --  3ベクトル

これは、全体的なメモリ節約方法ではなく、外れ値に対するガードレールとして使用してください。

検索の外観

クエリは以前と何も変わりません。チャンクテーブル、ネストされたフィールド、結合、GROUP BYはありません。以下は完全な例です。

DROP TABLE IF EXISTS notes;
CREATE TABLE notes (
  title text,
  body text,
  chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
    chunk_strategy='sentence' max_tokens='32'
);

INSERT INTO notes (id, title, body) VALUES
  (1, 'Certificate rotation',
   'The replication certificate is not the one the HTTP API uses. Generate the new key on the node being rotated and sign it with the cluster authority. Update the paths and reload, one node at a time, confirming every peer reports as synced before you move on.'),
  (2, 'Disk pressure',
   'When a data directory crosses eighty percent the merge scheduler stops compacting and the node starts refusing writes. Free space first, then trigger a manual OPTIMIZE. Adding a disk without draining the queue only postpones the problem.'),
  (3, 'Slow queries',
   'Turn the query log on before guessing. Most investigations end at a single query nobody knew was being issued, usually one that sorts on an unindexed attribute over the whole table.');

SELECT id, title, knn_dist() FROM notes
WHERE knn(chunks, 3, 'how do I replace an expiring certificate on every node');
+------+----------------------+------------+
| id   | title                | knn_dist() |
+------+----------------------+------------+
|    1 | Certificate rotation | 0.51039070 |
|    2 | Disk pressure        | 0.91703475 |
|    3 | Slow queries         | 1.02606630 |
+------+----------------------+------------+

max_tokens='32'は意図的に小さく設定されており、これらの短いメモが実際に分割され、トイデータセットでマルチベクタの動作を確認できるようにしています。ベクトル列のLENGTH()は、各ドキュメントがどのように分割されたかを示します。

SELECT id, title, LENGTH(chunks) AS n FROM notes ORDER BY n DESC LIMIT 5;
+------+----------------------+---+
| id   | title                | n |
+------+----------------------+---+
|    1 | Certificate rotation | 2 |
|    2 | Disk pressure        | 2 |
|    3 | Slow queries         | 2 |
+------+----------------------+---+

6つのベクトル、3行の結果。検索はドキュメントあたりの複数のベクトルで説明されているルールに従います。

  • ドキュメントが一致するのは、そのベクトルのいずれかがクエリベクトルに近い場合です。
  • Manticoreは各一致を正確に1回返します。knn_dist()は、最も近いチャンクへの距離です。
  • kはドキュメント数をカウントし、ベクトル数ではありません。knn(chunks, 3, ...)は3つのドキュメントを意味します。
  • ベクトルがないドキュメントは返されません。

HTTP経由での同じクエリ:

POST /search
{
  "table": "notes",
  "knn": {
    "field": "chunks",
    "query": "how do I replace an expiring certificate on every node",
    "k": 3
  },
  "_source": ["title"]
}
...
{
  "_id": 1,
  "_score": 1,
  "_knn_dist": 0.51039070,
  "_source": { "title": "Certificate rotation" }
}
...

KNNページ上の他のすべては以前と同様に機能します:フィルタリング、プリフィルターおよびポストフィルター戦略、量子化、早期終了、およびリランキング。

実際に役立つか?自社マニュアルでの数値

この機能をManticore英語マニュアルでテストしました — 189ページ、約298,000語で、2段落の短いメモから39,000語の変更履歴まで様々です。

クエリセットは機械的に生成されたもので、手作業で選ばれたものではありません。各ページについて、セクション見出しを取得し、マニュアル全体で一意の見出しのみを保持し、それらを2つに分割しました。

  • 深いコンテンツクエリ(419件) — そのページの最初の約1,200文字以降に出現する見出し。これは意図的に保守的な線です:モデルのウィンドウは512トークン、約2,000文字なので、これらのいくつかはtruncateが部分的に見ることができるテキストを指します。したがって、以下のギャップは誇張ではなく、控えめなものです。
  • ヘッドコンテンツクエリ(88件) — 最初の約1,200文字内の見出し。コントロールグループ:truncateがすでに表示できるコンテンツ。

クエリは、KNNがその見出しが含まれるページをトップk内に返した場合にヒットとみなされます。モデル:Xenova/all-MiniLM-L6-v2(384次元、512トークンウィンドウ)、ManticoreのONNXバックエンドで実行。ハードウェア:32スレッド。マルチベクタ戦略ではmax_tokens='256'、overlap_tokens='32'。

品質の数値は指定されたインデックスに対して決定的です。タイミングは、他の要素がアイドル状態のボックスでの各戦略の単一実行です。

深いコンテンツ — チャンキングが役立つもの
戦略ベクトル数インジェストインデックスRAMhit@1hit@5hit@10MRR
truncate18921 s4.2 MB33.7%55.1%63.2%0.44
mean18972 s4.2 MB43.9%65.2%74.7%0.54
fixed3,43073 s9.5 MB56.3%81.1%86.2%0.68
recursive4,66486 s11.7 MB58.7%83.3%89.5%0.70
sentence4,04179 s10.6 MB55.4%83.5%89.0%0.68

チャンキングは、コイントスを機能する検索に変えます。recall@5は55.1%から83.3%に向上し、正しい回答のランクも同様に改善します — MRR 0.44 → 0.70。truncateがトップ5で全く回答できなかったクエリのうち、recursiveは約3分の2を回復します。

meanは予想される結果になります:追加のストレージや検索コストなしで、約3分の1のギャップを無料で回復します。

ヘッドコンテンツ — コントロールグループ
戦略hit@1hit@5MRR
truncate65.9%86.4%0.74
mean59.1%83.0%0.69
fixed60.2%83.0%0.71
recursive58.0%86.4%0.70
sentence56.8%85.2%0.69

完全性のために、コントロールグループは注意深く読む価値があります。モデルがすでに表示できるコンテンツについては、truncateが依然としてランク1で最も正確です — recursiveの58.0%に対して65.9%。ドキュメント全体のベクトルは、ページの全体的なトピックを運びます。クエリがページの最初の主題に関するものであれば、そのコンテキストが役立ちます。

ランク5では、差はなくなります:recursiveは86.4%でtruncateと一致します。したがって、トレードオフは、コンテンツの冒頭部分におけるトップ1精度の数ポイントを、他のすべてに対する+28ポイントの再現率と交換することです。ドキュメント検索、ヘルプセンター、またはLLMに5〜10のパスを供給するRAGリトリーバーにとって、これは簡単な選択です。

コスト
  • インデックスRAM:4.2 MB → 11.7 MB、約2.5倍、約25倍のベクトル数に対して。ベクトルはRTテーブルが格納するもののすべてではありません。それらのベクトル上のHNSWグラフも、チャンク保存中およびOPTIMIZE中に構築するのに時間がかかりますが、Manticoreはすべてのコアで構築します。
  • データロード:189ドキュメントで21秒 → 86秒。チャンキングは、各ドキュメントの最初の380語ではなく、コーパス全体を埋め込むことを意味し、時間はそれに比例します。これは埋め込みコストであり、チャンキングコストではありません — 分割自体は推論の隣では測定できません。
  • クエリ応答時間:p50で6.3 ms → 8.5 ms。HNSWは、189ベクトルと同様に4,664ベクトルを容易に処理します — 2パスHNSW、バッチ距離、およびAVX-512を参照してください。

有料の埋め込みAPIを使用する場合、インジェスト番号を請求書として読んでください:チャンキングは、各ドキュメントの先頭ではなく、コーパス全体をモデルに送信し、そのすべてのトークンに対して支払います。ローカルONNXモデルにはトークンあたりのコストがないため、これらが高速である大きな理由の1つです。

チャンキング戦略を選択するための推奨事項

あなたのデータまず試すこと
タイトル、名前、短い説明、タグ、ログ行truncate
長いが単一トピック、またはRAMがハードリミット、または列が既存のfloat_vectormean
ドキュメント、Wiki、ナレッジベース、記事、READMErecursive、max_tokens 128–256
サポートチケット、メール、トランスクリプト、法律テキスト、レビューsentence、max_tokens 128–256
OCR、スクレイピングされたHTML、機械トランスクリプト、構造化されていないダンプfixed、max_tokens 256、オーバーラップ追加
チャンクがLLMにコンテキストとして渡される(RAG)sentence、max_tokens 384–512

オーバーラップは、それらのほとんどから意図的に除外されています:以下のスイープでは、構造化されたプロセでのメリットを測定できず、ベクトルを消費します。思考が境界を頻繁にまたぐ場合にそれを使用してください — 構造化されていないトランスクリプト、OCR、段落区切りがない長い物語。

チャンクのサイズはどれくらいにすべきか?

チャンクサイズは、結果に実際に影響を与える設定です。トレードオフは直接的です:小さいチャンクは1つのアイデアをシャープにマッチさせ、大きいチャンクはより多くのコンテキストを持ちますが、その中の各アイデアを希釈します。長いドキュメントに埋もれた段落は、チャンクサイズがそれ自身のベクトルを与えるのに十分小さくなるまでしか見つけられなくなります。

別のテストを実行しました:同じ189ページの мануал でrecursiveを使用し、3つのチャンクサイズ×3つのオーバーラップ設定、そして同じ419の深いクエリ。2つの傾向が際立っています:品質はチャンクが小さくなるにつれて向上し、オーバーラップは品質をあまり向上させずにコストを追加します。

[画像:チャンクサイズに対する検索品質とインデックスRAM、3つのオーバーラップ設定の場合]

Y軸がゼロではなく78%から始まっていることに注意してください — 全体の広がりは約6ポイントなので、ゼロベースの軸は直線に平坦化されます。チャートの背後にある数値:

max_tokensoverlap_tokensベクトル数インデックスRAMdeep hit@5deep MRR
12808,25617.3 MB85.2%0.718
128139,32818.7 MB85.7%0.705
1283211,62322.6 MB85.2%0.694
25603,98410.0 MB83.1%0.657
256264,52510.9 MB84.5%0.681
256645,56912.5 MB83.5%0.689
51201,9736.9 MB80.2%0.655
512512,1917.2 MB79.2%0.651
5121282,6668.0 MB79.7%0.660

私たちが目にするもの:

小さいチャンクが常に勝ちます。 512から128トークンにすると、recall@5で約5ポイント(80.2% → 85.2%)、ランキング品質で大幅なジャンプ(MRR 0.655 → 0.718)が得られます。ベクトル数は4倍、インデックスRAMは2.5倍になります。128未満ではチャンクは完全な思考を含まなくなるため、これは永遠に乗り続ける坂ではありませんが、長い技術的なプロセでは、128〜256は常に512を上回ります。

オーバーラップは品質に実質的に何も影響せず、無料でもありませんでした。 128トークンで、オーバーラップなしから25%のオーバーラップにすると、recall@5は85.2%から85.2%に移動しましたが、ベクトル数は41%増加し、RAMは5 MB増加しました。パターンはすべてのサイズで続きます:オーバーラップ設定間の広がり(±1.5ポイント)は、419クエリセットのノイズ範囲内ですが、コストはそうではありません。これはChromaのチャンキング評価と一致しており、200トークンでの単純な再帰的分割(オーバーラップなし)は88.1%の再現率を記録しました — LLM駆動型スプリッターの91.9%に数ポイント以内です — そして、ほとんどのRAGガイドで読まれる「常に10〜20%のオーバーラップを使用する」というアドバイスとは逆です。

正直な注意点:これは1つのコーパス、1つのモデル、そしてセクション見出しのようなクエリです。オーバーラップは、単一の事実が頻繁に境界をまたぐ場合に役立ちます — 長い途切れのない物語、構造のないトランスクリプト — そしてrecursiveはすでに段落と文の境界にカットをスナップしており、これは同じ作業の多くを行います。したがって、「オーバーラップなしで128〜256から始め、測定できる場合にのみオーバーラップを追加する」をデフォルトとして扱い、あなた自身のデータで以下のレシピで確認してください。

設定の比較

推測する必要はありませんし、2つのテーブルも必要ありません。1つのテーブルは、複数のモデルバックのベクトル列を、それぞれ独自の戦略で保持でき、すべて同じINSERTから同じフィールドに埋め込まれます。

CREATE TABLE ab (
  title text,
  body text,
  sent_256 float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
    chunk_strategy='sentence' max_tokens='256' overlap_tokens='32',
  rec_128 float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
    chunk_strategy='recursive' max_tokens='128' overlap_tokens='16'
);

コーパスを一度ロードし、各列に対して同じクエリを実行して比較します。例:

SELECT id, LENGTH(sent_256) AS sent_chunks, LENGTH(rec_128) AS rec_chunks FROM ab;
SELECT id, knn_dist() FROM ab WHERE knn(sent_256, 5, 'how do I rotate the replication certificate');
SELECT id, knn_dist() FROM ab WHERE knn(rec_128,  5, 'how do I rotate the replication certificate');

証明書セクションが最後にある短いランブックでは、sentence/256はドキュメント全体を単一のチャンクに収め、距離0.515で回答します。recursive/128はそれを2つに分割し、証明書段落を分離し、0.310で回答します。同じ行、同じモデル、同じクエリ — チャンクサイズのみが異なります。

信頼できる回答を持つ実際のクエリのデータセットを構築します — 50件でも十分です — そして、上記のマニュアルで行ったように、2〜3列間でrecall@5を比較します。その後、負けた列をALTER TABLE ... DROP COLUMNで削除し、勝った列を保持します。

レシピ

ドキュメントとヘルプセンター検索。長いMarkdownページ、ユーザーが自分の言葉で質問しています。構造でチャンク化し、それを検索します。

CREATE TABLE docs (
  url string,
  title text,
  body text,
  chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
    chunk_strategy='recursive' max_tokens='192'
);

from='title,body'に注意してください:フィールドはチャンク化される前に結合されるため、ページタイトルは最初のチャンクに配置され、コンテキストを提供します。この形状の完全なエンドツーエンドの例については、GitHubでのベクトル検索を参照してください。

サポートチケットとメールスレッド。スレッドは完全なメッセージのシーケンスです。途中でカットすると必要な事実が失われます。スレッドには自然な長さの制限がないため、チャンク数を制限します。

CREATE TABLE tickets (
  ticket_id bigint,
  customer string,
  status string,
  thread text,
  chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='thread'
    chunk_strategy='sentence' max_tokens='256' overlap_tokens='32' max_chunks='64'
);

SELECT ticket_id, knn_dist() FROM tickets
WHERE knn(chunks, 10, 'customer was charged twice after upgrading')
  AND status = 'closed';

フィルタリングは、単一ベクトル列の場合と同様に機能します。

契約書とポリシー文書。条項レベルの検索が目的です — 誰も「契約書」全体を求めているのではなく、免責条項を求めています。より小さいチャンク、十分なオーバーラップ:

chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='body'
  chunk_strategy='sentence' max_tokens='128' overlap_tokens='32'

製品カタログ(長い説明付き)。1つの製品は1つのトピックであり、カタログは大きいため、追加料金はかかりません。

embedding float_vector knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='name,description'
  chunk_strategy='mean'

RAG:LLMのための検索。取得されたものはすべてプロンプトに貼り付けられるため、チャンクはプロセとして読み取られるべきです — これは会話型検索のリトリーバル部分です。より大きなチャンク、文の境界、そしてより多くのチャンクを要求します。

chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='title,body'
  chunk_strategy='sentence' max_tokens='512' overlap_tokens='64'

既存のテーブルにチャンキングを追加する。マルチベクタ列はALTERでは追加できません — 既存の行にはベクトルがなく、まだバックフィルする方法がありません。単一ベクタ戦略は可能です。

ALTER TABLE docs ADD COLUMN v2 float_vector knn_type='hnsw' hnsw_similarity='cosine'
  model_name='Xenova/all-MiniLM-L6-v2' from='title,body' chunk_strategy='mean';

ALTER TABLE docs REBUILD EMBEDDINGS v2;

マルチベクタ列の場合は、新しいテーブルを列を配置して作成し、そこにインデックスを再作成します。

他のエンジンでの処理方法

すべてのベクトルエンジンは、現在、埋め込みを生成します。ドキュメントを分割してから実行するエンジンははるかに少なく、その中でもほとんどがパイプラインステージから組み立てることを要求します。

エンジンエンジン内埋め込みエンジン内チャンキング戦略検索時のドキュメントあたりの行数
Manticore Searchはい — ローカル + OpenAI / Voyage / Jinaはい — ベクトル列のchunk_strategytruncate, mean, fixed, recursive, sentenceはい、ネイティブ
Elasticsearchはい — 推論エンドポイントはいsentence (デフォルト), word, recursive (9.1+), noneはい — semantic_textがチャンクを隠す
OpenSearchはい — ML Commonsはい — 別個のインジェストプロセッサfixed_token_length, fixed_char_length, delimiterネストされたフィールド + ネストされたクエリが必要
Vespaはい — 内蔵埋め込みはい — インデックス式fixed-length, sentence, customはい
Azure AI Searchはい — 統合ベクトル化はい — スキルセットのSplitスキルpages (chars), sentencesいいえ — チャンクあたりの行数
PostgreSQL + pgaiはい — バックグラウンドワーカーはいcharacter, recursive characterいいえ — 別テーブル、結合と重複排除
Milvus / Zillizはい — 関数 (2.6+)いいえ — アプリ側——
Qdrantはい — クラウド推論いいえ — アプリ側——
Weaviateはい — ベクトル化モジュールいいえ — アプリ側——
Meilisearchはいいいえ — オープンリクエスト——
Typesenseはいいいえ — オープンリクエスト——
Apache Solrはい — LLMモジュール (9.8+)いいえ——
Pineconeはい — 統合推論いいえ — アプリ側——
MongoDB Atlasはい — 自動埋め込みいいえ — アプリ側——

チャンキング列のすべてのセルはソースへのリンクです。「はい」は機能のドキュメントへのリンクです。「アプリ側」は、そのベンダーがアプリケーションでのチャンキングに関する独自のガイダンスを公開していることを指します。これは、エンジン内オプションの代わりに公開されているものです。機能を見逃した場合、またはそれ以降にリリースされた場合は、お知らせくださいので修正します。

チェックバージョン - 2026年9月4日

その日の各製品の最新安定リリース:

製品バージョン
Elasticsearch9.5.3
OpenSearch3.8.0
Vespa8.750.13
Azure AI SearchREST API 2026-04-01
PostgreSQL + pgaiextension 0.11.2
Milvus / Zilliz2.6.23
Qdrant1.19.0
Weaviate1.38.13
Meilisearch1.53.1
Typesense30.2
Apache Solr10.0.0
Pinecone, MongoDB Atlasホストサービス、バージョンなし

2つの点が際立っています。

エンジン内チャンキングは依然としてまれです。 Milvus、Qdrant、Weaviate、Pinecone、MongoDB Atlas、Typesense、Meilisearch、そして(9.8 LLMモジュール以降)Apache Solrは、すべて埋め込みモデルを実行し、それぞれが4,000語のドキュメントを切り詰めることを喜んで行いますが、それを知らせることはありません。分割はあなたのアプリケーション内のあなたの問題であり、モデルがどのトークナイザーを使用しているかを知らないライブラリと言語で行われます。

チャンキングが存在する場合、配管は通常漏れます。 OpenSearchは、text_embeddingプロセッサにtext_chunkingプロセッサを供給し、ネストされたフィールドに書き込み、ネストされたクエリとスコアモードでクエリを実行することで、そこへ到達させます。Azure AI Searchは、Splitスキル、埋め込みスキル、インデックスプロジェクションを持つスキルセットを必要とします — そしてチャンクごとに1つの結果行を返すため、ドキュメントへのグループ化はあなたの責任です。pgai Vectorizerはチャンクを2番目のテーブルに書き込むため、すべてのクエリは結合とDISTINCT ONです。Elasticsearchのsemantic_textは、Manticoreのモデルに非常に近いです:推論エンドポイントでのチャンキング設定、フィールド内に隠されたチャンク、ドキュメントあたりのヒット1つ。

Manticoreは、より少ないサーフェスエリアで同じことを行います:戦略は列のオプションであり、チャンクは列の値であり、検索はドキュメントを返します。スタック全体をこの単一機能ではなく比較している場合は、ElasticsearchおよびTurbopufferとの比較も記述しました。

チャンキングが解決しないこと

チャンキングは1つの問題をうまく解決します — モデルのウィンドウより長いドキュメントは、もはや半分見えなくなることはありません。検索を完璧にするわけではなく、2つの既知のギャップは言及する価値があります。

チャンクはどこから来たのかを知りません。ドキュメントを分割すると、「ノードごとに1つずつ実行し、すべてのピアが同期されていることを確認する」という段落が得られますが、何がローテーションされているのか、またはどの製品に属しているのかを示す兆候はありません。Anthropicのコンテキストリトリーバルの研究は、これに数値を与えました:周囲のドキュメントの短い、チャンク固有の説明を埋め込む前に前置することで、トップ20のリトリーバル失敗が35%、組み合わせると49%削減されました。

Manticoreはこれをあなたのために行いません。FROMはフィールドをスペースで結合しますチャンク化の前に、したがってtitleを最初にリストすると、タイトルが分割されるテキストの先頭に配置されます — これは、最初のチャンクに配置され、それ以降のチャンクのみがそれを見ます。

-- 'title'が先頭に来るため、その単語はチャンク1に含まれます。チャンク2以降はそれらを見ません。
from='title,body'

すべてのチャンクにコンテキストを持たせたい場合は、挿入する前に自分で保存されているテキストに構築する必要があります — 例えば、bodyの各セクションの先頭に短い見出しを繰り返すなどです。現在、チャンクごとのプレフィックスオプションはありません。

チャンクの境界は、モデルがテキストを見る前に決定されます。 Manticoreは分割し、各部分を独立して埋め込みます — 標準的なアプローチであり、上記のテーブルのエンジン内チャンキングを持つすべてが実行することです。代わりに、遅延チャンキングと呼ばれるものはそれを反転させます:ドキュメント全体に対してまず長コンテキストモデルを実行し、次にトークン埋め込みをチャンクにプールします。これにより、各チャンクベクトルはドキュメントの残りのコンテキストを運びます。長コンテキストモデルとドキュメントあたりのより多くのコンピューティングが必要であり、Manticoreは現在それを行っていません。ドキュメントがクロス段落コンテキストに大きく依存している場合は、オプションが存在することを知っておく価値があります。

どちらのギャップも基本的な結果を変えません:長文ドキュメントの場合、チャンク化されたリトリーバルは切り詰められたリトリーバルを大幅に上回り、それに到達するには、1つの列にchunk_strategyを追加する必要があります。

制限と注意点

max_chunksはテキストを破棄します。 Manticoreはオーバーフローを最後の保持されたチャンクにマージし、モデルのウィンドウに切り詰めます。何も警告しません。これは外れ値に対するガードレールであり、全体的なメモリ節約方法ではありません。

リモートモデルはバイト単位でチャンク化し、トークン単位ではありません。 OpenAI、Voyage、Jinaにはローカルトークナイザーがないため、Manticoreは意図的に保守的な推定値であるトークンあたり3バイトにフォールバックします — チャンクはプロバイダーのキャップの下に着地し、それを超えることはありません。実際には、max_tokens='N'はN × 3バイトウィンドウになります。3,599バイトのドキュメントとfixed戦略を持つスタブエンドポイントに対して測定しました。

max_tokensバイトウィンドウ生成されたチャンク数
10030012
2006006
4001,2003

英語のプロセはトークンあたり約4バイトで実行されるため、リモートモデルでは要求した数値の約4分の1小さいチャンクが得られます — ローカルモデルの場合よりも約30%高いmax_tokensを設定して、同じ場所に到達するようにします。正確な境界が重要な場合は、ローカルモデルを使用してください。そこでは、分割はモデルの実際のトークン上で行われます。

マルチベクタ列はALTERでは追加できません。 モデルバックのfloat_vector_arrayに対するALTER TABLE ... ADD COLUMNおよびALTER TABLE ... REBUILD EMBEDDINGSは拒否されます。代わりにテーブルを再作成してください。どちらもfloat_vector、特にmeanでは正常に機能します。

チャンキングは自動埋め込みにのみ適用されます。自分で挿入したベクトルはそのまま保存されます — Manticoreは提供されたデータを再カットしません。model_nameなしのchunk_strategyは意図的にDDLエラーです。

embeddingsは予約語です。 EMBEDDINGSはDDLキーワード(ALTER TABLE ... REBUILD EMBEDDINGS)であるため、文字通りembeddingsという名前の列は、エスケープしない限り構文エラーです。その名前が必要な場合はエスケープを使用してください。

クエリはチャンク化されません。クエリは単一ベクトルとして全体が埋め込まれます。それが望ましいことです:チャンキングは、長いドキュメントを見つけやすくするために存在し、15語の質問を分割するためではありません。

DDLは、組み合わせが間違っている場合に、最初の挿入時ではなく、CREATE TABLE時に通知します。

mysql> CREATE TABLE t (title text, v float_vector ... chunk_strategy='sentence');
ERROR 1064: chunk_strategy='sentence' produces several vectors per document
            and requires a float_vector_array attribute

mysql> ... chunk_strategy='fixed' overlap_tokens='32');
ERROR 1064: overlap_tokens requires an explicit non-zero max_tokens

mysql> ... chunk_strategy='paragraph');
ERROR 1064: unknown chunk_strategy 'paragraph'; expected truncate, mean, fixed,
            recursive or sentence

mysql> ... chunk_strategy='truncate' max_tokens='128');
ERROR 1064: chunk_strategy='truncate' ignores max_tokens, overlap_tokens and max_chunks

試してみる

機能するチャンク化されたセマンティック検索への最も短いパス:

DROP TABLE IF EXISTS docs;

CREATE TABLE docs (
  title text,
  content text,
  chunks float_vector_array knn_type='hnsw' hnsw_similarity='cosine'
    model_name='Xenova/all-MiniLM-L6-v2' from='title,content'
    chunk_strategy='recursive' max_tokens='256' overlap_tokens='32'
);

INSERT INTO docs (id, title, content) VALUES
  (1, 'Backup and restore runbook', 'Nightly backups run at 02:00 UTC ... ');

SELECT id, title, knn_dist() FROM docs
WHERE knn(chunks, 5, 'how do I rotate the replication certificate');

手動でダウンロードするモデルなし、選択するスプリッターなし、維持するパイプラインなし。1つの列オプションで、ドキュメントの不可視だった部分が結果に表示され始めます。

完全な参照:チャンキング戦略とドキュメントあたりの複数のベクトルのマニュアル。質問とバグレポートはGitHubで受け付けています。