diff --git a/doc/README.md b/doc/README.md index 4259abf2..842899a5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -33,6 +33,7 @@ * [Hash Operations](usage/README.md) * [Hash Field Expiry](hash-field-expiry.md) (Redis 7.4+) * [GeoSpatial Indexes](geospatial.md) +* [VectorSet — AI/ML Similarity Search](vectorset.md) (Redis 8.0+) * [Redis Streams](streams.md) * [Pub/Sub Messaging](pubsub.md) * [Custom Serializer](usage/custom-serializer.md) diff --git a/doc/vectorset.md b/doc/vectorset.md new file mode 100644 index 00000000..9f8129ae --- /dev/null +++ b/doc/vectorset.md @@ -0,0 +1,143 @@ +# VectorSet (AI/ML Similarity Search) + +Redis 8.0 introduced VectorSet — a native data structure for storing and searching high-dimensional vectors. This is ideal for AI/ML applications like RAG, recommendations, and semantic search. + +> **Requires Redis 8.0+** + +## Overview + +```mermaid +graph LR + A[AI Model] -->|Generate Embedding| V[float array] + V -->|VectorSetAddAsync| R[("Redis VectorSet")] + Q[Query Text] -->|Generate Embedding| QV[float array] + QV -->|VectorSetSimilaritySearchAsync| R + R -->|Top K Results| Results[Similar Items] +``` + +## Adding Vectors + +```csharp +// Add a vector with a member name +var embedding = await aiModel.GetEmbeddingAsync("Red running shoes, size 42"); + +await redis.VectorSetAddAsync("products", + VectorSetAddRequest.Member("shoe-123", embedding)); + +// Add with JSON attributes (metadata) +await redis.VectorSetAddAsync("products", + VectorSetAddRequest.Member("shoe-456", embedding, + attributes: """{"category":"shoes","price":79.99,"brand":"Nike"}""")); +``` + +## Similarity Search + +The search returns a `Lease` which **must be disposed** after use to return pooled memory. + +```csharp +// Find the 5 most similar items to a query vector +var queryEmbedding = await aiModel.GetEmbeddingAsync("comfortable sneakers for running"); + +using var results = await redis.VectorSetSimilaritySearchAsync("products", + VectorSetSimilaritySearchRequest.ByVector(queryEmbedding) with { Count = 5 }); + +if (results is not null) +{ + foreach (var result in results.Span) + { + Console.WriteLine($"{result.Member}: score={result.Score:F4}"); + + // Get attributes for each result + var attrs = await redis.VectorSetGetAttributesJsonAsync("products", result.Member!); + Console.WriteLine($" Attributes: {attrs}"); + } +} +``` + +## Managing Vectors + +```csharp +// Check if a member exists +var exists = await redis.VectorSetContainsAsync("products", "shoe-123"); + +// Get cardinality +var count = await redis.VectorSetLengthAsync("products"); + +// Get vector dimensions +var dims = await redis.VectorSetDimensionAsync("products"); + +// Get a random member +var random = await redis.VectorSetRandomMemberAsync("products"); + +// Get multiple random members +var randoms = await redis.VectorSetRandomMembersAsync("products", 5); + +// Get info about the VectorSet +var info = await redis.VectorSetInfoAsync("products"); + +// Get the approximate vector for a member +using var vector = await redis.VectorSetGetApproximateVectorAsync("products", "shoe-123"); + +// Get HNSW graph neighbors +var links = await redis.VectorSetGetLinksAsync("products", "shoe-123"); +var linksWithScores = await redis.VectorSetGetLinksWithScoresAsync("products", "shoe-123"); + +// Remove a member +await redis.VectorSetRemoveAsync("products", "shoe-123"); +``` + +## Attributes (Metadata) + +```csharp +// Set JSON attributes on a member +await redis.VectorSetSetAttributesJsonAsync("products", "shoe-123", + """{"category":"shoes","price":99.99,"sizes":[40,41,42]}"""); + +// Get JSON attributes +var json = await redis.VectorSetGetAttributesJsonAsync("products", "shoe-123"); +``` + +## Use Cases + +### RAG (Retrieval-Augmented Generation) +```csharp +// Index documents +foreach (var doc in documents) +{ + var embedding = await aiModel.GetEmbeddingAsync(doc.Content); + await redis.VectorSetAddAsync("docs", + VectorSetAddRequest.Member(doc.Id, embedding, + attributes: $"""{{ "title": "{doc.Title}" }}""")); +} + +// Query: find relevant context for a prompt +using var context = await redis.VectorSetSimilaritySearchAsync("docs", + VectorSetSimilaritySearchRequest.ByVector(queryEmb) with { Count = 3 }); +``` + +### Recommendations +```csharp +// Find products similar to what the user just viewed +using var vector = await redis.VectorSetGetApproximateVectorAsync("products", viewedProductId); +if (vector is not null) +{ + using var similar = await redis.VectorSetSimilaritySearchAsync("products", + VectorSetSimilaritySearchRequest.ByVector(vector.Span.ToArray()) with { Count = 10 }); +} +``` + +### Semantic Search +```csharp +// Search by meaning, not keywords +var searchEmb = await aiModel.GetEmbeddingAsync("something warm for winter"); +using var results = await redis.VectorSetSimilaritySearchAsync("clothing", + VectorSetSimilaritySearchRequest.ByVector(searchEmb) with { Count = 20 }); +``` + +## Performance Notes + +- VectorSet uses HNSW (Hierarchical Navigable Small World) algorithm internally +- Approximate nearest neighbor search — extremely fast even with millions of vectors +- Memory efficient compared to external vector databases +- Vectors are stored directly in Redis — no external index to maintain +- `Lease` return types use pooled memory — always dispose after use diff --git a/src/core/StackExchange.Redis.Extensions.Core/Abstractions/IRedisDatabase.VectorSet.cs b/src/core/StackExchange.Redis.Extensions.Core/Abstractions/IRedisDatabase.VectorSet.cs new file mode 100644 index 00000000..cf9e7b49 --- /dev/null +++ b/src/core/StackExchange.Redis.Extensions.Core/Abstractions/IRedisDatabase.VectorSet.cs @@ -0,0 +1,135 @@ +// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace StackExchange.Redis.Extensions.Core.Abstractions; + +/// +/// The Redis Database VectorSet extensions for AI/ML similarity search. +/// Requires Redis 8.0+. +/// +public partial interface IRedisDatabase +{ + /// + /// Adds a vector to the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// The add request containing the member, vector, and optional attributes. + /// Behaviour markers associated with a given command. + /// True if the member was added, false if it already existed and was updated. + Task VectorSetAddAsync(string key, VectorSetAddRequest request, CommandFlags flag = CommandFlags.None); + + /// + /// Performs a similarity search against the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// The search request containing the query vector and parameters. + /// Behaviour markers associated with a given command. + /// The matching results with scores. The returned Lease must be disposed after use. + Task?> VectorSetSimilaritySearchAsync(string key, VectorSetSimilaritySearchRequest query, CommandFlags flag = CommandFlags.None); + + /// + /// Removes a member from the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// The member to remove. + /// Behaviour markers associated with a given command. + /// True if the member was removed, false if it did not exist. + Task VectorSetRemoveAsync(string key, string member, CommandFlags flag = CommandFlags.None); + + /// + /// Checks if a member exists in the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// The member to check. + /// Behaviour markers associated with a given command. + /// True if the member exists. + Task VectorSetContainsAsync(string key, string member, CommandFlags flag = CommandFlags.None); + + /// + /// Returns the number of members in the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// Behaviour markers associated with a given command. + /// The cardinality of the VectorSet, or 0 if the key does not exist. + Task VectorSetLengthAsync(string key, CommandFlags flag = CommandFlags.None); + + /// + /// Returns the number of dimensions of vectors in the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// Behaviour markers associated with a given command. + /// The number of dimensions. + Task VectorSetDimensionAsync(string key, CommandFlags flag = CommandFlags.None); + + /// + /// Gets the JSON attributes associated with a member in the VectorSet. + /// + /// The key of the VectorSet. + /// The member to retrieve attributes for. + /// Behaviour markers associated with a given command. + /// The JSON attributes string, or null if the member has no attributes. + Task VectorSetGetAttributesJsonAsync(string key, string member, CommandFlags flag = CommandFlags.None); + + /// + /// Sets JSON attributes on a member in the VectorSet. + /// + /// The key of the VectorSet. + /// The member to set attributes on. + /// The JSON attributes string. + /// Behaviour markers associated with a given command. + /// True if the attributes were set. + Task VectorSetSetAttributesJsonAsync(string key, string member, string attributesJson, CommandFlags flag = CommandFlags.None); + + /// + /// Returns information about the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// Behaviour markers associated with a given command. + /// Information about the VectorSet. + Task VectorSetInfoAsync(string key, CommandFlags flag = CommandFlags.None); + + /// + /// Returns a random member from the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// Behaviour markers associated with a given command. + /// A random member, or null if the VectorSet is empty. + Task VectorSetRandomMemberAsync(string key, CommandFlags flag = CommandFlags.None); + + /// + /// Returns multiple random members from the VectorSet stored at key. + /// + /// The key of the VectorSet. + /// The number of random members to return. + /// Behaviour markers associated with a given command. + /// An array of random members. + Task VectorSetRandomMembersAsync(string key, long count, CommandFlags flag = CommandFlags.None); + + /// + /// Returns the approximate vector for a member in the VectorSet. + /// + /// The key of the VectorSet. + /// The member to retrieve the vector for. + /// Behaviour markers associated with a given command. + /// The approximate vector as a Lease of floats. Must be disposed after use. Null if member not found. + Task?> VectorSetGetApproximateVectorAsync(string key, string member, CommandFlags flag = CommandFlags.None); + + /// + /// Returns the links (neighbors) for a member in the VectorSet's HNSW graph. + /// + /// The key of the VectorSet. + /// The member to retrieve links for. + /// Behaviour markers associated with a given command. + /// The linked member names. The returned Lease must be disposed after use. + Task?> VectorSetGetLinksAsync(string key, string member, CommandFlags flag = CommandFlags.None); + + /// + /// Returns the links (neighbors) with similarity scores for a member in the VectorSet's HNSW graph. + /// + /// The key of the VectorSet. + /// The member to retrieve links for. + /// Behaviour markers associated with a given command. + /// The links with scores. The returned Lease must be disposed after use. + Task?> VectorSetGetLinksWithScoresAsync(string key, string member, CommandFlags flag = CommandFlags.None); +} diff --git a/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.VectorSet.cs b/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.VectorSet.cs new file mode 100644 index 00000000..3e073fb5 --- /dev/null +++ b/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.VectorSet.cs @@ -0,0 +1,69 @@ +// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace StackExchange.Redis.Extensions.Core.Implementations; + +/// +public partial class RedisDatabase +{ + /// + public Task VectorSetAddAsync(string key, VectorSetAddRequest request, CommandFlags flag = CommandFlags.None) => + Database.VectorSetAddAsync(key, request, flag); + + /// + public Task?> VectorSetSimilaritySearchAsync(string key, VectorSetSimilaritySearchRequest query, CommandFlags flag = CommandFlags.None) => + Database.VectorSetSimilaritySearchAsync(key, query, flag); + + /// + public Task VectorSetRemoveAsync(string key, string member, CommandFlags flag = CommandFlags.None) => + Database.VectorSetRemoveAsync(key, member, flag); + + /// + public Task VectorSetContainsAsync(string key, string member, CommandFlags flag = CommandFlags.None) => + Database.VectorSetContainsAsync(key, member, flag); + + /// +#pragma warning disable RCS1174 // async/await required for cross-TFM int→long cast + public async Task VectorSetLengthAsync(string key, CommandFlags flag = CommandFlags.None) => + await Database.VectorSetLengthAsync(key, flag).ConfigureAwait(false); +#pragma warning restore RCS1174 + + /// +#pragma warning disable RCS1174 + public async Task VectorSetDimensionAsync(string key, CommandFlags flag = CommandFlags.None) => + await Database.VectorSetDimensionAsync(key, flag).ConfigureAwait(false); +#pragma warning restore RCS1174 + + /// + public Task VectorSetGetAttributesJsonAsync(string key, string member, CommandFlags flag = CommandFlags.None) => + Database.VectorSetGetAttributesJsonAsync(key, member, flag); + + /// + public Task VectorSetSetAttributesJsonAsync(string key, string member, string attributesJson, CommandFlags flag = CommandFlags.None) => + Database.VectorSetSetAttributesJsonAsync(key, member, attributesJson, flag); + + /// + public Task VectorSetInfoAsync(string key, CommandFlags flag = CommandFlags.None) => + Database.VectorSetInfoAsync(key, flag); + + /// + public Task VectorSetRandomMemberAsync(string key, CommandFlags flag = CommandFlags.None) => + Database.VectorSetRandomMemberAsync(key, flag); + + /// + public Task VectorSetRandomMembersAsync(string key, long count, CommandFlags flag = CommandFlags.None) => + Database.VectorSetRandomMembersAsync(key, count, flag); + + /// + public Task?> VectorSetGetApproximateVectorAsync(string key, string member, CommandFlags flag = CommandFlags.None) => + Database.VectorSetGetApproximateVectorAsync(key, member, flag); + + /// + public Task?> VectorSetGetLinksAsync(string key, string member, CommandFlags flag = CommandFlags.None) => + Database.VectorSetGetLinksAsync(key, member, flag); + + /// + public Task?> VectorSetGetLinksWithScoresAsync(string key, string member, CommandFlags flag = CommandFlags.None) => + Database.VectorSetGetLinksWithScoresAsync(key, member, flag); +}