Skip to content

Commit 935977d

Browse files
committed
Fix VectorSet review findings: async pattern, Lease types, missing methods, doc fixes
- Replace ContinueWith(OnlyOnRanToCompletion) with async/await for VectorSetLengthAsync and VectorSetDimensionAsync (was hanging on fault) - Fix doc examples: VectorSetAddRequest.Create() → .Member(), VectorSetSimilaritySearchRequest.Create() → .ByVector() - Add using/dispose pattern for Lease<T> in all doc examples - Add 5 missing VectorSet methods: RandomMembersAsync, GetApproximateVectorAsync, GetLinksAsync, GetLinksWithScoresAsync - Fix return types to match SE.Redis: Lease<RedisValue>?, Lease<VectorSetLink>?, Lease<float>? - Total: 15 VectorSet methods now covered
1 parent d8014e6 commit 935977d

File tree

3 files changed

+99
-25
lines changed

3 files changed

+99
-25
lines changed

doc/vectorset.md

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,35 @@ graph LR
2222
var embedding = await aiModel.GetEmbeddingAsync("Red running shoes, size 42");
2323

2424
await redis.VectorSetAddAsync("products",
25-
VectorSetAddRequest.Create("shoe-123", embedding));
25+
VectorSetAddRequest.Member("shoe-123", embedding));
2626

2727
// Add with JSON attributes (metadata)
2828
await redis.VectorSetAddAsync("products",
29-
VectorSetAddRequest.Create("shoe-456", embedding)
30-
.WithAttributes("""{"category":"shoes","price":79.99,"brand":"Nike"}"""));
29+
VectorSetAddRequest.Member("shoe-456", embedding,
30+
attributes: """{"category":"shoes","price":79.99,"brand":"Nike"}"""));
3131
```
3232

3333
## Similarity Search
3434

35+
The search returns a `Lease<T>` which **must be disposed** after use to return pooled memory.
36+
3537
```csharp
3638
// Find the 5 most similar items to a query vector
3739
var queryEmbedding = await aiModel.GetEmbeddingAsync("comfortable sneakers for running");
3840

39-
var results = await redis.VectorSetSimilaritySearchAsync("products",
40-
VectorSetSimilaritySearchRequest.Create(queryEmbedding, count: 5));
41+
using var results = await redis.VectorSetSimilaritySearchAsync("products",
42+
VectorSetSimilaritySearchRequest.ByVector(queryEmbedding) with { Count = 5 });
4143

42-
foreach (var result in results)
44+
if (results is not null)
4345
{
44-
Console.WriteLine($"{result.Member}: score={result.Score:F4}");
45-
46-
// Get attributes for each result
47-
var attrs = await redis.VectorSetGetAttributesJsonAsync("products", result.Member!);
48-
Console.WriteLine($" Attributes: {attrs}");
46+
foreach (var result in results.Span)
47+
{
48+
Console.WriteLine($"{result.Member}: score={result.Score:F4}");
49+
50+
// Get attributes for each result
51+
var attrs = await redis.VectorSetGetAttributesJsonAsync("products", result.Member!);
52+
Console.WriteLine($" Attributes: {attrs}");
53+
}
4954
}
5055
```
5156

@@ -64,9 +69,19 @@ var dims = await redis.VectorSetDimensionAsync("products");
6469
// Get a random member
6570
var random = await redis.VectorSetRandomMemberAsync("products");
6671

72+
// Get multiple random members
73+
var randoms = await redis.VectorSetRandomMembersAsync("products", 5);
74+
6775
// Get info about the VectorSet
6876
var info = await redis.VectorSetInfoAsync("products");
6977

78+
// Get the approximate vector for a member
79+
using var vector = await redis.VectorSetGetApproximateVectorAsync("products", "shoe-123");
80+
81+
// Get HNSW graph neighbors
82+
var links = await redis.VectorSetGetLinksAsync("products", "shoe-123");
83+
var linksWithScores = await redis.VectorSetGetLinksWithScoresAsync("products", "shoe-123");
84+
7085
// Remove a member
7186
await redis.VectorSetRemoveAsync("products", "shoe-123");
7287
```
@@ -91,29 +106,32 @@ foreach (var doc in documents)
91106
{
92107
var embedding = await aiModel.GetEmbeddingAsync(doc.Content);
93108
await redis.VectorSetAddAsync("docs",
94-
VectorSetAddRequest.Create(doc.Id, embedding)
95-
.WithAttributes($"""{{ "title": "{doc.Title}" }}"""));
109+
VectorSetAddRequest.Member(doc.Id, embedding,
110+
attributes: $"""{{ "title": "{doc.Title}" }}"""));
96111
}
97112

