IVFとApache Dorisでの使用方法
IVFインデックスは、近似最近傍(ANN)検索に使用される効率的なデータ構造です。検索時にベクトルの範囲を絞り込むことで、検索速度を大幅に改善します。Apache Doris 4.x以降、IVFベースのANNインデックスがサポートされています。このドキュメントでは、IVFアルゴリズム、主要パラメータ、エンジニアリングプラクティスについて説明し、本番環境のDorisクラスタでIVFベースのANNインデックスを構築・調整する方法を解説します。
IVFインデックスとは?
完全性のため、歴史的背景を説明します。IVF(inverted file)という用語は情報検索に由来します。
いくつかのテキストドキュメントの簡単な例を考えてみましょう。指定された単語を含むドキュメントを検索するために、転置インデックスは各ドキュメントの単語リストを格納します。関連するドキュメントを見つけるには、各ドキュメントを明示的に読む必要があります。
| Document | Words |
|---|---|
| Document 1 | the,cow,says,moo |
| Document 2 | the,cat,and,the,hat |
| Document 3 | the,dish,ran,away,with,the,spoon |
対照的に、逆転インデックスには検索可能なすべての単語の辞書が含まれ、各単語について、その単語が出現するドキュメントインデックスのリストがあります。これが逆転リスト(inverted file)であり、選択されたリストに検索を制限することができます。
| Word | Documents |
|---|---|
| the | Document 1, Document 3, Document 4, Document 5, Document 7 |
| cow | Document 2, Document 3, Document 4 |
| says | Document 5 |
| moo | Document 7 |
現在、テキストデータはベクトル埋め込みとして表現されることが多くあります。IVF手法はクラスタ中心を定義し、これらの中心は前述の例の単語辞書に類似しています。各クラスタ中心について、そのクラスタに属するベクトルインデックスのリストがあり、選択されたクラスタのみを検査すればよいため、検索が加速されます。
効率的なベクトル検索のためのIVFインデックスの使用
データセットが数百万、さらには数十億のベクトルに成長すると、クエリとデータベース内のすべてのベクトル間の距離を計算する徹底的な正確k近傍(kNN)検索の実行は、計算上実現困難になります。この総当たりアプローチは、大きな行列乗算に相当し、スケールしません。
幸い、多くのアプリケーションでは、わずかな精度と引き換えに速度の大幅な向上を図ることができます。これが近似最近傍(ANN)検索の領域であり、逆転ファイル(IVF)インデックスは最も広く使用され、効果的なANN手法の1つです。
IVFの基本原理は「分割統治」です。データセット全体を検索する代わりに、IVFは検索範囲をいくつかの有望な領域に知的に絞り込み、必要な比較回数を大幅に削減します。
IVFは、大きなベクトルデータセットをより小さく管理しやすいクラスタに分割し、それぞれを「セントロイド」と呼ばれる中心点で表現することで機能します。これらのセントロイドは、それぞれのパーティションのアンカーとして機能します。検索中、システムはクエリベクトルに最も近いセントロイドを持つクラスタを素早く特定し、それらの中でのみ検索を行い、データセットの残りの部分は無視します。

