TiDB ベクトル検索の実践(2026):VECTOR インデックス、ハイブリッド検索とRAG構築
なぜこの記事を読むべきか
LLM 時代において、ベクトル検索は「任意の実験機能」から「すべてのアプリの基盤」へと変貌した。しかし専用のベクトルデータベース(Milvus、Pinecone、Weaviate)と業務データベースを別々に運用すると、次の三重苦が発生する。
- 二重書き込み:1 件の業務レコード挿入に両方の DB への書き込みが必要で、2 相コミットや try-catch でも不整合が生じうる。
- 同期遅延:業務データが更新された後、ベクトル側のインデックス再構築にタイムラグが生じ、古い検索結果が返る。
- 運用コストの倍増:2 つのシステムは 2 倍のデプロイ、監視、バックアップ、容量計画を意味する。
TiDB は 7.5 からベクトル機能を分散 SQL データベースにネイティブ統合した。もう 2 つのシステムを同期する必要はない——TiDB の中に OLTP・OLAP・ベクトル検索のすべてがある。
本記事で扱う内容:VECTOR 型の使い方、HNSW / IVF インデックスの原理と選び方、ハイブリッドクエリの実装パターン、完全な RAG パイプライン、パフォーマンスチューニングの要点。
コアコンセプト
VECTOR データ型
TiDB は新しいデータ型 VECTOR(n) を導入した。n は次元数(最大 16384)。ベクトル値は JSON 配列リテラルで表現する。
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(256),
description TEXT,
embedding VECTOR(1536),
category VARCHAR(64),
price DECIMAL(10,2),
created_at DATETIME DEFAULT NOW()
);
ベクトルの挿入は通常のカラムと同じ:
INSERT INTO products (name, description, embedding, category, price)
VALUES (
'ワイヤレスノイズキャンセリングヘッドホン',
'40mm ドライバー、アクティブノイズキャンセリング、30 時間バッテリー',
'[0.0123, -0.0456, 0.0789, ...]',
'家電',
29900
);
ベクトル距離関数
TiDB は 4 つの距離関数を提供する。
| 関数 | 意味 | 主な用途 |
|---|---|---|
VEC_COSINE_DISTANCE(a, b) |
コサイン距離(1 − コサイン類似度) | テキスト意味検索(最も一般的) |
VEC_L2_DISTANCE(a, b) |
ユークリッド距離(二乗) | 画像・幾何特徴 |
VEC_INNER_PRODUCT(a, b) |
負の内積(−⟨a,b⟩) | 正規化時のコサイン等価 |
VEC_L1_DISTANCE(a, b) |
マンハッタン距離 | スパースベクトル・特殊用途 |
すべての距離関数は ORDER BY と WHERE で使用可能で、通常のカラムと全く同じ扱いになる。
ベクトルインデックス:HNSW と IVF
インデックスがなければ、すべての類似度検索が全表スキャンと距離計算になる——数百万件規模では許容できない。TiDB は 2 種類の近似最近傍(ANN)インデックスを提供する。
HNSW
HNSW は業界で最も評価の高い低遅延 ANN インデックス構造である。多層の「スキップリストグラフ」を構築する:
- 最下層(Layer 0):全ベクトルを含み、エッジが密で、高再現率を担保。
- 上層:確率でノードがランダムに昇格。エッジは疎で、高速な長距離ジャンプを可能にする。
- クエリ時:最上層から開始し貪欲に降下——各層で局所近傍エッジを使って真の最近傍に急速に接近する。
HNSW の特徴:
- クエリが速い:数百万件で top-10 ANN が通常 1–3 ms。
- メモリが多い:グラフ構造(ノードあたり 50–100 エッジ)を保存。追加メモリは生ベクトルの 2–3 倍。
- 構築が遅い:1 ノードずつ挿入しエッジを生成。時間計算量 O(N·logN·M)。
- 高 QPS オンラインサービス向け。
ALTER TABLE products
ADD VECTOR INDEX idx_embedding (embedding)
WITH (
distance = 'cosine',
type = 'hnsw',
m = 16,
ef_construction = 64
);
IVF
IVF はクラスタリングの考え方を用いる:全ベクトルを k-means で複数のクラスタに分割。クエリ時は最近傍のクラスタをまず見つけ、候補クラスタ内でのみ正確な距離を計算する。
- 構築が速い:k-means クラスタリング+割り当て。HNSW よりはるかに少ない計算量。
- メモリが少ない:クラスタ重心(数十 KB)+転置リスト(ID 配列)のみ。追加メモリはほぼゼロ。
- クエリがやや遅い:複数クラスタを走査。遅延は通常 HNSW の 2–5 倍。
- 書き込み頻度が高いシナリオ向け。
ALTER TABLE products
ADD VECTOR INDEX idx_embedding (embedding)
WITH (
distance = 'cosine',
type = 'ivf',
nlist = 256
);
HNSW vs IVF 判断表
| 次元 | HNSW | IVF |
|---|---|---|
| クエリ遅延(p50) | 1–3 ms | 5–15 ms |
| 構築時間 | 遅い(IVF の数倍) | 速い |
| 追加メモリ | 生ベクトルの 2–3 倍 | 最小(重心+リスト) |
| 再現率(top-10) | 95–99% | 90–97%(nlist 依存) |
| 増分挿入 | 可(グラフが劣化しうる) | 可(分布がずれうる) |
| 推奨シーン | オンライン、低遅延、高 QPS | 一括書込、メモリ制約、プロトタイプ |
ハイブリッド検索:TiDB の最大の武器
現実の RAG は「質問に最も似た 5 つのチャンクを見つける」だけではない。本当の要件は例えば:
- 「直近 7 日以内に公開された、'バックエンド' カテゴリに属する、質問に最も関連する 5 つの記事を探せ」
- 「¥10,000–50,000の価格帯で在庫ありの、検索語に最も類似した 10 商品を探せ」
ここで必要なのは 正確な SQL フィルタ+ベクトル類似度ソート を 1 つの操作で完結させること——TiDB のオプティマイザは両者を 1 つの SQL 内で統一スケジューリングする。
SELECT id, name, description, price,
VEC_COSINE_DISTANCE(embedding, :query_embedding) AS dist
FROM products
WHERE category = '家電'
AND price BETWEEN 10000 AND 50000
AND status = 'active'
ORDER BY dist
LIMIT 10;
実行計画:
- オプティマイザが
VECTOR INDEXを認識し、ANN リコールに使用(候補セット生成、サイズはLIMIT × ef_search程度)。 WHERE句で候補セットをフィルタ——条件不一致行を破棄。- 生き残った行を
distでソート、上位 10 件を返却。
重要なポイント:フィルタはベクトルリコールの後、正確な距離ソートの前に実行される。つまり「全表の距離を計算してからフィルタ」ではなく、「インデックスで意味的に絞り → WHERE で精査 → 最後にソート」——数百万件の遅延を秒からミリ秒に下げる。
RAG パイプライン構築
ステップ 1:チャンク分割と埋め込み
from openai import OpenAI
import tiktoken
client = OpenAI()
def chunk_text(text: str, max_tokens: int = 512) -> list[str]:
enc = tiktoken.get_encoding("cl100k_base")
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
chunks, current, current_tokens = [], "", 0
for p in paragraphs:
tokens = len(enc.encode(p))
if current_tokens + tokens > max_tokens:
chunks.append(current)
current, current_tokens = p, tokens
else:
current += "\n\n" + p
current_tokens += tokens
if current:
chunks.append(current)
return chunks
def embed(texts: list[str], model="text-embedding-3-small") -> list[list[float]]:
resp = client.embeddings.create(input=texts, model=model)
return [r.embedding for r in resp.data]
ステップ 2:TiDB に保存
import pymysql
conn = pymysql.connect(host="127.0.0.1", user="root", database="rag_db")
for i, (chunk, emb) in enumerate(zip(chunks, embeddings)):
emb_str = "[" + ",".join(f"{v:.6f}" for v in emb) + "]"
conn.cursor().execute(
"INSERT INTO documents (id, content, embedding, category) VALUES (%s, %s, %s, %s)",
(i, chunk, emb_str, "tech_blog")
)
conn.commit()
ステップ 3:インデックス構築
ALTER TABLE documents
ADD VECTOR INDEX idx_doc_embedding (embedding)
WITH (distance = 'cosine', type = 'hnsw', m = 16, ef_construction = 64);
大規模インポートのコツ:先にデータを入れ、後からインデックスを作成——行単位のオンライン構築より圧倒的に速い。
ステップ 4:RAG 検索
def rag_query(question: str, category: str | None = None, top_k: int = 5) -> str:
q_emb = embed([question])[0]
q_emb_str = "[" + ",".join(f"{v:.6f}" for v in q_emb) + "]"
if category:
rows = conn.cursor().execute(
"""SELECT content, VEC_COSINE_DISTANCE(embedding, %s) AS dist
FROM documents WHERE category = %s ORDER BY dist LIMIT %s""",
(q_emb_str, category, top_k)
).fetchall()
else:
rows = conn.cursor().execute(
"""SELECT content, VEC_COSINE_DISTANCE(embedding, %s) AS dist
FROM documents ORDER BY dist LIMIT %s""",
(q_emb_str, top_k)
).fetchall()
return "\n\n---\n\n".join(r[0] for r in rows)
ステップ 5:エンドツーエンド Q&A
context = rag_query("TiDB のベクトルインデックスはどう選べばいい?", category="tech_blog")
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": f"以下の参考資料を使って回答:\n\n{context}"},
{"role": "user", "content": "TiDB のベクトルインデックスはどう選べばいい?"}
]
)
print(response.choices[0].message.content)
文書取り込みから RAG Q&A まで、追加のベクトルデータベースは不要——TiDB がすべてを処理する。
パフォーマンスとチューニング
1. 次元数とストレージ
VECTOR(1536)は 1 行 6 KB(1536 × 4 バイト float32)。100 万行= 6 GB の生ベクトル、HNSW インデックス込みで約 15–20 GB。- 精度要件が中程度であれば、Matryoshka 埋め込みや PCA で 256–512 次元に削減——ストレージとメモリを半分以上削減できる。
2. 一括インポート
LOAD DATAまたはバッチ INSERT(500–1000 行/バッチ)を使用。autocommitを無効にし、トランザクション単位でコミット。- データを先にロードし、後でインデックスを作成。
tidb_ddl_reorg_batch_sizeを引き上げてインデックス構築を高速化。
3. クエリチューニング
LIMITとインデックスリコール:ベクトルインデックスの内部検索幅はLIMITに比例する。大きいLIMITはより広範な ANN 検索をトリガーする。内部検索数はおおむねLIMITの 3–5 倍。- カバリングインデックス:少数のカラムのみを参照するクエリでは、カバリングインデックスでベクトルインデックススキャン後のテーブルルックアップを回避可能。
- パーティションプルーニング:時間やカテゴリで自然に分割されている場合、ベクトルインデックス+パーティションプルーニングで検索空間を劇的に縮小。
4. 再現率と遅延のトレードオフ
mとef_constructionが大きいほどインデックス品質と再現率が向上するが、構築時間とメモリが増加。ef_search(検索幅)はクエリ時に動的調整可能。値を上げると再現率向上、遅延増加。
SET SESSION tidb_vector_index_ef_search = 40;
SELECT ... ORDER BY VEC_COSINE_DISTANCE(embedding, '[...]') LIMIT 10;
他ベクトル DB との比較
| 次元 | TiDB | Milvus | Pinecone | pgvector |
|---|---|---|---|---|
| ハイブリッド検索(ベクトル+SQL) | ✅ ネイティブ 1 SQL | ✅ 一部 | ❌(アプリ層) | ✅ ネイティブ |
| ACID トランザクション | ✅ 分散 | ❌ | ❌ | ✅ 単一ノード |
| 水平スケール | ✅ ネイティブ | ✅ | ✅(SaaS) | ❌ |
| SQL 互換性 | ✅ MySQL 互換 | ❌ | ❌ | ✅ PostgreSQL |
| デプロイ複雑度 | 中(分散) | 中〜高 | 低(SaaS) | 低(PG 拡張) |
| ベクトル検索遅延(100万件) | 2–15 ms | 2–10 ms | 5–20 ms(ネットワーク) | 3–10 ms |
TiDB の独自の利点:トランザクション一貫性とベクトル検索能力の二者択一が不要——同一クラスタ、同一 SQL、同一トランザクション。これがネイティブ統合の真の価値だ。
よくある質問(FAQ)
Q1:TiDB のベクトルインデックスと pgvector の本質的な違いは?
両者とも SQL+ベクトルのハイブリッドクエリに対応。決定的な違い:TiDB はネイティブ分散設計——pgvector のインデックスは単一ノード。データ量と QPS が単一マシンを超えたら分散レイヤ(Citus+pgvector 等)が必要。TiDB はネイティブに水平拡張する。
Q2:専用ベクトル DB を使うべきケースは?
「ベクトルを保存し、ベクトルを検索するだけ」——SQL フィルタ不要、トランザクション不要、業務テーブルとの JOIN 不要——の純粋なベクトル検索なら、専用ベクトル DB(Milvus、Qdrant)の方が深く最適化されている。しかし「ベクトル検索結果が業務ビューの一部」や「ベクトル検索+正確なフィルタ+トランザクション操作が同じリクエスト内で必要」になった瞬間、TiDB の優位性が明らかになる。
Q3:分散 TiDB でベクトルインデックスはどう動く?
各 TiKV ノードが担当 Region 内でローカルベクトルインデックスを構築。クエリ時、TiDB は ANN 検索を各 TiKV ノードにプッシュダウンして並列実行し、TiDB レイヤで集約ソート——TiDB の MPP アーキテクチャの自然な利点である。
Q4:pgvector から TiDB への移行は複雑?
複雑ではない。両者とも VECTOR(n)+JSON 配列リテラル形式。CSV にエクスポートし、LOAD DATA で TiDB にインポート。主な適応点はインデックス作成構文(TiDB の WITH 句のパラメータ名が一部異なる)。
Q5:本番で HNSW は大量挿入で劣化する?
する。定期的に ALTER TABLE ... REBUILD VECTOR INDEX でグラフ品質を回復できる。頻繁な挿入+リアルタイムクエリがワークロードなら、IVF(挿入耐性が高い)を使うか、定期再構築戦略を設計すること。
結論
TiDB のベクトル検索は、ベンチマークで専用ベクトル DB を倒すためのものではない。トランザクションデータベースとベクトルストアの間の同期ギャップをなくすためのものだ。「注文を書き、在庫を確認し、商品を検索する」を 1 つのシステムで行うとき、ベクトル検索を別サービスに任せる必要はない——1 つの SQL、1 回の実行。
推奨する学習パス:
tiup playgroundでローカルに TiDB を起動;- VECTOR カラム+HNSW インデックス付きのテーブルを作成;
- 公開データセット(Hugging Face の
sentence-transformers/all-MiniLM-L6-v2Wikipedia 埋め込み等)から 10 万件をインポート; - 純粋ベクトル検索、純粋 SQL フィルタ、ハイブリッドクエリの性能と再現率をテスト;
EXPLAIN ANALYZEで実行計画を見る——オプティマイザがベクトルインデックスと SQL フィルタをどう融合するか体感せよ。