.NETでMomento Vector Indexを始める

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

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

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

前提条件

Momento API Key

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

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

APIキーのページで、キャッシュが存在する場所に一致する情報を選択します:
1.クラウドプロバイダー – AWS
2.リージョン – us-west-2
3.キータイプ – スーパーユーザー
4.(オプション)有効期限

Momento .NET SDK

プログラムで使用するには、Momento SDKパッケージをインストールする必要があります。これはnugetで入手可能で、.NET CLIで追加できます。

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

これが、これから説明する基本的なモメント・ベクター・インデックスの例です。


using Momento.Sdk;
using Momento.Sdk.Auth;
using Momento.Sdk.Config;
using Momento.Sdk.Messages.Vector;
using Momento.Sdk.Requests.Vector;
using Momento.Sdk.Responses.Vector;

namespace VectorExample;

public static class Program
{
    public static async Task Main()
    {
        var config = VectorIndexConfigurations.Laptop.Latest();
        var authProvider = new EnvMomentoTokenProvider("MOMENTO_API_KEY");
        using IPreviewVectorIndexClient client = new PreviewVectorIndexClient(config, authProvider);

        // create a momento vector index
        const string indexName = "getting-started-index";
        var createIndexResponse =
            await client.CreateIndexAsync(indexName, numDimensions: 2,
                similarityMetric: SimilarityMetric.CosineSimilarity);
        switch (createIndexResponse)
        {
            case CreateIndexResponse.Success:
                Console.WriteLine($"Index with name {indexName} successfully created!");
                break;
            case CreateIndexResponse.AlreadyExists:
                Console.WriteLine($"Index with name {indexName} already exists");
                break;
            case CreateIndexResponse.Error error:
                Console.WriteLine($"Error while creating index {indexName}: {error.Message}");
                break;
        }

        // list all indexes
        var listIndexesResponse = await client.ListIndexesAsync();
        switch (listIndexesResponse)
        {
            case ListIndexesResponse.Success success:
                Console.WriteLine($"Found indexes: {string.Join(", ", success.Indexes.Select(i => i.Name))}");
                break;
            case ListIndexesResponse.Error error:
                Console.WriteLine($"Error while listing indexes: {error.Message}");
                break;
        }

        // upsert data into the index
        var items = new List
        {
            new("item_1", new List { 0.0f, 1.0f },
                new Dictionary { { "key1", "value1" } }),
            new("item_2", new List { 1.0f, 0.0f },
                new Dictionary { { "key2", 12345 }, { "key3", 678.9 } }),
            new("item_3", new List { -1.0f, 0.0f },
                new Dictionary { { "key1", new List { "value2", "value3" } } }),
            new("item_4", new List { 0.5f, 0.5f },
                new Dictionary { { "key4", true } })
        };
        var upsertResponse = await client.UpsertItemBatchAsync(indexName, items);
        switch (upsertResponse)
        {
            case UpsertItemBatchResponse.Success:
                Console.WriteLine("Successfully added items");
                break;
            case UpsertItemBatchResponse.Error error:
                Console.WriteLine($"Error while adding items to index {indexName}: {error.Message}");
                break;
        }

        // wait a short time to ensure the vectors are uploaded
        Thread.Sleep(2000);

        // search the index
        var searchResponse = await client.SearchAsync(indexName, new List { 1.0f, 0.0f }, topK: 4,
            metadataFields: MetadataFields.All);
        switch (searchResponse)
        {
            case SearchResponse.Success success:
                Console.WriteLine($"Search succeeded with {success.Hits.Count} matches:");
                foreach (var hit in success.Hits)
                {
                    Console.WriteLine($"ID: {hit.Id}, score: {hit.Score}, metadata:");
                    foreach (var (key, value) in hit.Metadata)
                    {
                        Console.WriteLine($"  {key}: {value}");
                    }
                }

                break;
            case SearchResponse.Error error:
                Console.WriteLine($"Error while searching on index {indexName}: {error.Message}");
                break;
        }

        // delete the index
        var deleteResponse = await client.DeleteIndexAsync(indexName);
        switch (deleteResponse)
        {
            case DeleteIndexResponse.Success:
                Console.WriteLine($"Index {indexName} deleted successfully!");
                break;
            case DeleteIndexResponse.Error error:
                Console.WriteLine($"Error while deleting index {indexName}: {error.Message}");
                break;
        }
    }
}

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

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

Index with name getting-started-index successfully created!

Found indexes: getting-started-index

Successfully added items

Search succeeded with 4 matches:

ID: item_2, score: 1, metadata:    

key3: 678.9  

key2: 12345

ID: item_4, score: 0.7071067690849304, metadata:

  key4: True

ID: item_1, score: 0, metadata:

   key1: value1

ID: item_3, score: -1, metadata:

  key1: value2, value3

Index getting-started-index deleted successfully!

