Engineering
自作ベクトルエンジンをQdrant・FAISSと比較してみた(1M vectors, 1024d)
Rustで書いたベクトル検索エンジンSchiftのベンチマークを取ってみました。Qdrantに8.7倍勝ち、FAISS Flatには負けます。勝つところも負けるところも全部出します。
結論から言うと、私たちの自作エンジン(Schift Engine)はQdrantに対してp50で8.7倍速く、FAISS HNSWに対して2.2倍速い、という結果になりました。一方で素のFAISS Flat brute-forceには3.2—7.5倍負けています。
「うちのエンジンはX倍速い」系の記事にありがちな、都合のいい条件だけ見せるやつはやりたくないので、勝っているところも負けているところも全部出します。テストコードも公開しています。
検証環境
計測は全て同一マシン、同一日(2026-03-26)に実施しています。
| 項目 | 値 |
|---|---|
| CPU | Apple M5 Pro |
| RAM | 48 GB |
| OS | Darwin 25.3.0 (macOS) |
| Rust | rustc 1.94.0 (2026-03-02) |
| ターゲット | aarch64-apple-darwin |
ベクトルの条件は以下のとおりです。
- 次元数: 1024
- ベクトル数: 1,000,000
- top-k: 10
- HNSW: M=32, efConstruction=200, efSearch=50
- クエリ数: 1000 (warmup 10回を除外)
- ベクトル生成:
PCG64(seed=42)による合成正規分布
※ 合成ベクトルなので、実際のテキストembedding分布とは異なる可能性があります。
検索は全てシングルスレッドで回しています。インデックスのビルドだけマルチスレッドを許可しました。
計測結果
1M vectors, dim=1024, top-k=10
| エンジン | p50 (us) | p95 (us) | p99 (us) | QPS | メモリ (MB) |
|---|---|---|---|---|---|
| Schift SQ8+HNSW | 277 | 392 | 502 | 3,400 | 1,024 |
| FAISS HNSW F32 | 621 | 941 | 1,653 | 1,503 | 4,096 |
| Qdrant (in-memory) | ~2,400 | ~3,300 | — | ~390 | — |
| FAISS Flat (brute force) | 37—87 | — | — | 11,000+ | 4,096 |
| pgvector | ~62,000+ | — | — | ~5—15 | — |
※ Qdrantとpgvectorは同一条件(1M, 1024d, ef=50)で別途Pythonスクリプトから計測しました。FAISS Flatはbrute-forceなのでパラメータなし、単一クエリで37—87usの範囲です。
この結果から分かること
注目すべきポイントは3つあります。
- Schift vs Qdrant: 8.7—9.8倍の差がある。 後述しますが、SQ8圧縮 + mmap + in-processの3つが効いています。
- Schift vs FAISS HNSW: 2.2倍の差。 同一アルゴリズムでの保存フォーマット差がそのままレイテンシに出ています。
- FAISS Flat: ぶっちゃけ速い。 素のbrute-forceが37—87usで、HNSWの277usより速い。ただし比較としては不適切で、理由は後で説明します。
勝っているところ: Qdrantとの差
8.7—9.8倍という差は、ちゃんと説明できる要因があります。
SQ8圧縮
F32(4バイト/次元)の代わりに8-bit scalar quantizationで保存しています。1024次元のベクトル1つが4,096バイトから1,024バイトに。メモリ帯域幅が1/4になるので、cache lineに4倍のベクトルが乗ります。これが一番効いています。
mmapベースのsegment
データをmmapで載せているので、OSのページキャッシュと連携してhot segmentは実質in-memoryとして動きます。物理メモリを超えた分はOSが勝手にevictしてくれるので、Qdrantのin-memoryモードよりメモリ管理がシンプルです。
Rust in-processの直接呼び出し
GC pauseなし、Python FFIのオーバーヘッドなし。QdrantもRust製ですが、Pythonクライアント経由のローカルモードだとgRPC/HTTPレイヤーとシリアライズのコストが乗ります。今回のベンチマークはRustバイナリの直接呼び出しなので、そのオーバーヘッドがありません。
※ つまりQdrantをサーバーモードやgRPC直接呼び出しで計測すれば差は縮まる可能性があります。この点は「限界」セクションで触れます。
というわけで、SQ8圧縮 + mmap segment + GC-free Rustの3つが組み合わさった結果です。
負けているところ: FAISS Flat brute-force
ここは正直に書きます。素のbrute-forceではFAISSに3.2—7.5倍負けています。
ただ、これは比較自体が適切ではないと考えています。
FAISS FlatはBLAS(OpenBLAS, Accelerate等)で最適化された行列積ベースのbrute-force探索です。グラフ探索は一切なく、1M個のベクトルを全数走査します。Apple SiliconのAMX加速がそのまま効くので、とにかく速い。
一方、私たちのエンジン(HNSW)はグラフ探索でapproximationを行います。精度を少し犠牲にして探索範囲を絞るのが本質です。
recallの差を見てみましょう。
| 方式 | recall@10 |
|---|---|
| F32 brute force (ground truth) | 1.000 |
| Schift SQ8+HNSW | 0.980 |
注目すべきポイント
2%のrecall損失で、p50基準で3—7倍速いレスポンスが得られます。そして1M以上のスケールではbrute-forceはメモリもレイテンシもつらくなってきます。Flat indexは1Mの時点で4GB使っていて、10Mだと40GBです。
FAISS Flatは小規模でprecisionが最優先の用途向けです。100万ベクトル超のリアルタイム検索に使うツールではありません。
pgvectorは土俵が違う
pgvectorとの225—751倍の差を見ると「PostgreSQLってそんなに遅いの?」と思うかもしれませんが、これも比較としては正しくないです。
pgvectorは汎用OLTPデータベースの上にベクトル検索を載せたものです。トランザクション、JOIN、フィルタ、リレーショナルクエリが必要なときに使います。純粋なベクトル検索のレイテンシを詰めるためのツールではありません。
「RDBが必要で、ベクトル検索もほしい」ならpgvectorが正解です。「ベクトル検索自体がボトルネック」ならば、専用エンジンを選ぶべきです。
FAISS HNSWとの比較が一番フェア
ここが一番見てほしいところです。FAISS HNSW F32との比較はapple-to-appleと言えます。
同一アルゴリズム(HNSW)、同一パラメータ(M=32, efConstruction=200, efSearch=50)、同一マシン。違いは保存フォーマットだけ。FAISSはF32のraw vectorを、SchiftはSQ8のquantized vectorを使っています。
Schift SQ8+HNSW: 277 us | 3,400 QPS | 1,024 MBFAISS HNSW F32: 621 us | 1,503 QPS | 4,096 MB ────────────────────────────────── 2.2x 高速 2.3x 高スループット 4x 省メモリ注目すべきポイント
SchiftがFAISS HNSWより遅い区間はありませんでした。メモリ圧縮によってcache効率が上がり、それがそのままレイテンシ改善に直結しています。ちなみにメモリ使用量は4分の1なので、同じマシンで4倍のデータを扱えます。
再現手順
全てのコードを公開しているので、手元で再現できます。
Rustベンチマーク
cargo test -p engine-store --test bench_competitors --release \ -- bench_1m_sq4_sq8 --nocaptureFAISS比較スクリプト
python3 -u tests/bench_1m_competitors.pyQdrant比較スクリプト
python3 -u tests/bench_qdrant.py3つのスクリプトは全て同じPCG64(seed=42)でベクトルを生成し、同じpercentile計算を使っています。seedが同じなので再現可能です。
このベンチマークの限界
ここもちゃんと書いておきます。
シングルマシン、シングルラン。 全ての数字はApple M5 Pro 1台での計測です。AMD Epyc、AWS c7g、GCP n4などでは結果が変わる可能性があります。特にIntel系CPUだとFAISSのAVX-512最適化がもっと効くかもしれません。
合成ベクトル。 PCG64正規分布で生成したベクトルは実際のテキストembeddingとは分布が異なります。実データには次元間の相関やクラスタ構造があるので、recallもQPSも変わりえます。
分散環境は未検証。 シングルノードのin-process計測のみです。Qdrantの分散モード、フィルタリングクエリ、書き込みと検索の同時実行などは一切測っていません。
Qdrantのローカルモード。 QdrantはPythonクライアントのin-memoryモードで計測しています。gRPC直接呼び出しやサーバーモードでは結果が変わる可能性があります。Qdrantチームがこの計測条件に同意するかは分かりません。
SQ8のrecall損失。 2%のrecall損失は多くのユースケースでは問題にならないレベルです。ただし法律文書の精密検索や医療RAGのようにprecisionが絶対的に重要なドメインでは、この2%が無視できない可能性があります。
今後やりたいこと
現在SQ8がproduction defaultです。まだ実験段階のものをいくつか紹介します。
- TQ4 (TurboQuant-inspired): 1回目の実装でrecall@10が0.49という結果になり、使い物にならなかったです。論文の実装をもっと忠実に追った再実装が必要です。
- SQ4: メモリをSQ8の半分(512MB)に抑えられますが、1MスケールでQPSが1,138とSQ8(3,400)に大きく劣ります。極端なメモリ制約環境向けとしてのみ検討中です。
- フィルタリングベンチマーク: 実際のプロダクションではmetadataフィルタ + ベクトル検索の組み合わせの方がずっと多いです。純粋なANNベンチマークよりこちらの方が現実的な比較になるので、次はこれを計測してみたいと思っています。
ベンチマークは継続的に更新していく予定です。環境が変われば数字も変わるので、そのたびに再計測します。