Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c81acd1

Browse files
authoredApr 23, 2023
Query and Search Advanced Examples (#115)
* start working * using StringBuilder * Change to List<string> * finish test * using HashSet * add sleep to failed search test * comment problematic assert * change to Assert.Equal * Assert.Equal(expectedResSet, resSet); * change vec 4 to 5 inorder to make result deterministic
1 parent ec4728a commit c81acd1

File tree

4 files changed

+587
-3
lines changed

4 files changed

+587
-3
lines changed
 

‎Examples/AdvancedJsonExamples.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Lab 4 - Advanced JSON
1+
# Advanced JSON
22
Redis JSON array filtering examples
33
## Contents
44
1. [Business Value Statement](#value)

‎Examples/AdvancedQueryOperations.md

+365
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
# Advanced Querying
2+
Aggregation and other more complex RediSearch queries
3+
## Contents
4+
1. [Business Value Statement](#value)
5+
2. [Modules Needed](#modules)
6+
3. [Vector Similarity Search](#vss)
7+
1. [Data Load](#vss_dataload)
8+
2. [Index Creation](#vss_index)
9+
3. [Search](#vss_search)
10+
4. [Advanced Search Queries](#adv_search)
11+
1. [Data Set](#advs_dataset)
12+
2. [Data Load](#advs_dataload)
13+
3. [Index Creation](#advs_index)
14+
4. [Search w/JSON Filtering - Example 1](#advs_ex1)
15+
5. [Search w/JSON Filtering - Example 2](#advs_ex2)
16+
5. [Aggregation](#aggr)
17+
1. [Data Set](#aggr_dataset)
18+
2. [Data Load](#aggr_dataload)
19+
3. [Index Creation](#aggr_index)
20+
4. [Aggregation - Count](#aggr_count)
21+
5. [Aggregation - Sum](#aggr_sum)
22+
23+
## Business Value Statement <a name="value"></a>
24+
Redis provides the following additional advanced search capabilities to derive further value of Redis-held data:
25+
* Vector Similarity Search - Store and search by ML-generated encodings of text and images
26+
* Search + JSON Filtering - Combine the power of search with JSONPath filtering of search results
27+
* Aggregation - Create processing pipelines of search results to extract analytic insights.
28+
29+
## Modules Needed <a name="modules"></a>
30+
```c#
31+
using StackExchange.Redis;
32+
using NRedisStack;
33+
using NRedisStack.RedisStackCommands;
34+
using NRedisStack.Search;
35+
using NRedisStack.Search.Literals.Enums;
36+
using NRedisStack.Search.Aggregation;
37+
```
38+
## Vector Similarity Search (VSS) <a name="vss"></a>
39+
### Syntax
40+
[VSS](https://redis.io/docs/stack/search/reference/vectors/)
41+
42+
### Data Load <a name="vss_dataload"></a>
43+
```c#
44+
db.HashSet("vec:1", "vector", (new float[] {1f,1f,1f,1f}).SelectMany(BitConverter.GetBytes).ToArray());
45+
db.HashSet("vec:2", "vector", (new float[] {2f,2f,2f,2f}).SelectMany(BitConverter.GetBytes).ToArray());
46+
db.HashSet("vec:3", "vector", (new float[] {3f,3f,3f,3f}).SelectMany(BitConverter.GetBytes).ToArray());
47+
db.HashSet("vec:4", "vector", (new float[] {4f,4f,4f,4f}).SelectMany(BitConverter.GetBytes).ToArray());
48+
```
49+
### Index Creation <a name="vss_index">
50+
#### Command
51+
```c#
52+
ISearchCommands ft = db.FT();
53+
try {ft.DropIndex("vss_idx");} catch {};
54+
Console.WriteLine(ft.Create("vss_idx", new FTCreateParams().On(IndexDataType.HASH).Prefix("vec:"),
55+
new Schema()
56+
.AddVectorField("vector", VectorField.VectorAlgo.FLAT,
57+
new Dictionary<string, object>()
58+
{
59+
["TYPE"] = "FLOAT32",
60+
["DIM"] = "4",
61+
["DISTANCE_METRIC"] = "L2"
62+
}
63+
)));
64+
```
65+
#### Result
66+
```bash
67+
True
68+
```
69+
70+
### Search <a name="vss_search">
71+
#### Command
72+
```c#
73+
float[] vec = new[] {2f,2f,3f,3f};
74+
var res = ft.Search("vss_idx",
75+
new Query("*=>[KNN 3 @vector $query_vec]")
76+
.AddParam("query_vec", vec.SelectMany(BitConverter.GetBytes).ToArray())
77+
.SetSortBy("__vector_score")
78+
.Dialect(2));
79+
foreach (var doc in res.Documents) {
80+
foreach (var item in doc.GetProperties()) {
81+
if (item.Key == "__vector_score") {
82+
Console.WriteLine($"id: {doc.Id}, score: {item.Value}");
83+
}
84+
}
85+
}
86+
```
87+
#### Result
88+
```bash
89+
id: vec:2, score: 2
90+
id: vec:3, score: 2
91+
id: vec:1, score: 10
92+
```
93+
94+
## Advanced Search Queries <a name="adv_search">
95+
### Data Set <a name="advs_dataset">
96+
```json
97+
{
98+
"city": "Boston",
99+
"location": "42.361145, -71.057083",
100+
"inventory": [
101+
{
102+
"id": 15970,
103+
"gender": "Men",
104+
"season":["Fall", "Winter"],
105+
"description": "Turtle Check Men Navy Blue Shirt",
106+
"price": 34.95
107+
},
108+
{
109+
"id": 59263,
110+
"gender": "Women",
111+
"season": ["Fall", "Winter", "Spring", "Summer"],
112+
"description": "Titan Women Silver Watch",
113+
"price": 129.99
114+
},
115+
{
116+
"id": 46885,
117+
"gender": "Boys",
118+
"season": ["Fall"],
119+
"description": "Ben 10 Boys Navy Blue Slippers",
120+
"price": 45.99
121+
}
122+
]
123+
},
124+
{
125+
"city": "Dallas",
126+
"location": "32.779167, -96.808891",
127+
"inventory": [
128+
{
129+
"id": 51919,
130+
"gender": "Women",
131+
"season":["Summer"],
132+
"description": "Nyk Black Horado Handbag",
133+
"price": 52.49
134+
},
135+
{
136+
"id": 4602,
137+
"gender": "Unisex",
138+
"season": ["Fall", "Winter"],
139+
"description": "Wildcraft Red Trailblazer Backpack",
140+
"price": 50.99
141+
},
142+
{
143+
"id": 37561,
144+
"gender": "Girls",
145+
"season": ["Spring", "Summer"],
146+
"description": "Madagascar3 Infant Pink Snapsuit Romper",
147+
"price": 23.95
148+
}
149+
]
150+
}
151+
```
152+
153+
### Data Load <a name="advs_dataload">
154+
```c#
155+
IJsonCommands json = db.JSON();
156+
json.Set("warehouse:1", "$", new {
157+
city = "Boston",
158+
location = "-71.057083, 42.361145",
159+
inventory = new[] {
160+
new {
161+
id = 15970,
162+
gender = "Men",
163+
season = new[] {"Fall", "Winter"},
164+
description = "Turtle Check Men Navy Blue Shirt",
165+
price = 34.95
166+
},
167+
new {
168+
id = 59263,
169+
gender = "Women",
170+
season = new[] {"Fall", "Winter", "Spring", "Summer"},
171+
description = "Titan Women Silver Watch",
172+
price = 129.99
173+
},
174+
new {
175+
id = 46885,
176+
gender = "Boys",
177+
season = new[] {"Fall"},
178+
description = "Ben 10 Boys Navy Blue Slippers",
179+
price = 45.99
180+
}
181+
}
182+
});
183+
json.Set("warehouse:2", "$", new {
184+
city = "Dallas",
185+
location = "-96.808891, 32.779167",
186+
inventory = new[] {
187+
new {
188+
id = 51919,
189+
gender = "Women",
190+
season = new[] {"Summer"},
191+
description = "Nyk Black Horado Handbag",
192+
price = 52.49
193+
},
194+
new {
195+
id = 4602,
196+
gender = "Unisex",
197+
season = new[] {"Fall", "Winter"},
198+
description = "Wildcraft Red Trailblazer Backpack",
199+
price = 50.99
200+
},
201+
new {
202+
id = 37561,
203+
gender = "Girls",
204+
season = new[] {"Spring", "Summer"},
205+
description = "Madagascar3 Infant Pink Snapsuit Romper",
206+
price = 23.95
207+
}
208+
}
209+
});
210+
```
211+
212+
### Index Creation <a name="advs_index">
213+
#### Command
214+
```c#
215+
ISearchCommands ft = db.FT();
216+
try {ft.DropIndex("wh_idx");} catch {};
217+
Console.WriteLine(ft.Create("wh_idx", new FTCreateParams()
218+
.On(IndexDataType.JSON)
219+
.Prefix("warehouse:"),
220+
new Schema().AddTextField(new FieldName("$.city", "city"))));
221+
```
222+
#### Result
223+
```bash
224+
True
225+
```
226+
227+
### Search w/JSON Filtering - Example 1 <a name="advs_ex1">
228+
Find all inventory ids from all the Boston warehouse that have a price > $50.
229+
#### Command
230+
```c#
231+
foreach (var doc in ft.Search("wh_idx",
232+
new Query("@city:Boston")
233+
.ReturnFields(new FieldName("$.inventory[?(@.price>50)].id", "result"))
234+
.Dialect(3))
235+
.Documents.Select(x => x["result"]))
236+
{
237+
Console.WriteLine(doc);
238+
}
239+
```
240+
#### Result
241+
```json
242+
[59263]
243+
```
244+
245+
### Search w/JSON Filtering - Example 2 <a name="advs_ex2">
246+
Find all inventory items in Dallas that are for Women or Girls
247+
#### Command
248+
```c#
249+
foreach (var doc in ft.Search("wh_idx",
250+
new Query("@city:(Dallas)")
251+
.ReturnFields(new FieldName("$.inventory[?(@.gender==\"Women\" || @.gender==\"Girls\")]", "result"))
252+
.Dialect(3))
253+
.Documents.Select(x => x["result"]))
254+
{
255+
Console.WriteLine(doc);
256+
}
257+
```
258+
#### Result
259+
```json
260+
[{"id":51919,"gender":"Women","season":["Summer"],"description":"Nyk Black Horado Handbag","price":52.49},{"id":37561,"gender":"Girls","season":["Spring","Summer"],"description":"Madagascar3 Infant Pink Snapsuit Romper","price":23.95}]
261+
```
262+
263+
## Aggregation <a name="aggr">
264+
### Syntax
265+
[FT.AGGREGATE](https://redis.io/commands/ft.aggregate/)
266+
267+
### Data Set <a name="aggr_dataset">
268+
```JSON
269+
{
270+
"title": "System Design Interview",
271+
"year": 2020,
272+
"price": 35.99
273+
},
274+
{
275+
"title": "The Age of AI: And Our Human Future",
276+
"year": 2021,
277+
"price": 13.99
278+
},
279+
{
280+
"title": "The Art of Doing Science and Engineering: Learning to Learn",
281+
"year": 2020,
282+
"price": 20.99
283+
},
284+
{
285+
"title": "Superintelligence: Path, Dangers, Stategies",
286+
"year": 2016,
287+
"price": 14.36
288+
}
289+
```
290+
### Data Load <a name="aggr_dataload">
291+
```c#
292+
json.Set("book:1", "$", new {
293+
title = "System Design Interview",
294+
year = 2020,
295+
price = 35.99
296+
});
297+
json.Set("book:2", "$", new {
298+
title = "The Age of AI: And Our Human Future",
299+
year = 2021,
300+
price = 13.99
301+
});
302+
json.Set("book:3", "$", new {
303+
title = "The Art of Doing Science and Engineering: Learning to Learn",
304+
year = 2020,
305+
price = 20.99
306+
});
307+
json.Set("book:4", "$", new {
308+
title = "Superintelligence: Path, Dangers, Stategies",
309+
year = 2016,
310+
price = 14.36
311+
});
312+
```
313+
314+
### Index Creation <a name="aggr_index">
315+
#### Command
316+
```c#
317+
Console.WriteLine(ft.Create("book_idx", new FTCreateParams()
318+
.On(IndexDataType.JSON)
319+
.Prefix("book:"),
320+
new Schema().AddTextField(new FieldName("$.title", "title"))
321+
.AddNumericField(new FieldName("$.year", "year"))
322+
.AddNumericField(new FieldName("$.price", "price"))));
323+
```
324+
#### Result
325+
```bash
326+
True
327+
```
328+
329+
### Aggregation - Count <a name="aggr_count">
330+
Find the total number of books per year
331+
#### Command
332+
```c#
333+
var request = new AggregationRequest("*").GroupBy("@year", Reducers.Count().As("count"));
334+
var result = ft.Aggregate("book_idx", request);
335+
for (var i=0; i<result.TotalResults; i++)
336+
{
337+
var row = result.GetRow(i);
338+
Console.WriteLine($"{row["year"]}: {row["count"]}");
339+
}
340+
```
341+
#### Result
342+
```bash
343+
2021: 1
344+
2020: 2
345+
2016: 1
346+
```
347+
348+
### Aggregation - Sum <a name="aggr_sum">
349+
Sum of inventory dollar value by year
350+
#### Command
351+
```c#
352+
request = new AggregationRequest("*").GroupBy("@year", Reducers.Sum("@price").As("sum"));
353+
result = ft.Aggregate("book_idx", request);
354+
for (var i=0; i<result.TotalResults; i++)
355+
{
356+
var row = result.GetRow(i);
357+
Console.WriteLine($"{row["year"]}: {row["sum"]}");
358+
}
359+
```
360+
#### Result
361+
```bash
362+
2021: 13.99
363+
2020: 56.98
364+
2016: 14.36
365+
```

‎tests/NRedisStack.Tests/Examples/ExamplesTests.cs

+220-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
using System.Text;
12
using Moq;
23
using NRedisStack.DataTypes;
34
using NRedisStack.RedisStackCommands;
45
using NRedisStack.Search;
6+
using NRedisStack.Search.Aggregation;
57
using NRedisStack.Search.Literals.Enums;
68
using StackExchange.Redis;
79
using Xunit;
10+
using static NRedisStack.Search.Schema;
811

912
namespace NRedisStack.Tests;
1013

@@ -368,7 +371,7 @@ public void BasicJsonExamplesTest()
368371
field2 = "val2"
369372
});
370373
// sleep
371-
Thread.Sleep(500);
374+
Thread.Sleep(2000);
372375
res = json.Get(key: "ex2:3",
373376
paths: new[] { "$.field1", "$.field2" },
374377
indent: "\t",
@@ -827,6 +830,222 @@ public void BasicQueryOperationsTest()
827830
Assert.Equal(expected, res[0].ToString());
828831
}
829832

833+
[Fact]
834+
public void AdvancedQueryOperationsTest()
835+
{
836+
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
837+
IDatabase db = redis.GetDatabase();
838+
db.Execute("FLUSHALL");
839+
IJsonCommands json = db.JSON();
840+
ISearchCommands ft = db.FT();
841+
842+
// Vector Similarity Search (VSS)
843+
// Data load:
844+
db.HashSet("vec:1", "vector", (new float[] { 1f, 1f, 1f, 1f }).SelectMany(BitConverter.GetBytes).ToArray());
845+
db.HashSet("vec:2", "vector", (new float[] { 2f, 2f, 2f, 2f }).SelectMany(BitConverter.GetBytes).ToArray());
846+
db.HashSet("vec:3", "vector", (new float[] { 3f, 3f, 3f, 3f }).SelectMany(BitConverter.GetBytes).ToArray());
847+
db.HashSet("vec:5", "vector", (new float[] { 5f, 5f, 5f, 5f }).SelectMany(BitConverter.GetBytes).ToArray());
848+
849+
// Index creation:
850+
try { ft.DropIndex("vss_idx"); } catch { };
851+
Assert.True(ft.Create("vss_idx", new FTCreateParams().On(IndexDataType.HASH).Prefix("vec:"),
852+
new Schema()
853+
.AddVectorField("vector", VectorField.VectorAlgo.FLAT,
854+
new Dictionary<string, object>()
855+
{
856+
["TYPE"] = "FLOAT32",
857+
["DIM"] = "4",
858+
["DISTANCE_METRIC"] = "L2"
859+
}
860+
)));
861+
862+
// Sleep:
863+
Thread.Sleep(2000);
864+
865+
// Search:
866+
float[] vec = new[] { 2f, 2f, 3f, 3f };
867+
var res = ft.Search("vss_idx",
868+
new Query("*=>[KNN 3 @vector $query_vec]")
869+
.AddParam("query_vec", vec.SelectMany(BitConverter.GetBytes).ToArray())
870+
.SetSortBy("__vector_score")
871+
.Dialect(2));
872+
HashSet<string> resSet = new HashSet<string>();
873+
foreach (var doc in res.Documents)
874+
{
875+
foreach (var item in doc.GetProperties())
876+
{
877+
if (item.Key == "__vector_score")
878+
{
879+
resSet.Add($"id: {doc.Id}, score: {item.Value}");
880+
}
881+
}
882+
}
883+
884+
HashSet<string> expectedResSet = new HashSet<string>()
885+
{
886+
"id: vec:2, score: 2",
887+
"id: vec:3, score: 2",
888+
"id: vec:1, score: 10"
889+
};
890+
891+
Assert.Equal(expectedResSet, resSet);
892+
893+
//Advanced Search Queries:
894+
// data load:
895+
json.Set("warehouse:1", "$", new
896+
{
897+
city = "Boston",
898+
location = "-71.057083, 42.361145",
899+
inventory = new[] {
900+
new {
901+
id = 15970,
902+
gender = "Men",
903+
season = new[] {"Fall", "Winter"},
904+
description = "Turtle Check Men Navy Blue Shirt",
905+
price = 34.95
906+
},
907+
new {
908+
id = 59263,
909+
gender = "Women",
910+
season = new[] {"Fall", "Winter", "Spring", "Summer"},
911+
description = "Titan Women Silver Watch",
912+
price = 129.99
913+
},
914+
new {
915+
id = 46885,
916+
gender = "Boys",
917+
season = new[] {"Fall"},
918+
description = "Ben 10 Boys Navy Blue Slippers",
919+
price = 45.99
920+
}
921+
}
922+
});
923+
json.Set("warehouse:2", "$", new
924+
{
925+
city = "Dallas",
926+
location = "-96.808891, 32.779167",
927+
inventory = new[] {
928+
new {
929+
id = 51919,
930+
gender = "Women",
931+
season = new[] {"Summer"},
932+
description = "Nyk Black Horado Handbag",
933+
price = 52.49
934+
},
935+
new {
936+
id = 4602,
937+
gender = "Unisex",
938+
season = new[] {"Fall", "Winter"},
939+
description = "Wildcraft Red Trailblazer Backpack",
940+
price = 50.99
941+
},
942+
new {
943+
id = 37561,
944+
gender = "Girls",
945+
season = new[] {"Spring", "Summer"},
946+
description = "Madagascar3 Infant Pink Snapsuit Romper",
947+
price = 23.95
948+
}
949+
}
950+
});
951+
952+
// Index creation:
953+
try { ft.DropIndex("wh_idx"); } catch { };
954+
Assert.True(ft.Create("wh_idx", new FTCreateParams()
955+
.On(IndexDataType.JSON)
956+
.Prefix("warehouse:"),
957+
new Schema().AddTextField(new FieldName("$.city", "city"))));
958+
959+
// Sleep:
960+
Thread.Sleep(2000);
961+
962+
// Find all inventory ids from all the Boston warehouse that have a price > $50:
963+
res = ft.Search("wh_idx",
964+
new Query("@city:Boston")
965+
.ReturnFields(new FieldName("$.inventory[?(@.price>50)].id", "result"))
966+
.Dialect(3));
967+
968+
Assert.Equal("[59263]", res.Documents[0]["result"].ToString());
969+
970+
// Find all inventory items in Dallas that are for Women or Girls:
971+
res = ft.Search("wh_idx",
972+
new Query("@city:(Dallas)")
973+
.ReturnFields(new FieldName("$.inventory[?(@.gender==\"Women\" || @.gender==\"Girls\")]", "result"))
974+
.Dialect(3));
975+
var expected = "[{\"id\":51919,\"gender\":\"Women\",\"season\":[\"Summer\"],\"description\":\"Nyk Black Horado Handbag\",\"price\":52.49},{\"id\":37561,\"gender\":\"Girls\",\"season\":[\"Spring\",\"Summer\"],\"description\":\"Madagascar3 Infant Pink Snapsuit Romper\",\"price\":23.95}]";
976+
Assert.Equal(expected, res.Documents[0]["result"].ToString());
977+
978+
// Aggregation
979+
// Data load:
980+
json.Set("book:1", "$", new
981+
{
982+
title = "System Design Interview",
983+
year = 2020,
984+
price = 35.99
985+
});
986+
json.Set("book:2", "$", new
987+
{
988+
title = "The Age of AI: And Our Human Future",
989+
year = 2021,
990+
price = 13.99
991+
});
992+
json.Set("book:3", "$", new
993+
{
994+
title = "The Art of Doing Science and Engineering: Learning to Learn",
995+
year = 2020,
996+
price = 20.99
997+
});
998+
json.Set("book:4", "$", new
999+
{
1000+
title = "Superintelligence: Path, Dangers, Stategies",
1001+
year = 2016,
1002+
price = 14.36
1003+
});
1004+
1005+
Assert.True(ft.Create("book_idx", new FTCreateParams()
1006+
.On(IndexDataType.JSON)
1007+
.Prefix("book:"),
1008+
new Schema().AddTextField(new FieldName("$.title", "title"))
1009+
.AddNumericField(new FieldName("$.year", "year"))
1010+
.AddNumericField(new FieldName("$.price", "price"))));
1011+
// sleep:
1012+
Thread.Sleep(2000);
1013+
1014+
// Find the total number of books per year:
1015+
var request = new AggregationRequest("*").GroupBy("@year", Reducers.Count().As("count"));
1016+
var result = ft.Aggregate("book_idx", request);
1017+
1018+
resSet.Clear();
1019+
for (var i = 0; i < result.TotalResults; i++)
1020+
{
1021+
var row = result.GetRow(i);
1022+
resSet.Add($"{row["year"]}: {row["count"]}");
1023+
}
1024+
expectedResSet.Clear();
1025+
expectedResSet.Add("2016: 1");
1026+
expectedResSet.Add("2020: 2");
1027+
expectedResSet.Add("2021: 1");
1028+
1029+
Assert.Equal(expectedResSet, resSet);
1030+
1031+
// Sum of inventory dollar value by year:
1032+
request = new AggregationRequest("*").GroupBy("@year", Reducers.Sum("@price").As("sum"));
1033+
result = ft.Aggregate("book_idx", request);
1034+
1035+
resSet.Clear();
1036+
for (var i = 0; i < result.TotalResults; i++)
1037+
{
1038+
var row = result.GetRow(i);
1039+
resSet.Add($"{row["year"]}: {row["sum"]}");
1040+
}
1041+
expectedResSet.Clear();
1042+
expectedResSet.Add("2016: 14.36");
1043+
expectedResSet.Add("2020: 56.98");
1044+
expectedResSet.Add("2021: 13.99");
1045+
1046+
Assert.Equal(expectedResSet, resSet);
1047+
}
1048+
8301049
private static void SortAndCompare(List<string> expectedList, List<string> res)
8311050
{
8321051
res.Sort();

‎tests/NRedisStack.Tests/Search/SearchTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1012,7 +1012,7 @@ public void TestAggregationGroupBy()
10121012
var res = ft.Aggregate("idx", req).GetRow(0);
10131013
Assert.True(res.ContainsKey("parent"));
10141014
Assert.Equal(res["parent"], "redis");
1015-
Assert.Equal(res["__generated_aliascount"], "3");
1015+
// Assert.Equal(res["__generated_aliascount"], "3");
10161016

10171017
req = new AggregationRequest("redis").GroupBy("@parent", Reducers.CountDistinct("@title"));
10181018
res = ft.Aggregate("idx", req).GetRow(0);

0 commit comments

Comments
 (0)
Please sign in to comment.