ククログ

株式会社クリアコード > ククログ > GroongaでANN検索(HNSWアルゴリズム)への道1: USearchに入門する準備

GroongaでANN検索(HNSWアルゴリズム)への道1: USearchに入門する準備

USearchに入門中の阿部です。

何のために入門しているかというとGroongaでHNSWアルゴリズムのANN検索をするためです。 そのためにUSearchを活用しようという魂胆です。

Groonga開発者に聞け!(グルカイ!)第70回Groonga開発者に聞け!(グルカイ!)第71回でより詳しい全体像を説明をしているので合わせてご覧ください。

この数行の中にも専門用語が何個か登場しました。用語がわからないことには何も始まらないので、用語のとても簡単な説明とUSearchに入門するためにやったことのまとめ記事です。

理解が深まったタイミングなど、区切りごとに記事にしていくので乞うご期待!

用語の簡単説明

予防線を張りまくりで恐縮ですが、まだまだ入門中の段階なのでだいぶ浅い説明です。 理解が深まったら追加記事で補足していくのご容赦ください。

  • Groonga
    • https://groonga.org/
    • Groonga は転置索引を用いた高速・高精度な全文検索エンジン
  • ANN検索
    • Approximate Nearest Neighbor(近似最近傍探索)
      • (Artificial Neural Networkではない)
    • いい感じにベクトル(= embedding)検索してそれっぽい結果を得る
      • 「それっぽい結果」がポイント。「近似最近傍探索」の「近似」の所以
  • HNSWアルゴリズム
    • Hierarchical Navigable Small World
    • ANN検索でよく使われるアルゴリズム
  • USearch

GroongaとANN検索

GroongaでANN検索をする狙いの1つはセマンティック検索(検索クエリの意味を踏まえて検索)です。

現在のGroongaではキーワードで検索するので、検索対象の文書にそのキーワードが含まれていないとヒットしません。 セマンティック検索では検索テキストから利用者の意図をくみ取り検索するので、検索対象の文書に含まれていないキーワードで検索しても得たい検索結果が得られるようになり便利です。

ということで、Groongaをより便利にするためにANN検索をできるようにしようという流れです。 GroongaでのANN検索の実現方法には自前で実装する選択肢もありましたが、今回はUSearchを利用することにしました。(USearchがマッチしなそうなことがわかったら違う選択肢を模索します。)

そういうわけで、USearchに入門していきます。

USearchに入門する準備

USearchについてよくわかっていないので、入門するための準備としてドキュメントの通りにビルドしてJavaScriptでとりあえず動かしてみるところまでやります。 (JavaScriptで動かしてみる理由は私が好きだからです。それ以上の深い理由はないです。)

参考ドキュメント:

環境構築例

USearchをビルドする & JavaScriptで動かしてみるために必要なパッケージをインストールする必要があります。 Ubuntu 24.04での環境構築例です。

# ビルド用
sudo apt-get update
sudo apt-get install -y -V \
  build-essential \
  cmake \
  g++-12 \
  gcc-12 \
  git \
  libjemalloc-dev \
  wget

# JavaScript用
sudo apt-get install -y -V npm
sudo wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ${HOME}/.nvm/nvm.sh
nvm install 20
npm install -g typescript

ビルドする

開発目的なので -DCMAKE_BUILD_TYPE=RelWithDebInfo でビルドします。 ドキュメントに書いてある通り実行すればビルドできます。 ドキュメントと同じ内容なので実行したコマンドの記載のみにとどめます。

git clone https://github.com/unum-cloud/usearch.git
cd usearch
git submodule update --init --recursive
cmake -DUSEARCH_BUILD_TEST_CPP=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo -B build_relwithdebinfo
cmake --build build_relwithdebinfo --config RelWithDebInfo

次のコマンドでテストが実行できます。

build_relwithdebinfo/test_cpp

JavaScriptで動かす

動かす前に準備します。 「ビルドする」で cd usearch した状態の前提で実行したコマンドを記載します。

npm install
npm run build-js

npm install で必要なパッケージもインストールしつつ、JavaScript用のバイナリをビルドします。 npm run build-jsjavascript/usearch.ts をJavaScriptから使えるようにビルドします。

動かす準備ができたので試しに動かしてみます。 以下のスクリプトを example.js など適当な名前で保存して実行します。 (ドキュメントにあるコード例と同じです。)

const assert = require('node:assert');
const usearch = require('usearch');
const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
const results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10);

assert(index.size() === 1);
assert.deepEqual(results.keys, new BigUint64Array([42n]));
assert.deepEqual(results.distances, new Float32Array([0]));

index.remove(42n);
$ node example.js

期待通りに実行できていればアウトプットは何もありません。

簡単にコードの解説です。

次のコードでインデックスを作成して、ベクトルデータを登録しています。 42n というIDで、[0.2, 0.6, 0.4] というベクトルを登録しています。

const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
index.add(42n, new Float32Array([0.2, 0.6, 0.4]));

そして検索するときは以下の通りです。 index.search() の最初の引数が検索対象です。このベクトルに近いベクトルを検索します。 2つ目の引数は取得する結果の最大数です。

const results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10);

assert は結果のチェックなどをしているだけなので説明は割愛。 最後に index.remove(42n) でインデックスからIDが 42n のデータを削除しています。

インデックスを作るときのオプション { metric: 'l2sq', connectivity: 16, dimensions: 3 } の意味が気になっていると思いますが、今回は割愛します。まだまだ入門中なので次回までに理解して説明します! (とりあえず動かした状況なので、まだよくわかっていない状態です :-))

まとめ

書いている本人も理解が浅い中で記事にしてしまい恐縮でしたが、さらに理解を深めてGroongaへ機能を追加していく所存です。 次回はUSearchの入門が終わった段階で、USearchについての詳しい説明がある記事になっていると思います!