ClickHouse に、SQL エンジンから LLM や埋め込みプロバイダーを直接呼び出す組み込みの AI Functions ファミリーが加わりました。モデルを lower() や sum() と同じように SQL から呼び出せるようになり、データがすでにある場所で処理が動きます。
これらの関数は現在ベータ版で、機能は継続的に追加、改善されています。これらの関数は 3 回のリリースに分けて追加されてきました。26.4 で aiGenerate()、aiClassify()、aiExtract()、aiTranslate()、26.6 で aiEmbed()、そして 26.8 で aiFilter()、aiRedact()、aiSimilarity() です。
ClickHouse Cloud で AI Functions を試す
ClickHouse Cloud では、以下で説明する追加の設定は不要で、関数はそのまま使えます。現在、AI Functions は ClickHouse Cloud でプライベートプレビュー中です。
プライベートプレビューに参加する開発の背景
ログ、製品レビュー、説明文、サポートチケットなど、データはすでにすべて ClickHouse にあります。RAG のような従来の LLM ワークフローでは、データベースからデータを別のスタックへ取り出して分類や埋め込みのタスクを実行し、結果を書き戻します。このやり方は遅く、エラーが起きやすく、運用の複雑さも増します。
AI Functions の基本的な考え方は、データをモデルへ運ぶのではなく、モデルをデータのそばへ持ってくることです。ClickHouse はすでにベクトルを保存し、ベクトル検索もできるため、RAG のサイクル全体を 1 つのシステムで実行できます。ベクトルデータベース、オーケストレーションフレームワーク、別の LLM API をつなぎ合わせる必要はありません。
簡単な例を示します。
SELECT aiClassify('I love this product!', ['positive', 'negative', 'neutral']);応答:
positiveベータ版に含まれるもの
26.8 では次の AI Functions を利用できます。テキスト関数:
- aiClassify - LLM プロバイダーを使い、与えられたテキストを指定したカテゴリのいずれかに分類します。
- aiExtract - LLM プロバイダーを使い、非構造化テキストから構造化された情報を抽出します。
- aiGenerate - LLM プロバイダーを使い、プロンプトから自由形式のテキストを生成します。
- aiTranslate - LLM プロバイダーを使い、与えられたテキストを指定した言語に翻訳します。
- aiFilter - LLM プロバイダーを使い、与えられたテキストに対して自然言語の条件を評価し、
WHERE、PREWHERE、JOIN ... ONで使えるブール値 (UInt8) を返します。 - aiRedact - LLM プロバイダーを使い、与えられたテキストに含まれる個人識別情報 (PII) を検出してマスクします。
埋め込み関数:
- aiEmbed - 設定済みの AI プロバイダーを使い、与えられたテキストの埋め込みベクトルを生成します。
- aiSimilarity - 設定済みの埋め込みプロバイダーを使い、2 つのテキストのセマンティックな類似度を計算します。
AI Functions は、入力ごとに (埋め込み関数では入力のバッチごとに) 設定済みプロバイダーの API へリモート HTTP 呼び出しを行い、解析した応答をネイティブな ClickHouse の値として返します。
セットアップの準備 (OSS のみ)
AI Functions を使い始めるには、名前付きコレクション (named collection) をテキスト用と埋め込み用に 1 つずつ作成し、プロバイダーの認証情報と設定を保存する必要があります。
設定 ai_function_text_default_credentials と ai_function_embedding_default_credentials に名前付きコレクションの名前を指定することを推奨します。こうすると、すべての AI Functions が自動的に正しいエンドポイントを使います。実行時に設定を上書きすることも可能です。
OpenAI プロバイダーの認証情報で名前付きコレクションを作成する文の例を示します。OpenAI の API キーが必要で、コレクションは chat エンドポイント用と embedding エンドポイント用に 1 つずつ作成します。
CREATE NAMED COLLECTION ai_text_credentials AS
provider = 'openai',
endpoint = 'https://api.openai.com/v1/chat/completions',
model = 'gpt-5.6-terra',
api_key = 'sk-...';
-- The embedding functions (`aiEmbed`, `aiSimilarity`) do not read `model` from the named collection.
CREATE NAMED COLLECTION ai_embedding_credentials AS
provider = 'openai',
endpoint = 'https://api.openai.com/v1/embeddings',
api_key = 'sk-...';注: OpenAI 互換の API (Ollama、LiteLLM など) は、
provider = 'openai'を設定してエンドポイントを自分のサービスに向けることで利用できます。したがって、ローカルモデルでも動作します。
次に、デフォルト認証情報の設定を行います。
SET ai_function_text_default_credentials = 'ai_text_credentials';
SET ai_function_embedding_default_credentials = 'ai_embedding_credentials';これで関数を実行する準備が整いました。いくつか例を見ていきましょう。
データを理解する: 分類とフィルタリング
以下の例では、Hacker News データセットを使います。ストーリーとコメント 2800 万行を、そのガイドの Parquet スキーマで読み込んだものです。注目する列は title、comment、author、score、type、timestamp です。上記のセットアップ手順を済ませておいてください。
aiClassify は文字列と定数のラベルリストを受け取り、そのラベルのうちちょうど 1 つを返します。モデルは分類先を選ぶよう求められるだけなので、創造性の余地は限られています。
フロントページのタイトルは短く、トークン使用量を抑えられるため、最初の題材に向いています。
SELECT
title,
aiClassify(title, ['space', 'security', 'databases', 'startups', 'programming', 'other']) AS topic
FROM hackernews
WHERE (type = 'story') AND (score < 3000) AND (title != '')
ORDER BY score DESC
LIMIT 3 FORMAT Vertical;応答:
Row 1:
──────
title: SpaceX’s Falcon Heavy successfully launches
topic: space
Row 2:
──────
title: Twitter Will Allow Employees to Work at Home Forever
topic: other
Row 3:
──────
title: No Cookie for You
topic: security出力は String なので、標準の SQL とそのまま組み合わせられます。サブクエリで aiClassify を使い、その結果を集計できます。何度もクエリする対象なら、毎回再計算せず、分類結果を列に格納してください。
Python ではなく SQL で分析
これが、高スコアの Hacker News ストーリーのトピック内訳です。Python は 1 行も書いていません。従来のワークフローなら、エクスポート、分類ジョブ、ClickHouse への再ロードが必要でした。
データセットを見るaiFilter は UInt8 を返すため、そのまま WHERE に入れて、他のブール条件と同じように扱えます。SQL がこれまでできなかったことを、最もはっきりと実現している関数です。トークン検索は「database」という単語を含む行を見つけます。aiFilter は、誰かがデータベースについて不満を述べている行を見つけます。
SELECT author, substring(comment, 1, 100) AS snippet
FROM (
SELECT author, comment
FROM hackernews
WHERE type = 'comment' AND ilike(comment, '%database%')
LIMIT 500
)
WHERE aiFilter(comment, 'the author is describing a production incident or outage they experienced') LIMIT 3 FORMAT Vertical;応答:
Row 1:
──────
author: 0x0
snippet: Wow, the database connection ip and dbname were taken from http cookies!
Row 2:
──────
author: AccountCreated
snippet: > The primary MCP database is comprised of 9 MongoDB shards
enough said.
Row 3:
──────
author: tomazzi
snippet: The link goes to "Database Error - Error establishing a database connection" which is kind of intere注: AI Function の呼び出しは処理に時間がかかることがあるため、まずサブクエリで軽い述語を適用し、外側のクエリで LLM の述語を呼び出すのが最善です。
データを変換する: 生成と翻訳
これまでの分類の例はテキストを分類先に振り分けるものでしたが、次の 2 つの関数は新しいテキストを生成します。データセットとセットアップは上記と同じものを使います。
aiGenerate はプロンプトを受け取り、モデルが書き返した内容をそのまま返します。SQL の文脈では、プロンプトは通常、列から concat で組み立てます。
Hacker News のコメントは長いものが多く、要約の題材として最適です。この例では params マップに system_prompt と temperature パラメーターも指定し、行をまたいで出力の形式を一定に保ちます。
SELECT
author,
length(comment) AS original_chars,
aiGenerate(
concat('Summarize this Hacker News comment in one sentence: ', comment),
map('system_prompt', 'You are terse. Reply with one sentence and no preamble.',
'temperature', '0.3',
'max_tokens', '2000'
)
) AS summary
FROM hackernews
WHERE type = 'comment' AND length(comment) > 1000
LIMIT 3 FORMAT Vertical;応答:
Row 1:
──────
author: 0-_-0
original_chars: 1076
summary: Some VPN providers have been court-verified to keep no logs, and given their financial incentive to protect their reputation plus mandatory data retention laws in many countries, using a reputable VPN is likely more private than relying on your local ISP.
Row 2:
──────
author: 0-_-0
original_chars: 1428
summary: The comment shares Our World in Data links comparing COVID-19 confirmed cases, deaths, and case fatality rates across several countries using 7-day rolling averages.
Row 3:
──────
author: 0-_-0
original_chars: 1337
summary: The commenter is drawing a parallel to Wim Hof, a man famous for extreme cold endurance feats and the ability to consciously control his immune system through a method combining cold exposure, breathing, and meditation.行ごとではなくグループごとに 1 回生成する、といった興味深いパターンも可能です。例を示します。
SELECT aiGenerate(
concat(
'Write a three-bullet digest of what Hacker News was discussing. Titles:\n',
arrayStringConcat(groupArray(title), '\n')
)
) AS digest;aiTranslate はテキストと対象言語を受け取ります。言語は名前でも BCP-47 コードでも指定できます。覚えておきたいパラメーターが instructions で、文体や方言に関する指示をモデルに渡します。
SELECT title, aiTranslate(title, 'Spanish', map('instructions', 'Use polite form. Keep technical terms and product names in English.')) AS title_es
FROM hackernews
WHERE type = 'story' AND score < 3000 AND title != ''
ORDER BY score DESC LIMIT 3 FORMAT Vertical;応答:
Row 1:
──────
title: SpaceX’s Falcon Heavy successfully launches
title_es: El Falcon Heavy de SpaceX se lanza con éxito
Row 2:
──────
title: Twitter Will Allow Employees to Work at Home Forever
title_es: Twitter Permitirá a sus Empleados Trabajar desde Casa para Siempre
Row 3:
──────
title: No Cookie for You
title_es: Sin Cookie para Tiどちらの関数も String を返すため、入れ子にできます。たとえば、長いコメントを要約してから、その要約を翻訳します。
aiTranslate(
aiGenerate( concat('Summarize in one sentence: ', comment),
map('system_prompt', 'Reply with one sentence, no preamble.') ),
'es-MX' ) AS resumen;RAG のサイクル全体をデータベース内で
aiEmbed とベクトル検索による検索拡張生成 (RAG)

