洋食の日記

「だ・である」調ではなく「です・ます」調で書きはじめれば良かったなと後悔してる人のブログです

近似最近傍探索ライブラリHnswlibのNode.js bindingを作っていた

はじめに

Hnswlibは、近似最近棒探索のベンチマークでも上位に位置する、Hierarchical Navigable Small World graphsによるC++の近似最近傍探索ライブラリである。Node.jsのbindingがなかったので、腱鞘炎のリハビリとnpm作成の勉強がてら作成していた。ずっと前に。

github.com

インストール

普通にnpmをインストールすれば使える(Windowsではnativeなnpmはビルドツールを別で入れる必要があるみたいですね)。ブラウザでは動かない。サーバーサイドで利用することを想定してる。

$ npm i hnswlib-node

使い方

アイテムベクトルをnumberのArrayで表現し、それをインデックスに追加する。

import { HierarchicalNSW } from 'hnswlib-node';

const numDimensions = 8; // ベクトルの次元数
const maxElements = 10; // 検索インデックスに入れるベクトルの最大数

// メトリックと次元数を指定してインスタンスを作成し, ベクトルの最大数を与えて初期化する.
const index = new HierarchicalNSW('l2', numDimensions);
index.initIndex(maxElements);

// numberのArrayで表されるベクトルを, 添字とともに検索インデックスに追加する.
for (let i = 0; i < maxElements; i++) {
  const point = new Array(numDimensions);
  for (let j = 0; j < numDimensions; j++) point[j] = Math.random();
  index.addPoint(point, i);
}

// 保存する.
index.writeIndexSync('foo.dat');

検索は、numberのArrayによるクエリベクトルと近傍数を与えればよい。

import { HierarchicalNSW } from 'hnswlib-node';

// メトリックと次元数を指定してインスタンスを作成し, 検索インデックスを読み込む.
const numDimensions = 8;
const index = new HierarchicalNSW('l2', numDimensions);
index.readIndexSync('foo.dat');

// 適当なクエリベクトルを用意する.
const query = new Array(numDimensions);
for (let j = 0; j < numDimensions; j++) query[j] = Math.random();

// 近傍数とともクエリベクトルを与えて検索する.
const numNeighbors = 3;
const result = index.searchKnn(query, numNeighbors);

// 距離とラベルによる検索結果が得られる.
console.table(result);

hnswlibのver. 0.7.0で追加されたmark_deleteや検索結果のfilteringにも対応している。

おわりに

勉強がてら作っていたnpmだが、ChatGPTが流行って、LangChainのJS/TS版のベクトル検索に採用されて利用者が増えた (実際にベクトル検索使ってる人は限られるだろうが) 。dalaiなんかもそうだが、Node.jsコミュニティは大規模言語モデル (Large Language Models, LLMs) への反応が速かった。今後、LLMsを容易に利用できるかは、重要になりそう。ちなみに、hnswlibのRuby bindingも開発を続けてます。