0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに: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専用」という認識はもう古い。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?