すべての段階を SQL で実行できるようになりました。
- マテリアライズドビュー内の
aiEmbed()で書き込み時に埋め込み、データの到着と同時にベクトルを生成する - データと埋め込みを並べて保存する
- ベクトル類似度インデックスでインデックスを作成する
- 埋め込み済みデータに対して
cosineDistanceで検索する - 取得したコンテキストをもとに
aiGenerateで回答を生成する
なお、aiSimilarity() はアドホックな作業向けの簡便な手段です。1 回の呼び出しで両方のテキストを埋め込み、コサイン類似度を返すため、セマンティックな重複排除や数千行のランキングに最適です。
支出に上限を設ける
ClickHouse のほとんどの関数と異なり、AI 関数には CPU サイクルやメモリ使用量だけでなく、呼び出しごとにトークンと金額のコストがかかります。こうした関数をデプロイする際には、クエリごとのトークンコストと使用量の制限が最大の関心事になるでしょう。暴走したクエリや、肝を冷やすような AI 利用料の請求を避けるため、AI 関数の使用量をクエリ単位で制限するクォータ設定を実装しました。
これらのクォータは、次のセッション設定で制御します。
- ai_function_max_input_tokens_per_query (デフォルト 1000000)
- ai_function_max_output_tokens_per_query (デフォルト 500000)
- ai_function_max_api_calls_per_query (デフォルト 1000)
さらに、クォータ上限に達したときのエラー動作を制御する設定もあります: ai_function_throw_on_quota_exceeded (デフォルト 1 - 例外をスローする)。
上記の設定は次のように使います。
SELECT
title,
aiClassify(title, ['space', 'security', 'databases', 'startups', 'programming', 'other']) AS topic
FROM hackernews
WHERE type = 'story' AND score > 100 AND title != ''
LIMIT 5000
SETTINGS ai_function_max_api_calls_per_query = 100;テキスト関数は行ごとに 1 リクエストを発行するため、呼び出し回数の予算は実質的に行数の予算になります。このクエリは 5000 行を要求していますが、許可されているのは 100 リクエストだけなので、そこで停止します。
Code: 290. DB::Exception: AI API call limit reached: 100 calls made, maximum: 100.
This is controlled by the 'ai_function_max_api_calls_per_query' setting. (LIMIT_EXCEEDED)クォータは各リクエストの送信前に確認されるため、この回数は正確で、クエリが呼び出し予算を超過することはありません。0 に設定すると制限は無効になります。
トークンのクォータも同じ仕組みですが、プロバイダーが実際に報告した値を追跡するため、請求額にかなり近くなります。長い Hacker News コメントの要約は、コメント全体がプロンプトに入るため、コストが高いケースです。
SELECT
author,
aiGenerate(
concat('Summarize this Hacker News comment in one sentence: ', comment),
map('system_prompt', 'You are terse. Reply with one sentence and no preamble.',
'temperature', '0.3')
) AS summary
FROM hackernews
WHERE type = 'comment' AND length(comment) > 1000
LIMIT 2000
SETTINGS
ai_function_max_input_tokens_per_query = 500000,
ai_function_max_output_tokens_per_query = 50000;
Code: 290. DB::Exception: AI input token limit reached or exceeded: 500642 tokens consumed,
maximum: 500000. This is controlled by the 'ai_function_max_input_tokens_per_query' setting.「reached or exceeded (到達または超過)」という表現に注意してください。呼び出しのトークンコストは応答が返ってくるまで分からないため、合計はスレッドごとに実行中のリクエスト 1 件分だけ上限を超えることがあります。上限を許容できる金額ちょうどに設定するのではなく、余裕を持たせてください。
中断はデフォルトとして適切ですが、常に望ましいわけではありません。長い分類処理が 99% の時点で止まって何も返ってこないのでは、大部分だけでも返ってくるより悪い結果です。ai_function_throw_on_quota_exceeded = 0 を指定すると、クォータはソフトストップになります。
SELECT
topic,
count() AS stories
FROM (
SELECT aiClassify(title, ['space', 'security', 'databases', 'startups', 'programming', 'other']) AS topic
FROM hackernews
WHERE type = 'story' AND score > 100 AND title != ''
LIMIT 5000
)
WHERE topic != ''
GROUP BY topic
ORDER BY stories DESC
SETTINGS
ai_function_max_api_calls_per_query = 1000,
ai_function_throw_on_quota_exceeded = 0;クォータを超えた行には列のデフォルト値 (String なら空文字列) が入り、クエリは成功します。WHERE topic != '' でそれらの行を除外するため、例外ではなく、部分的ではあっても正確な集計結果が得られます。
クエリが実際にどれだけ消費したかを確認するには、system.query_log のプロファイルイベントを読み取ります。
SELECT
ProfileEvents['AIAPICalls'] AS api_calls,
ProfileEvents['AIInputTokens'] AS input_tokens,
ProfileEvents['AIOutputTokens'] AS output_tokens,
ProfileEvents['AIRowsProcessed'] AS rows_processed,
ProfileEvents['AIRowsSkipped'] AS rows_skipped
FROM system.query_log
WHERE query_id = 'hn_classify' AND type = 'QueryFinish'
ORDER BY event_time DESC
LIMIT 1 FORMAT Vertical;
Row 1:
──────
api_calls: 1000
input_tokens: 24310
output_tokens: 3122
rows_processed: 1000
rows_skipped: 4000ai_function_throw_on_quota_exceeded = 0 のときに注目すべきは AIRowsSkipped です。クォータによる打ち切りかエラーかを問わず、黙ってデフォルト値に置き換えられた行の数です。クォータの値を決める最も安上がりな方法は、まず LIMIT 100 のサンプルで実行し、その結果をもとに掛け算で見積もることです。
クォータを設定する際に留意すべき点を挙げます。
- 最上位のクエリで設定する。 サブクエリの
SETTINGS句は、クォータ設定については無視されます。 - サーバーごと、クエリフラグメントごとに適用される。 1 つの実行コンテキスト内では上限は正確で、すべての AI 関数、ブロック、スレッドで共有されます。分散クエリでは各シャードが上限いっぱいまで呼び出せるため、上限はシャード数で割った値にしてください。
- トークンのクォータには使用量を報告するプロバイダーが必要。 OpenAI、Anthropic、vLLM は報告します。
usageオブジェクトを省略するプロバイダーではトークンカウンターが0のままとなり、これらの制限は発動しません。その場合は代わりにai_function_max_api_calls_per_queryで制限してください。 - 埋め込み関数は completion トークンを生成しないため、出力トークンの制限は
aiEmbedとaiSimilarityには適用されません。 - リトライも呼び出しクォータに数えられる。
ai_function_max_retriesのデフォルトは1なので、プロバイダーが一時的なエラーを返す場合、1000 の予算は 1000 行ではなく 1000 回の試行分になります。
本番投入前に知っておくべきこと
本番環境に投入する際は、次の懸念事項と制限に留意してください。
- プロンプトインジェクション: 入力テキストがモデルを操作するため、出力は信頼できないものとして扱い、生成された SQL やシェルコマンドに渡さないでください。
- 非決定性: 同じ行を LLM に入力しても異なる回答が返るため、temperature = 0 を使い、再計算するのではなく結果のマテリアライズを検討してください。
- コスト: コストとレイテンシは行数に比例して増えるため、必ず最初に LIMIT でテストし、上記のクォータ設定を活用してください。
- セキュリティ:
remote_url_allow_hostsで接続先プロバイダーを限定し、エンドポイントは HTTPS のままにし、TLS 終端後はプロバイダー側でデータが平文で見えることを忘れないでください。
まとめ
AI Functions により、ClickHouse の世界で分類、翻訳、埋め込み、生成が SQL の守備範囲に入りました。RAG のループ全体が、データのある場所で動くようになりました。この機能を使ってどのようなアプリケーションを構築されるか、ぜひお聞かせください。



