はじめに:pgvectorで詰んだ日のこと
「pgvectorで十分じゃん。Postgresにそのまま入れられるし、セットアップ5分だし。」
半年前の自分はそう思っていた。実際、小規模な社内RAGシステムをpgvectorで動かしていて、それなりに動いていた。
詰んだのは、ドキュメント数が数百万件を超えたあたりだった。
HNSWインデックスがメモリに乗り切らなくなったのだ。RAGのレスポンスタイムが秒単位になり、社内からクレームが来た。スケールアップしようにも、pgvectorのHNSWインデックスは分散できない。垂直スケールの限界にぶつかった。
そこで乗り換えたのがClickHouseだ。
この記事は、pgvectorからClickHouseへの移行記録と、AnthropicやOpenAIが「なぜペタバイト規模でClickHouseを選んだのか」が実感で分かるまでの話だ。
なぜClickHouseが「AI時代のDB」として語られるのか
先に言っておくと、ClickHouseはもともとベクトルDBではない。OLAPデータベースだ。大量の構造化データを爆速で集計するために作られた。
それがなぜAI/RAGの文脈で語られるようになったかというと、理由は2つある。
① ベクトル検索が2025年にGA(一般提供)になった
ClickHouseのベクトル検索機能の変遷
| バージョン | 出来事 |
|---|---|
| 22.9 | 実験的ベクトルインデックス初登場 |
| 23.8 | USearchライブラリ統合でANNが本格化 |
| 25.1 | ベクトルインデックス性能大幅改善 |
| 25.5 | ベータ。プレ/ポストフィルタリング対応 |
| 25.8 | GA。バイナリ量子化・インデックスのみ読み取り対応 |
2025年8月(v25.8)まで、ClickHouseのベクトル検索は実験的機能だった。今はプロダクション投入できる状態になっている。また25.8では相関サブクエリにも対応し、PostgreSQLからの移行コストも大幅に下がった。
② AnthropicもOpenAIもClickHouseを選んだ
- Anthropic:AI時代のオブザーバビリティをスケールさせるためにClickHouseを採用
- OpenAI:ペタバイト規模のオブザーバビリティにClickHouseを選択
2社ともベクトルDBのために使っているのではなく、「膨大なログ・メトリクスをリアルタイムに集計・分析する」ためにClickHouseを選んでいる。ここが本質だ。
pgvectorの「壁」は公開データで証明されている
ここは自分の実測ではなく、公開されているデータを正直に並べる。
pgvectorのHNSWはメモリ食いすぎ問題
1536次元のベクトル(OpenAI text-embedding-3-small)を使った場合の実測データがGitHubのIssueや技術記事に残っている。
| 件数 | HNSWインデックスのサイズ | 必要RAM(ビルド時) | クエリ速度 |
|---|---|---|---|
| 100万件 | 〜12〜18 GB | 〜1.5 GB | サブミリ秒(RAM内) |
| 500万件 | 〜60〜90 GB | 8〜16 GB | 遅延増加(RAM超え始め) |
| 1000万件 | 80〜120 GB | それ以上 | 数秒〜タイムアウト |
pgvectorのGitHub Issue #843 より:16GB RAMのマシンで500万件のHNSWインデックス構築を試みたところOOMが発生。また実際のPostgreSQLログには以下が記録された。
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 5,908,085 tuples
DETAIL: Building will take significantly more time.
HINT: Increase maintenance_work_mem to speed up builds.
HNSWインデックスがRAMを超えると何が起きるか。DEV Communityの検証記事によれば、サブミリ秒だったクエリが数秒単位になる。ディスクI/Oにフォールバックするためだ。チューニングでどうにかなる問題ではなく、容量の問題だ。
ClickHouseは同じハードで何倍速いか
ClickBenchベンチマーク(1億行のWebアナリティクスデータ、43クエリ)の結果:同一ハードウェアで、ClickHouseはPostgreSQLより10〜100倍高速。
また個人の実測記事(fiveonefour.com)では、1000万行のデータに対して最初のクエリが4,191msかかったところ、スキーマ設計を正しく直しただけで 75ms まで改善した(50倍以上)。
pgvectorが1000万件で数秒〜タイムアウト(しかもRAM増強が必要)になっていたのに対し、ClickHouseは同じ1000万件をミリ秒クラスでさばき切る。ここで完全に勝負がついた。
ただしClickHouseが速いのはOLAP(集計・分析)クエリの話。ポイントルックアップ(主キーで1件取得)はPostgreSQLのほうが速い。用途を間違えないこと。
pgvectorとClickHouseの正直な比較
| 項目 | pgvector | ClickHouse |
|---|---|---|
| 種別 | PostgreSQL拡張 | OLAPデータベース |
| ベクトル検索 | HNSWまたはIVFFlat | HNSW(v25.8でGA) |
| 量子化 | なし(Float32のみ) | bf16, i8, b1(バイナリ)対応 |
| スケール | 垂直スケール中心 | 水平スケール対応 |
| SQLサポート | PostgreSQL互換 | フルSQL(v25.8で相関サブクエリ対応) |
| メタデータフィルタリング | 可(遅くなる) | プレ/ポストフィルタリング対応 |
| セットアップ難易度 | ★(簡単) | ★★★(スキーマ設計が必要) |
ClickHouseでRAGを構築する
テーブル設計(重要)
ClickHouseはスキーマ設計が命だ。最初に間違えると後で詰む。
CREATE TABLE documents
(
id UUID DEFAULT generateUUIDv4(),
title String,
content String,
source LowCardinality(String),
category LowCardinality(String),
created_at DateTime DEFAULT now(),
updated_at DateTime DEFAULT now(),
-- 1536次元(text-embedding-3-small のデフォルト次元数)
embedding Array(Float32),
-- HNSWインデックス(v25.8以降でGA)
-- ※ 64・512 はチューニング値。デフォルトは 32・128
INDEX embedding_idx embedding
TYPE vector_similarity('hnsw', 'cosineDistance', 1536, 'bf16', 64, 512)
)
ENGINE = MergeTree
-- よく使う絞り込み条件をソートキーに入れる(ベクトルカラムは入れない)
ORDER BY (source, category, created_at)
PARTITION BY toYYYYMM(created_at);
ORDER BY にベクトルカラムを入れてはいけない。パーティションキーや日付・カテゴリなど、メタデータを入れる。これがClickHouseのベクトル検索設計の基本。
Pythonからの埋め込み挿入
import clickhouse_connect
import openai
from typing import List
client = clickhouse_connect.get_client(
host='localhost',
port=8123,
username='default',
password=''
)
oai = openai.OpenAI()
def embed(texts: List[str]) -> List[List[float]]:
"""OpenAIのembeddingモデルでベクトル化"""
resp = oai.embeddings.create(
model="text-embedding-3-small",
input=texts
)
return [r.embedding for r in resp.data]
def insert_documents(docs: List[dict]):
"""ClickHouseにドキュメントをバッチ挿入"""
texts = [d["content"] for d in docs]
embeddings = embed(texts)
rows = [
[
doc["title"],
doc["content"],
doc.get("source", "unknown"),
doc.get("category", "general"),
emb,
]
for doc, emb in zip(docs, embeddings)
]
client.insert(
"documents",
rows,
column_names=["title", "content", "source", "category", "embedding"],
)
RAGの検索クエリ
ClickHouseのHNSWインデックスが有効になるのは ORDER BY <距離関数>(...) LIMIT N の形のみ。WHERE cosineDistance(...) < 0.3 と書くと全件スキャンになりインデックスが使われない。
ベクトル検索と集計を組み合わせる場合はサブクエリで2ステップに分ける。
def search_similar(query: str, top_k: int = 5, category: str = None) -> List[dict]:
query_vec = embed([query])[0]
vec_str = "[" + ", ".join(map(str, query_vec)) + "]"
# カテゴリフィルタ(※本番はパラメータ化クエリを使うこと)
category_filter = f"AND category = '{category}'" if category else ""
sql = f"""
WITH {vec_str} AS query_vector
SELECT
id, title, content, category,
cosineDistance(embedding, query_vector) AS distance
FROM documents
WHERE 1=1 {category_filter}
ORDER BY cosineDistance(embedding, query_vector) ASC
LIMIT {top_k}
"""
result = client.query(sql)
return [
{
"id": row[0], "title": row[1],
"content": row[2], "category": row[3],
"score": 1 - row[4],
}
for row in result.result_rows
]
ベクトル検索の結果でカテゴリ集計もしたい場合(2ステップ版)
-- Step1:HNSWで候補ID絞り込み
WITH (
SELECT groupArray(id)
FROM (
WITH [0.12, 0.45, -0.23, ...] AS qv
SELECT id FROM documents
WHERE created_at >= now() - INTERVAL 30 DAY
ORDER BY cosineDistance(embedding, qv) ASC
LIMIT 1000
)
) AS candidate_ids,
[0.12, 0.45, -0.23, ...] AS query_vector
-- Step2:候補IDでカテゴリ集計
SELECT
category,
count() AS doc_count,
avg(cosineDistance(embedding, query_vector)) AS avg_distance,
groupArray(5)(title) AS top_titles
FROM documents
WHERE id IN candidate_ids
GROUP BY category
ORDER BY avg_distance ASC;
Gemini連携でRAGを完成させる
import google.generativeai as genai
genai.configure(api_key="YOUR_KEY")
model = genai.GenerativeModel("gemini-3.5-flash")
def ask(question: str) -> str:
docs = search_similar(question, top_k=5)
context = "\n\n".join([f"[{d['title']}]\n{d['content']}" for d in docs])
prompt = f"""
以下のドキュメントを参考に質問に答えてください。
ドキュメント:
{context}
質問: {question}
"""
return model.generate_content(prompt).text
移行してハマった失敗3選
失敗1:v25.8への移行でインデックスが全部壊れた
v25.8でAnnoyとuSearchインデックスが削除された。既存テーブルにこれらのインデックスが残っているとアップグレード自体がブロックされる。
Error: Table uses removed vector index type 'annoy'.
Drop the index before upgrading.
v25.8へのアップグレード前に必ず確認すること。ダウングレードはサポートされていないため、本番適用前にフォークしたサービスで動作検証を推奨。
-- 既存の annoy/usearch インデックスを確認
SELECT table, name, type
FROM system.data_skipping_indices
WHERE type IN ('annoy', 'usearch');
-- v25.8以降はHNSWに置き換える
ALTER TABLE documents DROP INDEX old_annoy_idx;
失敗2:HNSWインデックスのメモリ試算を間違えた
1M件 × 1536次元(text-embedding-3-small)でHNSWインデックスを張ろうとしたら、公式ドキュメントの計算式で必要RAMは以下の通りだ。
mv(ベクトル本体)= 1,000,000 × 1536 × 2(BFloat16)= 3,072 MB
mg(グラフ) = 1,000,000 × 64 × 4 × 2 = 512 MB
合計 ≈ 3.5 GB
インデックスだけで常時 3.5 GB を消費する。他クエリ用のメモリが圧迫されてクエリが死ぬ。量子化(b1:バイナリ)でメモリを約4分の1に削減できるが、精度とのトレードオフがある。
-- b1(バイナリ量子化)でメモリを大幅削減
INDEX embedding_idx embedding
TYPE vector_similarity('hnsw', 'cosineDistance', 1536, 'b1', 64, 512)
失敗3:BulkInsert中のHNSWインデックス再構築で詰んだ
大量データを一気に入れたらINSERTが異常に遅くなった。原因はMerge時にHNSWインデックスが毎回再構築されること。
-- 大量挿入前にMergeを止める
SYSTEM STOP MERGES documents;
-- 全データ投入
-- Mergeを再開(インデックスはここで1回だけ構築される)
SYSTEM START MERGES documents;
OPTIMIZE TABLE documents FINAL;
pgvectorを使い続けるべきケースも正直に書く
| ケース | 推奨 |
|---|---|
| ドキュメント数が100万件以下 | pgvector(過剰スペック不要) |
| トランザクション整合性が必要 | pgvector(ClickHouseはOLAP向け) |
| 既存のPostgreSQLに乗せたい | pgvector(セットアップコスト重視) |
| ベクトル + 集計 + 大規模フィルタリング | ClickHouse |
| ペタバイト級のデータ | ClickHouse |
| ベクトル検索 × リアルタイムログ分析 | ClickHouse |
pgvectorからClickHouseへの移行を検討するのは、「ベクトル検索を他の分析クエリと組み合わせたくなった時」か「データが数百万件を超えた時」が目安だ。
まとめ
AnthropicとOpenAIがClickHouseを選んだのは、ベクトル検索のためだけではない。
「膨大なイベントログ × メタデータ × ベクトル検索を、1つのDBで爆速に処理できる」 という特性が、AIシステムのオブザーバビリティとデータ基盤に完璧にハマったからだ。
個人開発・中小規模なら、最初はpgvectorで十分。ただし、スケールの壁に当たった時、あるいはベクトル検索を他のアナリティクスと組み合わせたくなった時は、ClickHouseへの移行を真剣に検討する価値がある。
v25.8でベクトル検索がGAになった今、「ClickHouseはOLAP専用」という認識はもう古い。
参考
- ClickHouse ベクトル検索ドキュメント
- Vector Search with ClickHouse - Part 1
- pgvector GitHub Issue #843:5M件でOOM
- pgvector GitHub Issue #844:HNSWグラフがメモリに乗らなくなったログ
- Scaling pgvector: Memory, Quantization, and Index Build Strategies
- PostgreSQL vs ClickHouse: What I Learned From My First Database Benchmark
- AnthropicがClickHouseを使ってAI時代のオブザーバビリティをスケールさせる方法
- なぜOpenAIはペタバイト規模のオブザーバビリティにClickHouseを選んだのか