多次元 (ベクトル) 空間において、ある点に対して最も近い N 個の点を見つける問題は、最近傍探索 (nearest neighbor search) 、または単にベクトル検索と呼ばれます。 ベクトル検索を実現する方法は、大きく 2 つあります。
- 厳密ベクトル検索では、与えられた点とベクトル空間内のすべての点との距離を計算します。これにより、可能な限り最高の精度が得られ、返される点が真の最近傍であることが保証されます。ベクトル空間を総当たりで探索するため、厳密ベクトル検索は実運用では遅すぎる場合があります。
- 近似ベクトル検索は、厳密ベクトル検索よりもはるかに高速に結果を求めるための手法群を指します (例: グラフやランダムフォレストのような特殊なデータ構造) 。結果の精度は通常、実用上は "十分良好" です。多くの近似手法では、結果精度と検索時間のトレードオフを調整するためのパラメータが用意されています。
ベクトル検索 (厳密または近似) は、SQL では次のように記述できます。
WITH [...] AS reference_vector
SELECT [...]
FROM table
WHERE [...] -- a WHERE clause is optional
ORDER BY <DistanceFunction>(vectors, reference_vector)
LIMIT <N>ベクトル空間内の点は、Array(Float64)、Array(Float32)、または Array(BFloat16) 型のカラム vectors に格納されます。
実用上は、一般的に BFloat16 配列の使用を推奨します。
参照ベクトルは定数配列で、共通テーブル式として指定します。
<DistanceFunction> は、参照点と格納されているすべての点の間の距離を計算します。
これには、利用可能な任意の距離関数を使用できます。
<N> は、返す近傍の数を指定します。
厳密ベクトル検索
厳密ベクトル検索は、上記の SELECT クエリをそのまま実行することで行えます。 このようなクエリの実行時間は、一般に格納されているベクトルの数、ベクトル要素数 (= 次元) 、およびベクトル要素のビット幅に比例します。 また、ClickHouse はすべてのベクトルを総当たりでスキャンするため、実行時間はクエリで使用されるスレッド数にも依存します (max_threads 設定を参照) 。
例
CREATE TABLE tab
(
id Int32,
vec Array(BFloat16)
)
ENGINE = MergeTree
ORDER BY id;
INSERT INTO tab VALUES (0, [1.0, 0.0]), (1, [1.1, 0.0]), (2, [1.2, 0.0]), (3, [1.3, 0.0]), (4, [1.4, 0.0]), (5, [1.5, 0.0]), (6, [0.0, 2.0]), (7, [0.0, 2.1]), (8, [0.0, 2.2]), (9, [0.0, 2.3]), (10, [0.0, 2.4]), (11, [0.0, 2.5]);
WITH [0., 2.] AS reference_vec
SELECT id, vec
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3;戻り値
┌─id─┬─vec─────┐
1. │ 6 │ [0,2] │
2. │ 7 │ [0,2.1] │
3. │ 8 │ [0,2.2] │
└────┴─────────┘近似ベクトル検索
ベクトル類似度索引
ClickHouse には、近似ベクトル検索を実行するための特別な「ベクトル類似度」索引が用意されています。
ベクトル類似度索引の作成
新しいテーブルには、次のようにベクトル類似度索引を作成できます。
CREATE TABLE table
(
[...],
vectors Array([Float64|Float32|BFloat16]),
INDEX <index_name> vectors TYPE vector_similarity(<type>, <distance_function>, <dimensions>) [GRANULARITY <N>]
)
ENGINE = MergeTree
ORDER BY [...]また、既存のテーブルにベクトル類似度索引を追加する場合は、次のようにします。
ALTER TABLE table ADD INDEX <index_name> vectors TYPE vector_similarity(<type>, <distance_function>, <dimensions>) [GRANULARITY <N>];ベクトル類似度索引は、特殊なデータスキップ索引の一種です (こちらおよびこちらを参照) 。
したがって、上記のALTER TABLEステートメントでは、今後テーブルに挿入される新しいデータに対してのみ索引が構築されます。
既存のデータに対しても索引を構築するには、これをマテリアライズする必要があります:
ALTER TABLE table MATERIALIZE INDEX <index_name> SETTINGS mutations_sync = 2;関数 <distance_function> には、次のいずれかを指定する必要があります。
L2Distance:ユークリッド空間における 2 点間を結ぶ線分の長さを表す ユークリッド距離cosineDistance:2 つの非ゼロベクトルの間の角度を表す cosine distancedotProduct:2 つのベクトルの要素ごとの積の総和を表す 内積 (inner product) 。正規化されたデータではcosineDistanceと等価です。
正規化されたデータでは通常 L2Distance が最適です。それ以外の場合は、スケールの違いを補正するために cosineDistance を推奨します。
<dimensions> は、基になるカラム内の配列のカーディナリティ (要素数) を指定します。
ClickHouse が索引の作成中に異なるカーディナリティの配列を見つけた場合、その索引は破棄され、エラーが返されます。
省略可能な GRANULARITY パラメーター <N> は、インデックスグラニュールのサイズを表します (こちらを参照) 。
通常のスキップ索引ではデフォルトのインデックスグラニュールは 1 ですが、ベクトル類似度索引ではデフォルトのインデックスグラニュールとして 1 億を使用します。
この値により、大きなパーツであっても内部的に構築される索引の数を少なく抑えられます。
インデックスグラニュールを変更するのは、その影響を十分に理解している上級ユーザーに限ることを推奨します (下記を参照) 。
ベクトル類似度索引は、異なる近似検索手法に対応できるという意味で汎用的です。
実際に使用される手法は、パラメーター <type> で指定します。
現時点で利用できる手法は HNSW のみです (academic paper) 。これは、階層近傍グラフに基づく、近似ベクトル検索のための一般的かつ最先端の手法です。
型として HNSW を使用する場合、ユーザーは必要に応じて HNSW 固有の追加パラメーターを指定できます。
CREATE TABLE table
(
[...],
vectors Array([Float64|Float32|BFloat16]),
INDEX index_name vectors TYPE vector_similarity('hnsw', <distance_function>, <dimensions>[, <quantization>, <hnsw_max_connections_per_layer>, <hnsw_candidate_list_size_for_construction>]) [GRANULARITY N]
)
ENGINE = MergeTree
ORDER BY [...]次の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はデフォルト値を使用することを意味します。
HNSW 固有パラメーターのデフォルト値は、ほとんどのユースケースで十分良好に機能します。 そのため、HNSW 固有パラメーターをカスタマイズすることは推奨していません。
さらに、次の制限があります。
- ベクトル類似度索引は、型 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です。 - 同様に、基になるカラム内の配列値は空 (
[]) であってはならず、デフォルト値 (これも[]) であってもなりません。
ストレージ使用量とメモリ使用量の見積もり
一般的な AI モデル (例: Large Language Model、LLMs) で使用するために生成されるベクトルは、数百から数千の浮動小数点値で構成されます。 そのため、1 つのベクトル値であっても、複数キロバイトのメモリを消費することがあります。 テーブル内の基になるベクトルカラムに必要なストレージ量と、ベクトル類似度索引に必要なメインメモリ量を見積もりたい場合は、以下の 2 つの式を使用できます。
テーブル内のベクトルカラムのストレージ使用量 (非圧縮) :
ストレージ消費量 = ベクトル数 * 次元数 * カラムデータ型のサイズStorage consumption = 1 million * 1536 * 4 (for Float32) = 6.1 GB検索を実行するには、ベクトル類似度索引をディスクから主記憶に完全に読み込む必要があります。 同様に、ベクトル索引もまずメモリ上に完全に構築してから、ディスクに保存されます。
ベクトル索引の読み込みに必要なメモリ使用量:
インデックス内のベクトルのメモリ (mv) = ベクトル数 * 次元数 * 量子化データ型のサイズ
インメモリグラフのメモリ (mg) = ベクトル数 * hnsw_max_connections_per_layer * Bytes_per_node_id (= 4) * Layer_node_repetition_factor (= 2)
メモリ消費量: mv + mgインデックス内のベクトルのメモリ (mv) = 100万 * 1536 * 2 (BFloat16の場合) = 3072 MB
インメモリグラフのメモリ (mg) = 100万 * 64 * 2 * 4 = 512 MB
メモリ消費量 = 3072 + 512 = 3584 MB上記の式では、事前割り当てバッファやcacheなどの実行時データ構造をベクトル類似度索引が確保するために必要な追加メモリは考慮されていません。
ベクトル類似度索引を使う
ベクトル類似度索引は、次の形式のSELECTクエリをサポートしています:
WITH [...] AS reference_vector
SELECT [...]
FROM table
WHERE [...] -- a WHERE clause is optional
ORDER BY <DistanceFunction>(vectors, reference_vector)
LIMIT <N>ClickHouseのクエリオプティマイザーは、上記のクエリテンプレートとの照合を試み、利用可能なベクトル類似度索引を活用します。 クエリがベクトル類似度索引を使用できるのは、SELECTクエリの距離関数と索引定義の距離関数が一致する場合に限ります。
上級ユーザーは、設定 hnsw_candidate_list_size_for_search (HNSWハイパーパラメーター "ef_search" とも呼ばれます) にカスタム値を指定することで、検索時の候補リストのサイズを調整できます (例: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 を追加します。
例として、次のクエリ
EXPLAIN indexes = 1
WITH [0.462, 0.084, ..., -0.110] AS reference_vec
SELECT id, vec
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 10;返す可能性があります
┌─explain─────────────────────────────────────────────────────────────────────────────────────────┐
1. │ Expression (Project names) │
2. │ Limit (preliminary LIMIT (without OFFSET)) │
3. │ Sorting (Sorting for ORDER BY) │
4. │ Expression ((Before ORDER BY + (Projection + Change column names to column identifiers))) │
5. │ ReadFromMergeTree (default.tab) │
6. │ Indexes: │
7. │ PrimaryKey │
8. │ Condition: true │
9. │ Parts: 1/1 │
10. │ Granules: 575/575 │
11. │ Skip │
12. │ Name: idx │
13. │ Description: vector_similarity GRANULARITY 100000000 │
14. │ Parts: 1/1 │
15. │ Granules: 10/575 │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘この例では、次元数1536のdbpediaデータセットのベクトル100万件が575個のグラニュールに格納されており、1グラニュールあたり約1.7k行になります。 クエリは10個の近傍を求めており、ベクトル類似度索引はそれら10個の近傍を10個の別々のグラニュールで見つけます。 これら10個のグラニュールはクエリ実行時に読み取られます。
出力に Skip およびベクトル索引の名前とタイプ (この例では idx と vector_similarity) が含まれている場合、ベクトル類似度索引が使用されています。
この場合、ベクトル類似度索引は4つのグラニュールのうち2つ、つまりデータの50%をスキップしました。
スキップできるグラニュールが多いほど、索引の使用効率が向上します。
ポストフィルタリングとプレフィルタリング
ユーザーはオプションで、SELECT クエリに追加のフィルタ条件を含む WHERE 句を指定できます。
ClickHouse はこれらのフィルタ条件を、ポストフィルタリングまたはプリフィルタリングの戦略で評価します。
どちらの戦略も、フィルタが評価される順序を決定するものです。
- ポストフィルタリングとは、まずベクトル類似度索引を評価し、その後で ClickHouse が
WHERE句で指定された追加のフィルタを評価することを意味します。 - プリフィルタリングとは、フィルタの評価順序がその逆であることを意味します。
これらの戦略には、それぞれ異なるトレードオフがあります。
- ポストフィルタリングの一般的な問題は、
LIMIT <N>句で要求した行数より少ない結果しか返されない可能性があることです。これは、ベクトル類似度索引が返した結果行のうち1行以上が追加のフィルタを満たさない場合に発生します。 - プリフィルタリングは、一般には未解決の問題です。一部の特化型ベクトルデータベースはプリフィルタリングのアルゴリズムを提供していますが、ほとんどのリレーショナルデータベース (ClickHouse を含む) では、正確な近傍探索、つまり索引を使わない総当たりスキャンにフォールバックします。
どの戦略が使用されるかは、フィルタ条件によって異なります。
追加フィルタがパーティションキーの一部である場合
追加のフィルタ条件がパーティションキーの一部である場合、ClickHouse はパーティションプルーニングを適用します。
例として、テーブルがカラム year で範囲パーティション分割されていて、次のクエリを実行するとします。
WITH [0., 2.] AS reference_vec
SELECT id, vec
FROM tab
WHERE year = 2025
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3;ClickHouse は、2025 年のものを除くすべてのパーティションをプルーニングします。
追加のフィルタは索引を使って評価できない
追加のフィルタ条件を索引 (主キー索引、スキップ索引) で評価できない場合、ClickHouse はポストフィルタリングを適用します。
追加のフィルタは主キー索引を使って評価できる
追加のフィルタ条件を 主キー で評価できる場合 (つまり、それらが主キーのプレフィックスを構成する場合) で、かつ
- フィルタ条件によってパーツ内で少なくとも 1 行が除外される場合、ClickHouse はそのパーツ内の "残った" 範囲に対してプリフィルタリングにフォールバックします。
- フィルタ条件によってパーツ内で 1 行も除外されない場合、ClickHouse はそのパーツに対してポストフィルタリングを行います。
実運用のユースケースでは、後者のケースが発生する可能性はかなり低いです。
追加のフィルタはスキップ索引を使って評価できる
追加のフィルタ条件を スキップ索引 (minmax 索引、set 索引など) で評価できる場合、ClickHouse はポストフィルタリングを行います。 このような場合、ベクトル類似度索引は他のスキップ索引と比べて最も多くの行を除外すると見込まれるため、最初に評価されます。
ポストフィルタリングとプリフィルタリングをより細かく制御するには、2 つの設定を使用できます。
設定 vector_search_filter_strategy (デフォルト: 上記のヒューリスティックを実装する auto) は prefilter に設定できます。
これは、追加のフィルタ条件の選択性が極めて高い場合に、プリフィルタリングを強制するのに役立ちます。
たとえば、次のクエリはプリフィルタリングの恩恵を受ける可能性があります。
SELECT bookid, author, title
FROM books
WHERE price < 2.00
ORDER BY cosineDistance(book_vector, getEmbedding('Books on ancient Asian empires'))
LIMIT 102ドル未満の本がごくわずかしかないとすると、ポストフィルタリングでは 0 行しか返らない可能性があります。これは、ベクトル索引から返される上位 10 件の一致結果がすべて 2 ドルを超えている可能性があるためです。
プリフィルタリングを強制すると (クエリに SETTINGS vector_search_filter_strategy = 'prefilter' を追加) 、ClickHouse はまず価格が 2 ドル未満の本をすべて見つけ、その後、見つかった本に対して総当たりのベクトル検索を実行します。
上記の問題を解決する別の方法として、vector_search_index_fetch_multiplier (デフォルト: 1.0、最大: 1000.0) を 1.0 より大きい値 (たとえば 2.0) に設定することもできます。
ベクトル索引から取得する最近傍の数はこの設定値に応じて増加し、その後、それらの行に追加のフィルタが適用されて LIMIT 件の行が返されます。
たとえば、乗数を 3.0 にして再度クエリできます。
SELECT bookid, author, title
FROM books
WHERE price < 2.00
ORDER BY cosineDistance(book_vector, getEmbedding('Books on ancient Asian empires'))
LIMIT 10
SETTING vector_search_index_fetch_multiplier = 3.0;ClickHouse は各パーツのベクトル索引から 3.0 x 10 = 30 件の最近傍を取得し、その後で追加のフィルタ条件を適用します。
返されるのは、そのうち最も近い 10 件の近傍のみです。
vector_search_index_fetch_multiplier を設定することでこの問題を軽減できますが、極端な場合 (WHERE 条件の選択性が非常に高い場合) には、要求した N 行より少ない行しか返されない可能性が依然としてあります。
rescoring
ClickHouse のスキップ索引は通常、グラニュール単位でフィルタリングを行います。つまり、スキップ索引でのルックアップは (内部的には) 一致する可能性のあるグラニュールのリストを返し、後続のスキャンで読み込むデータ量を減らします。
これはスキップ索引全般では有効に機能しますが、ベクトル類似度索引では "granularity mismatch" を引き起こします。
もう少し詳しく言うと、ベクトル類似度索引は、指定された参照ベクトルに対して最も類似する N 個のベクトルの行番号を特定します。
設定 vector_search_with_rescoring = 1 では、ClickHouse は候補行の元のフル精度ベクトルを読み取り、通常の SQL パイプラインで最終的な距離を計算します。
クエリプランで可能な場合、ClickHouse は最終的な距離計算の前に、ベクトル索引が返した候補行にスキャンを絞り込みます。
このステップは rescoring と呼ばれ、特に量子化されたベクトル索引では精度を向上させることができます。これは、最終的なランキングで索引距離ではなく保存されたベクトルを使用するためです。
追加のフィルタによって候補が削られすぎる場合や、より高い再現率が必要な場合は、設定 vector_search_index_fetch_multiplier を増やして、ベクトル索引が rescoring 用により多くの候補行を返すようにしてください。
そこで ClickHouse は、rescoring を無効にして、最も類似するベクトルとその距離を索引から直接返す最適化を提供しています。
この最適化はデフォルトで有効になっています。設定 vector_search_with_rescoring を参照してください。
大まかな仕組みとしては、ClickHouse は最も類似するベクトルとその距離を仮想カラム _distance として利用できるようにします。
これを確認するには、EXPLAIN header = 1 を付けてベクトル検索クエリを実行します。
EXPLAIN header = 1
WITH [0., 2.] AS reference_vec
SELECT id
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3
SETTINGS vector_search_with_rescoring = 0Query id: a2a9d0c8-a525-45c1-96ca-c5a11fa66f47
┌─explain────────────────────────────────────────────────────────────────────────────────────────────────────┐
1. │ Expression (Project names) │
2. │ Header: id Int32 │
3. │ Limit (preliminary LIMIT (without OFFSET)) │
4. │ Header: L2Distance(__table1.vec, _CAST([0., 2.]_Array(BFloat16), 'Array(BFloat16)'_String)) BFloat16 │
5. │ __table1.id Int32 │
6. │ Sorting (Sorting for ORDER BY) │
7. │ Header: L2Distance(__table1.vec, _CAST([0., 2.]_Array(BFloat16), 'Array(BFloat16)'_String)) BFloat16 │
8. │ __table1.id Int32 │
9. │ Expression ((Before ORDER BY + (Projection + Change column names to column identifiers))) │
10. │ Header: L2Distance(__table1.vec, _CAST([0., 2.]_Array(BFloat16), 'Array(BFloat16)'_String)) BFloat16 │
11. │ __table1.id Int32 │
12. │ ReadFromMergeTree (default.tab) │
13. │ Header: id Int32 │
14. │ _distance BFloat16 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘パフォーマンスチューニング
圧縮の調整
ほぼすべてのユースケースで、基盤となるカラム内のベクトルは高密度であり、圧縮してもあまり効果がありません。
その結果、圧縮 を有効にすると、ベクトルカラムへの書き込みや読み取りが遅くなります。
そのため、圧縮は無効にすることを推奨します。
そのためには、次のようにベクトルカラムに CODEC(NONE) を指定します。
CREATE TABLE tab
(
id Int32,
vec Array(BFloat16) CODEC(NONE),
INDEX idx vec TYPE vector_similarity('hnsw', 'L2Distance', 2)
)
ENGINE = MergeTree ORDER BY id;索引作成のチューニング
ベクトル類似度索引のライフサイクルは、パーツのライフサイクルと連動しています。 つまり、ベクトル類似度索引が定義された新しいパーツが作成されるたびに、その索引も作成されます。 これは通常、データが挿入されるとき、またはマージ中に発生します。 残念ながら、HNSW は索引の作成に時間がかかることで知られており、挿入やマージを大幅に遅くする可能性があります。 ベクトル類似度索引は、データが不変である場合や、変更頻度が低い場合にのみ使用するのが理想的です。
索引作成を高速化するには、次の手法を使用できます。
まず、索引作成は並列化できます。 索引作成スレッドの最大数は、サーバー設定max_build_vector_similarity_index_thread_pool_sizeで設定できます。 最適な性能を得るには、この設定値を CPU コア数に合わせて設定してください。
次に、INSERT ステートメントを高速化するために、ユーザーはセッション設定materialize_skip_indexes_on_insertを使用して、新たに挿入されたパーツでのスキップ索引の作成を無効にできます。 そのようなパーツに対する SELECT クエリは、厳密検索にフォールバックします。 挿入されたパーツはテーブル全体のサイズと比べて小さい傾向があるため、性能への影響はごくわずかであると見込まれます。
3 つ目に、マージを高速化するために、ユーザーはセッション設定materialize_skip_indexes_on_mergeを使用して、マージ後のパーツでのスキップ索引の作成を無効にできます。 これにより、ステートメントALTER TABLE […] MATERIALIZE INDEX […]と組み合わせて、ベクトル類似度索引のライフサイクルを明示的に制御できます。 たとえば、すべてのデータが取り込まれるまで、または週末のようにシステム負荷が低い時間帯まで、索引作成を遅らせることができます。
索引利用のチューニング
SELECT クエリでベクトル類似度索引を使用するには、それをメインメモリに読み込む必要があります。 同じベクトル類似度索引がメインメモリに繰り返し読み込まれるのを防ぐため、ClickHouse はこの種の索引専用のインメモリ cache を提供しています。 この cache が大きいほど、不要な読み込みは少なくなります。 キャッシュサイズの最大サイズは、サーバー設定vector_similarity_index_cache_sizeで設定できます。 デフォルトでは、cache は最大 5 GB まで拡張できます。
次のログメッセージ (system.text_log) は、ベクトル類似度索引が読み込まれていることを示します。
異なるベクトル検索クエリに対してこのようなメッセージが繰り返し表示される場合は、キャッシュサイズが小さすぎることを示しています。
2026-02-03 07:39:10.351635 [1386] f0ac5c85-1b1c-4f35-8848-87a1d1aa00ba : VectorSimilarityIndex Start loading vector similarity index
<...>
2026-02-03 07:40:25.217603 [1386] f0ac5c85-1b1c-4f35-8848-87a1d1aa00ba : VectorSimilarityIndex Loaded vector similarity index: max_level = 2, connectivity = 64, size = 1808111, capacity = 1808111, memory_usage = 8.00 GiB, bytes_per_vector = 4096, scalar_words = 1024, nodes = 1808111, edges = 51356964, max_edges = 233395072ベクトル検索クエリが遅い場合は、まずベクトル索引キャッシュを確認し、必要に応じて増やすことが最初のステップであることを、改めて強調しておきます。
現在のベクトル類似度索引キャッシュのサイズは、system.metrics に表示されます。
SELECT metric, value
FROM system.metrics
WHERE metric = 'VectorSimilarityIndexCacheBytes'特定のクエリIDを持つクエリのキャッシュヒット数とミス数は、system.query_log から取得できます。
SYSTEM FLUSH LOGS query_log;
SELECT ProfileEvents['VectorSimilarityIndexCacheHits'], ProfileEvents['VectorSimilarityIndexCacheMisses']
FROM system.query_log
WHERE type = 'QueryFinish' AND query_id = '<...>'
ORDER BY event_time_microseconds;本番環境でのユースケースでは、すべてのベクトル索引が常時メモリ上に保持されるよう、cache のサイズを十分に大きくすることを推奨します。
量子化の調整
量子化は、ベクトルのメモリ使用量と、ベクトル索引の構築および走査にかかる計算コストを削減する手法です。 ClickHouse のベクトル索引は、以下の量子化オプションをサポートしています。
| Quantization | Name | Storage per dimension |
|---|---|---|
| f32 | 単精度 | 4 bytes |
| f16 | 半精度 | 2 bytes |
| bf16 (default) | 半精度 (brain float) | 2 bytes |
| i8 | 4分の1精度 | 1 byte |
| b1 | バイナリ | 1 bit |
量子化を行うと、元のフル精度の浮動小数点値 (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_client.embeddings.create(input = "[Good Books]", model='text-embedding-3-large', dimensions=1536).data[0].embedding
params = {'search_v': search_v}
result = chclient.query(
"SELECT id FROM items
ORDER BY cosineDistance(vector, %(search_v)s)
LIMIT 10",
parameters = params)埋め込みベクトル (上記のスニペットの search_v) は、非常に大きな次元数になることがあります。
たとえば OpenAI は、1536 次元、さらには 3072 次元の埋め込みベクトルを生成するモデルを提供しています。
上記のコードでは、ClickHouse Python ドライバーが埋め込みベクトルを人間が読める文字列に変換し、その後 SELECT クエリ全体を文字列として送信します。
埋め込みベクトルが 1536 個の単精度浮動小数点値で構成されているとすると、送信される文字列の長さは 20 kB に達します。
その結果、トークン化、パース、および数千回に及ぶ文字列から浮動小数点値への変換によって、CPU 使用率が高くなります。
また、ClickHouse サーバーのログファイルにもかなりの容量が必要になり、system.query_log も肥大化します。
なお、ほとんどの LLM モデルは、埋め込みベクトルをネイティブな浮動小数点数のリストまたは NumPy 配列として返します。 そのため、Python アプリケーションでは、次のスタイルを使用して参照ベクトルのパラメータをバイナリ形式でバインドすることを推奨します。
search_v = openai_client.embeddings.create(input = "[Good Books]", model='text-embedding-3-large', dimensions=1536).data[0].embedding
params = {'$search_v_binary$': np.array(search_v, dtype=np.float32).tobytes()}
result = chclient.query(
"SELECT id FROM items
ORDER BY cosineDistance(vector, reinterpret($search_v_binary$, 'Array(Float32)'))
LIMIT 10"
parameters = params)この例では、参照ベクトルはバイナリ形式のまま送信され、サーバー上で浮動小数点数の配列として再解釈されます。
これにより、サーバー側のCPU時間を節約でき、サーバーログや system.query_log の肥大化も防げます。
管理と監視
ベクトル類似度索引のディスク上のサイズは、system.data_skipping_indices で確認できます。
SELECT database, table, name, formatReadableSize(data_compressed_bytes)
FROM system.data_skipping_indices
WHERE type = 'vector_similarity';出力例:
┌─database─┬─table─┬─name─┬─formatReadab⋯ssed_bytes)─┐
│ default │ tab │ idx │ 348.00 MB │
└──────────┴───────┴──────┴──────────────────────────┘通常の スキッピング索引 との違い
通常の スキッピング索引 と同様に、ベクトル類似度索引は グラニュール 単位で構築され、各 index block は GRANULARITY = [N] 個の グラニュール で構成されます (通常の スキッピング索引 のデフォルトでは [N] = 1) 。
たとえば、table のプライマリインデックスの granularity が 8192 (設定 index_granularity = 8192) で、GRANULARITY = 2 の場合、各 index block には 16384 行が含まれます。
しかし、近似近傍探索のための data structure と algorithm は、本質的に row-oriented です。
これらは行の集合を compact に表現して保持し、ベクトル検索 queries に対して行を返します。
そのため、ベクトル類似度索引の動作には、通常の スキッピング索引 と比べてやや直感に反する違いがあります。
ユーザーが column にベクトル類似度索引を定義すると、ClickHouse は内部的に各 index block ごとにベクトル類似度の「sub-index」を作成します。
この sub-index は、自身が属する index block 内の行しか認識しないという意味で「local」です。
前述の例で、ある column に 65536 行あるとすると、4 つの index blocks (8 つの グラニュール にまたがる) と、各 index block に対応するベクトル類似度 sub-index が作成されます。
理論上、sub-index は、その index block 内で最も近い N 個の points に対応する行を直接返せます。
vector_search_with_rescoring = 1 のクエリでは、クエリプラン がこの最適化を許可している場合、ClickHouse はこれらの行位置を使用して、保存されているベクトルから最終的な距離を計算する前に行を filter できます。
rescoring を行わない場合、ClickHouse は virtual column _distance を介してベクトル索引からの距離を直接使用します。
どちらのモードでも、読み取りのスケジューリングには周囲の グラニュール 範囲が引き続き使用されます。これは、通常の スキッピング索引 が index block 単位で data をスキップするのとは異なります。
GRANULARITY parameter は、作成されるベクトル類似度 sub-index の数を決定します。
GRANULARITY の値が大きいほど、ベクトル類似度 sub-index の数は少なくなりますが、それぞれは大きくなり、最終的には column (または column の data part) に sub-index が 1 つだけになる場合もあります。
その場合、その sub-index は column のすべての行を「global」に把握できるため、関連する行を含む column (part) の グラニュール を直接すべて返せます (そのような グラニュール の数は最大でも LIMIT [N] 個です) 。
vector_search_with_rescoring = 1 の場合、ClickHouse はその後、該当する行位置を読み取り、それらの行について正確な距離を計算できます。
GRANULARITY の値が小さい場合は、各 sub-index が最大 LIMIT N 個の candidate rows を返せます。
その結果、より多くの candidate rows を読み込んで後段で filter する必要が生じる場合があります。
どちらの場合でも検索精度は同等で、異なるのは processing 性能だけである点に注意してください。
一般に、ベクトル類似度索引では大きな GRANULARITY を使用し、ベクトル類似度 structure の memory consumption が大きすぎるといった問題がある場合にのみ、より小さい GRANULARITY の値を使うことが推奨されます。
ベクトル類似度索引で GRANULARITY が指定されていない場合、デフォルト値は 1 億です。
例
クエリ:
CREATE TABLE tab
(
id Int32,
vec Array(BFloat16),
INDEX idx vec TYPE vector_similarity('hnsw', 'L2Distance', 2)
)
ENGINE = MergeTree
ORDER BY id;
INSERT INTO tab VALUES (0, [1.0, 0.0]), (1, [1.1, 0.0]), (2, [1.2, 0.0]), (3, [1.3, 0.0]), (4, [1.4, 0.0]), (5, [1.5, 0.0]), (6, [0.0, 2.0]), (7, [0.0, 2.1]), (8, [0.0, 2.2]), (9, [0.0, 2.3]), (10, [0.0, 2.4]), (11, [0.0, 2.5]);
WITH [0., 2.] AS reference_vec
SELECT id, vec
FROM tab
ORDER BY L2Distance(vec, reference_vec) ASC
LIMIT 3; ┌─id─┬─vec─────┐
1. │ 6 │ [0,2] │
2. │ 7 │ [0,2.1] │
3. │ 8 │ [0,2.2] │
└────┴─────────┘近似ベクトル検索を使用する、そのほかのサンプルデータセット:
量子化コーデックを使用したベクトル検索
Quantized コーデックを有効にするには、まず SET enable_quantized_codec = 1 を実行してください。
問題が発生した場合は、ClickHouseリポジトリで issue を作成してください。
はじめに
ベクトル類似度索引は、グラフを走査して最近傍クエリに応答します。グラフ全体をメモリに保持できる場合、非常に高い性能を発揮します。 ただし、次の2つの条件により適用範囲が制限されます。
- 構築コストが高い。 ベクトル類似度索引の構築には多大なコストがかかります。
- メモリ使用量が多い。 ベクトル類似度索引は大量のメモリを消費し、ベクトル自体のメモリ使用量に加えて、大きなコスト要因となります。
- フィルタリング。 選択性の高い
WHEREフィルターでは、グラフ走査は有効に機能しません。述語を満たす少数の行に到達できないか、それらを見つけるために不釣り合いに多くの候補を調べる必要があるためです。
網羅スキャンには、これらの制限はいずれもありません。補助データ構造を必要とせず、パーツは連結するだけで自然にマージでき、フィルタリングではスキャン対象の行数が減るだけです。
網羅スキャンの最大の欠点は、読み取る必要があるデータ量が多いことです。フル精度の Float32 または BFloat16 で保存されたベクトルをスキャンする場合、ベクトルカラム全体をディスクから読み込む必要があるため、ストレージ I/O が支配的になります。
Quantized カラムコーデックは、この欠点を解消します。
各ベクトルを、元のフル精度の値とコンパクトな量子化表現の2通りで保存します。
ベクトル検索クエリでは、まず低コストで SIMD に適した距離関数を用いて量子化コードをスキャンし、最も有望な結果候補のショートリストを作成します。
次に、これらの候補をフル精度ベクトルに対して再ランク付けします。
量子化コードに対する初期スキャンでストレージから読み取るバイト数は、元のベクトルをスキャンする場合より大幅に少なくなります。
このコーデックは、コストの高い処理であるスキャンが、まさに ClickHouse エンジンが得意とする処理であるため、ClickHouse に適しています。
- ベクトル化。 スキャンカーネルは SIMD 命令を活用し、CPU 使用量を削減します。
- コアとパーツをまたいだ並列処理。 スキャンは容易に並列化できます。利用可能なすべてのスレッドとテーブルのすべてのパーツにわたって、距離が同時に計算されます。
- 分散。 分片化されたクラスターでは、処理が複数のマシンに分散されます。各分片が自身のスライスを並列にスキャンし、コーディネーターがショートリストをマージします。
- 追加の構築コストなし。 量子化コードはベクトルの書き込み時に生成されます。構築、調整、再構築が必要な追加の索引はないため、テーブルはデータが書き込まれるとすぐに検索可能になります。
コーデックの適用
Array(Float32) (または Array(Float64) / Array(BFloat16)) カラムに Quantized(...) コーデックを指定します。
SET enable_quantized_codec = 1;
CREATE TABLE vectors
(
id UInt32,
vec Array(BFloat16) CODEC(Quantized('rabitq', 1536))
)
ENGINE = MergeTree
ORDER BY ...;コーデックは後から ALTER TABLE で追加または変更することはできません。
量子化方式
各量子化方式には、サイズと精度の間で異なるトレードオフがあります。dimensions 引数はベクトル長です。
Quantized('rabitq', dimensions)— 各座標に 1 ビットの符号ビットと、バイアスのないコサイン補正係数 (dimensions/8 + 4バイト) を持ちます。小さく、popcountでの計算負荷が低い、強力なデフォルトです。cosineDistanceのみ対応。Quantized('turboquant', dimensions)— 各座標に 2 ビット (1 ビットの MSE コードと 1 ビットの残差コード) を使い、より高忠実度な候補を実現します (dimensions/4 + 4バイト) 。cosineDistanceのみ対応。Quantized('int8', dimensions)— 各座標に 1 つのInt8コードとベクトルノルムを持ちます (dimensions + 4バイト) 。サイズは最大ですが、最も忠実なフラットコードです。L2DistanceとcosineDistanceに対応します。Quantized('prefix', dimensions, leading_dimensions, 'int8'|'bf16')— Matryoshka: 先頭のleading_dimensions個の座標だけを、Int8(ベクトルごとの scale 付き) またはBFloat16として保持します。Matryoshka Representation Learning で学習した埋め込み向けの非常に小さなコードです。L2DistanceとcosineDistanceに対応します。Quantized('product', dimensions, nbits, m)— Product Quantization: パーツごとのコードブックを k-means で学習し、各ベクトルをnbitsビットのm個のコードに変換します (そのためdimensionsはmの倍数である必要があります) 。最もコンパクトな選択肢で、1 バイトあたりの再現率も最も高い一方、insert 時に学習ステップが必要です。L2DistanceとcosineDistanceに対応します。
rabitq と turboquant では、dimensions は 8 の倍数である必要があります。
コーデック の使用
通常の上位 k クエリ形式では、コーデック が自動的に使用されます (完全検索を参照) 。
WITH [...] AS reference_vec
SELECT id
FROM vectors
ORDER BY cosineDistance(vec, reference_vec) ASC
LIMIT 10
SETTINGS vector_search_use_quantized_codes = 1;vector_search_use_quantized_codes = 1 を設定すると、オプティマイザはクエリを2フェーズのスキャンに書き換えます。
この設定はデフォルトで無効です。
この設定を使用しない場合、同じクエリは元のベクトルに対する通常の完全スキャンとして実行されます。
すべての手法で距離関数 cosineDistance がサポートされます。手法 int8、prefix、product では、距離関数として L2Distance もサポートされます。
vector_search_index_fetch_multiplier の設定では、クエリの LIMIT に対してショートリストに含める候補数を指定します。
乗数を大きくすると、再スコアリングが増える代わりに再現率が向上します。
デフォルトは 1 (オーバーサンプリングなし) です。良好な再現率を得るには、通常これを (たとえば 10 に) 増やす必要があります。
量子化ビット (QBit)
厳密ベクトル検索を高速化する一般的な方法の 1 つは、より低精度の float data type を使用することです。
たとえば、ベクトルを Array(Float32) ではなく Array(BFloat16) として保存すると、データサイズは半分になり、クエリの実行時間もそれに応じて短くなることが期待されます。
この手法は量子化として知られています。計算は高速になりますが、すべてのベクトルを総当たりで走査していても、結果の精度が低下する可能性があります。
従来の量子化では、検索時とデータ保存時の両方で精度が失われます。上の例では、Float32 ではなく BFloat16 を保存することになるため、あとから必要になっても、より高精度な検索は実行できません。別の方法として、量子化済みデータとフル精度データの 2 つのコピーを保存するやり方があります。これは機能しますが、余分なストレージが必要です。たとえば、元データが Float64 で、異なる精度 (16 ビット、32 ビット、または完全な 64 ビット) で検索を実行したいケースを考えてみましょう。この場合、データのコピーを 3 つ別々に保存する必要があります。
ClickHouse は、こうした制約に対処する Quantized Bit (QBit) データ型を提供しており、次のことが可能です。
- 元のフル精度データを保存する。
- 量子化の精度をクエリ時に指定する。
これは、データをビットごとにグループ化した形式 (つまり、すべてのベクトルの i 番目のビットをまとめて保存する形式) で保存することで実現され、必要な精度レベルでのみ読み取れるようになります。これにより、必要に応じて元のデータをすべて利用できる状態を保ちながら、量子化による I/O と計算量の削減による高速化のメリットを得られます。最大精度を選択した場合、検索は厳密になります。
QBit 型のカラムを宣言するには、次の構文を使用します。
column_name QBit(element_type, dimension[, stride])ここで:
element_type– 各ベクトル要素の型。サポートされている型はInt8、BFloat16、Float32、Float64ですdimension– 各ベクトルの要素数stride– 任意。dimensionの約数で、次元をdimension / stride個の連続したグループに分割し、それぞれを別々のストリームに格納します。これにより、先頭側の次元だけを対象とする検索では、読み取るストリーム数を減らせます (Matryoshka 埋め込みで有用です)。既定値はdimensionで、この場合、この型はストライドなしのQBitとバイト単位で同一です。詳細はQBitdata type page を参照してください。
QBit テーブルの作成とデータの追加
CREATE TABLE fruit_animal (
word String,
vec QBit(Float64, 5)
) ENGINE = MergeTree
ORDER BY word;
INSERT INTO fruit_animal VALUES
('apple', [-0.99105519, 1.28887844, -0.43526649, -0.98520696, 0.66154391]),
('banana', [-0.69372815, 0.25587061, -0.88226235, -2.54593015, 0.05300475]),
('orange', [0.93338752, 2.06571317, -0.54612565, -1.51625717, 0.69775337]),
('dog', [0.72138876, 1.55757105, 2.10953259, -0.33961248, -0.62217325]),
('cat', [-0.56611276, 0.52267331, 1.27839863, -0.59809804, -1.26721048]),
('horse', [-0.61435682, 0.48542571, 1.21091247, -0.62530446, -1.33082533]);QBit を使ったベクトル検索
L2距離を使って、単語「lemon」を表すベクトルの最近傍を見つけてみましょう。距離関数の3番目のパラメータでは、ビット単位の精度を指定します。値が大きいほど精度は高くなりますが、そのぶん必要な計算量も増えます。
QBit で使用できる距離関数の一覧は、こちらで確認できます。
フル精度検索 (64ビット) :
SELECT
word,
L2DistanceTransposed(vec, [-0.88693672, 1.31532824, -0.51182908, -0.99652702, 0.59907770], 64) AS distance
FROM fruit_animal
ORDER BY distance; ┌─word───┬────────────distance─┐
1. │ apple │ 0.14639757188169716 │
2. │ banana │ 1.998961369007679 │
3. │ orange │ 2.039041552613732 │
4. │ cat │ 2.752802631487914 │
5. │ horse │ 2.7555776805484813 │
6. │ dog │ 3.382295083120104 │
└────────┴─────────────────────┘低精度検索:
SELECT
word,
L2DistanceTransposed(vec, [-0.88693672, 1.31532824, -0.51182908, -0.99652702, 0.59907770], 12) AS distance
FROM fruit_animal
ORDER BY distance; ┌─word───┬───────────distance─┐
1. │ apple │ 0.757668703053566 │
2. │ orange │ 1.5499475034938677 │
3. │ banana │ 1.6168396735102937 │
4. │ cat │ 2.429752230904804 │
5. │ horse │ 2.524650475528617 │
6. │ dog │ 3.17766975527459 │
└────────┴────────────────────┘12ビットの量子化では、距離を十分に良好に近似しつつ、クエリをより高速に実行できることがわかります。相対的な順序もおおむね保たれており、'apple' が引き続き最も近い一致です。
パフォーマンスに関する考慮事項
QBit のパフォーマンス上の利点は、精度を下げるほどストレージから読み込むデータ量が減り、I/O 操作が少なくなることにあります。さらに、QBit に Float32 データが含まれている場合、精度パラメーターが 16 以下であれば、計算量の削減によるさらなる効果も得られます。精度パラメーターは、精度と速度のトレードオフを直接左右します。
- より高い精度 (元のデータ幅に近い) : 結果はより正確になりますが、クエリは遅くなります
- より低い精度: 近似結果になる代わりにクエリが高速化し、メモリ使用量も削減されます
参考資料
ブログ: