Skip to content

Commit 7ebee4f

Browse files
committed
more working-esque integration tests
1 parent c992d83 commit 7ebee4f

File tree

4 files changed

+165
-12
lines changed

4 files changed

+165
-12
lines changed

src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
NRedisStack.Search.Parameters
22
static NRedisStack.Search.Parameters.From<T>(T obj) -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!
3+
[NRS001]const NRedisStack.Search.HybridSearchQuery.Fields.Key = "@__key" -> string!
4+
[NRS001]const NRedisStack.Search.HybridSearchQuery.Fields.Score = "@__score" -> string!
35
[NRS001]NRedisStack.ISearchCommands.HybridSearch(string! indexName, NRedisStack.Search.HybridSearchQuery! query, System.Collections.Generic.IReadOnlyDictionary<string!, object!>? parameters = null) -> NRedisStack.Search.HybridSearchResult!
46
[NRS001]NRedisStack.ISearchCommandsAsync.HybridSearchAsync(string! indexName, NRedisStack.Search.HybridSearchQuery! query, System.Collections.Generic.IReadOnlyDictionary<string!, object!>? parameters = null) -> System.Threading.Tasks.Task<NRedisStack.Search.HybridSearchResult!>!
57
[NRS001]NRedisStack.Search.ApplyExpression
@@ -15,6 +17,7 @@ static NRedisStack.Search.Parameters.From<T>(T obj) -> System.Collections.Generi
1517
[NRS001]NRedisStack.Search.HybridSearchQuery.Combiner
1618
[NRS001]NRedisStack.Search.HybridSearchQuery.Combiner.Combiner() -> void
1719
[NRS001]NRedisStack.Search.HybridSearchQuery.ExplainScore(bool explainScore = true) -> NRedisStack.Search.HybridSearchQuery!
20+
[NRS001]NRedisStack.Search.HybridSearchQuery.Fields
1821
[NRS001]NRedisStack.Search.HybridSearchQuery.Filter(string! expression) -> NRedisStack.Search.HybridSearchQuery!
1922
[NRS001]NRedisStack.Search.HybridSearchQuery.GroupBy(params string![]! fields) -> NRedisStack.Search.HybridSearchQuery!
2023
[NRS001]NRedisStack.Search.HybridSearchQuery.GroupBy(string! field) -> NRedisStack.Search.HybridSearchQuery!
@@ -89,5 +92,6 @@ static NRedisStack.Search.Parameters.From<T>(T obj) -> System.Collections.Generi
8992
[NRS001]static NRedisStack.Search.VectorData.implicit operator NRedisStack.Search.VectorData!(string! name) -> NRedisStack.Search.VectorData!
9093
[NRS001]static NRedisStack.Search.VectorData.implicit operator NRedisStack.Search.VectorData!(System.ReadOnlyMemory<float> vector) -> NRedisStack.Search.VectorData!
9194
[NRS001]static NRedisStack.Search.VectorData.Parameter(string! name) -> NRedisStack.Search.VectorData!
95+
[NRS001]static NRedisStack.Search.VectorData.Raw(System.ReadOnlyMemory<byte> bytes) -> NRedisStack.Search.VectorData!
9296
[NRS001]static NRedisStack.Search.VectorSearchMethod.NearestNeighbour(int count = 10, int? maxTopCandidates = null, string? distanceAlias = null) -> NRedisStack.Search.VectorSearchMethod!
9397
[NRS001]static NRedisStack.Search.VectorSearchMethod.Range(double radius, double? epsilon = null, string? distanceAlias = null) -> NRedisStack.Search.VectorSearchMethod!

