Skip to content

Commit 5b64c77

Browse files
committed
using lease for VRANGE
1 parent 7cf108f commit 5b64c77

File tree

10 files changed

+79
-54
lines changed

10 files changed

+79
-54
lines changed

docs/VectorSets.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,23 +135,26 @@ Retrieve members in lexicographical order:
135135

136136
```csharp
137137
// Get all members
138-
var allMembers = await db.VectorSetRangeAsync(key);
138+
using var allMembers = await db.VectorSetRangeAsync(key);
139+
// ... access allMembers.Span, etc
139140
140141
// Get members in a specific range
141-
var rangeMembers = await db.VectorSetRangeAsync(
142+
using var rangeMembers = await db.VectorSetRangeAsync(
142143
key,
143144
start: "product-100",
144145
end: "product-200",
145146
count: 50
146147
);
148+
// ... access rangeMembers.Span, etc
147149
148150
// Exclude boundaries
149-
var members = await db.VectorSetRangeAsync(
151+
using var members = await db.VectorSetRangeAsync(
150152
key,
151153
start: "product-100",
152154
end: "product-200",
153155
exclude: Exclude.Both
154156
);
157+
// ... access members.Span, etc
155158
```
156159

157160
### Enumerating Large Result Sets
@@ -194,7 +197,7 @@ await db.VectorSetAddAsync(key, request);
194197

195198
### Dimension Reduction
196199

197-
Use random projection to reduce vector dimensions:
200+
Use projection to reduce vector dimensions:
198201

199202
```csharp
200203
var request = VectorSetAddRequest.Member("product-123", vector.AsMemory());
@@ -331,17 +334,20 @@ await Task.WhenAll(tasks);
331334

332335
### Range Query Pagination
333336

334-
Use enumeration for large result sets to avoid loading everything into memory:
337+
Prefer enumeration for large result sets to avoid loading everything into memory:
335338

336339
```csharp
337-
// Good: Processes in batches
338-
await foreach (var member in db.VectorSetRangeEnumerateAsync(key, count: 1000))
340+
// Good: loads results in batches, processes items individually
341+
await foreach (var member in db.VectorSetRangeEnumerateAsync(key))
339342
{
340343
await ProcessMemberAsync(member);
341344
}
342345