Apache DorisにおけるIVF
Apache Dorisは、バージョン4.x以降でIVFベースのANNインデックスの構築をサポートしています。
インデックス構築
ここで使用されるインデックスタイプはANNです。ANNインデックスを作成する方法は2つあります:テーブル作成時に定義する方法と、CREATE/BUILD INDEX構文を使用する方法です。この2つのアプローチは、インデックスが構築される方法とタイミングが異なるため、異なるシナリオに適合します。
アプローチ1:テーブル作成時にベクトルカラムでANNインデックスを定義します。データがロードされると、セグメントが作成される際にANNインデックスが構築されます。利点は、データロードが完了すると、インデックスが既に構築されており、クエリがすぐにそれを加速に使用できることです。欠点は、同期的なインデックス構築によりデータ取り込みが遅くなり、コンパクション中にインデックスの追加再構築が発生し、リソースの無駄につながる可能性があることです。
CREATE TABLE sift_1M (
id int NOT NULL,
embedding array<float> NOT NULL COMMENT "",
INDEX ann_index (embedding) USING ANN PROPERTIES(
"index_type"="ivf",
"metric_type"="l2_distance",
"dim"="128",
"nlist"="1024"
)
) ENGINE=OLAP
DUPLICATE KEY(id) COMMENT "OLAP"
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
INSERT INTO sift_1M
SELECT *
FROM S3(
"uri" = "https://selectdb-customers-tools-bj.oss-cn-beijing.aliyuncs.com/sift_database.tsv",
"format" = "csv");
CREATE/BUILD INDEX
アプローチ2: CREATE/BUILD INDEX。
CREATE TABLE sift_1M (
id int NOT NULL,
embedding array<float> NOT NULL COMMENT ""
) ENGINE=OLAP
DUPLICATE KEY(id) COMMENT "OLAP"
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num" = "1"
);
INSERT INTO sift_1M
SELECT *
FROM S3(
"uri" = "https://selectdb-customers-tools-bj.oss-cn-beijing.aliyuncs.com/sift_database.tsv",
"format" = "csv");
データがロードされた後、CREATE INDEXを実行できます。この時点でインデックスはテーブル上で定義されますが、既存のデータに対してはまだインデックスは構築されていません。
CREATE INDEX idx_test_ann ON sift_1M (`embedding`) USING ANN PROPERTIES (
"index_type"="ivf",
"metric_type"="l2_distance",
"dim"="128",
"nlist"="1024"
);
SHOW DATA ALL FROM sift_1M;
mysql> SHOW DATA ALL FROM sift_1M;
+-----------+-----------+--------------+----------+----------------+---------------+----------------+-----------------+----------------+-----------------+
| TableName | IndexName | ReplicaCount | RowCount | LocalTotalSize | LocalDataSize | LocalIndexSize | RemoteTotalSize | RemoteDataSize | RemoteIndexSize |
+-----------+-----------+--------------+----------+----------------+---------------+----------------+-----------------+----------------+-----------------+
| sift_1M | sift_1M | 10 | 1000000 | 170.093 MB | 170.093 MB | 0.000 | 0.000 | 0.000 | 0.000 |
| | Total | 10 | | 170.093 MB | 170.093 MB | 0.000 | 0.000 | 0.000 | 0.000 |
+-----------+-----------+--------------+----------+----------------+---------------+----------------+-----------------+----------------+-----------------+
その後、BUILD INDEX文を使用してインデックスを構築できます:
BUILD INDEX idx_test_ann ON sift_1M;
BUILD INDEXは非同期で実行されます。ジョブステータスを確認するには、SHOW BUILD INDEX(一部のバージョンではSHOW ALTER)を使用できます。
SHOW BUILD INDEX WHERE TableName = "sift_1M";
mysql> SHOW BUILD INDEX WHERE TableName = "sift_1M";
+---------------+-----------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| JobId | TableName | PartitionName | AlterInvertedIndexes | CreateTime | FinishTime | TransactionId | State | Msg | Progress |
+---------------+-----------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| 1764392359610 | sift_1M | sift_1M | [ADD INDEX idx_test_ann (`embedding`) USING ANN PROPERTIES("dim" = "128", "index_type" = "ivf", "metric_type" = "l2_distance", "nlist" = "1024")], | 2025-12-01 14:18:22.360 | 2025-12-01 14:18:27.885 | 5036 | FINISHED | | NULL |
+---------------+-----------+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
1 row in set (0.00 sec)
mysql> SHOW DATA ALL FROM sift_1M;
+-----------+-----------+--------------+----------+----------------+---------------+----------------+-----------------+----------------+-----------------+
| TableName | IndexName | ReplicaCount | RowCount | LocalTotalSize | LocalDataSize | LocalIndexSize | RemoteTotalSize | RemoteDataSize | RemoteIndexSize |
+-----------+-----------+--------------+----------+----------------+---------------+----------------+-----------------+----------------+-----------------+
| sift_1M | sift_1M | 10 | 1000000 | 671.084 MB | 170.093 MB | 500.991 MB | 0.000 | 0.000 | 0.000 |
| | Total | 10 | | 671.084 MB | 170.093 MB | 500.991 MB | 0.000 | 0.000 | 0.000 |
+-----------+-----------+--------------+----------+----------------+---------------+----------------+-----------------+----------------+-----------------+
2 rows in set (0.00 sec)
DROP INDEX
不適切なANNインデックスはALTER TABLE sift_1M DROP INDEX idx_test_annで削除できます。インデックスの削除と再作成は、ハイパーパラメータチューニング時に、望ましいrecallを達成するために異なるパラメータの組み合わせをテストする必要がある場合によく行われます。
Querying
ANNインデックスはTop‑N searchとrange searchの両方をサポートしています。
ベクトル列が高次元の場合、クエリベクトル自体のリテラル表現が追加の解析オーバーヘッドを発生させる可能性があります。そのため、本番環境では、特に高い同時実行性の下で、完全なクエリベクトルを生のSQLに直接埋め込むことは推奨されません。より良い方法は、繰り返しのSQL解析を避けるprepared statementsを使用することです。
doris-vector-search pythonライブラリの使用を推奨します。これはprepared statementsに基づいてDorisでのベクトル検索に必要な操作をラップし、便利な下流AI アプリケーション開発のためにDorisクエリ結果をPandas DataFrameにマップするデータ変換ユーティリティを含んでいます。
from doris_vector_search import DorisVectorClient, AuthOptions
auth = AuthOptions(
host="127.0.0.1",
query_port=9030,
user="root",
password="",
)
client = DorisVectorClient(database="test", auth_options=auth)
tbl = client.open_table("sift_1M")
query = [0.1] * 128 # Example 128-dimensional vector
# SELECT id FROM sift_1M ORDER BY l2_distance_approximate(embedding, query) LIMIT 10;
result = tbl.search(query, metric_type="l2_distance").limit(10).select(["id"]).to_pandas()
print(result)
サンプル出力:
id
0 123911
1 926855
2 123739
3 73311
4 124493
5 153178
6 126138
7 123740
8 125741
9 124048
Recall最適化
ベクトル検索では、recallが最も重要なメトリクスです。パフォーマンス数値は、特定のrecallレベルにおいてのみ意味があります。recallに影響を与える主な要因は以下の通りです:
- IVFのインデックス時パラメータ(
nlist)とクエリ時パラメータ(nprobe) - ベクトル量子化
- セグメントサイズとセグメント数
本記事では、(1)と(3)がrecallに与える影響に焦点を当てます。ベクトル量子化については、別の文書で説明します。
インデックスハイパーパラメータ
IVFインデックスは、ベクトルを複数のクラスタに組織化します。インデックス構築時に、ベクトルはクラスタリングを使用してグループに分割されます。検索プロセスでは、最も関連性の高いクラスタのみに焦点を当てます。ワークフローは大まかに以下の通りです:
インデックス時:
- クラスタリング:すべてのベクトルは、クラスタリングアルゴリズム(例:k-means)を使用して
nlist個のクラスタに分割されます。各クラスタの重心が計算され、保存されます。 - ベクトル割り当て:各ベクトルは、重心が最も近いクラスタに割り当てられ、そのクラスタの転置リストに追加されます。
クエリ時:
- nprobeを使用したクラスタ選択:クエリベクトルに対して、すべての
nlist個の重心との距離が計算されます。検索対象として最も近いnprobe個のクラスタのみが選択されます。 - 選択されたクラスタ内での全数検索:クエリは、選択されたnprobe個のクラスタ内のすべてのベクトルと比較され、最近傍を見つけます。
まとめ:
nlistはクラスタ数(転置リスト数)を定義します。これはrecall、メモリオーバーヘッド、構築時間に影響します。より大きなnlistは、より細かいクラスタを作成し、クエリの最近傍が適切に局所化されている場合は検索速度を向上させる可能性がありますが、クラスタリングのコストと近傍が複数のクラスタに分散されるリスクも増加させます。
nprobeはクエリ時に検索するクラスタ数を定義します。より大きなnprobeはrecallとクエリレイテンシを増加させます(より多くのベクトルが検査されます)。より小さなnprobeはクエリを高速化しますが、プローブされないクラスタに存在する近傍を見逃す可能性があります。
デフォルトでは、Dorisはnlist = 1024とnprobe = 64を使用します。
上記はこれら2つのハイパーパラメータの定性的分析です。以下の表は、SIFT_1Mデータセットでの実証結果を示しています:
| nlist | nprobe | recall_at_100 |
|---|---|---|
| 1024 | 64 | 0.9542 |
| 1024 | 32 | 0.9034 |
| 1024 | 16 | 0.8299 |
| 1024 | 8 | 0.7337 |
| 512 | 32 | 0.9384 |
| 512 | 16 | 0.8763 |
| 512 | 8 | 0.7869 |
事前に単一の最適な設定を提供することは困難ですが、ハイパーパラメータ選択のための実用的なワークフローに従うことができます:
- インデックスのないテーブル
table_multi_indexを作成します。これは2つまたは3つのベクトル列を含むことができます。 - Stream Loadまたは他の取り込み方法を使用して
table_multi_indexにデータを読み込みます。 CREATE INDEXとBUILD INDEXを使用して、すべてのベクトル列にANNインデックスを構築します。- 異なる列で異なるインデックスパラメータ設定を使用します。インデックス構築完了後、各列でrecallを計算し、最良のパラメータ組み合わせを選択します。
例:
ALTER TABLE tbl DROP INDEX idx_embedding;
CREATE INDEX idx_embedding ON tbl (`embedding`) USING ANN PROPERTIES (
"index_type"="ivf",
"metric_type"="inner_product",
"dim"="768",
"nlist"="1024"
);
BUILD INDEX idx_embedding ON tbl;
インデックスごとにカバーされる行数
内部的に、Dorisは複数の層でデータを整理します。
- 最上位はテーブルで、これは配布キーを使用してN個のtabletに分割されます。Tabletは、データシャーディング、再配置、リバランスの単位として機能します。
- 各データ取り込みまたはコンパクションにより、tablet配下に新しいrowsetが生成されます。Rowsetは、データのバージョン管理されたコレクションです。
- Rowset内のデータは、実際にはsegmentファイルに格納されます。
転置インデックスと同様に、ベクターインデックスはsegmentレベルで構築されます。Segmentサイズは、write_buffer_sizeやvertical_compaction_max_segment_sizeなどのBE構成オプションによって決定されます。取り込みとコンパクション中に、メモリ内のmemtableが一定のサイズに達すると、segmentファイルとしてディスクにフラッシュされ、そのsegmentに対してベクターインデックス(または複数のベクター列に対する複数のインデックス)が構築されます。インデックスは、そのsegment内の行のみをカバーします。
固定されたIVFパラメータセットが与えられた場合、インデックスが高いリコールを維持できるベクター数には常に制限があります。Segment内のベクター数がその制限を超えて増加すると、リコールが劣化し始めます。
SHOW TABLETS FROM tableを使用してテーブルのコンパクションステータスを調査できます。対応するURLをたどることで、segmentがいくつあるかを確認できます。
コンパクションがリコールに与える影響
コンパクションは、より大きなsegmentを作成する可能性があるため、リコールに影響を与える可能性があります。これは、元のハイパーパラメータによって暗示される「カバレッジ容量」を超える可能性があります。結果として、コンパクション前に達成されたリコールレベルは、コンパクション後にはもはや維持されない可能性があります。
BUILD INDEXを実行する前に完全なコンパクションをトリガーすることを推奨します。完全にコンパクションされたsegmentでインデックスを構築することで、リコールが安定し、インデックス再構築による書き込み増幅も削減されます。
クエリパフォーマンス
インデックスファイルのコールドローディング
DorisのIVF ANNインデックスは、MetaのオープンソースライブラリFaissを使用して実装されています。IVFインデックスは、メモリにロードされた後に有効になります。したがって、高同時実行ワークロードを実行する前に、関連するすべてのsegmentインデックスがメモリにロードされることを確認するために、いくつかのウォームアップクエリを実行することを推奨します。そうでなければ、ディスクI/Oオーバーヘッドがクエリパフォーマンスを著しく悪化させる可能性があります。
メモリフットプリント vs. パフォーマンス
量子化や圧縮を行わない場合、IVFインデックスのメモリフットプリントは、インデックス対象のすべてのベクターのメモリフットプリントの約1.02-1.1倍です。
たとえば、100万の128次元ベクターの場合、IVF-FLATインデックスには約次のメモリが必要です:
128 * 4 * 1,000,000 * 1.02 ≈ 500 MB。
参考値:
| dim | rows | estimated memory |
|---|---|---|
| 128 | 1M | 496 MB |
| 768 | 1M | 2.9 GB |
安定したパフォーマンスを維持するために、各BEが十分なメモリを持つことを確認してください。そうでなければ、頻繁なスワッピングとインデックスファイルのI/Oがクエリレイテンシを深刻に劣化させます。
ベンチマーク
ベンチマーク時は、デプロイメントモデルは本番環境のセットアップに従い、FEとBEを分離してデプロイし、クライアントは別の独立したマシンで実行する必要があります。
ベンチマークフレームワークとしてVectorDBBenchを使用できます。
Performance768D1M
ベンチマークコマンド:
# load
NUM_PER_BATCH=1000000 python3 -m vectordbbench doris --host 127.0.0.1 --port 9030 --case-type Performance768D1M --db-name Performance768D1M --stream-load-rows-per-batch 500000 --index-prop index_type=ivf,nlist=1024 --skip-search-serial --skip-search-concurrent
# search
NUM_PER_BATCH=1000000 python3 -m vectordbbench doris --host 127.0.0.1 --port 9030 --case-type Performance768D1M --db-name Performance768D1M --search-concurrent --search-serial --num-concurrency 10,40,80 --stream-load-rows-per-batch 500000 --index-prop index_type=ivf,nlist=1024 --session-var ivf_nprobe=64 --skip-load --skip-drop-old