src/NRedisStack/Search/HybridSearchQuery.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@ namespace NRedisStack.Search;
1111
[Experimental(Experiments.Server_8_4, UrlFormat = Experiments.UrlFormat)]
1212
public sealed partial class HybridSearchQuery
1313
{
14+
/// <summary>
15+
/// Well-known fields for use with <see cref="ReturnFields(string[])"/>
16+
/// </summary>
17+
public static class Fields
18+
{
19+
// ReSharper disable InconsistentNaming
20+
21+
/// <summary>
22+
/// The key of the indexed item in the database.
23+
/// </summary>
24+
public const string Key = "@__key";
25+
26+
/// <summary>
27+
/// The score from the query.
28+
/// </summary>
29+
public const string Score = "@__score";
30+
// ReSharper restore InconsistentNaming
31+
}
1432
private bool _frozen;
1533
private SearchConfig _search;
1634
private VectorSearchConfig _vsim;
@@ -69,7 +87,7 @@ public HybridSearchQuery Combine(Combiner combiner, string? scoreAlias = null)
6987
private object? _loadFieldOrFields;
7088

7189
/// <summary>
72-
/// Add the list of fields to return in the results.
90+
/// Add the list of fields to return in the results. Well-known fields are available via <see cref="Fields"/>.
7391
/// </summary>
7492
public HybridSearchQuery ReturnFields(params string[] fields) // naming for consistency with SearchQuery
7593
{
@@ -79,7 +97,7 @@ public HybridSearchQuery Combine(Combiner combiner, string? scoreAlias = null)
7997
}
8098

8199
/// <summary>
82-
/// Add the list of fields to return in the results.
100+
/// Add the list of fields to return in the results. Well-known fields are available via <see cref="Fields"/>.
83101
/// </summary>
84102
public HybridSearchQuery ReturnFields(string field) // naming for consistency with SearchQuery
85103
{

src/NRedisStack/Search/VectorData.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Buffers;
22
using System.Diagnostics.CodeAnalysis;
33
using System.Runtime.InteropServices;
4+
using StackExchange.Redis;
45

56
namespace NRedisStack.Search;
67

@@ -16,6 +17,11 @@ private protected VectorData()
1617
/// </summary>
1718
public static VectorData Create(ReadOnlyMemory<float> vector) => new VectorDataSingle(vector);
1819

20+
/// <summary>
21+
/// A raw vector payload.
22+
/// </summary>
23+
public static VectorData Raw(ReadOnlyMemory<byte> bytes) => new VectorDataRaw(bytes);
24+
1925
/// <summary>
2026
/// Represent a vector as a parameter to be supplied later.
2127
/// </summary>
@@ -58,6 +64,11 @@ private string ToBase64()
5864
}
5965
}
6066

