TiDB ベクトル検索の実践(2026):VECTOR インデックス、ハイブリッド検索とRAG構築

数据库(更新: 2026年7月20日)

なぜこの記事を読むべきか

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 BYWHERE で使用可能で、通常のカラムと全く同じ扱いになる。


ベクトルインデックス: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;

実行計画

  1. オプティマイザが VECTOR INDEX を認識し、ANN リコールに使用(候補セット生成、サイズは LIMIT × ef_search 程度)。
  2. WHERE 句で候補セットをフィルタ——条件不一致行を破棄。
  3. 生き残った行を 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. 再現率と遅延のトレードオフ

  • mef_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 回の実行。

推奨する学習パス

  1. tiup playground でローカルに TiDB を起動;
  2. VECTOR カラム+HNSW インデックス付きのテーブルを作成;
  3. 公開データセット(Hugging Face の sentence-transformers/all-MiniLM-L6-v2 Wikipedia 埋め込み等)から 10 万件をインポート;
  4. 純粋ベクトル検索、純粋 SQL フィルタ、ハイブリッドクエリの性能と再現率をテスト;
  5. EXPLAIN ANALYZE で実行計画を見る——オプティマイザがベクトルインデックスと SQL フィルタをどう融合するか体感せよ。
#TiDB#向量搜索#向量索引#混合检索#HTAP#RAG