JavaScriptでMomento Vector Indexを始める

Momentoを使用すると、JavaScriptアプリケーション用の強力で完全なベクトルインデックスを、わずか5回のAPIコールで作成できます

ベクトルインデックスとは、高次元ベクトルの格納と検索に最適化された特殊なデータストアのことです。リレーショナルデータベースのように行や列指向のデータを検索するのではなく、ベクトルインデックスはベクトル間の類似性を計算するように設計されています。テキスト検索、画像検索、レコメンデーション、画像認識、音声認識などのタスクに特に有用です。ベクトル検索は、データがベクトルとして表現でき、類似性検索が重要なあらゆるアプリケーションで活躍します。ベクトル・インデックスのより詳細な紹介は、Kirk Kirnellのベクトル・インデックスについての記事を参照してください。ベクトル検索がどのように機能し、どのように機械学習ワークフローに組み込むかについてのより詳細な情報は、ベクトル検索に関するMichael Landisの講演を参照してください。

Momento Vector Indexは、ベクターインデックスを素早く簡単に始める方法を提供します。この記事では、ベクターインデックスを設定し、そこにデータを追加し、そのデータを検索する簡単なTypeScriptプログラムを作成します。

前提条件

Momento API Key

Momento Vector Indexを使用するには、AWS us-west-2リージョンのスーパーユーザーキーが必要です。Momentoコンソールにアクセスし、指示に従ってメールアドレス、Googleアカウント、またはGitHubアカウントでログインします。

コンソールで、APIキーメニューオプションを選択します。

APIキーのページで、キャッシュが存在する場所に一致する情報を選択します:

1.クラウドプロバイダー – AWS
2.リージョン – us-west-2
3.キータイプ – スーパーユーザー
4.(オプション)有効期限

Momento JavaScript SDK

プログラムで使用するには、Momento SDKの依存関係をインストールする必要があります。

Nodeアプリケーションには@gomomento/sdkを使います。npmで提供されており、npm install @gomomento/sdkでインストールできます。

Webアプリケーションには、@gomomento/sdk-webを使います。これはnpmにもあり、npm install @gomomento/sdk-webでインストールできます。

最初のMomento Vector Indexプログラムを書く

import {
  CreateVectorIndex,
  CredentialProvider,
  DeleteVectorIndex,
  ListVectorIndexes,
  PreviewVectorIndexClient,
  VectorIndexConfigurations,
  VectorIndexItem,
  VectorSearch,
  VectorUpsertItemBatch,
} from '@gomomento/sdk';
import {ALL_VECTOR_METADATA} from '@gomomento/sdk-core';

