1+ using System . Buffers ;
2+ using System . Numerics ;
13using System . Runtime . CompilerServices ;
4+ using System . Runtime . InteropServices ;
5+ using System . Text ;
26using NRedisStack . RedisStackCommands ;
37using NRedisStack . Search ;
8+ using StackExchange . Redis ;
49using Xunit ;
510using Xunit . Abstractions ;
611
@@ -9,44 +14,73 @@ namespace NRedisStack.Tests.Search;
914public 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