Scaling HNSWs

Salvatore Sanfilippo

HNSWのスケーリング

原文は Salvatore Sanfilippo により に公開されました。 このブログを購読する

HNSWの開発は数週間ほど休止している(今は別のデータ構造に取り組んでいて、近々お知らせする予定だ)。現時点で、Redisに追加した新しい型は十分に安定し、完成度も高まったので、HNSWについて学んだことを整理してブログ記事にするには絶好のタイミングだ。AI以前にはよく見かけた、頭の中をそのまま吐き出すようなブレインダンプも、今では少し珍しくなったかもしれない。ほぼ1年にわたってHNSWやベクトル類似度について考え、実装してきたのだから、そろそろ文章にまとめる時だろう。ただしこれはHNSWの入門記事ではない。入門記事はすでに溢れるほど存在するからだ。これはむしろ「もう一歩先」の話だ。HNSWを知っている人に向けて、より「発展的な」知見を共有したい。とりわけ、HNSWを「Redisらしい」体験ができるほど高速にするという文脈での知見だ。ご存じのとおりRedisは低レイテンシと高パフォーマンスのために設計されているが、HNSWはどちらかといえばそれに抗う存在で、HNSWを抽象的なデータ構造として提供するにはいくつもの課題があった。

このブログ記事はいくつかのセクションに分かれている。同じ本の異なるページ、同じ経験の異なる章だと思ってほしい。ちなみに、この記事は一度書き上げてから失くしてしまった :D [macOSと悪い習慣にまつわる長くて悲しい話だ――90年代の停電以来、こんな形で何かを失ったことはなかった]。だから今回は、数日前に書いたことを思い出すのが主な課題で、ついでにあまり気に入っていなかった部分をよりうまく言い換えていくことになる。

HNSWの現状について一言

HNSWの内部構造や最適化に踏み込む前に、HNSWについて一言述べておきたい。HNSWを初めて紹介した原論文は素晴らしい計算機科学の文献であり、HNSW自体も驚くべきデータ構造だが、距離関数に従って近傍ベクトルを貪欲に探索する手法として、これが最終形だとは思っていない。論文はどこか「欠けている」感じがして、研究者たちがあと半年あればもっと多くのことを探求し、語れたのではないかという印象すら受ける。例えば、私自身が論文を拡張して、エントリの削除に対応させた。単に削除済みとしてマークして後で回収するトゥームストーン削除ではなく、実際に削除する本当の削除だ。原論文には項目の削除についてまったく触れられていない。同様に、今まさに「HNSW」の「H」が本当に必要なのか、単一レイヤーのフラットなデータ構造でも性能はほぼ変わらないのではないか、という検証も進められている(この件については将来もう少し詳しく書きたいと思っている。私の感覚では真実はその中間にあり、レベル選択関数を調整して一定の閾値以上のレベルのみを持つようにするのが理にかなっている)。

要するに、もしあなたがデータ構造の研究に興味があるなら、HNSWの進化や改良を考えることは非常に有望な領域だと思う。ただし「ディスク対応にしてみよう」(Microsoftの取り組みを参照)のような方向だけに囚われるべきではない。さて、前置きはこのくらいにして、実際の低レイヤーの話に入ろう :)

メモリのスケーリング

Redisはインメモリシステムであり、HNSWもベクトルもどちらも非常にメモリを食うという不幸な性質を持っている。理由は3つある。1. HNSWは多くのポインタを持つ。16本、32本、あるいはそれ以上(これはHNSWのチューニング可能なパラメータだ)の隣接ノードへのポインタだ。2. HNSWはスキップリストのようなデータ構造で、多くのレイヤーを持つ。これが1つ目の問題をさらに悪化させる。3. HNSWの付随データは浮動小数点数のベクトルであり、通常のケースでは1成分あたり4バイトで、通常300〜3000成分を持つ。これが一般的な範囲だ。

では、ここから得られた教訓は何だろうか。ポインタを圧縮する人もいる。64ビットシステムでは多くのポインタ(8バイト)の上位4バイトが同じになる可能性が高いからだ。これは賢いやり方だが、Redisでは速度が求められるため、まだ実装していない。空間と時間のトレードオフだからだ。やる価値があるかもしれないし、ないかもしれない。さらに掘り下げてみるつもりだ。

