🐥

Microsoft.Extensions.VectorData を触ってみよう

2024/11/24に公開

先日、Microsoft.Extensions.AI を AI サービスを共通的に使うための抽象化レイヤーとして紹介しました
今回、紹介する Microsoft.Extensions.VectorData はベクトルストアを共通的に扱うための抽象化レイヤーです。

Microsoft.Extensions.VectorData とは

Microsoft.Extensions.VectorData は一般的なベクトル ストアに対して CRUD をするための基本的なインターフェイスを提供します。
厳密には Microsoft.Extensions.VectorData.Abstractions に抽象化レイヤーのインターフェースが定義されています。

Microsoft.Extensions.VectorData.Abstractions に対して各ベクトル ストアに対しての実装をすることで共通のインターフェースでベクトル ストアを扱うことができます。このパッケージは、Microsoft.SemanticKernel.Plugins.Memory という Semantic Kernel にあったベクトル ストアの抽象化レイヤーを進化させたものです。実際に最新の Semantic Kernel では Microsoft.SemanticKernel.Plugins.Memory から Microsoft.Extensions.VectorData に移行しています。

Microsoft.Extensions.VectorData の使い方

Microsoft.Extensions.VectorData.Abstractions の実装は、現時点では Semantic Kernel の中にあります。例えば OSS のベクトル ストアの Qdrant の実装は Microsoft.SemanticKernel.Connectors.Qdrant というパッケージで公開されています。理想的には、各ベクトル ストアの実装は各ベクトル ストアのベンダーが作ってくれるのが理想ですが、現状は Semantic Kernel のパッケージを使うことになります。

まぁ、まだ Preview 版で出たばかりなので、これからベクトル ストアのベンダーが実装してくれることを期待しましょう。

では、使ってみましょう。

簡単な使い方

今回は、Qdrant を使ってみます。まずは、Qdrant を使うために以下の 2 つのパッケージをインストールします。

  • Microsoft.Extensions.VectorData.Abstractions
  • Microsoft.SemanticKernel.Connectors.Qdrant

Microsoft.SemanticKernel.Connectors.Qdrant は Qdrant 公式が公開している Qdrant.Client パッケージに依存して、このパッケージが公開している QdrantClient をラップする形で QdrantVectorStore が実装されています。QdrantVectorStoreMicrosoft.Extensions.VectorData.IVectorStore インターフェースを実装しています。この IVectorStore インターフェースが Microsoft.Extensions.VectorData.Abstractions に定義されているベクトル ストアを抽象化するインターフェースです。

実際に使う際には以下のようにインスタンス化します。

var client = new QdrantClient("localhost");
IVectorStore vectorStore = new QdrantVectorStore(client);

IVectorStore インターフェースはベクトルデータのコレクション (RDBMS でいうテーブル) を扱うためのインターフェースの IVectorStoreRecordCollection<TKey, TRecord> を取得するための GetCollection<TKey, TRecord>(string collectionName) メソッドを持っています。厳密にはコレクションのスキーマも一緒に渡すオーバーロードもありますが、ここでは名前指定で取ってこれるメソッドと覚えておけばいいと思います。

このコレクションを使って、ベクトル ストアにコレクションを作成したり削除したりすることができます。(RDBMS でいうテーブルの作成と削除)
さらに、基本的な CRUD 操作ができるメソッドも提供しています。

コレクションのスキーマは以下のように VectorStoreRecordKeyVectorStoreRecordDataVectorStoreRecrodVector という 3 つの属性を付けたプロパティを持つクラスとして定義します。例えば Id (キー項目) と NameVector という属性を持つクラスを定義すると以下のようになります。VectorStoreRecrodVector にはベクトルの次元数を指定します。

class MyStoreData
{
    [VectorStoreRecordKey]
    public required ulong Id { get; init; }

    [VectorStoreRecordData]
    public required string Name { get; init; }

    [VectorStoreRecordVector(1024)]
    public required ReadOnlyMemory<float> Vector { get; init; }
}

こうしておくことで、ベクトル ストアにコレクションを作成したり削除したりする際のスキーマを定義することができます。そして、このクラスに対して IVectorStoreRecordCollection<TKey, TRecord> が提供するメソッドを使って CRUD 操作を行うことができます。

使ってみよう

ということで使ってみます。ベクトル ストアを使う場合はベクトル化するためのモデルが必要なので、ちょっと手間がかかります。今回は .NET Aspire の機能を使って Ollama の bge-large というモデルを使ってベクトル可を行います。全部ローカルで完結するようにしてみましょう。