98113
// Query: find relevant context for a prompt
99-
var queryEmb = await aiModel.GetEmbeddingAsync(userQuestion);
100-
var context = await redis.VectorSetSimilaritySearchAsync("docs",
101-
VectorSetSimilaritySearchRequest.Create(queryEmb, count: 3));
114+
using var context = await redis.VectorSetSimilaritySearchAsync("docs",
115+
VectorSetSimilaritySearchRequest.ByVector(queryEmb) with { Count = 3 });
102116
```
103117

104118
### Recommendations
105119
```csharp
106120
// Find products similar to what the user just viewed
107-
var viewedProduct = await redis.VectorSetGetApproximateVectorAsync("products", productId);
108-
// Use the vector to find similar items
121+
using var vector = await redis.VectorSetGetApproximateVectorAsync("products", viewedProductId);
122+
if (vector is not null)
123+
{
124+
using var similar = await redis.VectorSetSimilaritySearchAsync("products",
125+
VectorSetSimilaritySearchRequest.ByVector(vector.Span.ToArray()) with { Count = 10 });
126+
}
109127
```
110128

111129
### Semantic Search
112130
```csharp
113131
// Search by meaning, not keywords
114132
var searchEmb = await aiModel.GetEmbeddingAsync("something warm for winter");
115-
var results = await redis.VectorSetSimilaritySearchAsync("clothing",
116-
VectorSetSimilaritySearchRequest.Create(searchEmb, count: 20));
133+
using var results = await redis.VectorSetSimilaritySearchAsync("clothing",
134+
VectorSetSimilaritySearchRequest.ByVector(searchEmb) with { Count = 20 });
117135
```
118136

119137
## Performance Notes
@@ -122,3 +140,4 @@ var results = await redis.VectorSetSimilaritySearchAsync("clothing",
122140
- Approximate nearest neighbor search — extremely fast even with millions of vectors
123141
- Memory efficient compared to external vector databases
124142
- Vectors are stored directly in Redis — no external index to maintain
143+
- `Lease<T>` return types use pooled memory — always dispose after use

src/core/StackExchange.Redis.Extensions.Core/Abstractions/IRedisDatabase.VectorSet.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,40 @@ public partial interface IRedisDatabase
9696
/// <param name="flag">Behaviour markers associated with a given command.</param>
9797
/// <returns>A random member, or null if the VectorSet is empty.</returns>
9898
Task<RedisValue> VectorSetRandomMemberAsync(string key, CommandFlags flag = CommandFlags.None);
99+
100+
/// <summary>
101+
/// Returns multiple random members from the VectorSet stored at key.
102+
/// </summary>
103+
/// <param name="key">The key of the VectorSet.</param>
104+
/// <param name="count">The number of random members to return.</param>
105+
/// <param name="flag">Behaviour markers associated with a given command.</param>
106+
/// <returns>An array of random members.</returns>
107+
Task<RedisValue[]> VectorSetRandomMembersAsync(string key, long count, CommandFlags flag = CommandFlags.None);
108+
109+
/// <summary>
110+
/// Returns the approximate vector for a member in the VectorSet.
111+
/// </summary>
112+
/// <param name="key">The key of the VectorSet.</param>
113+
/// <param name="member">The member to retrieve the vector for.</param>
114+
/// <param name="flag">Behaviour markers associated with a given command.</param>
115+
/// <returns>The approximate vector as a Lease of floats. Must be disposed after use. Null if member not found.</returns>
116+
Task<Lease<float>?> VectorSetGetApproximateVectorAsync(string key, string member, CommandFlags flag = CommandFlags.None);
117+
118+
/// <summary>
119+
/// Returns the links (neighbors) for a member in the VectorSet's HNSW graph.
120+
/// </summary>
121+
/// <param name="key">The key of the VectorSet.</param>
122+
/// <param name="member">The member to retrieve links for.</param>
123+
/// <param name="flag">Behaviour markers associated with a given command.</param>
124+
/// <returns>The linked member names. The returned Lease must be disposed after use.</returns>
125+
Task<Lease<RedisValue>?> VectorSetGetLinksAsync(string key, string member, CommandFlags flag = CommandFlags.None);
126+
127+
/// <summary>
128+
/// Returns the links (neighbors) with similarity scores for a member in the VectorSet's HNSW graph.
129+
/// </summary>
130+
/// <param name="key">The key of the VectorSet.</param>
131+
/// <param name="member">The member to retrieve links for.</param>
132+
/// <param name="flag">Behaviour markers associated with a given command.</param>
133+
/// <returns>The links with scores. The returned Lease must be disposed after use.</returns>
134+
Task<Lease<VectorSetLink>?> VectorSetGetLinksWithScoresAsync(string key, string member, CommandFlags flag = CommandFlags.None);
99135
}

src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.VectorSet.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Copyright (c) Ugo Lattanzi. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
22

3-
using System.Threading;
43
using System.Threading.Tasks;
54

65
namespace StackExchange.Redis.Extensions.Core.Implementations;
@@ -25,12 +24,16 @@ public Task<bool> VectorSetContainsAsync(string key, string member, CommandFlags
2524
Database.VectorSetContainsAsync(key, member, flag);
2625

2726
/// <inheritdoc/>
28-
public Task<long> VectorSetLengthAsync(string key, CommandFlags flag = CommandFlags.None) =>
29-
Database.VectorSetLengthAsync(key, flag).ContinueWith(t => (long)t.Result, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
27+
#pragma warning disable RCS1174 // async/await required for cross-TFM int→long cast
28+
public async Task<long> VectorSetLengthAsync(string key, CommandFlags flag = CommandFlags.None) =>
29+
await Database.VectorSetLengthAsync(key, flag).ConfigureAwait(false);
30+
#pragma warning restore RCS1174
3031

3132
/// <inheritdoc/>
32-
public Task<long> VectorSetDimensionAsync(string key, CommandFlags flag = CommandFlags.None) =>
33-
Database.VectorSetDimensionAsync(key, flag).ContinueWith(t => (long)t.Result, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
33+
#pragma warning disable RCS1174
34+
public async Task<long> VectorSetDimensionAsync(string key, CommandFlags flag = CommandFlags.None) =>
35+
await Database.VectorSetDimensionAsync(key, flag).ConfigureAwait(false);
36+
#pragma warning restore RCS1174
3437

3538
/// <inheritdoc/>
3639
public Task<string?> VectorSetGetAttributesJsonAsync(string key, string member, CommandFlags flag = CommandFlags.None) =>
@@ -47,4 +50,20 @@ public Task<bool> VectorSetSetAttributesJsonAsync(string key, string member, str
4750
/// <inheritdoc/>
4851
public Task<RedisValue> VectorSetRandomMemberAsync(string key, CommandFlags flag = CommandFlags.None) =>
4952
Database.VectorSetRandomMemberAsync(key, flag);
53+
54+
/// <inheritdoc/>
55+
public Task<RedisValue[]> VectorSetRandomMembersAsync(string key, long count, CommandFlags flag = CommandFlags.None) =>
56+
Database.VectorSetRandomMembersAsync(key, count, flag);
57+
58+
/// <inheritdoc/>
59+
public Task<Lease<float>?> VectorSetGetApproximateVectorAsync(string key, string member, CommandFlags flag = CommandFlags.None) =>
60+
Database.VectorSetGetApproximateVectorAsync(key, member, flag);
61+
62+
/// <inheritdoc/>
63+
public Task<Lease<RedisValue>?> VectorSetGetLinksAsync(string key, string member, CommandFlags flag = CommandFlags.None) =>
64+
Database.VectorSetGetLinksAsync(key, member, flag);
65+
66+
/// <inheritdoc/>
67+
public Task<Lease<VectorSetLink>?> VectorSetGetLinksWithScoresAsync(string key, string member, CommandFlags flag = CommandFlags.None) =>
68+
Database.VectorSetGetLinksWithScoresAsync(key, member, flag);
5069
}

0 commit comments

Comments
 (0)