ANN検索プロダクトの速度比較

手元の環境でも、ANN検索ができるオープンソースプロダクトの検索について、応答時間を比較してみる。この分野は、進化が速いので、現時点での参考くらいにしかならないと思いますが…。

比較を簡単に実行できるようにcodelibs/search-ann-benchmarkを作成しているが、GitHub Actionsで実行した結果をまとめてみる。

利用するデータは、768次元の10万件のベクトルをHNSWでインデクシングして、1万件のクエリーを投げて、平均応答時間を出すようにしてます。1回の検索で10件, 100件, 400件を取得した場合の応答時間です。GitHub Actionsでの実行環境は以下のとおりです。

  • OS: Ubuntu 22.04.4 LTS
  • CPU Model: AMD EPYC 7763 64-Core Processor
  • Memory: 15981 MB
  • Docker: 24.0.9

実行結果は以下でした。

10件100件400件
Elasticsearch 8.12.22.74269.850336.4273
Milvus 2.4.0-rc.14.129225.051267.5851
OpenSearch 2.12.01.794211.909238.9887
Qdrant 1.8.20.4475720.6266441.22407
Vespa 8.319.91.96562.26674.6405
Weaviate 1.24.45.718236.650111.8378
ベクトル検索の平均応答時間(ミリ秒)

今回、安定して速いのは、Qdrantでした。Vespaももっとチューニングとかすれば、もう少し改善するかもしれません。Lucene系は取得件数が増えるほど、劣化する現象がありますが、毎回見る現象ですね。

この結果は、参考までにという感じで、search-ann-benchmarkをcloneすれば、手元の環境でも実行できると思うので、興味があればお試しください。その他、補足的なコメントをまとめておくと、

  • 設定をチューニングすれば、改善余地はあるかも
  • HNSWで同じような設定値にはしているが、この辺もデータ数などによって、見直したほうが良いかも
  • 量子化を適用したものもありますが、結果の精度も比較したい
  • PG Vectorなど、対象にできていないものも追加したい
  • 実行結果は自動で収集して、一覧できるようにしたい

という感じで、まだまだツッコミどころもあったりするので、気になるところはプルリクとかもらえると嬉しいです。

ベクトル検索製品の性能比較

ANNを利用できるプロダクトも増えてきているけど、手元の環境を利用して、同じような条件で比較できる方法が見当たらない気がする。あったとしても、そのプロダクトに最適化された比較で参考にならなかったり、試すのに手間がかかりすぎたりと、気軽に試すことができないと思う。

そんなわけで、ノートブックでポチポチやるだけで、検索の応答時間が出せるようなものをsearch-ann-benchmarkとして、作り始めた。シンプルに試すことを目的にしているので、ノートブック内でDockerを起動して、環境を構築して利用する感じなので、たぶん、利用しやすいと思う。

比較対象の製品も増やしていきたいと思うし、データセットや設定などは設定ファイル化して、いろいろと比較できるようにしたいとは思っています。けど、どこまでできるかは不明…。

そんな感じで、手元で気軽にベクトル検索を実行して応答時間を確認できる環境、を目指すrepositoryです。

Elasticsearchでunit-length vectorsのエラー

Elasticsearchでdense_vector型を利用する際に、similarityをdot_productにして、単位ベクトルにしていても、The [dot_product] similarity can only be used with unit-length vectors. Preview of invalid vector: [... みたいなエラーが発生する場合がある。しかも、1万件中数件くらいしか、発生しないので、よくわからない感じにもなる…。

単位ベクトルを以下のようにnumpyで単位ベクトルに変換していた。

embedding = embedding / np.linalg.norm(embedding)

変換の仕方に問題はないのだが、embeddingのndarrayがnp.float16だったりすると、場合によっては、今回の問題が発生する可能性がある。なので、

embedding = embedding.astype(np.float32)
embedding = embedding / np.linalg.norm(embedding)

という感じで、np.float32にしてから単位ベクトルにして、Elasticsearchに投げてあげると、エラーにならなくなる。