67+
private sealed class VectorDataRaw(ReadOnlyMemory<byte> bytes) : VectorData
68+
{
69+
internal override object GetSingleArg() => (RedisValue)bytes;
70+
}
71+
6172
private sealed class VectorParameter : VectorData
6273
{
6374
private readonly string name;
Lines changed: 130 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
using System.Buffers;
2+
using System.Numerics;
13
using System.Runtime.CompilerServices;
4+
using System.Runtime.InteropServices;
5+
using System.Text;
26
using NRedisStack.RedisStackCommands;
37
using NRedisStack.Search;
8+
using StackExchange.Redis;
49
using Xunit;
510
using Xunit.Abstractions;
611

@@ -9,44 +14,73 @@ namespace NRedisStack.Tests.Search;
914
public class HybridSearchIntegrationTests(EndpointsFixture endpointsFixture, ITestOutputHelper log)
1015
: AbstractNRedisStackTest(endpointsFixture, log), IDisposable
1116
{
12-
private readonly struct Api(SearchCommands ft, string index)
17+
private readonly struct Api(SearchCommands ft, string index, IDatabase db)
1318
{
1419
public string Index { get; } = index;
1520
public SearchCommands FT { get; } = ft;
21+
public IDatabase DB { get; } = db;
1622
}
1723

18-
private Api CreateIndex(string endpointId, [CallerMemberName] string caller = "", bool populate = true)
24+
private const int V1DIM = 5;
25+
26+
private async Task<Api> CreateIndexAsync(string endpointId, [CallerMemberName] string caller = "", bool populate = true)
1927
{
2028
var index = $"ix_{caller}";
2129
var db = GetCleanDatabase(endpointId);
2230
// ReSharper disable once RedundantArgumentDefaultValue
2331
var ft = db.FT(2);
32+
2433
var vectorAttrs = new Dictionary<string, object>()
2534
{
26-
["TYPE"] = "FLOAT32",
27-
["DIM"] = "2",
35+
["TYPE"] = "FLOAT16",
36+
["DIM"] = V1DIM,
2837
["DISTANCE_METRIC"] = "L2",
2938
};
3039
Schema sc = new Schema()
3140
// ReSharper disable once RedundantArgumentDefaultValue
3241
.AddTextField("text1", 1.0, missingIndex: true)
3342
.AddTagField("tag1", missingIndex: true)
3443
.AddNumericField("numeric1", missingIndex: true)
35-
.AddGeoField("geo1", missingIndex: true)
36-
.AddGeoShapeField("geoshape1", Schema.GeoShapeField.CoordinateSystem.FLAT, missingIndex: true)
3744
.AddVectorField("vector1", Schema.VectorField.VectorAlgo.FLAT, vectorAttrs, missingIndex: true);
3845

3946
var ftCreateParams = FTCreateParams.CreateParams();
40-
Assert.True(ft.Create(index, ftCreateParams, sc));
47+
Assert.True(await ft.CreateAsync(index, ftCreateParams, sc));
48+
49+
if (populate)
50+
{
51+
#if NET
52+
Task last = Task.CompletedTask;
53+
var rand = new Random(12345);
54+
string[] tags = ["foo", "bar", "blap"];
55+
for (int i = 0; i < 16; i++)
56+
{
57+
byte[] vec = new byte[V1DIM * sizeof(ushort)];
58+
var halves = MemoryMarshal.Cast<byte, Half>(vec);
59+
for (int j = 1; j < V1DIM; j++)
60+
{
61+
halves[j] = (Half)rand.NextDouble();
62+
}
63+
HashEntry[] entry = [
64+
new("text1", $"Search entry {i}"),
65+
new("tag1", tags[rand.Next(tags.Length)]),
66+
new("numeric1", rand.Next(0, 32)),
67+
new("vector1", vec)];
68+
last = db.HashSetAsync($"{index}_entry{i}", entry);
69+
}
70+
await last;
71+
#else
72+
throw new PlatformNotSupportedException("FP16");
73+
#endif
74+
}
4175

42-
return new(ft, index);
76+
return new(ft, index, db);
4377
}
4478

4579
[SkipIfRedisTheory(Comparison.LessThan, "8.3.224")]
4680
[MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))]
47-
public void TestSetup(string endpointId)
81+
public async Task TestSetup(string endpointId)
4882
{
49-
var api = CreateIndex(endpointId, populate: false);
83+
var api = await CreateIndexAsync(endpointId, populate: false);
5084
Dictionary<string, object> args = new() { ["x"] = "abc" };
5185
var query = new HybridSearchQuery()
5286
.Search("*")
@@ -58,4 +92,90 @@ public void TestSetup(string endpointId)
5892
Assert.Empty(result.Warnings);
5993
Assert.Empty(result.Results);
6094
}
95+
96+
[SkipIfRedisTheory(Comparison.LessThan, "8.3.224")]
97+
[MemberData(nameof(EndpointsFixture.Env.StandaloneOnly), MemberType = typeof(EndpointsFixture.Env))]
98+
public async Task TestSearch(string endpointId)
99+
{
100+
var api = await CreateIndexAsync(endpointId, populate: true);
101+
102+
var hash = (await api.DB.HashGetAllAsync($"{api.Index}_entry2")).ToDictionary(k => k.Name, v => v.Value);
103+
var vec = (byte[])hash["vector1"]!;
104+
var text = (string)hash["text1"]!;
105+
var query = new HybridSearchQuery()
106+
.Search(text)
107+
.VectorSearch("@vector1", VectorData.Raw(vec))
108+
.ReturnFields("@text1", HybridSearchQuery.Fields.Key, HybridSearchQuery.Fields.Score);
109+
110+
WriteArgs(api.Index, query);
111+
112+
var result = api.FT.HybridSearch(api.Index, query);
113+
Assert.Equal(10, result.TotalResults);
114+
Assert.NotEqual(TimeSpan.Zero, result.ExecutionTime);
115+
Assert.Empty(result.Warnings);
116+
Assert.Equal(10, result.Results.Length);
117+
}
118+
119+
private void WriteArgs(string indexName, HybridSearchQuery query, IReadOnlyDictionary<string, object>? parameters = null)
120+
{
121+
byte[] scratch = [];
122+
123+
var sb = new StringBuilder(query.Command).Append(' ');
124+
var args = query.GetArgs(indexName, parameters);
125+
foreach (var arg in args)
126+
{
127+
sb.Append(' ');
128+
if (arg is string s)
129+
{
130+
sb.Append('"').Append(s.Replace("\"","\\\"")).Append('"');
131+
}
132+
else if (arg is RedisValue v)
133+
{
134+
var len = v.GetByteCount();
135+
if (len > scratch.Length)
136+
{
137+
ArrayPool<byte>.Shared.Return(scratch);
138+
scratch = ArrayPool<byte>.Shared.Rent(len);
139+
}
140+
141+
v.CopyTo(scratch);
142+
WriteEscaped(scratch.AsSpan(0, len), sb);
143+
}
144+
else
145+
{
146+
sb.Append(arg);
147+
}
148+
}
149+
log.WriteLine(sb.ToString());
150+
151+
ArrayPool<byte>.Shared.Return(scratch);
152+
153+
static void WriteEscaped(ReadOnlySpan<byte> span, StringBuilder sb)
154+
{
155+
// write resp-cli style
156+
sb.Append("\"");
157+
foreach (var b in span)
158+
{
159+
if (b < ' ' | b >= 127 | b == '"' | b == '\\')
160+
{
161+
switch (b)
162+
{
163+
case (byte)'\\': sb.Append("\\\\"); break;
164+
case (byte)'"': sb.Append("\\\""); break;
165+
case (byte)'\n': sb.Append("\\n"); break;
166+
case (byte)'\r': sb.Append("\\r"); break;
167+
case (byte)'\t': sb.Append("\\t"); break;
168+
case (byte)'\b': sb.Append("\\b"); break;
169+
case (byte)'\a': sb.Append("\\a"); break;
170+
default: sb.Append("\\x").Append(b.ToString("X2")); break;
171+
}
172+
}
173+
else
174+
{
175+
sb.Append((char)b);
176+
}
177+
}
178+
sb.Append('"');
179+
}
180+
}
61181
}

0 commit comments

Comments
 (0)