343-
// Avoid: Loads all results at once
344-
var allMembers = await db.VectorSetRangeAsync(key); // Could be millions
346+
// Avoid: loads all results at once
347+
using var allMembers1 = await db.VectorSetRangeAsync(key);
348+
349+
// Avoid: loads resultsin batches, but still loads everything into memory at once
350+
var allMembers2 = await VectorSetRangeEnumerateAsync(key).ToArrayAsync();
345351
```
346352

347353
## Common Patterns

src/StackExchange.Redis/Interfaces/IDatabase.VectorSets.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,10 @@ bool VectorSetSetAttributesJson(
190190
/// <param name="count">The maximum number of members to return (-1 for all).</param>
191191
/// <param name="exclude">Whether to exclude the start and/or end values.</param>
192192
/// <param name="flags">The flags to use for this operation.</param>
193-
/// <returns>Members in the specified range.</returns>
193+
/// <returns>Members in the specified range as a pooled memory lease.</returns>
194194
/// <remarks><seealso href="https://redis.io/commands/vrange"/></remarks>
195195
[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
196-
RedisValue[] VectorSetRange(
196+
Lease<RedisValue> VectorSetRange(
197197
RedisKey key,
198198
RedisValue start = default,
199199
RedisValue end = default,

src/StackExchange.Redis/Interfaces/IDatabaseAsync.VectorSets.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Task<bool> VectorSetSetAttributesJsonAsync(
9595

9696
/// <inheritdoc cref="IDatabase.VectorSetRange(RedisKey, RedisValue, RedisValue, long, Exclude, CommandFlags)"/>
9797
[Experimental(Experiments.VectorSets, UrlFormat = Experiments.UrlFormat)]
98-
Task<RedisValue[]> VectorSetRangeAsync(
98+
Task<Lease<RedisValue>?> VectorSetRangeAsync(
9999
RedisKey key,
100100
RedisValue start = default,
101101
RedisValue end = default,

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.VectorSets.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public Task<bool> VectorSetSetAttributesJsonAsync(RedisKey key, RedisValue membe
5757
CommandFlags flags = CommandFlags.None) =>
5858
Inner.VectorSetSimilaritySearchAsync(ToInner(key), query, flags);
5959

60-
public Task<RedisValue[]> VectorSetRangeAsync(
60+
public Task<Lease<RedisValue>?> VectorSetRangeAsync(
6161
RedisKey key,
6262
RedisValue start = default,
6363
RedisValue end = default,

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.VectorSets.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public bool VectorSetSetAttributesJson(RedisKey key, RedisValue member, string a
5454
CommandFlags flags = CommandFlags.None) =>
5555
Inner.VectorSetSimilaritySearch(ToInner(key), query, flags);
5656

57-
public RedisValue[] VectorSetRange(
57+
public Lease<RedisValue> VectorSetRange(
5858
RedisKey key,
5959
RedisValue start = default,
6060
RedisValue end = default,

src/StackExchange.Redis/Lease.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public sealed class Lease<T> : IMemoryOwner<T>
1818

1919
private T[]? _arr;
2020

21+
/// <summary>
22+
/// Gets whether this lease is empty.
23+
/// </summary>
24+
public bool IsEmpty => Length == 0;
25+
2126
/// <summary>
2227
/// The length of the lease.
2328
/// </summary>
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#nullable enable
2-
[SER001]StackExchange.Redis.IDatabase.VectorSetRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
2+
StackExchange.Redis.Lease<T>.IsEmpty.get -> bool
3+
[SER001]StackExchange.Redis.IDatabase.VectorSetRange(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.Lease<StackExchange.Redis.RedisValue>!
34
[SER001]StackExchange.Redis.IDatabase.VectorSetRangeEnumerate(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IEnumerable<StackExchange.Redis.RedisValue>!
4-
[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue[]!>!
5+
[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = -1, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.Lease<StackExchange.Redis.RedisValue>?>!
56
[SER001]StackExchange.Redis.IDatabaseAsync.VectorSetRangeEnumerateAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue start = default(StackExchange.Redis.RedisValue), StackExchange.Redis.RedisValue end = default(StackExchange.Redis.RedisValue), long count = 100, StackExchange.Redis.Exclude exclude = StackExchange.Redis.Exclude.None, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.IAsyncEnumerable<StackExchange.Redis.RedisValue>!

src/StackExchange.Redis/RedisDatabase.VectorSets.cs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ static RedisValue GetTerminator(RedisValue value, Exclude exclude, bool isStart)
215215
: Message.Create(Database, flags, RedisCommand.VRANGE, key, from, to, count);
216216
}
217217

218-
public RedisValue[] VectorSetRange(
218+
public Lease<RedisValue> VectorSetRange(
219219
RedisKey key,
220220
RedisValue start = default,
221221
RedisValue end = default,
@@ -224,10 +224,10 @@ public RedisValue[] VectorSetRange(
224224
CommandFlags flags = CommandFlags.None)
225225
{
226226
var msg = GetVectorSetRangeMessage(key, start, end, count, exclude, flags);
227-
return ExecuteSync(msg, ResultProcessor.RedisValueArray)!; // returns empty array if no key
227+
return ExecuteSync(msg, ResultProcessor.LeaseRedisValue)!;
228228
}
229229

230-
public Task<RedisValue[]> VectorSetRangeAsync(
230+
public Task<Lease<RedisValue>?> VectorSetRangeAsync(
231231
RedisKey key,
232232
RedisValue start = default,
233233
RedisValue end = default,
@@ -236,7 +236,7 @@ public Task<RedisValue[]> VectorSetRangeAsync(
236236
CommandFlags flags = CommandFlags.None)
237237
{
238238
var msg = GetVectorSetRangeMessage(key, start, end, count, exclude, flags);
239-
return ExecuteAsync(msg, ResultProcessor.RedisValueArray)!; // returns empty array if no key
239+
return ExecuteAsync(msg, ResultProcessor.LeaseRedisValue);
240240
}
241241

242242
public IEnumerable<RedisValue> VectorSetRangeEnumerate(
@@ -250,15 +250,16 @@ public IEnumerable<RedisValue> VectorSetRangeEnumerate(
250250
// intentionally not using "scan" naming in case a VSCAN command is added later
251251
while (true)
252252
{
253-
var batch = VectorSetRange(key, start, end, count, exclude, flags);
253+
using var batch = VectorSetRange(key, start, end, count, exclude, flags);
254254
exclude |= Exclude.Start; // on subsequent iterations, exclude the start (we've already yielded it)
255255

256-
if (batch.Length == 0) yield break;
257-
for (int i = 0; i < batch.Length; i++)
256+
if (batch is null || batch.IsEmpty) yield break;
257+
var segment = batch.ArraySegment;
258+
for (int i = 0; i < segment.Count; i++)
258259
{
259-
yield return batch[i];
260+
// note side effect: use the last value as the exclusive start of the next batch
261+
yield return start = segment.Array![segment.Offset + i];
260262
}
261-
start = batch[batch.Length - 1]; // use the last value as the exclusive start of the next batch
262263
if (batch.Length < count || (!end.IsNull && end == start)) yield break; // no need to issue a final query
263264
}
264265
}
@@ -279,15 +280,16 @@ async IAsyncEnumerable<RedisValue> WithCancellationSupport([EnumeratorCancellati
279280
while (true)
280281
{
281282
cancellationToken.ThrowIfCancellationRequested();
282-
var batch = await VectorSetRangeAsync(key, start, end, count, exclude, flags);
283+
using var batch = await VectorSetRangeAsync(key, start, end, count, exclude, flags);
283284
exclude |= Exclude.Start; // on subsequent iterations, exclude the start (we've already yielded it)
284285

285-
if (batch.Length == 0) yield break;
286-
for (int i = 0; i < batch.Length; i++)
286+
if (batch is null || batch.IsEmpty) yield break;
287+
var segment = batch.ArraySegment;
288+
for (int i = 0; i < segment.Count; i++)
287289
{
288-
yield return batch[i];
290+
// note side effect: use the last value as the exclusive start of the next batch
291+
yield return start = segment.Array![segment.Offset + i];
289292
}
290-
start = batch[batch.Length - 1]; // use the last value as the exclusive start of the next batch
291293
if (batch.Length < count || (!end.IsNull && end == start)) yield break; // no need to issue a final query
292294
}
293295
}

src/StackExchange.Redis/ResultProcessor.VectorSets.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ internal abstract partial class ResultProcessor
1111

1212
public static readonly ResultProcessor<Lease<RedisValue>?> VectorSetLinks = new VectorSetLinksProcessor();
1313

14+
public static readonly ResultProcessor<Lease<RedisValue>?> LeaseRedisValue = new LeaseRedisValueProcessor();
15+
1416
public static ResultProcessor<VectorSetInfo?> VectorSetInfo = new VectorSetInfoProcessor();
1517

1618
private sealed class VectorSetLinksWithScoresProcessor : FlattenedLeaseProcessor<VectorSetLink>
@@ -43,6 +45,15 @@ protected override bool TryReadOne(in RawResult result, out RedisValue value)
4345
}
4446
}
4547

48+
private sealed class LeaseRedisValueProcessor : LeaseProcessor<RedisValue>
49+
{
50+
protected override bool TryParse(in RawResult raw, out RedisValue parsed)
51+
{
52+
parsed = raw.AsRedisValue();
53+
return true;
54+
}
55+
}
56+
4657
private sealed partial class VectorSetInfoProcessor : ResultProcessor<VectorSetInfo?>
4758
{
4859
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)

tests/StackExchange.Redis.Tests/VectorSetIntegrationTests.cs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -694,12 +694,12 @@ public async Task VectorSetRange_BasicOperation()
694694
}
695695

696696
// Get all members - should be in lexicographical order
697-
var result = await db.VectorSetRangeAsync(key);
697+
using var result = await db.VectorSetRangeAsync(key);
698698

699699
Assert.NotNull(result);
700700
Assert.Equal(4, result.Length);
701701
// Lexicographical order: alpha, beta, delta, gamma
702-
Assert.Equal(new[] { "alpha", "beta", "delta", "gamma" }, result.Select(r => (string?)r).ToArray());
702+
Assert.Equal(new[] { "alpha", "beta", "delta", "gamma" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
703703
}
704704

705705
[Fact]
@@ -721,11 +721,11 @@ public async Task VectorSetRange_WithStartAndEnd()
721721
}
722722

723723
// Get range from "banana" to "date" (inclusive)
724-
var result = await db.VectorSetRangeAsync(key, start: "banana", end: "date");
724+
using var result = await db.VectorSetRangeAsync(key, start: "banana", end: "date");
725725

726726
Assert.NotNull(result);
727727
Assert.Equal(3, result.Length);
728-
Assert.Equal(new[] { "banana", "cherry", "date" }, result.Select(r => (string?)r).ToArray());
728+
Assert.Equal(new[] { "banana", "cherry", "date" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
729729
}
730730

731731
[Fact]
@@ -747,7 +747,7 @@ public async Task VectorSetRange_WithCount()
747747
}
748748

749749
// Get only 5 members
750-
var result = await db.VectorSetRangeAsync(key, count: 5);
750+
using var result = await db.VectorSetRangeAsync(key, count: 5);
751751

752752
Assert.NotNull(result);
753753
Assert.Equal(5, result.Length);
@@ -772,11 +772,11 @@ public async Task VectorSetRange_WithExcludeStart()
772772
}
773773

774774
// Get range excluding start
775-
var result = await db.VectorSetRangeAsync(key, start: "a", end: "d", exclude: Exclude.Start);
775+
using var result = await db.VectorSetRangeAsync(key, start: "a", end: "d", exclude: Exclude.Start);
776776

777777
Assert.NotNull(result);
778778
Assert.Equal(3, result.Length);
779-
Assert.Equal(new[] { "b", "c", "d" }, result.Select(r => (string?)r).ToArray());
779+
Assert.Equal(new[] { "b", "c", "d" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
780780
}
781781

782782
[Fact]
@@ -798,11 +798,11 @@ public async Task VectorSetRange_WithExcludeEnd()
798798
}
799799

800800
// Get range excluding end
801-
var result = await db.VectorSetRangeAsync(key, start: "a", end: "d", exclude: Exclude.Stop);
801+
using var result = await db.VectorSetRangeAsync(key, start: "a", end: "d", exclude: Exclude.Stop);
802802

803803
Assert.NotNull(result);
804804
Assert.Equal(3, result.Length);
805-
Assert.Equal(new[] { "a", "b", "c" }, result.Select(r => (string?)r).ToArray());
805+
Assert.Equal(new[] { "a", "b", "c" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
806806
}
807807

808808
[Fact]
@@ -824,11 +824,11 @@ public async Task VectorSetRange_WithExcludeBoth()
824824
}
825825

826826
// Get range excluding both boundaries
827-
var result = await db.VectorSetRangeAsync(key, start: "a", end: "e", exclude: Exclude.Both);
827+
using var result = await db.VectorSetRangeAsync(key, start: "a", end: "e", exclude: Exclude.Both);
828828

829829
Assert.NotNull(result);
830830
Assert.Equal(3, result.Length);
831-
Assert.Equal(new[] { "b", "c", "d" }, result.Select(r => (string?)r).ToArray());
831+
Assert.Equal(new[] { "b", "c", "d" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
832832
}
833833

834834
[Fact]
@@ -841,10 +841,10 @@ public async Task VectorSetRange_EmptySet()
841841
await db.KeyDeleteAsync(key, CommandFlags.FireAndForget);
842842

843843
// Don't add any members
844-
var result = await db.VectorSetRangeAsync(key);
844+
using var result = await db.VectorSetRangeAsync(key);
845845

846846
Assert.NotNull(result);
847-
Assert.Empty(result);
847+
Assert.Empty(result.Span.ToArray());
848848
}
849849

850850
[Fact]
@@ -866,10 +866,10 @@ public async Task VectorSetRange_NoMatches()
866866
}
867867

868868
// Query range with no matching members
869-
var result = await db.VectorSetRangeAsync(key, start: "x", end: "z");
869+
using var result = await db.VectorSetRangeAsync(key, start: "x", end: "z");
870870

871871
Assert.NotNull(result);
872-
Assert.Empty(result);
872+
Assert.Empty(result.Span.ToArray());
873873
}
874874

875875
[Fact]
@@ -891,11 +891,11 @@ public async Task VectorSetRange_OpenStart()
891891
}
892892

893893
// Get from beginning to "beta"
894-
var result = await db.VectorSetRangeAsync(key, end: "beta");
894+
using var result = await db.VectorSetRangeAsync(key, end: "beta");
895895

896896
Assert.NotNull(result);
897897
Assert.Equal(2, result.Length);
898-
Assert.Equal(new[] { "alpha", "beta" }, result.Select(r => (string?)r).ToArray());
898+
Assert.Equal(new[] { "alpha", "beta" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
899899
}
900900

901901
[Fact]
@@ -917,11 +917,11 @@ public async Task VectorSetRange_OpenEnd()
917917
}
918918

919919
// Get from "beta" to end
920-
var result = await db.VectorSetRangeAsync(key, start: "beta");
920+
using var result = await db.VectorSetRangeAsync(key, start: "beta");
921921

922922
Assert.NotNull(result);
923923
Assert.Equal(2, result.Length);
924-
Assert.Equal(new[] { "beta", "gamma" }, result.Select(r => (string?)r).ToArray());
924+
Assert.Equal(new[] { "beta", "gamma" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
925925
}
926926

927927
[Fact]
@@ -943,13 +943,13 @@ public async Task VectorSetRange_SyncVsAsync()
943943
}
944944

945945
// Call both sync and async
946-
var syncResult = db.VectorSetRange(key, start: "m05", end: "m15");
947-
var asyncResult = await db.VectorSetRangeAsync(key, start: "m05", end: "m15");
946+
using var syncResult = db.VectorSetRange(key, start: "m05", end: "m15");
947+
using var asyncResult = await db.VectorSetRangeAsync(key, start: "m05", end: "m15");
948948

949949
Assert.NotNull(syncResult);
950950
Assert.NotNull(asyncResult);
951951
Assert.Equal(syncResult.Length, asyncResult.Length);
952-
Assert.Equal(syncResult.Select(r => (string?)r), asyncResult.Select(r => (string?)r));
952+
Assert.Equal(syncResult.Span.ToArray().Select(r => (string?)r), asyncResult.Span.ToArray().Select(r => (string?)r));
953953
}
954954

955955
[Fact]
@@ -971,12 +971,12 @@ public async Task VectorSetRange_WithNumericLexOrder()
971971
}
972972

973973
// Get all - should be in lexicographical order, not numeric
974-
var result = await db.VectorSetRangeAsync(key);
974+
using var result = await db.VectorSetRangeAsync(key);
975975

976976
Assert.NotNull(result);
977977
Assert.Equal(5, result.Length);
978978
// Lexicographical order: "1", "10", "2", "20", "3"
979-
Assert.Equal(new[] { "1", "10", "2", "20", "3" }, result.Select(r => (string?)r).ToArray());
979+
Assert.Equal(new[] { "1", "10", "2", "20", "3" }, result.Span.ToArray().Select(r => (string?)r).ToArray());
980980
}
981981

982982
[Fact]

0 commit comments

Comments
 (0)