ベクトルとは何ですか?
ベクトルは、数学、物理学、工学科学などの多くの自然科学における基本概念であり、方向と長さを持つ量であり、空間幾何学、力学、信号処理などの問題を記述するために使用されます。コンピューター サイエンスでは、ベクトルはテキスト、画像、音声などのデータを表すために使用されます。さらに、ベクトルは、テキスト、画像、音声、ビデオなどの非構造化データに対する AI モデルの印象も表します。
ベクトル類似性検索の基本原理
ベクトル類似度検索の基本原理は、データ セット内の各要素をベクトルにマッピングし、コサイン類似度ベース、ユークリッド類似度ベース、または Jaccard ベースの類似度アルゴリズムなどの特定の類似度計算アルゴリズムを使用して、クエリ ベクトルを見つけることです。は互いに最もよく似ています。
Redis はベクトル類似性検索を実装します
原理を理解した後、Redis を使用してベクトル類似検索を実装する方法を実装していきます。Redis を使用すると、FT.SEARCH コマンドでベクトル類似性クエリを使用できます。Redis ハッシュまたは JSON ドキュメントのフィールドとして保存されたベクトルのロード、インデックス付け、およびクエリを実行できるようにします。
//関連ドキュメントのアドレス
1. Redis Searchのインストール
Redis Search のインストールと使用方法については、ここでは詳しく説明しませんので、詳しくない場合は、以前の記事を参照してください。
C#+Redis 検索: Redis を使用して高性能の全文検索を実装する方法
2. ベクトルインデックスライブラリを作成する
ここでは、NRedisStack と StackExchange.Redis の 2 つのライブラリを使用して Redis と対話します。
//Redis 接続を作成します static ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost"); //Redis データベースを取得します static IDatabase db = mux.GetDatabase(); //RediSearch クライアントを作成します static SearchCommands ft = new SearchCommands(db ,ヌル);
ベクトル検索を実行する前に、まずインデックスを定義して作成し、類似性アルゴリズムを指定する必要があります。
public static async Task CreateIndexAsync() { await ft.CreateAsync(indexName, new FTCreateParams() .On(IndexDataType.HASH) .Prefix(prefix), new Schema() .AddTagField("タグ") .AddTextField("コンテンツ") .AddVectorField("vector", VectorField.VectorAlgo.HNSW, new Dictionary<string, object>() { ["TYPE"] = "FLOAT32", ["DIM"] = 2, ["DISTANCE_METRIC"] = "コサイン" })); }
このコードの意味は次のとおりです。
- インデックスの作成には、非同期メソッド ft.CreateAsync が使用されます。これは、インデックス名indexName、FTCreateParamsオブジェクト、およびSchemaオブジェクトの3つのパラメータを受け入れます。
- FTCreateParams クラスは、インデックスのパラメーターを指定するためのパラメーター オプションをいくつか提供します。ここでは、.On(IndexDataType.HASH) メソッドを使用してインデックス データ型をハッシュとして指定し、.Prefix(prefix) メソッドを使用してインデックス データのプレフィックスを指定します。
- スキーマ クラスは、インデックス内のフィールドとフィールド タイプを定義するために使用されます。ここでは、フィルタリングされたデータを区別するためにタグ フィールドが定義されます。テキスト フィールドは元のデータを格納するために定義され、ベクトル フィールドは元のデータから変換されたベクトル データを格納するために使用されます。
- VectorField.VectorAlgo.HNSW は、ベクトル アルゴリズムを HNSW (Hierarchical Navigable Small World) として指定するために使用されます。ベクトル場のパラメータを設定するために辞書オブジェクトも渡されます。このうち、キーは文字列型、値はオブジェクト型です。
現在、Redis は 2 つの類似性アルゴリズムをサポートしています。
HNSW 階層ナビゲーション スモール ワールド アルゴリズムは、スモール ワールド ネットワークを使用してインデックスを構築します。クエリ速度が速く、メモリ フットプリントが小さいです。時間計算量は O(logn) で、大規模なインデックス作成に適しています。
FLAT ブルート フォース アルゴリズムは、すべてのキーと値のペアをスキャンし、キーと値のペア間の距離に基づいて最短パスを計算します。時間計算量は O(n) (n はキーと値のペアの数) です。このアルゴリズムは時間の複雑さが非常に高いため、小規模なインデックスにのみ適しています。
3. インデックス ライブラリにベクトルを追加します。
インデックスが作成されたら、インデックスにデータを追加します。
public async Task SetAsync(string docId, string prefix, string tag, string content, float[] Vector) { await db.HashSetAsync($"{prefix}{docId}", new HashEntry[] { new HashEntry ("tag",タグ)、 新しい HashEntry ("コンテンツ"、コンテンツ)、 新しい HashEntry ("ベクトル"、vector.SelectMany(BitConverter.GetBytes).ToArray()) } ); }
SetAsync メソッドは、指定されたドキュメント ID、プレフィックス、タグ、コンテンツ、およびコンテンツを持つベクトルをインデックス ライブラリに保存するために使用されます。そして、SelectMany() メソッドと BitConverter.GetBytes() メソッドを使用して、ベクトルをバイト配列に変換します。
4. ベクトル検索
Redis は、KNN クエリと Range クエリの 2 種類のベクター クエリをサポートしており、2 種類のクエリを混合することもできます。
KNNクエリ
KNN クエリは、クエリ ベクトルが与えられた上位 N 個の最も類似したベクトルを見つけるために使用されます。
public async IAsyncEnumerable<(string Content, double Score)> SearchAsync(float[] Vector, int limit) { var query = new Query($"*=>[KNN {limit} @vector $vector AS スコア]") .AddParam ("ベクター", ベクター.SelectMany(BitConverter.GetBytes).ToArray()) .SetSortBy("スコア") .ReturnFields("コンテンツ", "スコア") .Limit(0, 制限) .Dialect(2); var result = await ft.SearchAsync(indexName, query).ConfigureAwait(false); foreach (var document in result.Documents) { yield return (document["content"],Convert. ToDouble(ドキュメント["スコア"])); } }
このコードの意味は次のとおりです。
- クエリオブジェクトクエリを作成し、クエリ条件を設定します。クエリ条件には次のものが含まれます。
- "*=>[KNN {limit} @vector $vector AS スコア]": KNN アルゴリズムを使用してベクトル類似性検索を実行し、結果の数を制限して、指定されたベクトル ベクトルをクエリ ベクトルとして使用し、クエリ結果をスコアリングします。類似度に従って並べ替えます。
- AddParam("vector", Vector.SelectMany(BitConverter.GetBytes).ToArray()): 浮動小数点配列をバイト配列に変換し、それをクエリ パラメーターとしてクエリに渡します。
- SetSortBy("score"): 類似性スコアに従って結果を並べ替えます。
- ReturnFields("content", "score"): 結果セットからコンテンツとスコアの 2 つのフィールドを返します。
- Limit(0,limit): 結果セットの開始位置を 0 に制限し、結果の数を制限します。
- Dialect(2): クエリ方言を 2 に設定します。これは、Redis、Redis プロトコルのデフォルトのクエリ言語です。
- 非同期検索メソッド ft.SearchAsync(indexName, query) を呼び出し、検索結果を待ちます。
- 検索結果セット result.Documents を反復処理し、各ドキュメントを (文字列 Content, double Score) タプルに変換し、yield ステートメントを反復処理します。
範囲クエリ:
範囲クエリは、Redis のベクトル フィールドと事前定義されたしきい値 (半径) に基づくクエリ ベクトルの間の距離に基づいて結果をフィルターする方法を提供します。NUMERIC 句や GEO 句と同様に、特に KNN を使用した混合検索の場合、クエリ内で複数回出現する可能性があります。
public static async IAsyncEnumerable<(string タグ, string コンテンツ, double Score)> SearchAsync(string タグ, float[] ベクトル, int 制限) { var query = new Query($"(@tag:{tag})=>[KNN] {limit} @vector $vector AS スコア]") .AddParam("vector", Vector.SelectMany(BitConverter.GetBytes).ToArray()) .SetSortBy("スコア") .ReturnFields("タグ", "コンテンツ", "スコア") .Limit(0, 限界) .Dialect(2); var result = await ft.SearchAsync(indexName, query).ConfigureAwait(false); foreach (var document in result.Documents) { yield return (document["tag"], } }
このコードは、KNN と Range の混合クエリを使用します。前のコードと比較すると、新しい @tag パラメーターが追加されており、結果が指定されたタグのコンテンツのみを含むように制限されます。これにより、クエリの精度が向上し、クエリの効率が向上します。
5. インデックスライブラリからベクトルを削除する
public async Task DeleteAsync(string docId, string prefix) { await db.KeyDeleteAsync($"{prefix}{docId}"); }
このメソッドは、指定されたベクトルに関連付けられたハッシュ キャッシュ キーを削除することによって、指定されたベクトル データをインデックス ライブラリから削除します。
6. ベクトルインデックスライブラリを削除します。
public async Task DropIndexAsync() { await ft.DropIndexAsync(indexName, true); }
このメソッド await ft.DropIndexAsync は、indexName と true の 2 つのパラメーターを受け入れます。IndexName はインデックス ライブラリの名前を示し、true はインデックスを削除するときにインデックス ファイルを削除するかどうかを示します。
7. インデックスデータベース情報のクエリ
public async Task<InfoResult> InfoAsync() { return await ft.InfoAsync(indexName); }
await ft.InfoAsync(indexName) メソッドを通じて、指定されたインデックス ライブラリのサイズ、ドキュメントの数、およびその他の関連するインデックス ライブラリ情報を取得できます。
完全なデモは次のとおりです。
using NRedisStack; using NRedisStack.Search; using NRedisStack.Search.DataTypes; using NRedisStack.Search.Literals.Enums; using StackExchange.Redis; using static NRedisStack.Search.Schema; namespace RedisVectorExample { class Program { //Redis 接続を作成する static ConnectionMultiplexer mux = ConnectionMultiplexer.Connect("localhost"); //Redis データベースを取得します static IDatabase db = mux.GetDatabase(); //RediSearch クライアントを作成します static SearchCommands ft = new SearchCommands(db, null); //インデックス名 static stringindexName = "test:index"; //インデックスプレフィックス static string prefix = "test:data"; static async Task Main(string[] args) { //ベクトル インデックスを作成 await CreateIndexAsync(); //いくつかのベクトルをインデックスに追加 await SetAsync("1", "A " , "テストデータ A1", new float[] { 0.1f, 0.2f }); await SetAsync("2", "A", "テストデータ A2", new float[] { 0.3f, 0.4f }) ; await SetAsync("3", "B", "テストデータ B1", new float[] { 0.5f, 0.6f }); await SetAsync("4", "C" , "テストデータ C1", new float [ ] { 0.7f、0.8f }); //ベクトルを削除 await DeleteAsync("4"); //KUN 検索 await foreach (var (Content, Score) in SearchAsync(new float[] { 0.1f, 0.2f }, 2)) { Console.WriteLine($"Content: {Content}, 類似性スコア: {Score}"); } // 混合 await foreach (var (Tag, Content, Score) in SearchAsync("A", new float[] { 0.1 f, 0.2f }, 2)) { Console.WriteLine($"Tag: {Tag}, content: {Content}, 類似度スコア: {Score}"); } // インデックスが存在する かどうかを確認します var info = await InfoAsync (); if (info != null) await DropIndexAsync(); //インデックスが存在する場合は削除します } public static async Task CreateIndexAsync() { await ft.CreateAsync(indexName, new FTCreateParams() ["DISTANCE_METRIC"] = "COSINE" })); } public static async Task SetAsync(string docId, string tag, string content, float[] Vector) { await db.HashSetAsync($"{prefix}{docId}", new HashEntry[] { new HashEntry ("tag", tag) 、 新しい HashEntry ("コンテンツ", コンテンツ)、 新しい HashEntry ("ベクター", Vector.SelectMany(BitConverter.GetBytes).ToArray()) } ); public static async Task DeleteAsync(string docId) { await db.KeyDeleteAsync($"{prefix}{docId}") ; public static async Task DropIndexAsync() { await ft.DropIndexAsync(indexName, public static async Task<InfoResult> InfoAsync() { return await ft.InfoAsync(indexName) ; public static async IAsyncEnumerable<(string Content, double Score)> SearchAsync(float[] Vector, int limit) { var query = new Query($"*=>[KNN {limit} @vector $vector AS スコア]" ) .AddParam("ベクター", Vector.SelectMany(BitConverter.GetBytes).ToArray()) .SetSortBy("スコア") .ReturnFields("コンテンツ", "スコア") .Limit(0, 制限) .Dialect(2) ; var result = await ft.SearchAsync(indexName, query).ConfigureAwait(false); foreach (var document in result.Documents) { yield return (document["content"], Convert.ToDouble(document["score"])); public static async IAsyncEnumerable<(string タグ, string コンテンツ, double Score )> SearchAsync(string タグ, float[] ベクトル, int 制限) { var query = new Query($"(@tag:{tag})= > [KNN {制限} @vector $vector AS スコア]") .AddParam("vector", Vector.SelectMany(BitConverter.GetBytes).ToArray()) .SetSortBy("スコア") .ReturnFields("タグ", "コンテンツ", "スコア") .Limit(0, 制限) .Dialect(2); var result = await ft.SearchAsync(indexName, query).ConfigureAwait(false); foreach (var document in result.Documents) { yield return (document["tag"], document["content"], Convert.ToDouble(document["score"])); } } } }