しかし計算してみると、レイヤーが多いことは見た目ほど*ひどく*はない。平均すると、ノードごとの複数レイヤーによるオーバーヘッドは約1.3倍にしかならない(レベル選択関数でレベルが上がる確率が0.25の場合)。多くのノードはレイヤー0にしか存在しないからだ。とはいえ1.3は1より大きいし、もしHNSWの「H」が*それほど*有用でないのなら……[ネタバレすると、私が見つけたのは、すべてをレイヤー0に置いた場合、探索時間は長くなるということだ。貪欲探索のメインループはより最適でない場所から開始することになり、最終的には正しいクラスタに到達するものの、より多くの計算時間がかかる。ただしこれはまだ初期的な結果だ。]

だから、ここでの*本当の*おいしい果実はベクトルの量子化だ。私が見つけたのは、8ビット量子化を使うとほぼ4倍の高速化が得られ、ベクトル自体も4分の1のサイズになる(ただしノード全体が4分の1になるわけではない。ポインタはそのまま残り、多くの領域を占める)うえに、現実のユースケースではリコールがほぼ変わらないということだ。これがRedisのVector Setsがデフォルトで8ビット量子化を使っている理由だ。VADDのオプションでフル精度のベクトルやバイナリ量子化されたベクトルを指定することもできる。バイナリ量子化では符号だけを取るのだが、フルサイズのベクトルとバイナリ量子化ベクトルの両方を使うことには懐疑的だ。それらについて語る前に、8ビットでどんな量子化を使っているか見てみよう。

私がやっているのは、各ベクトルの成分の絶対値の最大値を計算し(つまり量子化はベクトルごとに行われる)、符号付き8ビットの-127から127の値で量子化して表現することだ。これは最小値と最大値の両方を保存するほど精度は良くないが、コサイン類似度を計算する際には高速になる。なぜなら次のようにできるからだ。

/* Each vector is quantized from [-max_abs, +max_abs] to [-127, 127]
 * where range = 2*max_abs. */
const float scale_product = (range_a/127) * (range_b/127);

そして整数領域で掛け合わせる(実際のコードではメインループがアンロールされ、複数のアキュムレータを使って現代のCPUをより忙しく働かせている)。

for (; i < dim; i++) dot0 += ((int32_t)x[i]) * ((int32_t)y[i]);

そして最後に、浮動小数点の距離に戻すには次のようにする。

float dotf = dot0 * scale_product;

詳しくはvectors_distance_q8()を見てほしいが、考え方は掴めたと思う。整数の量子化領域から、量子化されていない内積へと、ごく単純な操作で戻せるのだ。

つまり8ビット量子化は非常にお得で、フル精度は*必須*の機能だった。ベクトルの生成方法によってはごくわずかな差が重要になるようなことをする人も出てくるだろうからだ(いや、学習されたベクトルではそうはならないのだが……)。では、なぜバイナリ量子化なのか? それは、*元々の*情報がすでにバイナリである場合に、ユーザーが無駄に容量を使わずに済む簡単な方法を提供したかったからだ。例えば、ユーザーの集合があり、yes/noのプロパティを持っていて、似たユーザーを、あるいはアイテムなどを探したいとする。そういった場面こそバイナリ量子化を使うべきところだ。これもまたVADDコマンドのオプションに過ぎない。

速度のスケーリング:スレッドと局所性

ああ、自分のことについて少し話しておかなければ。私は、単一コアでできるだけのことをやり、その上でシェアードナッシングアーキテクチャで複数コアを使うことができるなら、スレッド化されたシステムはあまり好まない。しかしHNSWは別だ。HNSWは*遅い*し、ほとんどのユースケースではほぼ常に読み取り専用でアクセスされる。だからこそ、私のVector Setsの実装は完全にスレッド化されている。読み取りだけでなく、書き込みも部分的にスレッド化されており、特にRedisのようなシステムで、キーがバックグラウンドの保存プロセスやクライアントなどさまざまな方法でアクセスされうる中で、これがどうやって混乱を招かずに可能なのか不思議に思うかもしれない。