.NET Aspire 空のアプリ (ここでは VectorDataSampleApp という名前で作りました) を作成して AppHost プロジェクトに以下のパッケージをインストールします。.NET Aspire の CommunityToolkit に Ollama 用のホスティング パッケージがあるので、それを使います。またベクトル ストアとして Qdrant を使うので、Qdrant 用のパッケージもインストールします。

  • CommunityToolkit.Aspire.Hosting.Ollama (2024/11/23 時点でプレビュー版しかありません)
  • Aspire.Hosting.Qdrant

そして AppHost プロジェクトの Program.cs を編集して Ollama の bge-large モデルと、Qdrant を起動するようにします。

Program.cs
var builder = DistributedApplication.CreateBuilder(args);

// ollama を追加してデータの永続化と、コンテナがデバッグ終了時に落ちないようにする
var ollama = builder.AddOllama("ollama")
    .WithDataVolume()
    .WithLifetime(ContainerLifetime.Persistent);
// ベクトル用のモデルを追加
var embeddingModel = ollama.AddModel("bge-large");

// qdrant を追加してデータの永続化と、コンテナがデバッグ終了時に落ちないようにする
var qdrant = builder.AddQdrant("qdrant")
    .WithDataVolume()
    .WithLifetime(ContainerLifetime.Persistent);

builder.Build().Run();

この状態で AppHost プロジェクトを起動すると自動的に Ollama と Qdrant が起動します。初回起動はコンテナイメージやモデルの pull が走るので時間がかかりますが、しばらく待っていると以下のように 3 つとも Running 状態になります。(Docker が必要なので注意)

では、これを使うアプリを作っていきましょう。

データを登録するアプリを作る

Worker サービスのプロジェクトを DataInitializerApp という名前で作成します。作成時に「.NET Aspire オーケストレーションへの参加」のチェックボックスにチェックを入れておきます。こうすることで、AppHost プロジェクトを起動するときに自動的に起動するようになります。

このプロジェクトには以下の 4 つのパッケージをインストールします。

  • Aspire.Qdrant.Client
    • .NET Aspire コンポーネントの Qdrant クライアント。.NET Aspire の AppHost プロジェクトで設定した名前の接続文字列で Qdrant に接続するためのクライアントを DI コンテナに登録します。
  • Microsoft.Extensions.VectorData.Abstractions (2024/11/23 時点でプレビュー版しかありません)
    • ベクトル ストアを共通的に扱うための抽象化レイヤーのインターフェースを提供します。
  • Microsoft.SemanticKernel.Connectors.Qdrant (2024/11/23 時点でプレビュー版しかありません)
    • Qdrant のベクトル ストアを使うための Microsoft.Extensions.VectorData.Abstractions の実装です。
  • CommunityToolkit.Aspire.OllamaSharp (2024/11/23 時点でプレビュー版しかありません)
    • .NET Aspire コンポーネントの Ollama クライアント。.NET Aspire の AppHost プロジェクトで設定した名前の接続文字列で Ollama に接続するためのクライアントを DI コンテナに登録します。

このパッケージを使って DataInitializerApp プロジェクトの Program.cs を編集して各種サービスにつなぐためのクライアントを DI コンテナに登録します。

Program.cs
using DataInitializerApp;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.Qdrant;
using Qdrant.Client;

var builder = Host.CreateApplicationBuilder(args);

builder.AddServiceDefaults();
// qdrant のクライアントを追加
builder.AddQdrantClient("qdrant");
// ollama の IEmbeddingGenerator を追加
builder.AddOllamaSharpEmbeddingGenerator("ollama-bge-large");
// IVectorStore の Qdrant 用実装を追加
builder.Services.AddSingleton<IVectorStore>(sp => 
    new QdrantVectorStore(sp.GetRequiredService<QdrantClient>()));

builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();

そして Worker.cs で Qdrant に、いくつかデータを追加します。

Worker.cs
using Microsoft.Extensions.AI;
using Microsoft.Extensions.VectorData;

namespace DataInitializerApp;

public class Worker(
    IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
    IVectorStore vectorStore,
    ILogger<Worker> logger) : BackgroundService
{
    // 登録するデータ
    private static readonly string[] _sourceData = [
        "My name is Kazuki Ota.",
        "My favorite programming language is C#.",
        "My favorite game is Genshin.",
        "My birthday is 1981/01/30.",
    ];

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // データを登録するコレクションを取得
        var c = vectorStore.GetCollection<ulong, MyStoreData>("my-collection");
        // データをクリアするために、コレクションが存在していれば削除
        if (await c.CollectionExistsAsync(stoppingToken))
        {
            await c.DeleteCollectionAsync(stoppingToken);
        }
        // コレクションを作成
        await c.CreateCollectionAsync(stoppingToken);

        // データを登録
        ulong id = 0;
        foreach (var data in _sourceData)
        {
            // データから埋め込みベクトルを生成
            var embedding = await embeddingGenerator.GenerateEmbeddingAsync(data, cancellationToken: stoppingToken);
            // データを登録
            var r = new MyStoreData
            {
                Id = id++,
                Data = data,
                Vector = embedding.Vector,
            };
            await c.UpsertAsync(r, cancellationToken: stoppingToken);
            logger.LogInformation("{data} was added.", data);
        }
    }
}

// データのモデル
class MyStoreData
{
    // Id 列
    [VectorStoreRecordKey]
    public required ulong Id { get; init; }

    // Data 列
    [VectorStoreRecordData]
    public required string Data { get; init; }

    // Vector 列 (Ollama の bge-large は 1024 次元)
    [VectorStoreRecordVector(1024)]
    public required ReadOnlyMemory<float> Vector { get; init; }
}

これで、データを登録するアプリができました。最後に AppHost プロジェクトの Program.cs に、このアプリから Ollama や Qdrant にアクセスするために接続文字列が設定されるようにします。

Program.cs
var builder = DistributedApplication.CreateBuilder(args);

// ollama を追加してデータの永続化と、コンテナがデバッグ終了時に落ちないようにする
var ollama = builder.AddOllama("ollama")
    .WithDataVolume()
    .WithLifetime(ContainerLifetime.Persistent);
// ベクトル用のモデルを追加
var embeddingModel = ollama.AddModel("bge-large");

// qdrant を追加してデータの永続化と、コンテナがデバッグ終了時に落ちないようにする
var qdrant = builder.AddQdrant("qdrant")
    .WithDataVolume()
    .WithLifetime(ContainerLifetime.Persistent);

// DataInitializerApp を追加
builder.AddProject<Projects.DataInitializerApp>("datainitializerapp")
    // bge-large を使うための設定
    .WithReference(embeddingModel)
    // bge-large の起動が完了するまで起動を待つ
    .WaitFor(embeddingModel)
    // qdrant を使うための設定
    .WithReference(qdrant)
    // qdrant の起動が完了するまで起動を待つ
    .WaitFor(qdrant);

builder.Build().Run();

これで、AppHost プロジェクトを起動すると、Ollama と Qdrant が起動し、その後に DataInitializerApp が起動してデータを登録します。まだ Qdrant からデータを読むプログラムを作っていないので確認はできませんが、以下のようなログが DataInitializerApp で出ていれば登録できています。

2024-11-23T23:31:43 info: DataInitializerApp.Worker[0]
2024-11-23T23:31:43       My name is Kazuki Ota. was added.
2024-11-23T23:31:43 info: DataInitializerApp.Worker[0]
2024-11-23T23:31:43       My favorite programming language is C#. was added.
2024-11-23T23:31:43 info: DataInitializerApp.Worker[0]
2024-11-23T23:31:43       My favorite game is Genshin. was added.
2024-11-23T23:31:44 info: DataInitializerApp.Worker[0]
2024-11-23T23:31:44       My birthday is 1981/01/30. was added.

データを取得するアプリを作る

次に、このベクトル ストアからデータを読み取るアプリを作ります。
DataReadApp という名前で Blazor Web App を作成します。作成時に以下のように設定をしてください。

  • Interactive render mode: Server
  • Interactivity location: Per page/component
  • Include sample pages: チェックを外す
  • .NET Aspire オーケストレーションへの参加: チェックを入れる

ここにも DataInitializerApp と同じように以下の 4 つのパッケージをインストールします。

  • Aspire.Qdrant.Client
  • Microsoft.Extensions.VectorData.Abstractions
  • Microsoft.SemanticKernel.Connectors.Qdrant
  • CommunityToolkit.Aspire.OllamaSharp

このプロジェクトでは IAsyncEnumerable<T> に対して LINQ が使いたいので追加で以下のパッケージもインストールします。

  • System.Linq.Async