async function main() {
  const credentialProvider = CredentialProvider.fromEnvironmentVariable({
    environmentVariableName: 'MOMENTO_API_KEY',
  });
  const configuration = VectorIndexConfigurations.Laptop.latest();
  const client = new PreviewVectorIndexClient({credentialProvider, configuration});

  // create a momento vector index
  const indexName = 'getting-started-index';
  const createIndexResult = await client.createIndex(indexName, 2, VectorSimilarityMetric.COSINE_SIMILARITY);
  if (createIndexResult instanceof CreateVectorIndex.Success) {
    console.log(`Index with name ${indexName} successfully created!`);
  } else if (createIndexResult instanceof CreateVectorIndex.AlreadyExists) {
    console.log(`Index with name ${indexName} already exists`);
  } else if (createIndexResult instanceof CreateVectorIndex.Error) {
    console.log(`Error while creating index: ${createIndexResult.errorCode()}: ${createIndexResult.toString()}`);
  }

  // list all indexes
  const listIndexesResult = await client.listIndexes();
  if (listIndexesResult instanceof ListVectorIndexes.Success) {
    console.log(
      `Indexes:\n${listIndexesResult
        .getIndexes()
        .map(listIndexesResult => JSON.stringify(listIndexesResult.getName()))
        .join('\n')}`
    );
  } else if (listIndexesResult instanceof ListVectorIndexes.Error) {
    console.log(`Error while listing indexes: ${listIndexesResult.errorCode()}: ${listIndexesResult.toString()}`);
  }

  // upsert data into the index
  const items: Array = [
    {
      id: 'item_1',
      vector: [0.0, 1.0],
      metadata: {key1: 'value1'},
    },
    {
      id: 'item_2',
      vector: [1.0, 0.0],
      metadata: {key2: 12345, key3: 678.9},
    },
    {
      id: 'item_3',
      vector: [-1.0, 0.0],
      metadata: {key1: ['value2', 'value3']},
    },
    {
      id: 'item_4',
      vector: [0.5, 0.5],
      metadata: {key4: true},
    },
  ];
  const upsertResult = await client.upsertItemBatch(indexName, items);
  if (upsertResult instanceof VectorUpsertItemBatch.Success) {
    console.log('Successfully added items');
  } else if (upsertResult instanceof VectorUpsertItemBatch.Error) {
    console.log(`Error while adding items to index: ${upsertResult.errorCode()}: ${upsertResult.toString()}`);
  }

  // wait a short time to ensure the vectors are uploaded
  await new Promise(resolve => setTimeout(resolve, 1000));

  // search the index
  const searchResult = await client.search(indexName, [1.0, 0.0], {topK: 4, metadataFields: ALL_VECTOR_METADATA});
  if (searchResult instanceof VectorSearch.Success) {
    console.log(`Search succeeded with ${searchResult.hits().length} matches`);
    console.log(searchResult.hits());
  } else if (searchResult instanceof VectorSearch.Error) {
    console.log(`Error while searching index ${indexName}: ${searchResult.errorCode()}: ${searchResult.toString()}`);
  }

  // delete the index
  const deleteResponse = await client.deleteIndex(indexName);
  if (deleteResponse instanceof DeleteVectorIndex.Success) {
    console.log(`Index ${indexName} deleted successfully!`);
  } else if (deleteResponse instanceof DeleteVectorIndex.Error) {
    console.log(`Failed to delete index ${indexName}: ${deleteResponse.errorCode()}: ${deleteResponse.toString()}`);
  }
}

main().catch(e => {
  throw e;
});

これから説明する基本的なMomento Vector Indexの例です。これは @gomomento/sdk依存関係を使用してビルドされています。Web アプリケーションの場合は web 依存関係を使用し、文字列から読み込んだ限定的な API キーを使用します。

インデックスを作成し、すべてのインデックスをリストアップし、データをアップロードし、そのデータを検索し、最後にインデックスを削除します。

Momentoの依存関係をインストールした後、プログラムを実行すると、次のような出力が得られます:

Index with name getting-started-index successfully created!

Indexes:

“getting-started-index”

Successfully added items

Search succeeded with 4 matches

[

{ id: ‘item_2’, score: 1, metadata: { key2: 12345, key3: 678.9 } },

{ id: ‘item_4’, score: 0.7071067690849304, metadata: { key4: true } },

{ id: ‘item_1’, score: 0, metadata: { key1: ‘value1’ } },

{ id: ‘item_3’, score: -1, metadata: { key1: [Array] } }

]

Index getting-started-index deleted successfully!

次のセクションでは、この出力がどのように作られたかを説明しましょう。

クライアントの作成

最初にしなければならないのは、ベクター・インデックス・クライアントを作ることです:


const credentialProvider = CredentialProvider.fromEnvironmentVariable({
  environmentVariableName: 'MOMENTO_API_KEY',
});
const configuration = VectorIndexConfigurations.Laptop.latest();
const client = new PreviewVectorIndexClient({credentialProvider, configuration});

ベクトルインデックスクライアントは、他の Momento クライアントと同様に、設定オブジェクトと認証プロバイダを必要とします。認証プロバイダは Momento API キーを読み込み、解析します。環境変数から読み込むことも、文字列から直接読み込むこともできます。