まず読み取りに焦点を当てよう。データ構造への書き込みが誰も行っていない限り、近傍ベクトルを貪欲に収集するスレッドを立ち上げ、結果をブロックされたクライアントに返せる。しかし、私のHNSW実装はゼロから書いたものだ。つまりvimで空のCファイルを開くところから始めて、他のシステムが使っている2つの実装とはコードの共有が0%であり、いくつか「新規性」がある。その一つが、すでに訪問したノードを再訪しないようにするために、(ハッシュテーブルのような)別のデータ構造でマークするのではなく、各ノードに格納された「エポック」と呼ばれる整数を使うことだ。ハッシュテーブルでマークする方法はかなり遅いと思う。一方エポックはノードに局所的で、グローバルなデータ構造が探索ごとにエポックをインクリメントする。だから各探索のコンテキストでは、見つかるエポックは必ず現在のエポック以下であり、現在のエポックを使って訪問済みノードをマークできるのだ。

しかしスレッドがあると、複数の探索が同時に発生する! そう、必要だったのはエポックの配列だった。

typedef struct hnswNode {
    uint32_t level;         /* Node's maximum level */
    … many other stuff …
    uint64_t visited_epoch[HNSW_MAX_THREADS];
}

それがhnsw.hで読めるものだ。これもまた空間と時間のトレードオフであり、ここでも時間が空間に勝った。

では、どうやってスレッド化された書き込みを可能にしたのか? トリックは、HNSWへの挿入では、近傍候補を探すのに多くの時間が費やされるという点にある。だから書き込みは読み取り部分とコミット部分に分割され、後者だけが書き込みロックを必要とする。そして、最初の段階で集めた候補が、その間にHNSWが変化した場合には破棄されるようにし、一部のノードが無効になっている可能性に備えるための細かい工夫がいくつかある。しかし、もう一つ問題がある。バックグラウンドスレッドがその値に取り組んでいる間に、ユーザーがキーを削除したらどうなるか? このシナリオのために、実際にオブジェクトを解放する前にバックグラウンド操作が完了するのを待つ関数を用意している。こうした工夫により、現実のベクトルワークロードで50k ops/秒を容易に達成できる。これはredis-benchmark自体から得られた数値で、すべてのオーバーヘッドを含んだものだ。フラットなHNSWライブラリ自体の素の数値は、はるかに高い。

メモリのスケーリング:適切な解放

複数のインスタンスを使って大規模なユースケースにHNSWをスケールさせる方法や、なぜRedisのVector Setsがデータ構造そのものをユーザーの目の前に晒しているのかについて話す前に(私はプログラマは賢く、過保護にする必要はないと思っているが、それ*だけ*が理由ではない)、もう一度メモリの話に戻りたい。この点について語るべき興味深い話があるからだ。

ほとんどのHNSW実装は、グラフからノードを削除したときにメモリを直接回収することができない。これには主に2つの理由があると思う。

1. 人々が原論文を特定の点で誤解している。隣接ノード間のリンクは相互でなくてもよいと信じているのだ。そして彼らがそう考えるのには明確な理由がある。

2. 論文ではノードの削除や、ノードが消えて接続の「網」に欠損が生じた後にグラフをどう修復するかについて何も述べられていない。

1つ目の問題は、論文の明確さの欠如と、HNSWを実装する際に人々が直面する特定の問題が組み合わさったものだと思う(そう信じている)。新しいノードを挿入する際に、既存ノードの中から良い隣接ノードを探すと、候補がすでに最大数の外向きリンクを持っていることがよくある。この場合どうすべきか? この問題は、新しく挿入するノードから、すでに外向きリンクで「満杯」の候補へ一方向にリンクすることで解決されることが多い。しかし、ノードを削除する必要が生じたとき、そのノードへのすべての入力リンクを解決できなくなるため、メモリを本当に回収することができない。フラグで削除済みとしてマークし、後でグラフを再構築して古いノードを「ガベージコレクト」したり、時にはメモリをリークさせたままにしたりするのだ。

そこで、Redisでの私の実装では、リンクを双方向にすることを強制することで、違うやり方をしている。AがBにリンクしていれば、BもAにリンクする。しかし、Aがビジーである場合にどうやってそれを実現するのか? これは複雑な領域に入るが、実際には既存のノードから、他とよく接続されている別の隣接ノードへのリンクをドロップするためのヒューリスティクスが使われる。もし我々のノードがターゲットノードにとってもより良い候補であればそうするし、そうでない場合でも、新しいノードが少なくとも最小数のリンクを持つように強制する他の方法があり、常にグラフのスモールワールド性を満たそうとしている。

