- 厳密ベクトル検索では、与えられた点とベクトル空間内のすべての点との距離を計算します。これにより、可能な限り最高の精度が得られ、返される点が真の最近傍であることが保証されます。ベクトル空間を総当たりで探索するため、厳密ベクトル検索は実運用では遅すぎる場合があります。
- 近似ベクトル検索は、厳密ベクトル検索よりもはるかに高速に結果を求めるための手法群を指します (例: グラフやランダムフォレストのような特殊なデータ構造) 。結果の精度は通常、実用上は “十分良好” です。多くの近似手法では、結果精度と検索時間のトレードオフを調整するためのパラメータが用意されています。
vectors に格納されます。たとえば、Array(Float64)、Array(Float32)、または Array(BFloat16) です。
参照ベクトルは定数配列で、共通テーブル式として指定します。
<DistanceFunction> は、参照点と格納されているすべての点の間の距離を計算します。
これには、利用可能な任意の距離関数を使用できます。
<N> は、返す近傍の数を指定します。
厳密ベクトル検索
例
近似ベクトル検索
ベクトル類似度索引
ベクトル類似度索引は、ClickHouse バージョン 25.8 以降で利用できます。
問題が発生した場合は、ClickHouse リポジトリ で issue を登録してください。
ベクトル類似度索引の作成
ALTER TABLEステートメントでは、今後テーブルに挿入される新しいデータに対してのみ索引が構築されます。
既存のデータに対しても索引を構築するには、これをマテリアライズする必要があります。
<distance_function> には、次のいずれかを指定する必要があります。
L2Distance:ユークリッド空間における 2 点間を結ぶ線分の長さを表す ユークリッド距離cosineDistance:2 つの非ゼロベクトルの間の角度を表す cosine distancedotProduct:2 つのベクトルの要素ごとの積の総和を表す 内積 (inner product) 。正規化されたデータではcosineDistanceと等価です。
L2Distance が最適です。それ以外の場合は、スケールの違いを補正するために cosineDistance を推奨します。
距離関数
L2Distance と cosineDistance では、値が小さいほど類似度が高くなります。一方、dotProduct では、値が大きいほど類似度が高くなります。
そのため、L2Distance と cosineDistance を使用するベクトル索引は SELECT [...] ORDER BY [...] ASC クエリでのみ使用でき (ASC は ORDER BY のデフォルトです) 、dotProduct 用に構築されたベクトル索引は SELECT [...] ORDER BY [...] DESC クエリでのみ使用できます。<dimensions> は、基になるカラム内の配列のカーディナリティ (要素数) を指定します。
ClickHouse が索引の作成中に異なるカーディナリティの配列を見つけた場合、その索引は破棄され、エラーが返されます。
省略可能な GRANULARITY パラメーター <N> は、インデックスグラニュールのサイズを表します (こちらを参照) 。
通常のスキップ索引ではデフォルトのインデックスグラニュールは 1 ですが、ベクトル類似度索引ではデフォルトのインデックスグラニュールとして 1 億を使用します。
この値により、大きなパーツであっても内部的に構築される索引の数を少なく抑えられます。
インデックスグラニュールを変更するのは、その影響を十分に理解している上級ユーザーに限ることを推奨します (下記を参照) 。
ベクトル類似度索引は、異なる近似検索手法に対応できるという意味で汎用的です。
実際に使用される手法は、パラメーター <type> で指定します。
現時点で利用できる手法は HNSW のみです (academic paper) 。これは、階層近傍グラフに基づく、近似ベクトル検索のための一般的かつ最先端の手法です。
型として HNSW を使用する場合、ユーザーは必要に応じて HNSW 固有の追加パラメーターを指定できます。
<quantization>は、近傍グラフ内のベクトルの量子化を制御します。設定可能な値はf64、f32、f16、bf16、i8、b1です。デフォルト値はbf16です。なお、このパラメーターは、基になるカラム内でのベクトル表現には影響しません。<hnsw_max_connections_per_layer>は、グラフの各ノードの近傍数 (HNSW ハイパーパラメーターMとも呼ばれます) を制御します。デフォルト値は32です。値0はデフォルト値を使用することを意味します。<hnsw_candidate_list_size_for_construction>は、HNSW グラフの構築時に使用される動的候補リストのサイズ (HNSW ハイパーパラメーターef_constructionとも呼ばれます) を制御します。デフォルト値は128です。値0はデフォルト値を使用することを意味します。
- ベクトル類似度索引は、型 Array(Float32)、Array(Float64)、または Array(BFloat16) のカラムに対してのみ作成できます。
Array(Nullable(Float32))やArray(LowCardinality(Float32))のような、Nullable や low-cardinality の float の Array は使用できません。 - ベクトル類似度索引は、単一のカラムに対してのみ作成できます。
- ベクトル類似度索引は計算式 (例:
INDEX index_name arraySort(vectors) TYPE vector_similarity([...])) に対して作成することもできますが、そのような索引は後で近似近傍探索には使用できません。 - ベクトル類似度索引では、基になるカラム内のすべての配列が
<dimension>個の要素を持っている必要があります。これは索引作成時に検査されます。この要件への違反をできるだけ早く検出するために、ユーザーはベクトルカラムに 制約 を追加できます。たとえばCONSTRAINT same_length CHECK length(vectors) = 256です。 - 同様に、基になるカラム内の配列値は空 (
[]) であってはならず、デフォルト値 (これも[]) であってもなりません。
ベクトル類似度索引を使う
ベクトル類似度索引を使用するには、設定 compatibility を
'' (デフォルト値) にするか、'25.1' 以降に設定する必要があります。SELECT [...] SETTINGS hnsw_candidate_list_size_for_search = <value>) 。
この設定のデフォルト値は 256 であり、大多数のユースケースで適切に機能します。
設定値を大きくするほど精度は向上しますが、その分パフォーマンスは低下します。
クエリがベクトル類似度索引を使用できる場合、ClickHouse は SELECT クエリで指定された LIMIT <N> が妥当な範囲内にあるかを確認します。
具体的には、<N> が設定 max_limit_for_vector_search_queries の値 (デフォルト値は 100) を超えている場合、エラーが返されます。
LIMIT の値が大きすぎると検索が遅くなる可能性があり、通常は使用方法の誤りを示しています。
SELECT クエリがベクトル類似度索引を使用しているかどうかを確認するには、クエリの先頭に EXPLAIN indexes = 1 を追加します。
例として、次のクエリ
Skip およびベクトル索引の名前とタイプ (この例では idx と vector_similarity) が含まれている場合、ベクトル類似度索引が使用されています。
この場合、ベクトル類似度索引は4つのgranuleのうち2つ、つまりデータの50%をスキップしました。
スキップできるgranuleが多いほど、索引の使用効率が向上します。
ポストフィルタリングとプレフィルタリング
ユーザーはオプションで、SELECT クエリに追加のフィルタ条件を含む WHERE 句を指定できます。
ClickHouse はこれらのフィルタ条件を、ポストフィルタリングまたはプリフィルタリングの戦略で評価します。
どちらの戦略も、フィルタが評価される順序を決定するものです。
- ポストフィルタリングでは、まずベクトル類似度索引が評価され、その後 ClickHouse が
WHERE句で指定された追加のフィルタを評価します。 - 事前フィルタリングとは、フィルタの評価順序が通常とは逆になることを意味します。
- ポストフィルタリングの一般的な問題は、
LIMIT <N>句で要求した行数より少ない結果しか返されない可能性があることです。これは、ベクトル類似度索引が返した結果行のうち1行以上が追加のフィルタを満たさない場合に発生します。 - プリフィルタリングは、一般には未解決の問題です。一部の特化型ベクトルデータベースはプリフィルタリングのアルゴリズムを提供していますが、ほとんどのリレーショナルデータベース (ClickHouse を含む) では、正確な近傍探索、つまり索引を使わない総当たりスキャンにフォールバックします。
year で範囲パーティション分割されていて、次のクエリを実行するとします。
- フィルタ条件によってパート内で少なくとも 1 行が除外される場合、ClickHouse はそのパート内の「残った」範囲に対してプリフィルタリングにフォールバックします。
- フィルタ条件によってパート内で 1 行も除外されない場合、ClickHouse はそのパートに対してポストフィルタリングを行います。
auto) は prefilter に設定できます。
これは、追加のフィルタ条件の選択性が極めて高い場合に、プリフィルタリングを強制するのに役立ちます。
たとえば、次のクエリはプリフィルタリングの恩恵を受ける可能性があります。
SETTINGS vector_search_filter_strategy = 'prefilter' を追加) 、ClickHouse はまず価格が 2 ドル未満の本をすべて見つけ、その後、見つかった本に対して総当たりのベクトル検索を実行します。
上記の問題を解決する別の方法として、vector_search_index_fetch_multiplier (デフォルト: 1.0、最大: 1000.0) を 1.0 より大きい値 (たとえば 2.0) に設定することもできます。
ベクトル索引から取得する最近傍の数はこの設定値に応じて増加し、その後、それらの行に追加のフィルタが適用されて LIMIT 件の行が返されます。
たとえば、乗数を 3.0 にして再度クエリできます。
vector_search_index_fetch_multiplier を設定することでこの問題を軽減できますが、極端な場合 (WHERE 条件の選択性が非常に高い場合) には、要求した N 行より少ない行しか返されない可能性が依然としてあります。
再スコアリング
ClickHouse のスキップ索引は通常、グラニュール単位でフィルタリングを行います。つまり、スキップ索引でのルックアップは (内部的には) 一致する可能性のあるグラニュールのリストを返し、後続のスキャンで読み込むデータ量を減らします。
これはスキップ索引全般では有効に機能しますが、ベクトル類似度索引では “granularity mismatch” を引き起こします。
もう少し詳しく言うと、ベクトル類似度索引は、指定された参照ベクトルに対して最も類似する N 個のベクトルの行番号を特定しますが、その後、それらの行番号をグラニュール番号に外挿する必要があります。
その後 ClickHouse はこれらのグラニュールをディスクから読み込み、それらのグラニュール内のすべてのベクトルに対して距離計算を繰り返します。
このステップは再スコアリング と呼ばれ、理論上は精度を向上できる可能性があります。ベクトル類似度索引が返すのは 近似 結果にすぎないことを思い出してください。しかし、パフォーマンスの観点では最適ではないのは明らかです。
そこで ClickHouse は、再スコアリング を無効にして、最も類似するベクトルとその距離を索引から直接返す最適化を提供しています。
この最適化はデフォルトで有効になっています。設定 vector_search_with_rescoring を参照してください。
大まかな仕組みとしては、ClickHouse は最も類似するベクトルとその距離を仮想カラム _distances として利用できるようにします。
これを確認するには、EXPLAIN header = 1 を付けてベクトル検索クエリを実行します。
再スコアリングなし (
vector_search_with_rescoring = 0) で実行し、かつ並列レプリカが有効なクエリでは、再スコアリングにフォールバックする場合があります。パフォーマンスチューニング
CODEC(NONE) を指定します。
system.text_log) は、ベクトル類似度索引が読み込まれていることを示します。
異なるベクトル検索クエリに対してこのようなメッセージが繰り返し表示される場合は、cache サイズが小さすぎることを示しています。
ベクトル類似度索引キャッシュには、ベクトル索引のインデックスグラニュールが格納されます。
個々のベクトル索引のインデックスグラニュールがキャッシュサイズより大きい場合は、キャッシュされません。
したがって、ベクトル索引のサイズ (「ストレージとメモリ消費量の見積もり」の式、または system.data_skipping_indices に基づく) を必ず計算し、それに応じてキャッシュサイズを設定してください。
量子化を行うと、元のフル精度の浮動小数点値 (
f32) を用いた検索と比べて、ベクトル検索の精度は低下します。
ただし、ほとんどのデータセットでは、半精度の brain float 量子化 (bf16) による精度低下はごくわずかであるため、ベクトル類似度索引ではこの量子化手法がデフォルトで使用されます。
4分の1精度 (i8) およびバイナリ (b1) の量子化では、ベクトル検索の精度が目に見えて低下します。
これら 2 つの量子化は、ベクトル類似度索引のサイズが利用可能な DRAM 容量を大幅に上回る場合にのみ推奨します。
この場合は、精度を向上させるために、rescoring (vector_search_index_fetch_multiplier、vector_search_with_rescoring) も有効にすることを推奨します。
バイナリ量子化を推奨するのは、1) 正規化された埋め込み (つまりベクトル長 = 1。OpenAI のモデルは通常正規化されています) であり、かつ 2) 距離関数としてコサイン距離を使用する場合に限られます。
バイナリ量子化では、内部的にハミング距離を使用して近接グラフを構築し、検索を行います。
rescoring のステップでは、テーブルに格納された元のフル精度ベクトルを使用して、コサイン距離により最近傍を特定します。
データ転送の調整
ベクトル検索クエリにおける参照ベクトルはユーザーから与えられ、通常は大規模言語モデル (LLM) を呼び出して取得します。
ClickHouse でベクトル検索を実行する一般的な Python コードは、次のようになります
search_v) は、非常に大きな次元数になることがあります。
たとえば OpenAI は、1536 次元、さらには 3072 次元の埋め込みベクトルを生成するモデルを提供しています。
上記のコードでは、ClickHouse Python ドライバーが埋め込みベクトルを人間が読める文字列に変換し、その後 SELECT クエリ全体を文字列として送信します。
埋め込みベクトルが 1536 個の単精度浮動小数点値で構成されているとすると、送信される文字列の長さは 20 kB に達します。
その結果、トークン化、パース、および数千回に及ぶ文字列から浮動小数点値への変換によって、CPU 使用率が高くなります。
また、ClickHouse サーバーのログファイルにもかなりの容量が必要になり、system.query_log も肥大化します。
なお、ほとんどの LLM モデルは、埋め込みベクトルをネイティブな浮動小数点数のリストまたは NumPy 配列として返します。
そのため、Python アプリケーションでは、次のスタイルを使用して参照ベクトルのパラメータをバイナリ形式でバインドすることを推奨します。
system.query_log の肥大化も防げます。
管理と監視
通常の skipping indexes との違い
GRANULARITY = [N] 個の granule で構成されます (通常の skipping indexes のデフォルトでは [N] = 1) 。
たとえば、table のプライマリインデックスの granularity が 8192 (設定 index_granularity = 8192) で、GRANULARITY = 2 の場合、各 index block には 16384 行が含まれます。
しかし、近似近傍探索のための data structure と algorithm は、本質的に row-oriented です。
これらは行の集合を compact に表現して保持し、ベクトル検索 queries に対して行を返します。
そのため、ベクトル類似度索引の動作には、通常の skipping indexes と比べてやや直感に反する違いがあります。
ユーザーが column にベクトル類似度索引を定義すると、ClickHouse は内部的に各 index block ごとにベクトル類似度の「sub-index」を作成します。
この sub-index は、自身が属する index block 内の行しか認識しないという意味で「local」です。
前述の例で、ある column に 65536 行あるとすると、4 つの index blocks (8 つの granule にまたがる) と、各 index block に対応するベクトル類似度 sub-index が作成されます。
理論上、sub-index は、その index block 内で最も近い N 個の points に対応する行を直接返せます。
しかし、ClickHouse は granule 単位で data をディスクから memory に読み込むため、sub-index は一致した行を granule granularity に外挿します。
これは、通常の skipping indexes が index block 単位で data をスキップするのとは異なります。
GRANULARITY parameter は、作成されるベクトル類似度 sub-index の数を決定します。
GRANULARITY の値が大きいほど、ベクトル類似度 sub-index の数は少なくなりますが、それぞれは大きくなり、最終的には column (または column の data part) に sub-index が 1 つだけになる場合もあります。
その場合、その sub-index は column のすべての行を「global」に把握できるため、関連する行を含む column (part) の granule を直接すべて返せます (そのような granule の数は最大でも LIMIT [N] 個です) 。
次のステップで、ClickHouse はこれらの granule を読み込み、granule 内のすべての行に対して総当たりで距離計算を行い、実際に最適な行を特定します。
GRANULARITY の値が小さい場合は、各 sub-index が最大 LIMIT N 個の granule を返します。
その結果、より多くの granule を読み込んで後段で filter する必要があります。
どちらの場合でも検索精度は同等で、異なるのは processing 性能だけである点に注意してください。
一般に、ベクトル類似度索引では大きな GRANULARITY を使用し、ベクトル類似度 structure の memory consumption が大きすぎるといった問題がある場合にのみ、より小さい GRANULARITY の値を使うことが推奨されます。
ベクトル類似度索引で GRANULARITY が指定されていない場合、デフォルト値は 1 億です。
例
Query
Response
量子化ビット (QBit)
Array(Float32) ではなく Array(BFloat16) として保存すると、データサイズは半分になり、クエリの実行時間もそれに応じて短くなることが期待されます。
この手法は量子化として知られています。計算は高速になりますが、すべてのベクトルを総当たりで走査していても、結果の精度が低下する可能性があります。
従来の量子化では、検索時とデータ保存時の両方で精度が失われます。上の例では、Float32 ではなく BFloat16 を保存することになるため、あとから必要になっても、より高精度な検索は実行できません。別の方法として、量子化済みデータとフル精度データの 2 つのコピーを保存するやり方があります。これは機能しますが、余分なストレージが必要です。たとえば、元データが Float64 で、異なる精度 (16 ビット、32 ビット、または完全な 64 ビット) で検索を実行したいケースを考えてみましょう。この場合、データのコピーを 3 つ別々に保存する必要があります。
ClickHouse は、こうした制約に対処する Quantized Bit (QBit) データ型を提供しており、次のことが可能です。
- 元のフル精度データを保存する。
- 量子化の精度をクエリ時に指定する。
QBit 型のカラムを宣言するには、次の構文を使用します。
element_type– 各ベクトル要素の型。サポートされている型はBFloat16、Float32、Float64ですdimension– 各ベクトルの要素数
QBit テーブルの作成とデータの追加
QBit を使ったベクトル検索
QBit で使用できる距離関数の一覧は、こちらで確認できます。
フル精度検索 (64ビット) :
パフォーマンスに関する考慮事項
QBit のパフォーマンス上の利点は、精度を下げるほどストレージから読み込むデータ量が減り、I/O 操作が少なくなることにあります。さらに、QBit に Float32 データが含まれている場合、精度パラメーターが 16 以下であれば、計算量の削減によるさらなる効果も得られます。精度パラメーターは、精度と速度のトレードオフを直接左右します。
- より高い精度 (元のデータ幅に近い) : 結果はより正確になりますが、クエリは遅くなります
- より低い精度: 近似結果になる代わりにクエリが高速化し、メモリ使用量も削減されます