コンフィギュレーションには、基礎となるgRPCクライアントの設定と、顧客が設定可能な機能が含まれます。毎回コンフィギュレーションを作成する必要がないように、あらかじめコンフィギュレーションを用意しています。VectorIndexConfigurations.Laptop.latest()はLaptopコンフィギュレーションの最新バージョンで、ローカル開発用に構築されています。後方互換性のためにコンフィギュレーションをバージョンアップしているので、コンフィギュレーションを変更しても顧客のデプロイメントには影響しません。

createIndex

クライアントができたので、インデックスを作成します:


const indexName = 'getting-started-index';
const createIndexResult = await client.createIndex(indexName, 2, VectorSimilarityMetric.COSINE_SIMILARITY);
if (createIndexResult instanceof CreateVectorIndex.Success) {
  console.log(`Index with name ${indexName} successfully created!`);
} else if (createIndexResult instanceof CreateVectorIndex.AlreadyExists) {
  console.log(`Index with name ${indexName} already exists`);
} else if (createIndexResult instanceof CreateVectorIndex.Error) {
  console.log(`Error while creating index: ${createIndexResult.errorCode()}: ${createIndexResult.toString()}`);
}

createIndex関数は3つの引数を取ります。indexName – インデックスの名前、numDimensions – インデックスの次元数、similarityMetric – 検索でベクトルを比較するためのメトリック。

この例では、2次元のインデックスを作成します。次元は複雑なデータの特徴や属性を表すので、実際のインデックスには何百もある可能性があります。このようにするのは、検索時にインデックスがベクトルをどのように比較するかを視覚化しやすくするためです。また、類似度の指標として余弦類似度を使用しています。これはベクトル間の角度を比較するもので、-1 から 1 の間で正規化されます。

この関数は、Momento API が使用するエラー処理のパターンを示しています。クライアントメソッドは決して例外を投げてはいけません。その代わりに、さまざまなタイプのコール結果を表すレスポンスオブジェクトを返します。インデックスが作成された場合は Success、 既にその名前のインデックスが存在した場合は AlreadyExists、 呼び出しに失敗した場合は Errorとなります。すべてのMomentoコールは、特定の失敗についての詳細を含むエラーオブジェクトを返すことができます。

listIndexes

インデックスを作成したので、すべてのインデックスを一覧表示することでインデックスを確認できます:


const listIndexesResult = await client.listIndexes();
if (listIndexesResult instanceof ListVectorIndexes.Success) {
  console.log(
    `Indexes:\n${listIndexesResult
      .getIndexes()
      .map(listIndexesResult => JSON.stringify(listIndexesResult.getName()))
      .join('\n')}`
  );
} else if (listIndexesResult instanceof ListVectorIndexes.Error) {
  console.log(`Error while listing indexes: ${listIndexesResult.errorCode()}: ${listIndexesResult.toString()}`);
}

listIndexes 関数は引数をとらず、あなたの地域のあなたのアカウントのすべてのインデックスに関する情報のリストを持つ Success オブジェクトを返します。この関数は、コードがハードコードされたインデックスを使用しておらず、インデックスを調べる必要がある場合や、インデックスを作成しすぎていないことを確認するためにインデックス数を追跡する必要がある場合に便利です。

upsertItemBatch

これで新しいインデックスにベクトルを追加できます:


const items: Array = [
  {
    id: 'item_1',
    vector: [0.0, 1.0],
    metadata: {key1: 'value1'},
  },
  {
    id: 'item_2',
    vector: [1.0, 0.0],
    metadata: {key2: 12345, key3: 678.9},
  },
  {
    id: 'item_3',
    vector: [-1.0, 0.0],
    metadata: {key1: ['value2', 'value3']},
  },
  {
    id: 'item_4',
    vector: [0.5, 0.5],
    metadata: {key4: true},
  },
];
const upsertResult = await client.upsertItemBatch(indexName, items);
if (upsertResult instanceof VectorUpsertItemBatch.Success) {
  console.log('Successfully added items');
} else if (upsertResult instanceof VectorUpsertItemBatch.Error) {
  console.log(`Error while adding items to index: ${upsertResult.errorCode()}: ${upsertResult.toString()}`);
}