この方法により、RedisがVector Setからノードを削除する際には、常にそのノードへのすべてのポインタを削除する方法がある。しかし、リンクを1本失った残りのノードをどうするか? 私がやっているのは、それらの間に距離行列を作り、古いノードの隣接ノード同士を、平均距離が最小になるようにリンクし直そうとすることだ。基本的に、行列内のi,jの各ペアについて、それらの接続がどれだけ良いか(ベクトルがどれだけ似ているか)と、それらをリンクすることが*残りの*可能なペアにどれだけ悪影響を与えるか(特定の2つのノードをリンクしてしまうと、良いペアを得られない要素が残ってしまう可能性があるため)を計算する。このスコアの行列を構築した後、貪欲なペアリングのステップに進む。

これは非常によく機能し、数百万要素を持つ大きなHNSWを構築した後で、その要素の95%を削除しても、残りのグラフは依然として良好なリコールを持ち、孤立したノードも生じないといった具合だ。

私がHNSWには新しい論文がその研究を継続する余地があると言うとき、それはこういうことを意味している。

HNSWを複数プロセスにスケールさせる

私がRedis Vector Setsに取り組み始めたとき、Redisの世界にはすでにベクトル類似度の実装が存在していた。具体的にはRediSearchのインデックス種別の一つとしてだ。そしてこれが、ほとんどの人がHNSWについて考える際の捉え方だ。既存のデータに対する一種のインデックスという捉え方だ。

しかし私は、Redisにまったく異なる形で公開される新しいHNSW実装を提供したかった。どんな形か想像できるだろうか? もちろんデータ構造としてだ。そしてこれは、長年のRedis経験で私の頭がどれだけRedis的に形作られているか、あるいは最初から私の頭がRedis的で、Redisの方が私の頭に合わせて形作られているのか、という物語を物語っている。というのも、HNSWをユーザーに直接公開するRedisデータ構造をどう設計するかをすぐに思い描くことができ、なぜRedisでのベクトルの取り組みがまさにそのように行われていなかったのか不思議に思ったからだ。

同時に、設計ドキュメントをRedisの同僚たちに渡したとき、彼らがすぐにそれを「自明なこと」として理解したとは言えない。私の考えはこうだった。ベクトルはRedisのSorted Setにおけるスコアのようなものだが、全順序を持つスカラーのスコアではないという違いがある。それでもVADDやVREMで要素を追加・削除でき、そしてZRANGEの代わりにVSIMを呼ぶことで*類似した*要素を得られる。これはAPIとして理にかなっているだけでなく、HNSWは強くコンポーザブルで、特定のユースケース(テキスト埋め込みや画像埋め込み、あるいは必ずしも*学習された*埋め込みにすら)に結びついていないと考えたからだ。次のようにする。

VADD my_vector_set VALUES [… components …] my_element_string

つまり、コンポーネントに何が入っていようとRedisは気にせず、VSIMを呼び出せば類似した要素を返してくれる。

しかしこれはまた、もし同じユースケースについて異なるベクトルが異なるインスタンス/キーに分割されている場合、すべてのインスタンスに対して同じクエリベクトルでVSIMを問い合わせ、WITHSCORESオプション(コサイン距離を返す)を付けて、結果をクライアント側でマージすれば、数億のベクトルを複数のインスタンスに分割して、データセットをN分割して魔法のようにスケールできることも意味する[このユースケースについて興味深いのは、クライアントライブラリが十分に賢ければ、多重化を使ってN個のインスタンスに並列にクエリできることだ]。

HNSWをこのような生の形で公開することのもう一つの非常に注目すべき点は、書き込みを非常に簡単にスケールできることだ。要素をNで割った余りでハッシュし、結果のRedisキー/インスタンスをターゲットにするだけでよい。複数のインスタンスが(遅いとはいえ、HNSWの基準では依然として高速な)書き込みを同時に吸収し、本来は非常に遅いプロセスを並列化できるのだ。

HNSWを公開するこの方法は、小さくスケールする際にも非常に大きな意味を持つ。ユーザー/アイテム/プロダクトなど、扱っているものごとにHNSWを持ちたい場合がある。これは何かの上にインデックスがある場合にはモデル化するのが非常に難しいが、HNSWがデータ構造であれば話は簡単だ。アイテムごとにVector Setキーを持つだけでよく、要素はほんのわずかでよい。そしてもちろん、他のRedisキーと同様に、キーに有効期限を設定すれば、後で自動的に削除される。

