この記事は、AI時代のデータベースについて自分なりに考えた記事です。ClickHouseを「ベクトル検索もできる高速OLAP」ではなく、「RAGやLLMアプリの失敗を後から見直すためのDB」として見ています。
本文中のClickHouse機能は、2026年6月1日に公式ドキュメントと公式ブログを確認しました。SQLは設計例です。手元環境にClickHouseを入れての実測値は載せていません。
はじめに
RAGを作ると、最初はだいたいベクトル検索に目が行きます。
分かります。
RAGを作るなら、文章をembeddingにして、近い文書を検索して、LLMに渡す。
この流れはかなり自然です。
でも、実際にサービスとして考えると、ベクトル検索だけではRAGは直せません。
たとえば、ユーザーから「この回答、違います」と言われたとします。
そのときに見たいのは、近いベクトルが取れたかどうかだけではありません。
- ユーザーが何を聞いたか
- どの文書が検索されたか
- その文書は本当に回答に使われたか
- LLMの回答はユーザーの役に立ったか
- 失敗した質問にはどんな傾向があるか
- モデルを変えたあとに品質やコストがどう変わったか
ここまで見るなら、欲しいのは「近いベクトルを探す場所」だけではありません。
大量のイベントを速く集計できて、検索ログとユーザー行動をつなげて見られる場所が必要です。
そこでClickHouseです。
自分がこの記事で言いたいのは、ClickHouseを「ベクトルDBの代わり」として見るより、「AIアプリの失敗を分析する場所」として見るとかなり強い、という話です。
ClickHouseを入れればRAGが勝手に賢くなる、という話ではありません。プロンプト、文書分割、embeddingモデル、評価データ、UI、運用の全部が効きます。この記事では、DB側から見た設計の話に絞ります。
この記事で言いたいこと
- AI時代のDBで増える仕事は、ベクトル検索だけではなくログ分析にもある
- RAGは、検索候補、距離、順位、回答、評価を残さないと改善しにくい
- ClickHouseは、ベクトル検索と大量イベント集計を同じSQLで扱えるので、RAGの観測台として使いやすい
- PostgreSQLやMySQLを置き換えるのではなく、分析と観測をClickHouseに逃がすのが現実的
AI時代のDBに増えた仕事
昔からDBは、データを保存して検索する場所でした。
今もそこは変わりません。
ただ、LLMアプリではDBに入るものが増えました。
たとえば普通のWebサービスなら、ユーザー、投稿、注文、決済、設定みたいなテーブルを考えます。
RAGを入れると、ここに別のデータが乗ってきます。
| 種類 | 例 |
|---|---|
| 元データ | ドキュメント、FAQ、記事、社内Wiki |
| 検索用データ | embedding、文書チャンク、メタデータ |
| 実行ログ | 質問、検索結果、プロンプト、回答 |
| 評価データ | thumbs up/down、再質問、離脱、手動評価 |
| コスト情報 | token数、モデル名、レイテンシ、リトライ回数 |
この中で一番派手なのはembeddingです。
でも、運用で効くのはむしろログです。
検索が外れたのか。
検索は当たったけどLLMが使わなかったのか。
そもそも文書が足りないのか。
ユーザーの聞き方と文書の粒度が合っていないのか。
ここを見られないと、RAGの改善が感覚になります。
「なんか回答が微妙」から先に進めません。
ClickHouseをどこに置くか
自分なら、最初から全部をClickHouseに寄せるより、役割を分けます。
| 役割 | 使うもの |
|---|---|
| ユーザー、課金、権限など | PostgreSQLやMySQL |
| 文書の原本 | オブジェクトストレージや既存DB |
| 文書チャンクとembedding | ClickHouse |
| 検索ログ、LLM実行ログ、評価ログ | ClickHouse |
| 管理画面の重い集計 | ClickHouse |
PostgreSQLやMySQLを捨てる話ではありません。
むしろ、トランザクションが大事な部分は今まで通り普通のRDBに置きたいです。
ClickHouseは、後から大量に読むところに置きます。
RAGだと、検索候補、実行ログ、評価ログ、分析画面あたりです。
ClickHouse公式でも、PostgreSQLをシステムの記録元にし、分析をClickHouseに任せる構成が紹介されています。
この考え方はRAGにも合います。
最小構成のテーブルを考える
たとえば、社内FAQや技術記事をRAGで検索するなら、まず文書チャンクをこう置けます。
CREATE TABLE rag_chunks
(
chunk_id UUID,
document_id UUID,
title String,
url String,
section String,
body String,
document_version String,
embedding_model LowCardinality(String),
embedding Array(Float32),
updated_at DateTime
)
ENGINE = MergeTree
ORDER BY (document_id, section, chunk_id);
embeddingはArray(Float32)で持ちます。
ClickHouseではL2DistanceやcosineDistanceのような距離関数を使って、ベクトル同士の近さをSQLで計算できます。
検索はこういう形です。
WITH {query_embedding:Array(Float32)} AS query_embedding
SELECT
chunk_id,
title,
url,
section,
body,
cosineDistance(embedding, query_embedding) AS distance
FROM rag_chunks
ORDER BY distance ASC
LIMIT 8;
まずはこれで十分です。
最初から巨大な専用構成を組まなくても、SQLで検索できる状態にしておくと試行錯誤しやすいです。
データが増えて線形検索が重くなってきたら、HNSWなどの近似検索を検討します。
ClickHouseの動画でも、HNSWのvector similarity indexを使って、線形検索から近似検索へ移る流れが紹介されています。
ClickHouse公式ドキュメントでは、vector similarity indexはClickHouse 25.8以降で利用可能とされています。古いバージョンで動かす場合は、まず利用中のClickHouseのバージョンを確認してください。
たとえば1536次元のembeddingなら、こういう形です。
CREATE TABLE rag_chunks
(
chunk_id UUID,
document_id UUID,
title String,
url String,
section String,
body String,
document_version String,
embedding_model LowCardinality(String),
embedding Array(Float32),
updated_at DateTime,
CONSTRAINT embedding_length CHECK length(embedding) = 1536,
INDEX embedding_hnsw embedding TYPE vector_similarity('hnsw', 'cosineDistance', 1536)
)
ENGINE = MergeTree
ORDER BY (document_id, section, chunk_id);
後から足すならこうです。
ALTER TABLE rag_chunks
ADD INDEX embedding_hnsw embedding
TYPE vector_similarity('hnsw', 'cosineDistance', 1536);
ALTER TABLE rag_chunks
MATERIALIZE INDEX embedding_hnsw
SETTINGS mutations_sync = 2;
ベクトル検索は、速ければ良いというものでもありません。近似検索は速度と精度の交換です。まずは少ないデータで検索結果を目で見て、評価用の質問セットを作ってから速くした方が失敗しにくいです。L2DistanceとcosineDistanceでは値が小さいほど近いので、ORDER BY ... ASCで使います。
また、ClickHouse公式ドキュメントでは、vector similarity indexは検索時にメモリへロードされると説明されています。大きいembeddingを大量に持つなら、indexを貼れば全部解決、とは考えない方がよさそうです。
RAGで本当に欲しいのは検索ログ
自分がClickHouseを使いたいと思う一番の理由は、検索ログです。
RAGは「検索して答える」だけならデモで終わります。
運用では、検索と回答の間にある失敗を見たいです。
たとえばイベントをこう置きます。
CREATE TABLE rag_events
(
event_time DateTime64(3),
request_id UUID,
user_id String,
session_id String,
model String,
prompt_version LowCardinality(String),
question String,
answer String,
input_tokens UInt32,
output_tokens UInt32,
latency_ms UInt32,
status LowCardinality(String),
feedback LowCardinality(String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, request_id);
検索されたチャンクも別で持ちます。
CREATE TABLE rag_retrievals
(
event_time DateTime64(3),
request_id UUID,
chunk_id UUID,
retrieval_rank UInt8,
distance Float32,
title String,
url String,
document_version String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, request_id, retrieval_rank);
これで、RAGの動きが後から見えます。
ここで大事なのは、回答だけでなく「検索された候補」を残すことです。
RAGの失敗は、回答文だけ見ても原因が分かりません。
自分なら、最低でもこの4つは残します。
| 残すもの | 後で分かること |
|---|---|
request_id |
質問、検索、回答、評価をつなげられる |
retrieval_rank |
上位候補に何が出たか分かる |
distance |
検索が近かったのか遠かったのか見える |
document_version |
古い文書で答えていないか追える |
たとえば、低評価が多い質問を探します。
SELECT
question,
count() AS cnt,
avg(latency_ms) AS avg_latency_ms,
avg(input_tokens + output_tokens) AS avg_tokens
FROM rag_events
WHERE feedback = 'bad'
AND event_time >= now() - INTERVAL 7 DAY
GROUP BY question
ORDER BY cnt DESC
LIMIT 20;
低評価の回答で、どの文書が検索されていたかも見られます。
SELECT
r.title,
r.url,
count() AS bad_count,
avg(r.distance) AS avg_distance
FROM rag_events AS e
INNER JOIN rag_retrievals AS r
ON e.request_id = r.request_id
WHERE e.feedback = 'bad'
AND r.retrieval_rank <= 3
AND e.event_time >= now() - INTERVAL 7 DAY
GROUP BY
r.title,
r.url
ORDER BY bad_count DESC
LIMIT 20;
この結果を見ると、改善の方向が少し具体的になります。
- いつも同じ古い文書が上に来るなら、文書更新や重み付けを見る
- distanceが近いのに低評価なら、プロンプトや回答生成を見る
- distanceが遠いものばかりなら、文書分割やembeddingモデルを見る
- 特定の質問だけ失敗するなら、FAQやメタデータを足す
こういう分析は、ベクトルDB単体よりOLAPの世界に近いです。
だからClickHouseをRAGの横に置く意味があります。
失敗を4つに分けて見る
RAGの改善で一番もったいないのは、全部を「回答が悪い」にしてしまうことです。
それだと、プロンプトをいじるしかなくなります。
自分なら、まず失敗をこの4つに分けます。
| 失敗 | ログで見るところ | 直す場所 |
|---|---|---|
| 文書がない | どの検索候補も遠い、同じ質問が何度も出る | FAQ追加、ドキュメント整備 |
| 検索が外れた | 低評価のときにdistanceが遠い | chunkサイズ、embeddingモデル、メタデータ |
| 回答生成が外れた | distanceは近いのに低評価 | プロンプト、引用ルール、モデル |
| 運用がつらい | latencyやtokenが急に増える | モデル選定、キャッシュ、検索件数 |
この分け方をすると、ClickHouseで見るべきクエリも決まります。
たとえば「検索が外れた」なら、低評価かつ上位候補のdistanceが大きいものを見ます。
SELECT
e.question,
min(r.distance) AS best_distance,
any(r.title) AS top_title,
any(r.url) AS top_url
FROM rag_events AS e
INNER JOIN rag_retrievals AS r
ON e.request_id = r.request_id
WHERE e.feedback = 'bad'
AND r.retrieval_rank = 1
AND e.event_time >= now() - INTERVAL 7 DAY
GROUP BY e.question
ORDER BY best_distance DESC
LIMIT 20;
逆に「回答生成が外れた」なら、上位候補は近いのに低評価になっているものを見ます。
SELECT
e.question,
e.answer,
r.title,
r.url,
r.distance
FROM rag_events AS e
INNER JOIN rag_retrievals AS r
ON e.request_id = r.request_id
WHERE e.feedback = 'bad'
AND r.retrieval_rank = 1
AND r.distance < 0.2
AND e.event_time >= now() - INTERVAL 7 DAY
ORDER BY e.event_time DESC
LIMIT 20;
0.2は適当な例です。
実際にはembeddingモデル、正規化の有無、距離関数、データの性質で見方が変わります。
だからこそ、最初は固定のしきい値で決めつけず、distanceの分布を見たいです。
「AI時代のDB」は、LLMの失敗を説明できる必要がある
LLMアプリを作っていて怖いのは、失敗の原因がぼやけることです。
普通の機能なら、バグの場所を追いやすいです。
SQLが間違っている。
APIが落ちている。
バリデーションが抜けている。
RAGでは、もっと曖昧です。
検索対象の文書が悪いのか。
検索クエリが悪いのか。
embeddingが合っていないのか。
LLMが拾った文書を無視したのか。
ユーザーの期待と回答方針がずれているのか。
このへんを毎回ログから追うのはしんどいです。
だから、最初から分析できる形で残しておきたいです。
ClickHouseは、大量のログやイベントをあとから集計するのが得意です。
OpenAIのオブザーバビリティ事例でも、ClickHouseを使ってペタバイト規模のログを扱っている話が出ています。
自分の個人開発でそんな量は出ません。
でも、考え方は同じです。
RAGも観測できないと改善できません。
ダッシュボードで見るならこのあたり
もし管理画面を作るなら、最初はこのくらいを見たいです。
| 見たいもの | 理由 |
|---|---|
| 1日の質問数 | 利用量を見る |
| p50/p95レイテンシ | 体感速度を見る |
| token数の推移 | コストを見る |
| feedback別の質問 | 失敗パターンを見る |
| retrieval_rank 1のdistance分布 | 検索の当たり具合を見る |
| 参照された文書ランキング | 使われている知識を知る |
| 低評価に出やすい文書 | 古い文書や曖昧な文書を探す |
たとえば、日ごとの利用量と低評価率です。
SELECT
toDate(event_time) AS day,
count() AS requests,
countIf(feedback = 'bad') AS bad_requests,
countIf(feedback = 'bad') / count() AS bad_rate,
quantile(0.95)(latency_ms) AS p95_latency_ms
FROM rag_events
GROUP BY day
ORDER BY day;
モデル変更の前後比較もできます。
SELECT
model,
count() AS requests,
avg(input_tokens + output_tokens) AS avg_tokens,
quantile(0.95)(latency_ms) AS p95_latency_ms,
countIf(feedback = 'good') / count() AS good_rate
FROM rag_events
WHERE event_time >= now() - INTERVAL 30 DAY
GROUP BY model
ORDER BY requests DESC;
AIアプリでは、モデルを変えたときに「なんとなく良くなった気がする」で終わりがちです。
でもログを残しておくと、遅くなったのか、安くなったのか、低評価が減ったのかを見られます。
ClickHouseが合うところ、合わないところ
ClickHouseが合いそうなのは、こういう部分です。
- 検索ログを大量に保存する
- LLMの実行ログを集計する
- token数やレイテンシを分析する
- 文書チャンクとメタデータをSQLで絞り込む
- ベクトル検索と普通の集計を同じ場所で試す
- PostgreSQLやMySQLの分析負荷を逃がす
逆に、何でもClickHouseに入れたいわけではありません。
- ユーザーの残高更新
- 注文処理
- 認可の中心データ
- 強い整合性が必要な書き込み
- 1行ずつ頻繁に更新するデータ
こういうところは、PostgreSQLやMySQLの方が自然です。
AI時代のDBは、1つのDBが全部やるというより、役割分担が大事になりそうです。
RDBはサービスの状態を守る。
ClickHouseは大量のイベントを読み解く。
ベクトル検索は、その中の1機能として使う。
自分にはこの見方が一番しっくり来ています。
ClickHouseをAIスタックに入れる理由
ClickHouseをAIスタックに入れる理由は、単に「速いから」だけではないと思っています。
速いのはもちろん大事です。
でも、それだけならベクトル検索専用DBや検索エンジンも候補になります。
ClickHouseの良さは、検索した後の世界までSQLで触れるところです。
RAGでは、ベクトルで近い文書を取ったあとに、普通の条件で絞りたいことがあります。
- 公開範囲
- 言語
- 更新日時
- 製品カテゴリ
- ユーザーの契約プラン
- 文書の種類
さらに、検索後の結果をログとして残し、日次やモデル別に集計したいです。
このとき、構造化データと半構造化データとベクトルが別々の場所に散っていると、分析が面倒になります。
ClickHouseに寄せると、少なくとも「読む」「集計する」「比較する」は同じSQLでできます。
ここがAI時代のDBっぽいところです。
小さく始めるなら
自分なら、最初はこの順で試します。
- PostgreSQLやMySQLの本体データはそのままにする
- 文書チャンクとembeddingをClickHouseに入れる
- RAGの検索ログとLLM実行ログをClickHouseに入れる
- 低評価や遅い質問をSQLで見られるようにする
- データが増えてから、vector similarity indexやパーティション設計を見る
最初から「AI基盤」を作ろうとすると大きくなりすぎます。
でも、検索ログを残すだけなら始めやすいです。
そして、ログはあとから作れません。
取っていなかった検索結果は、あとで分析できません。
RAGを少しでも本番に近い形で動かすなら、回答本文だけでなく、検索された候補と距離と順位も残したいです。
まとめ
AI時代のデータベースで変わるのは、ベクトルを持てるかどうかだけではないと思います。
もちろんベクトル検索は大事です。
でも、RAGやLLMアプリを運用するなら、もっと泥くさいログも必要です。
ユーザーが何を聞いたか。
どの文書が返ったか。
LLMがどう答えたか。
どれくらい遅かったか。
どれくらいtokenを使ったか。
ユーザーは満足したか。
これを後から速く見られるDBがあると、改善がしやすくなります。
ClickHouseは、AIスタックの中で「ベクトル検索をする場所」にもなれます。
でもそれ以上に、「AIアプリを観測して改善する場所」として強いと思いました。
RAGは作って終わりではありません。
ログを見て、外れた検索を直して、古い文書を直して、モデル変更の影響を見る。
その作業をちゃんと回すためのDBとして、ClickHouseはかなり現実的な選択肢だと思います。
参考
- ClickHouse公式サイト
- ClickHouse: Real-Time Data Analytics Platform
- Exact and Approximate Vector Search | ClickHouse Docs
- Distance functions | ClickHouse Docs
- LowCardinality(T) | ClickHouse Docs
- Why OpenAI chose ClickHouse for petabyte-scale observability
- PostgreSQL + ClickHouse as the Open Source unified data stack
- Approx vector search in ClickHouse
- We built a vector search engine that lets you choose precision at query time