upsertItemBatch関数は、インデックス名とベクトルを表すItemのリストを受け取り、それらをインデックスに挿入し、IDが一致する既存のベクトルを置き換えます。Itemには、一意のID、インデックスの次元に一致するベクトル、オプションのメタデータが含まれます。メタデータのキーは文字列でなければなりませんが、値は文字列、int、float、boolean、または文字列のリストにすることができます。

ベクトル[-1.0, 0.0]、[0.0, 1.0]、[0.5, 0.5]、[1.0, 0.0]をアップロードした。これらは2次元なので、可視化することができます:

角度の違いによって、クエリーベクトルをこれらのベクトルと比較できることがおわかりいただけるでしょうか。

search

インデックスにデータが格納されたので、それを検索することができます:


const searchResult = await client.search(indexName, [1.0, 0.0], {topK: 4, metadataFields: ALL_VECTOR_METADATA});
if (searchResult instanceof VectorSearch.Success) {
  console.log(`Search succeeded with ${searchResult.hits().length} matches`);
  console.log(searchResult.hits());
} else if (searchResult instanceof VectorSearch.Error) {
  console.log(`Error while searching index ${indexName}: ${searchResult.errorCode()}: ${searchResult.toString()}`);
}

検索関数は、インデックス名、インデックスの次元数に一致するクエリベクトル、返す結果の数を表すオプションのtopK引数、そしてどのメタデータを返して欲しいかを表すオプションのmetadataFields引数を取ります。ここではALL_VECTOR_METADATAのセンチネル値を使っていますが、これはすべてのメタデータを返すという意味です: ['key1','key3']のようにフィールドのリストを指定することもできます。metadataFields が指定されていない場合は、メタデータは返されません。

検索成功のレスポンスには、検索ヒットのリストが含まれる。それぞれのヒットには、ベクトルのID、スコア、つまりそのベクトルと検索ベクトルの類似度(コサイン類似度で-1から1まで)、そしてリクエストされたメタデータが含まれます。以下はプログラムの出力例です:

[

{ id: ‘item_2’, score: 1, metadata: { key2: 12345, key3: 678.9 } },

{ id: ‘item_4’, score: 0.7071067690849304, metadata: { key4: true } },

{ id: ‘item_1’, score: 0, metadata: { key1: ‘value1’ } },

{ id: ‘item_3’, score: -1, metadata: { key1: [Array] } }

]

マッチはスコア順に返されます。ベクトル[1.0, 0.0]を検索したので、そのベクトルと完全に一致するitem_2のスコアは1.0です。ベクトル[-1.0, 0.0]を持つitem_3は正反対で、スコアは-1.0です。検索ベクトルと直交するベクトルを持つitem_1のスコアは0.0です。より高次元のベクトルは簡単に視覚化できませんが、パターンは同じです:ベクトルが検索ベクトルに近いほど、そのスコアは1.0に近くなります。ベクトルが検索ベクトルの反対側に一致すればするほど、-1.0に近づきます。‍

deleteIndex

最後に、この例の後始末をするためにインデックスを削除します:


const deleteResponse = await client.deleteIndex(indexName);
if (deleteResponse instanceof DeleteVectorIndex.Success) {
  console.log(`Index ${indexName} deleted successfully!`);
} else if (deleteResponse instanceof DeleteVectorIndex.Error) {
  console.log(`Failed to delete index ${indexName}: ${deleteResponse.errorCode()}: ${deleteResponse.toString()}`);
}

deleteIndex関数は、インデックス名を受け取り、そのインデックスとその中のすべてのデータを削除します。削除に成功するか、削除するインデックスがない場合は成功を返します。

Ready to start?

Momentoでは、シンプルさを念頭に置いてサービスに取り組んでいます。私たちは、あなたが実際に関心のある問題の解決に集中できるよう、市場で最も速く、最も簡単な開発者体験を提供することを目指しています。Momento Vector Indexは完全にサーバーレスのベクトルインデックスであり、わずか5回のAPIコールで最大限に活用できます。

Momento Vector Indexについてもっと知りたい方は、こちらの開発ドキュメントをお読みください。また、LangChainとの統合についてはこちらのブログをご覧ください。