これらすべては、私たちの業界でもっと意識されるべき一つの原則に集約できる。多くのプログラマは賢く、アクセスできない魔法のようなシステムを作る代わりに、データ構造やトレードオフを彼らに見せれば、彼らはより多くのものを作り、ユースケースを特定の方法でモデル化できる。そしてあなたのシステムもよりシンプルになるのだ。

ロード時間のスケーリング

スレッドを使わなければ、私のHNSWライブラリはword2vec(各ベクトル300成分)を単一スレッドで毎秒5000要素のペースでHNSWに追加でき、結果として得られたHNSWに対しては毎秒9万クエリを実行できる。ご覧のとおり大きな隔たりがある。

これは、Redisのダンプファイルから数百万要素を持つHNSWをメモリにロードし直すのに非常に時間がかかることを意味する。そしてこの時間はレプリケーションにも影響する。あまり良くない。しかしこれは、要素をディスクからメモリへ最も単純な方法、つまり「要素、ベクトル」をディスクに保存してからメモリ内でHNSWを再構築しようとする場合にのみ当てはまる。ここにも学ぶべき教訓がある。HNSWを使うときは、ノードや隣接ノードをそのままの形でシリアライズする必要があり、そうすればメモリ内で隣接ノードのIDをポインタに変換するだけで、割り当てを行うだけで全体を再構築できる。これにより100倍の高速化が得られた。

しかし本当に話はここで終わると思うだろうか? ふふ。最近のRedisはより強力なセキュリティ機能を持ち、RDBファイルが攻撃者によって破損した場合でも危険なことをしないようにしている。だから私がやらなければならなかったのは、シリアライズされたデータ構造にエラーや破損があっても、ロード後にHNSWが有効であることを保証することだった。これには多くのトリックが伴ったが、ここでは私が書いたコメントの一つをそのまま載せる自由を取らせてもらおう。相互性のチェックが特にクールだと思うからだ。

/* Second pass: fix pointers of all the neighbors links.
 * As we scan and fix the links, we also compute the accumulator
 * register "reciprocal", that is used in order to guarantee that all
 * the links are reciprocal.
 *
 * This is how it works, we hash (using a strong hash function) the
 * following key for each link that we see from A to B (or vice versa):
 *
 *      hash(salt || A || B || link-level)
 *
 * We always sort A and B, so the same link from A to B and from B to A
 * will hash the same. Then we xor the result into the 128 bit accumulator.
 * If each link has its own backlink, the accumulator is guaranteed to
 * be zero at the end.
 *
 * Collisions are extremely unlikely to happen, and an external attacker
 * can't easily control the hash function output, since the salt is
 * unknown, and also there would be to control the pointers.
 *
 * This algorithm is O(1) for each node so it is basically free for
 * us, as we scan the list of nodes, and runs on constant and very
 * small memory. */

ユースケースのスケーリング:JSONフィルタ

Vector Setsの最初の動作する実装が完成したと感じた日のことを覚えている。すべてが期待通りに動き、改良や追加機能に取り掛かるための出発点となった。

しかし過去数週間から数ヶ月の間に、ほとんどのユースケースでは何らかの混合検索が必要だというフィードバックを内部で受け取った。特定のクエリベクトルに近いベクトルが欲しいが(何かに最も似ている映画のような)、同時に何らかのフィルタリングもしたい(2000年から2010年の間に公開されたものだけ、といった)のだ。私の感覚では、異なるパラメータでクエリする必要性はプロダクトの人々が考えているほど頻繁ではなく、ほとんどの場合、例えばこの特定のケースでは各年を異なるVector Setキーに追加することで、より効率的にこれを実現できると思っている(これはHNSWをインデックスの一種としてではなくデータ構造として表現することのコンポーザビリティの別の例だ)。

しかし私は、HNSWの貪欲探索のメインループについて考えていた。それは次のようなものだ。

// Simplified HNSW greedy search algorithm. Don’t trust it too much.
while(candidates.len() > 0) {
    c = candidates.pop_nearest(query);
    worst_distance = results.get_worst_dist(query);
    if (distance(query,c) > worst_distance) break;
    foreach (neighbor from c) {
        if (neighbor.already_visited()) continue;
        neighbor.mark_as_visited();
        if (results.has_space() OR neighbor.distance(query) < worst_distance) {
            candidates.add(neighbor);
            results.add(neighbor);
        }
    }
}
return results;