Process finished with exit code 0.

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

クライアントの作成

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

var config = VectorIndexConfigurations.Laptop.Latest();
var authProvider = new EnvMomentoTokenProvider("MOMENTO_API_KEY");
using IPreviewVectorIndexClient client = new PreviewVectorIndexClient(config, authProvider);

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

コンフィギュレーションには、基礎となるgRPCクライアントの設定と、顧客が設定可能な機能が含まれます。毎回コンフィギュレーションを作成する必要がないように、あらかじめコンフィギュレーションを用意しています。VectorIndexConfigurations.Laptop.Latest() は Laptop コンフィギュレーションの最新バージョンです。latestは常に最新バージョンを指します。クライアントはIDisposableなので、usingステートメントを使用することで、コンテキストから外れたときに自動的にクリーンアップすることができます。

CreateIndexAsync

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


// create a momento vector index
const string indexName = "getting-started-index";
var createIndexResponse =
    await client.CreateIndexAsync(indexName, numDimensions: 2,
        similarityMetric: SimilarityMetric.CosineSimilarity);
switch (createIndexResponse)
{
    case CreateIndexResponse.Success:
        Console.WriteLine($"Index with name {indexName} successfully created!");
        break;
    case CreateIndexResponse.AlreadyExists:
        Console.WriteLine($"Index with name {indexName} already exists");
        break;
    case CreateIndexResponse.Error error:
        Console.WriteLine($"Error while creating index {indexName}: {error.Message}");
        break;
}

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

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

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

ListIndexesAsync

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


var listIndexesResponse = await client.ListIndexesAsync();
switch (listIndexesResponse)
{
    case ListIndexesResponse.Success success:
        Console.WriteLine($"Found indexes: {string.Join(", ", success.Indexes.Select(i => i.Name))}");
        break;
    case ListIndexesResponse.Error error:
        Console.WriteLine($"Error while listing indexes: {error.Message}");
        break;
}

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

UpsertItemBatchAsync

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


var items = new List
{
    new("item_1", new List { 0.0f, 1.0f },
        new Dictionary { { "key1", "value1" } }),
    new("item_2", new List { 1.0f, 0.0f },
        new Dictionary { { "key2", 12345 }, { "key3", 678.9 } }),
    new("item_3", new List { -1.0f, 0.0f },
        new Dictionary { { "key1", new List { "value2", "value3" } } }),
    new("item_4", new List { 0.5f, 0.5f },
        new Dictionary { { "key4", true } })
};
var upsertResponse = await client.UpsertItemBatchAsync(indexName, items);
switch (upsertResponse)
{
    case UpsertItemBatchResponse.Success:
        Console.WriteLine("Successfully added items");
        break;
    case UpsertItemBatchResponse.Error error:
        Console.WriteLine($"Error while adding items to index {indexName}: {error.Message}");
        break;
}

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

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

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

SearchAsync

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


var searchResponse = await client.SearchAsync(indexName, new List { 1.0f, 0.0f }, topK: 4,
    metadataFields: MetadataFields.All);
switch (searchResponse)
{
    case SearchResponse.Success success:
        Console.WriteLine($"Search succeeded with {success.Hits.Count} matches:");
        foreach (var hit in success.Hits)
        {
            Console.WriteLine($"ID: {hit.Id}, score: {hit.Score}, metadata:");
            foreach (var (key, value) in hit.Metadata)
            {
                Console.WriteLine($"  {key}: {value}");
            }
        }

        break;
    case SearchResponse.Error error:
        Console.WriteLine($"Error while searching on index {indexName}: {error.Message}");
        break;
}

SearchAsync関数は、インデックス名、インデックスの次元数に一致するクエリ・ベクトル、返す結果の数を表すオプションのtopK引数、そしてどのメタデータを返してほしいかを表すオプションのmetadataFields引数を受け取ります。ここではMetadataFields.Allのセンチネル値を使用しています。つまり、すべてのメタデータを返す必要があるということです。例えばnew List {"key1", "key3"}を指定すると、それらのフィールド名にマッチするメタデータのみが返されるようになります。metadataFieldsが指定されていない場合は、メタデータは返されません。

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

ID: item_2, score: 1, metadata:

  key3: 678.9

  key2: 12345

ID: item_4, score: 0.7071067690849304, metadata:

 key4: True

ID: item_1, score: 0, metadata:

 key1: value1

ID: item_3, score: -1, metadata:

 key1: value2, value3

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

DeleteIndexAsync

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


var deleteResponse = await client.DeleteIndexAsync(indexName);
switch (deleteResponse)
{
    case DeleteIndexResponse.Success:
        Console.WriteLine($"Index {indexName} deleted successfully!");
        break;
    case DeleteIndexResponse.Error error:
        Console.WriteLine($"Error while deleting index {indexName}: {error.Message}");
        break;
}

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

準備はできましたか?

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

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