そして DataInitializerApp と同じように Program.cs に以下のようにクライアントを登録します。

Program.cs
using DataReadApp.Components;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel.Connectors.Qdrant;
using Qdrant.Client;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();
// qdrant のクライアントを追加
builder.AddQdrantClient("qdrant");
// ollama の IEmbeddingGenerator を追加
builder.AddOllamaSharpEmbeddingGenerator("ollama-bge-large");
// IVectorStore の Qdrant 用実装を追加
builder.Services.AddSingleton<IVectorStore>(sp =>
    new QdrantVectorStore(sp.GetRequiredService<QdrantClient>()));

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

var app = builder.Build();
// 以下省略

最後に Components/Pages/Home.razor に入力された文字列に対してベクトル検索をして結果を表示するコードを追加します。

Home.razor
@page "/"
@rendermode InteractiveServer
@using Microsoft.Extensions.AI
@using Microsoft.Extensions.VectorData
@inject IVectorStore VectorStore
@inject IEmbeddingGenerator<string, Embedding<float>> EmbeddingGenerator

<PageTitle>Home</PageTitle>

<h1>Microsoft.Extensions.VectorData サンプル</h1>

<div>
    <input type="text" @bind="InputText" />
    <button @onclick="OnClickAsync">ベクトル検索</button>
</div>

<div>
    <span>@Result</span>
</div>

@code {
    private string InputText { get; set; } = "";
    private string Result { get; set; } = "";

    private async Task OnClickAsync()
    {
        // ベクトルデータのコレクションを取得
        var collection = VectorStore.GetCollection<ulong, MyStoreData>("my-collection");
        // 入力テキストからベクトルを生成
        var embedding = await EmbeddingGenerator.GenerateEmbeddingAsync(InputText);
        // ベクトル検索
        var searchResult = await collection.VectorizedSearchAsync(
            embedding.Vector,
            new VectorSearchOptions
            {
                // 一番近いデータを 1 件だけ取得
                Top = 1,
            });
        // 検索結果のデータを取得して表示
        var foundData = await searchResult.Results.FirstOrDefaultAsync();
        Result = foundData == null ? "Not found" : $"{foundData.Record.Data}(Score: {foundData.Score})";
    }

    // データのモデル
    class MyStoreData
    {
        // Id 列
        [VectorStoreRecordKey]
        public required ulong Id { get; init; }

        // Data 列
        [VectorStoreRecordData]
        public required string Data { get; init; }

        // Vector 列 (Ollama の bge-large は 1024 次元)
        [VectorStoreRecordVector(1024)]
        public required ReadOnlyMemory<float> Vector { get; init; }
    }
}

アプリができたので AppHost プロジェクトの Program.csDataReadApp に対して Ollama や Qdrant にアクセスするために接続文字列が設定されるようにします。

Program.cs
// 自動的に追加されている datareadapp に対する定義を以下のように変更
builder.AddProject<Projects.DataReadApp>("datareadapp")
    // bge-large を使うための設定
    .WithReference(embeddingModel)
    // bge-large の起動が完了するまで起動を待つ
    .WaitFor(embeddingModel)
    // qdrant を使うための設定
    .WithReference(qdrant)
    // qdrant の起動が完了するまで起動を待つ
    .WaitFor(qdrant);

これで完成です。AppHost プロジェクトを起動しましょう。以下のように .NET Aspire のダッシュボードが起動して、すべてが Running になるまで待ちます。

datareadapp のエンドポイントをクリックして DataReadApp の画面を開いて適当な文字列を入力してボタンを押したら以下のように検索結果が表示されます。

ちゃんと一番近いデータが表示されていますね。Azure OpenAI Service の text-embedding-3-large でやると、もう少しスコアもいい感じになるかもしれませんが、ローカルで動くモデルでもちゃんと動いていることが確認できました。

ソースコード一式

この記事を書きながら作成したソースコードは以下のリポジトリにあります。

https://github.com/runceel/VectorDataSampleApp

まとめ

Microsoft.Extensions.VectorData はベクトル ストアを共通的に扱うための抽象化レイヤーです。これを使うことで、ベクトル ストアを共通のインターフェースで扱うことができます。現時点では Semantic Kernel のパッケージを使うことになりますが、将来的には各ベクトル ストアのベンダーが実装してくれることを期待しましょう。

Microsoft (有志)

Discussion