そこで、各ノードにJSONのメタデータの集合を関連付けたらどうかというアイデアで遊び始めた。{“year”: 1999}のようなものがあれば、貪欲探索を実行しながらフィルタリングするのに十分ではないか? もちろん探索は制限される必要があるが、ここには重要な洞察がある。私はまずクエリベクトルに*近い*要素が欲しいので、JSON属性の条件が多くのノードで満たされない場合にグラフ全体を探索する必要は本当はないのだ。ユーザーに労力(effort)を指定させればよいし、いずれにせよフィルタに一致する非常に遠くの結果は役に立たない。

だからこれもまた、私のHNSWが異なるもう一つの点だ。プログラミング言語の「if」文の中に書けるような式でフィルタリングをサポートしているのだ。そしてVector Set内の要素には、そのプロパティを表すJSONブロブを紐付けられる。すると次のようなことができる。

VSIM movies VALUES … your vector components here… FILTER '.year >= 1980 and .year < 1990'

メモリ使用量について一言

HNSWの致命的な問題は――理論上は――通常メモリから提供されることだ。実際には、ディスクアクセスのレイテンシという観点からはより適したデータ構造があるとはいえ、HNSWをディスク上に実装することもできる。しかしRedisとVector Setsの特定のケースでは、非常に高速で扱いやすいものを提供するという考えがある。インメモリデータ構造の柔軟性はそれに役立つ。だから問題は次のように絞られる。本当にメモリ使用量はそんなに悪いのか?

300万件のWord2Vecエントリをデフォルトのint8量子化でRedisにロードすると3GBのRAMを消費し、1エントリあたり1KBだ。多くのユースケースでは数千万件、あるいはそれよりずっと少ないエントリしか持たない。そして、適切に実装され、メモリ内にあるHNSWから得られるものは非常に良いパフォーマンスであり、これは定義上遅いデータ構造やワークロードにおいて極めて重要だ。私のMacBookでは、このword2vecデータセットを保持するキーに対してredis-benchmarkとVSIMで毎秒48k opsが出る。インメモリHNSWのメモリ使用量は多くのユースケースで十分に許容可能だというのが私の感覚だ。そして、ベクトルの大部分をディスクに置きたいユースケースでさえ、たとえ遅いパフォーマンスという代償を払うとしても、ホットセットはおそらくRAMから提供されるべきだろう。

これが、HNSWの研究に携わることが良い考えだと私が信じる理由の一つだ。HNSWがほとんどのユースケースで近いうちに置き換えられるとは思わない。ユースケースやデータサイズに応じて、RAM用とディスク用で理想的なデータ構造が異なり続ける可能性の方が高いように思える。さらに、最近Hacker Newsのフロントページを眺めているだけでも、数百万件のアイテムを抱えながら必要以上に遅かったり複雑だったりするシステムと格闘している人々を見かける。HNSWを適切な方法で慎重に公開すれば、そうしたすべてを回避できるのだ。

おわりに

HNSWが好きだし、それらに取り組み実装することは本当に楽しいことだった。ベクトルはRedisに非常によく合うと思う。AIのない世界でもそうだ(例えば数ヶ月前、私はHacker Newsユーザーのフィンガープリントを作成するためにそれらを使った。過去にHNで公開された古い研究を再現したのだ)。HNSWは多くのユースケースにとって単純にクールで強力すぎるし、AIや学習された埋め込みによって、その潜在的なユースケースは無数に拡大する。しかし、Redisのほとんどの機能と同様に、人々がそれらが有用で強力であり、どう使えばよいかに気づくまでには多くの時間がかかるだろうと予想している(いや、RAGだけの話ではないのだ)。これはStreamsでも起こったことだ。ようやく何年も経って大規模な普及が進んだ。

もしむしろHNSWや私が書いた実装により興味があるなら、コードはかなり読みやすく、大量にコメントが付いていると思う。

https://github.com/redis/redis/blob/unstable/modules/vector-sets/hnsw.c

Redis Vector Setsについてもっと知りたい場合は、私自身が書いたREADMEファイルを読んでみてほしい。公式のRedisドキュメントもあるが、まずはここから始めることをお勧めする。

https://github.com/redis/redis/tree/unstable/modules/vector-sets

こんなに長いブログ記事を読んでくれてありがとう! 良い一日を。

参考文献。この「H」 in HNSWがどれだけ有用かについての論文はこちら -> https://arxiv.org/abs/2412.01940

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント