Microsoft.Extensions.VectorData を触ってみよう
先日、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
が実装されています。QdrantVectorStore
は Microsoft.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 操作ができるメソッドも提供しています。
コレクションのスキーマは以下のように VectorStoreRecordKey
と VectorStoreRecordData
と VectorStoreRecrodVector
という 3 つの属性を付けたプロパティを持つクラスとして定義します。例えば Id
(キー項目) と Name
と Vector
という属性を持つクラスを定義すると以下のようになります。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 を起動するようにします。
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
の実装です。
- Qdrant のベクトル ストアを使うための
-
CommunityToolkit.Aspire.OllamaSharp
(2024/11/23 時点でプレビュー版しかありません)- .NET Aspire コンポーネントの Ollama クライアント。.NET Aspire の AppHost プロジェクトで設定した名前の接続文字列で Ollama に接続するためのクライアントを DI コンテナに登録します。
このパッケージを使って DataInitializerApp
プロジェクトの Program.cs
を編集して各種サービスにつなぐためのクライアントを DI コンテナに登録します。
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 に、いくつかデータを追加します。
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 にアクセスするために接続文字列が設定されるようにします。
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
に以下のようにクライアントを登録します。
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
に入力された文字列に対してベクトル検索をして結果を表示するコードを追加します。
@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.cs
で DataReadApp
に対して Ollama や Qdrant にアクセスするために接続文字列が設定されるようにします。
// 自動的に追加されている 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
でやると、もう少しスコアもいい感じになるかもしれませんが、ローカルで動くモデルでもちゃんと動いていることが確認できました。
ソースコード一式
この記事を書きながら作成したソースコードは以下のリポジトリにあります。
まとめ
Microsoft.Extensions.VectorData
はベクトル ストアを共通的に扱うための抽象化レイヤーです。これを使うことで、ベクトル ストアを共通のインターフェースで扱うことができます。現時点では Semantic Kernel のパッケージを使うことになりますが、将来的には各ベクトル ストアのベンダーが実装してくれることを期待しましょう。
Discussion