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)に実施しています。

項目
CPUApple M5 Pro
RAM48 GB
OSDarwin 25.3.0 (macOS)
Rustrustc 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+HNSW2773925023,4001,024
FAISS HNSW F326219411,6531,5034,096
Qdrant (in-memory)~2,400~3,300~390
FAISS Flat (brute force)37—8711,000+4,096
pgvector~62,000+~5—15

※ Qdrantとpgvectorは同一条件(1M, 1024d, ef=50)で別途Pythonスクリプトから計測しました。FAISS Flatはbrute-forceなのでパラメータなし、単一クエリで37—87usの範囲です。

この結果から分かること

注目すべきポイントは3つあります。

  1. Schift vs Qdrant: 8.7—9.8倍の差がある。 後述しますが、SQ8圧縮 + mmap + in-processの3つが効いています。
  2. Schift vs FAISS HNSW: 2.2倍の差。 同一アルゴリズムでの保存フォーマット差がそのままレイテンシに出ています。
  3. 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+HNSW0.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 MB
FAISS HNSW F32: 621 us | 1,503 QPS | 4,096 MB
──────────────────────────────────
2.2x 高速 2.3x 高スループット 4x 省メモリ

注目すべきポイント

SchiftがFAISS HNSWより遅い区間はありませんでした。メモリ圧縮によってcache効率が上がり、それがそのままレイテンシ改善に直結しています。ちなみにメモリ使用量は4分の1なので、同じマシンで4倍のデータを扱えます。


再現手順

全てのコードを公開しているので、手元で再現できます。

Rustベンチマーク

engine/crates/engine-store/tests/bench_competitors.rs
cargo test -p engine-store --test bench_competitors --release \
-- bench_1m_sq4_sq8 --nocapture

FAISS比較スクリプト

engine/crates/engine-store/tests/bench_1m_competitors.py
python3 -u tests/bench_1m_competitors.py

Qdrant比較スクリプト

engine/crates/engine-store/tests/bench_qdrant.py
python3 -u tests/bench_qdrant.py

3つのスクリプトは全て同じ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ベンチマークよりこちらの方が現実的な比較になるので、次はこれを計測してみたいと思っています。

ベンチマークは継続的に更新していく予定です。環境が変われば数字も変わるので、そのたびに再計測します。


参考

Ready to try Schift?

Switch embedding models without re-embedding. Start free.

Get started free