From 2e3735dd9c8bc0a3583ff726b0d142c1b7a46649 Mon Sep 17 00:00:00 2001 From: shacharPash Date: Thu, 13 Apr 2023 12:45:08 +0300 Subject: [PATCH 01/12] adding basic json example --- Examples/BasicJsonExamples.md | 468 ++++++++++++++++++ .../Examples/ExamplesTests.cs | 67 +++ 2 files changed, 535 insertions(+) create mode 100644 Examples/BasicJsonExamples.md diff --git a/Examples/BasicJsonExamples.md b/Examples/BasicJsonExamples.md new file mode 100644 index 00000000..8dfb0071 --- /dev/null +++ b/Examples/BasicJsonExamples.md @@ -0,0 +1,468 @@ +# Basic JSON Operations +CRUD (Create, Read, Update, Delete) operations with the Redis JSON data type + +## Contents +1. [Business Value Statement](#value) +2. [Create](#create) + 1. [Key Value Pair](#kvp) + 2. [Single String Property](#single_string) + 3. [Multiple Properties](#multiple_properties) + 4. [Multiple Properties + Data Types](#multiple_types) + 5. [JSON Arrays](#arrays) + 6. [JSON Objects](#objects) + 7. [Mix](#mix) +3. [Read](#read) + 1. [Key Fetch](#key_fetch) + 2. [Single Property Fetch](#single_fetch) + 3. [Multi-Property Fetch](#multiple_fetch) + 4. [Nested Property Fetch](#nested_fetch) + 5. [Array Fetch](#array_fetch) +4. [Update](#update) + 1. [Entire Object](#entire_update) + 2. [Single Property](#single_update) + 3. [Nested Property](#nested_update) + 4. [Array Item](#array_update) +5. [Delete](#delete) + 1. [Entire Object](#entire_delete) + 2. [Single Property](#single_delete) + 3. [Nested Property](#nested_delete) + 4. [Array Item](#array_delete) + +## Business Value Statement +Document stores are a NoSQL database type that provide flexible schemas and access patterns that are familiar to developers. Redis can provide document store functionality natively with its JSON data type. This allows Redis to complement existing document store databases such as MongoDB or provide standalone JSON document storage. + +## Create +### Syntax +[JSON.SET](https://redis.io/commands/json.set/) + +### Key Value Pair +Insert a simple KVP as a JSON object. +#### Command +```c# +IJsonCommands json = db.JSON(); +Console.WriteLine(json.Set("ex1:1", "$", "\"val\"")); +``` +#### Result +```bash +True +``` + +### Single String Property +Insert a single-property JSON object. +#### Command +```c# +Console.WriteLine(json.Set("ex1:2", "$", new {field1 = "val1" })); +``` +#### Result +```bash +True +``` + +### Multiple Properties +Insert a JSON object with multiple properties. +#### Command +```c# +Console.WriteLine(json.Set("ex1:3", "$", new { + field1 = "val1", + field2 = "val2" +})); +``` +#### Result +```bash +True +``` + +### Multiple Properties + Data Types +Insert a JSON object with multiple properties of different data types. +#### Command +```c# +Console.WriteLine(json.Set("ex1:4", "$", new { + field1 = "val1", + field2 = "val2", + field3 = true, + field4 = (string?) null +})); +``` +#### Result +```bash +True +``` + +### JSON Arrays +Insert a JSON object that contains an array. +#### Command +```c# +Console.WriteLine(json.Set("ex1:5", "$", new { + arr1 = new [] {"val1", "val2", "val3"} +})); +``` +#### Result +```bash +True +``` + +### JSON Objects +Insert a JSON object that contains a nested object. +#### Command +```c# +Console.WriteLine(json.Set("ex1:6", "$", new { + obj1 = new { + str1 = "val1", + num2 = 2 + } +})); +``` +#### Result +```bash +True +``` + +### Mix +Insert a JSON object with a mixture of property data types. +#### Command +```c# +Console.WriteLine(json.Set("ex1:7", "$", new { + str1 = "val1", + str2 = "val2", + arr1 = new [] {1,2,3,4}, + obj1 = new { + num1 = 1, + arr2 = new [] {"val1","val2", "val3"} + } +})); +``` +#### Result +```bash +True +``` + +## Read +### Syntax +[JSON.GET](https://redis.io/commands/json.get/) + +### Key Fetch +Set and Fetch a simple JSON KVP. +#### Command +```c# +json.Set("ex2:1", "$", "\"val\""); +Console.WriteLine(json.Get(key: "ex2:1", + path: "$", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +[ + "val" +] +``` + +### Single Property Fetch +Set and Fetch a single property from a JSON object. +#### Command +```c# +json.Set("ex2:2", "$", new { + field1 = "val1" +}); +Console.WriteLine(json.Get(key: "ex2:2", + path: "$.field1", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +[ + "val1" +] +``` + +### Multi-Property Fetch +Fetch multiple properties. +#### Command +```c# +json.Set("ex2:3", "$", new { + field1 = "val1", + field2 = "val2" +}); +Console.WriteLine(json.Get(key: "ex2:3", + paths: new[] {"$.field1", "$.field2" }, + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "$.field1":[ + "val1" + ], + "$.field2":[ + "val2" + ] +} +``` + +### Nested Property Fetch +Fetch a property nested in another JSON object. +#### Command +```c# +json.Set("ex2:4", "$", new { + obj1 = new { + str1 = "val1", + num2 = 2 + } +}); +Console.WriteLine(json.Get(key: "ex2:4", + path: "$.obj1.num2", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +[ + 2 +] +``` + +### Array Fetch +Fetch properties within an array and utilize array subscripting. +#### Command +```c# +json.Set("ex2:5", "$",new { + str1 = "val1", + str2 = "val2", + arr1 = new[] {1,2,3,4}, + obj1 = new { + num1 = 1, + arr2 = new[] {"val1","val2", "val3"} + } +}); +Console.WriteLine(json.Get(key: "ex2:5", + path: "$.obj1.arr2", + indent: "\t", + newLine: "\n" +)); +Console.WriteLine(json.Get(key: "ex2:5", + path: "$.arr1[1]", + indent: "\t", + newLine: "\n" +)); +Console.WriteLine(json.Get(key: "ex2:5", + path: "$.obj1.arr2[0:2]", + indent: "\t", + newLine: "\n" +)); +Console.WriteLine(json.Get(key: "ex2:5", + path: "$.arr1[-2:]", + indent: "\t", + newLine: "\n" +)); +``` +#### Results +```bash +[ + [ + "val1", + "val2", + "val3" + ] +] +[ + 2 +] +[ + "val1", + "val2" +] +[ + 3, + 4 +] +``` + +## Update +### Syntax +[JSON.SET](https://redis.io/commands/json.set/) + +### Entire Object +Update an entire JSON object. +#### Command +```c# +json.Set("ex3:1", "$", new {field1 = "val1"}); +json.Set("ex3:1", "$", new {foo = "bar"}); +Console.WriteLine(json.Get(key: "ex3:1", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "foo":"bar" +} +``` + +### Single Property +Update a single property within an object. +#### Command +```c# +json.Set("ex3:2", "$", new { + field1 = "val1", + field2 = "val2" +}); +json.Set("ex3:2", "$.field1", "\"foo\""); +Console.WriteLine(json.Get(key: "ex3:2", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "field1":"foo", + "field2":"val2" +} +``` + +### Nested Property +Update a property in an embedded JSON object. +#### Command +```c# +json.Set("ex3:3", "$", new { + obj1 = new { + str1 = "val1", + num2 = 2 + } +}); +json.Set("ex3:3", "$.obj1.num2", 3); +Console.WriteLine(json.Get(key: "ex3:3", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "obj1":{ + "str1":"val1", + "num2":3 + } +} +``` + +### Array Item +Update an item in an array via index. +#### Command +```c# +json.Set("ex3:4", "$", new { + arr1 = new[] {"val1", "val2", "val3"} +}); +json.Set("ex3:4", "$.arr1[0]", "\"foo\""); +Console.WriteLine(json.Get(key: "ex3:4", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "arr1":[ + "foo", + "val2", + "val3" + ] +} +``` + +## Delete +### Syntax +[JSON.DEL](https://redis.io/commands/json.del/) + +### Entire Object +Delete entire object/key. +#### Command +```c# +json.Set("ex4:1", "$", new {field1 = "val1"}); +json.Del("ex4:1"); +Console.WriteLine(json.Get(key: "ex4:1", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash + +``` + +### Single Property +Delete a single property from an object. +#### Command +```c# +json.Set("ex4:2", "$", new { + field1 = "val1", + field2 = "val2" +}); +json.Del("ex4:2", "$.field1"); +Console.WriteLine(json.Get(key: "ex4:2", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "field2":"val2" +} +``` + +### Nested Property +Delete a property from an embedded object. +#### Command +```c# +json.Set("ex4:3", "$", new { + obj1 = new { + str1 = "val1", + num2 = 2 + } +}); +json.Del("ex4:3", "$.obj1.num2"); +Console.WriteLine(json.Get(key: "ex4:3", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "obj1":{ + "str1":"val1" + } +} +``` + +### Array Item +Delete a single item from an array. +#### Command +```c# +Console.WriteLine("\n*** Lab 2 - Delete a single item from an array ***"); +json.Set("ex4:4", "$", new { + arr1 = new[] {"val1", "val2", "val3"} +}); +json.Del("ex4:4", "$.arr1[0]"); +Console.WriteLine(json.Get(key: "ex4:4", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```bash +{ + "arr1":[ + "val2", + "val3" + ] +} +``` \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index bdbc15af..d3cd6cd0 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -282,4 +282,71 @@ public void TestJsonConvert() Assert.Equal(10, docs.Count()); } + + [Fact] + public void BasicJsonExamplesTest() + { + ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + IJsonCommands json = db.JSON(); + + // Insert a simple KVP as a JSON object: + Assert.True(json.Set("ex1:1", "$", "\"val\"")); + // Insert a single-property JSON object: + Assert.True(json.Set("ex1:2", "$", new { field1 = "val1" })); + // Insert a JSON object with multiple properties: + Assert.True(json.Set("ex1:3", "$", new + { + field1 = "val1", + field2 = "val2" + })); + + // Insert a JSON object with multiple properties of different data types: + Assert.True(json.Set("ex1:4", "$", new + { + field1 = "val1", + field2 = "val2", + field3 = true, + field4 = (string?)null + })); + + // Insert a JSON object that contains an array: + Assert.True(json.Set("ex1:5", "$", new + { + arr1 = new[] { "val1", "val2", "val3" } + })); + + // Insert a JSON object that contains a nested object: + Assert.True(json.Set("ex1:6", "$", new + { + obj1 = new + { + str1 = "val1", + num2 = 2 + } + })); + + // Insert a JSON object with a mixture of property data types: + Assert.True(json.Set("ex1:7", "$", new + { + str1 = "val1", + str2 = "val2", + arr1 = new[] { 1, 2, 3, 4 }, + obj1 = new + { + num1 = 1, + arr2 = new[] { "val1", "val2", "val3" } + } + })); + + // Set and Fetch a simple JSON KVP: + json.Set("ex2:1", "$", "\"val\""); + var res = json.Get(key: "ex2:1", + path: "$", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("[\n\t\"val\"\n]", res.ToString()); + } } From be44cc2b523f036264099189a3229eb3c2f28900 Mon Sep 17 00:00:00 2001 From: shacharPash Date: Sun, 16 Apr 2023 11:06:35 +0300 Subject: [PATCH 02/12] finish BasicJsonExamplesTest --- Examples/BasicJsonExamples.md | 1 - .../Examples/ExamplesTests.cs | 180 ++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/Examples/BasicJsonExamples.md b/Examples/BasicJsonExamples.md index 8dfb0071..59812d0b 100644 --- a/Examples/BasicJsonExamples.md +++ b/Examples/BasicJsonExamples.md @@ -447,7 +447,6 @@ Console.WriteLine(json.Get(key: "ex4:3", Delete a single item from an array. #### Command ```c# -Console.WriteLine("\n*** Lab 2 - Delete a single item from an array ***"); json.Set("ex4:4", "$", new { arr1 = new[] {"val1", "val2", "val3"} }); diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index d3cd6cd0..35a11581 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -1,4 +1,5 @@ using Moq; +using Newtonsoft.Json; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using NRedisStack.Search; @@ -348,5 +349,184 @@ public void BasicJsonExamplesTest() newLine: "\n" ); Assert.Equal("[\n\t\"val\"\n]", res.ToString()); + + // Set and Fetch a single property from a JSON object: + json.Set("ex2:2", "$", new + { + field1 = "val1" + }); + res = json.Get(key: "ex2:2", + path: "$.field1", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("[\n\t\"val1\"\n]", res.ToString()); + + // Fetch multiple properties: + json.Set("ex2:3", "$", new + { + field1 = "val1", + field2 = "val2" + }); + res = json.Get(key: "ex2:3", + paths: new[] { "$.field1", "$.field2" }, + indent: "\t", + newLine: "\n" + ); + var jsonObj = JsonConvert.SerializeObject(res.ToString()); + Assert.Equal("{\n\t\"$.field1\":[\n\t\t\"val1\"\n\t],\n\t\"$.field2\":[\n\t\t\"val2\"\n\t]\n}", res.ToString()); + + // Fetch a property nested in another JSON object: + json.Set("ex2:4", "$", new + { + obj1 = new + { + str1 = "val1", + num2 = 2 + } + }); + res = json.Get(key: "ex2:4", + path: "$.obj1.num2", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("[\n\t2\n]", res.ToString()); + + // Fetch properties within an array and utilize array subscripting: + json.Set("ex2:5", "$", new + { + str1 = "val1", + str2 = "val2", + arr1 = new[] { 1, 2, 3, 4 }, + obj1 = new + { + num1 = 1, + arr2 = new[] { "val1", "val2", "val3" } + } + }); + res = json.Get(key: "ex2:5", + path: "$.obj1.arr2", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("[\n\t[\n\t\t\"val1\",\n\t\t\"val2\",\n\t\t\"val3\"\n\t]\n]", res.ToString()); + res = json.Get(key: "ex2:5", + path: "$.arr1[1]", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("[\n\t2\n]", res.ToString()); + res = json.Get(key: "ex2:5", + path: "$.obj1.arr2[0:2]", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("[\n\t\"val1\",\n\t\"val2\"\n]", res.ToString()); + res = json.Get(key: "ex2:5", + path: "$.arr1[-2:]", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("[\n\t3,\n\t4\n]", res.ToString()); + + // Update an entire JSON object: + json.Set("ex3:1", "$", new { field1 = "val1" }); + json.Set("ex3:1", "$", new { foo = "bar" }); + res = json.Get(key: "ex3:1", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("{\n\t\"foo\":\"bar\"\n}", res.ToString()); + + // Update a single property within an object: + json.Set("ex3:2", "$", new + { + field1 = "val1", + field2 = "val2" + }); + json.Set("ex3:2", "$.field1", "\"foo\""); + res = json.Get(key: "ex3:2", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("{\n\t\"field1\":\"foo\",\n\t\"field2\":\"val2\"\n}", res.ToString()); + + // Update a property in an embedded JSON object: + json.Set("ex3:3", "$", new + { + obj1 = new + { + str1 = "val1", + num2 = 2 + } + }); + json.Set("ex3:3", "$.obj1.num2", 3); + res = json.Get(key: "ex3:3", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("{\n\t\"obj1\":{\n\t\t\"str1\":\"val1\",\n\t\t\"num2\":3\n\t}\n}", res.ToString()); + + // Update an item in an array via index: + json.Set("ex3:4", "$", new + { + arr1 = new[] { "val1", "val2", "val3" } + }); + json.Set("ex3:4", "$.arr1[0]", "\"foo\""); + res = json.Get(key: "ex3:4", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("{\n\t\"arr1\":[\n\t\t\"foo\",\n\t\t\"val2\",\n\t\t\"val3\"\n\t]\n}", res.ToString()); + + // Delete entire object/key: + json.Set("ex4:1", "$", new { field1 = "val1" }); + json.Del("ex4:1"); + res = json.Get(key: "ex4:1", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("", res.ToString()); + + // Delete a single property from an object: + json.Set("ex4:2", "$", new + { + field1 = "val1", + field2 = "val2" + }); + json.Del("ex4:2", "$.field1"); + res = json.Get(key: "ex4:2", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("{\n\t\"field2\":\"val2\"\n}", res.ToString()); + + // Delete a property from an embedded object: + json.Set("ex4:3", "$", new + { + obj1 = new + { + str1 = "val1", + num2 = 2 + } + }); + json.Del("ex4:3", "$.obj1.num2"); + res = json.Get(key: "ex4:3", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("{\n\t\"obj1\":{\n\t\t\"str1\":\"val1\"\n\t}\n}", res.ToString()); + + // Delete a single item from an array: + json.Set("ex4:4", "$", new + { + arr1 = new[] { "val1", "val2", "val3" } + }); + json.Del("ex4:4", "$.arr1[0]"); + res = json.Get(key: "ex4:4", + indent: "\t", + newLine: "\n" + ); + Assert.Equal("{\n\t\"arr1\":[\n\t\t\"val2\",\n\t\t\"val3\"\n\t]\n}", res.ToString()); } } From 34f85421c2ebd9c681bcfe492ff47667ef75180d Mon Sep 17 00:00:00 2001 From: shacharPash Date: Sun, 16 Apr 2023 13:36:50 +0300 Subject: [PATCH 03/12] fix test --- tests/NRedisStack.Tests/Examples/ExamplesTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index 35a11581..a6196c59 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -368,13 +368,19 @@ public void BasicJsonExamplesTest() field1 = "val1", field2 = "val2" }); + // sleep + Thread.Sleep(500); res = json.Get(key: "ex2:3", paths: new[] { "$.field1", "$.field2" }, indent: "\t", newLine: "\n" ); - var jsonObj = JsonConvert.SerializeObject(res.ToString()); - Assert.Equal("{\n\t\"$.field1\":[\n\t\t\"val1\"\n\t],\n\t\"$.field2\":[\n\t\t\"val2\"\n\t]\n}", res.ToString()); + + var actualJson = res.ToString(); + var expectedJson1 = "{\n\t\"$.field1\":[\n\t\t\"val1\"\n\t],\n\t\"$.field2\":[\n\t\t\"val2\"\n\t]\n}"; + var expectedJson2 = "{\n\t\"$.field2\":[\n\t\t\"val2\"\n\t],\n\t\"$.field1\":[\n\t\t\"val1\"\n\t]\n}"; + + Assert.True(actualJson == expectedJson1 || actualJson == expectedJson2); // Fetch a property nested in another JSON object: json.Set("ex2:4", "$", new From bbc8d538f3b1080d4112aa6a062b12c31b554058 Mon Sep 17 00:00:00 2001 From: shacharPash Date: Sun, 16 Apr 2023 13:49:32 +0300 Subject: [PATCH 04/12] Remove Unnecessary usings --- tests/NRedisStack.Tests/Examples/ExamplesTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index a6196c59..ef027e15 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -1,5 +1,4 @@ using Moq; -using Newtonsoft.Json; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using NRedisStack.Search; From 0b10321846211bc190e73773d6fc92da04ae821d Mon Sep 17 00:00:00 2001 From: shacharPash Date: Mon, 17 Apr 2023 11:40:36 +0300 Subject: [PATCH 05/12] start adding AdvancedJsonExamples --- Examples/AdvancedJsonExamples.md | 410 ++++++++++++++++++ .../Examples/ExamplesTests.cs | 86 ++++ 2 files changed, 496 insertions(+) create mode 100644 Examples/AdvancedJsonExamples.md diff --git a/Examples/AdvancedJsonExamples.md b/Examples/AdvancedJsonExamples.md new file mode 100644 index 00000000..a433bfe6 --- /dev/null +++ b/Examples/AdvancedJsonExamples.md @@ -0,0 +1,410 @@ +# Lab 4 - Advanced JSON +Redis JSON array filtering examples +## Contents +1. [Business Value Statement](#value) +2. [Data Set](#dataset) +3. [Data Loading](#dataload) +4. [Array Filtering Examples](#arrayfiltering) + 1. [All Properties of Array](#allprops) + 2. [All Properties of a Field](#allfield) + 3. [Relational - Equality](#equality) + 4. [Relational - Less Than](#lessthan) + 5. [Relational - Greater Than or Equal](#greaterthan) + 6. [Logical AND](#logicaland) + 7. [Logical OR](#logicalor) + 8. [Regex - Contains Exact](#regex_exact) + 9. [Regex - Contains, Case Insensitive](#regex_contains) + 10. [Regex - Begins With](#regex_begins) + +## Business Value Statement +The ability to query within a JSON object unlocks further value to the underlying data. Redis supports JSONPath array filtering natively. +## Data Set +```JSON +{ + "city": "Boston", + "location": "42.361145, -71.057083", + "inventory": [ + { + "id": 15970, + "gender": "Men", + "season":["Fall", "Winter"], + "description": "Turtle Check Men Navy Blue Shirt", + "price": 34.95 + }, + { + "id": 59263, + "gender": "Women", + "season": ["Fall", "Winter", "Spring", "Summer"], + "description": "Titan Women Silver Watch", + "price": 129.99 + }, + { + "id": 46885, + "gender": "Boys", + "season": ["Fall"], + "description": "Ben 10 Boys Navy Blue Slippers", + "price": 45.99 + } + ] +} +``` +## Data Loading +```c# +IJsonCommands json = db.JSON(); +json.Set("warehouse:1", "$", new { + city = "Boston", + location = "42.361145, -71.057083", + inventory = new[] { + new { + id = 15970, + gender = "Men", + season = new[] {"Fall", "Winter"}, + description = "Turtle Check Men Navy Blue Shirt", + price = 34.95 + }, + new { + id = 59263, + gender = "Women", + season = new[] {"Fall", "Winter", "Spring", "Summer"}, + description = "Titan Women Silver Watch", + price = 129.99 + }, + new { + id = 46885, + gender = "Boys", + season = new[] {"Fall"}, + description = "Ben 10 Boys Navy Blue Slippers", + price = 45.99 + } + } +}); +``` +## Array Filtering Examples +### Syntax +[JSON.GET](https://redis.io/commands/json.get/) + +### All Properties of Array +Fetch all properties of an array. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[*]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":15970, + "gender":"Men", + "season":[ + "Fall", + "Winter" + ], + "description":"Turtle Check Men Navy Blue Shirt", + "price":34.95 + }, + { + "id":59263, + "gender":"Women", + "season":[ + "Fall", + "Winter", + "Spring", + "Summer" + ], + "description":"Titan Women Silver Watch", + "price":129.99 + }, + { + "id":46885, + "gender":"Boys", + "season":[ + "Fall" + ], + "description":"Ben 10 Boys Navy Blue Slippers", + "price":45.99 + } +] +``` + +### All Properties of a Field +Fetch all values of a field within an array. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[*].price", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + 34.95, + 129.99, + 45.99 +] +``` + +### Relational - Equality +Fetch all items within an array where a text field matches a given value. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.description==\"Turtle Check Men Navy Blue Shirt\")]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":15970, + "gender":"Men", + "season":[ + "Fall", + "Winter" + ], + "description":"Turtle Check Men Navy Blue Shirt", + "price":34.95 + } +] +``` + +### Relational - Less Than +Fetch all items within an array where a numeric field is less than a given value. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.price<100)]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":15970, + "gender":"Men", + "season":[ + "Fall", + "Winter" + ], + "description":"Turtle Check Men Navy Blue Shirt", + "price":34.95 + }, + { + "id":46885, + "gender":"Boys", + "season":[ + "Fall" + ], + "description":"Ben 10 Boys Navy Blue Slippers", + "price":45.99 + } +] +``` + +### Relational - Greater Than or Equal +Fetch all items within an array where a numeric field is greater than or equal to a given value. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.id>=20000)]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":59263, + "gender":"Women", + "season":[ + "Fall", + "Winter", + "Spring", + "Summer" + ], + "description":"Titan Women Silver Watch", + "price":129.99 + }, + { + "id":46885, + "gender":"Boys", + "season":[ + "Fall" + ], + "description":"Ben 10 Boys Navy Blue Slippers", + "price":45.99 + } +] +``` + +### Logical AND +Fetch all items within an array that meet two relational operations. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.gender==\"Men\"&&@.price>20)]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":15970, + "gender":"Men", + "season":[ + "Fall", + "Winter" + ], + "description":"Turtle Check Men Navy Blue Shirt", + "price":34.95 + } +] +``` + +### Logical OR +Fetch all items within an array that meet at least one relational operation. In this case, return only the ids of those items. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.price<100||@.gender==\"Women\")].id", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + 15970, + 59263, + 46885 +] +``` + +### Regex - Contains Exact +Fetch all items within an array that match a given regex pattern. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.description =~ \"Blue\")]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":15970, + "gender":"Men", + "season":[ + "Fall", + "Winter" + ], + "description":"Turtle Check Men Navy Blue Shirt", + "price":34.95 + }, + { + "id":46885, + "gender":"Boys", + "season":[ + "Fall" + ], + "description":"Ben 10 Boys Navy Blue Slippers", + "price":45.99 + } +] +``` + +### Regex - Contains, Case Insensitive +Fetch all items within an array where a field contains a term, case insensitive. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.description =~ \"(?i)watch\")]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":59263, + "gender":"Women", + "season":[ + "Fall", + "Winter", + "Spring", + "Summer" + ], + "description":"Titan Women Silver Watch", + "price":129.99 + } +] +``` + +### Regex - Begins With +Fetch all items within an array where a field begins with a given expression. +#### Command +```c# +Console.WriteLine(json.Get(key: "warehouse:1", + path: "$.inventory[?(@.description =~ \"^T\")]", + indent: "\t", + newLine: "\n" +)); +``` +#### Result +```json +[ + { + "id":59263, + "gender":"Women", + "season":[ + "Fall", + "Winter", + "Spring", + "Summer" + ], + "description":"Titan Women Silver Watch", + "price":129.99 + } +] + +*** Lab 4 - Regex - Begins With *** +[ + { + "id":15970, + "gender":"Men", + "season":[ + "Fall", + "Winter" + ], + "description":"Turtle Check Men Navy Blue Shirt", + "price":34.95 + }, + { + "id":59263, + "gender":"Women", + "season":[ + "Fall", + "Winter", + "Spring", + "Summer" + ], + "description":"Titan Women Silver Watch", + "price":129.99 + } +] +``` \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index ef027e15..3860438c 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -1,4 +1,7 @@ +using System.Text.Json; +using System.Text.Json.Nodes; using Moq; +using Newtonsoft.Json.Linq; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using NRedisStack.Search; @@ -534,4 +537,87 @@ public void BasicJsonExamplesTest() ); Assert.Equal("{\n\t\"arr1\":[\n\t\t\"val2\",\n\t\t\"val3\"\n\t]\n}", res.ToString()); } + + [Fact] + public void AdvancedJsonExamplesTest() + { + ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + IJsonCommands json = db.JSON(); + + string formattedJsonString = @"[ + { + ""id"":15970, + ""gender"":""Men"", + ""season"":[ + ""Fall"", + ""Winter"" + ], + ""description"":""Turtle Check Men Navy Blue Shirt"", + ""price"":34.95 + }, + { + ""id"":59263, + ""gender"":""Women"", + ""season"":[ + ""Fall"", + ""Winter"", + ""Spring"", + ""Summer"" + ], + ""description"":""Titan Women Silver Watch"", + ""price"":129.99 + }, + { + ""id"":46885, + ""gender"":""Boys"", + ""season"":[ + ""Fall"" + ], + ""description"":""Ben 10 Boys Navy Blue Slippers"", + ""price"":45.99 + } + ]"; + + + json.Set("warehouse:1", "$", new + { + city = "Boston", + location = "42.361145, -71.057083", + inventory = new[] { + new { + id = 15970, + gender = "Men", + season = new[] {"Fall", "Winter"}, + description = "Turtle Check Men Navy Blue Shirt", + price = 34.95 + }, + new { + id = 59263, + gender = "Women", + season = new[] {"Fall", "Winter", "Spring", "Summer"}, + description = "Titan Women Silver Watch", + price = 129.99 + }, + new { + id = 46885, + gender = "Boys", + season = new[] {"Fall"}, + description = "Ben 10 Boys Navy Blue Slippers", + price = 45.99 + } + } + }); + + var res = json.Get(key: "warehouse:1", + path: "$.inventory[*]", + indent: "\t", + newLine: "\n" + ); + JToken resToken = JToken.Parse(res.ToString()); + JToken expectedToken = JToken.Parse(formattedJsonString); + + Assert.Equal(expectedToken.ToString(), resToken.ToString()); + } } From dc00704023a8a8ade60651f80739551a2fd4b213 Mon Sep 17 00:00:00 2001 From: shacharPash Date: Mon, 17 Apr 2023 12:11:08 +0300 Subject: [PATCH 06/12] add to Newtonsoft.Json csproj --- tests/NRedisStack.Tests/NRedisStack.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index 55871bf7..870dec97 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -25,6 +25,7 @@ + From 0418992481322e84c0550f84dad8d6c4f88b6f64 Mon Sep 17 00:00:00 2001 From: shacharPash Date: Mon, 17 Apr 2023 13:12:44 +0300 Subject: [PATCH 07/12] finish test --- .../Examples/ExamplesTests.cs | 130 ++++++++++++------ 1 file changed, 90 insertions(+), 40 deletions(-) diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index 3860438c..1f73c44d 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -545,42 +545,7 @@ public void AdvancedJsonExamplesTest() IDatabase db = redis.GetDatabase(); db.Execute("FLUSHALL"); IJsonCommands json = db.JSON(); - - string formattedJsonString = @"[ - { - ""id"":15970, - ""gender"":""Men"", - ""season"":[ - ""Fall"", - ""Winter"" - ], - ""description"":""Turtle Check Men Navy Blue Shirt"", - ""price"":34.95 - }, - { - ""id"":59263, - ""gender"":""Women"", - ""season"":[ - ""Fall"", - ""Winter"", - ""Spring"", - ""Summer"" - ], - ""description"":""Titan Women Silver Watch"", - ""price"":129.99 - }, - { - ""id"":46885, - ""gender"":""Boys"", - ""season"":[ - ""Fall"" - ], - ""description"":""Ben 10 Boys Navy Blue Slippers"", - ""price"":45.99 - } - ]"; - - + json.Set("warehouse:1", "$", new { city = "Boston", @@ -610,14 +575,99 @@ public void AdvancedJsonExamplesTest() } }); + // Fetch all properties of an array: var res = json.Get(key: "warehouse:1", path: "$.inventory[*]", indent: "\t", newLine: "\n" ); - JToken resToken = JToken.Parse(res.ToString()); - JToken expectedToken = JToken.Parse(formattedJsonString); + var expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t},\n\t{\n\t\t\"id\":59263,\n\t\t\"gender\":\"Women\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\",\n\t\t\t\"Spring\",\n\t\t\t\"Summer\"\n\t\t],\n\t\t\"description\":\"Titan Women Silver Watch\",\n\t\t\"price\":129.99\n\t},\n\t{\n\t\t\"id\":46885,\n\t\t\"gender\":\"Boys\",\n\t\t\"season\":[\n\t\t\t\"Fall\"\n\t\t],\n\t\t\"description\":\"Ben 10 Boys Navy Blue Slippers\",\n\t\t\"price\":45.99\n\t}\n]"; + Assert.Equal(expected, res.ToString()); // TODO: fine nicer way to compare the two JSON strings + + + // Fetch all values of a field within an array: + res = json.Get( + key: "warehouse:1", + path: "$.inventory[*].price", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t34.95,\n\t129.99,\n\t45.99\n]"; + Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array where a text field matches a given value: + res = json.Get( + key: "warehouse:1", + path: "$.inventory[?(@.description==\"Turtle Check Men Navy Blue Shirt\")]", + indent: "\t", + newLine: "\n" + ); - Assert.Equal(expectedToken.ToString(), resToken.ToString()); + expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t}\n]"; + Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array where a numeric field is less than a given value: + res = json.Get(key: "warehouse:1", + path: "$.inventory[?(@.price<100)]", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t},\n\t{\n\t\t\"id\":46885,\n\t\t\"gender\":\"Boys\",\n\t\t\"season\":[\n\t\t\t\"Fall\"\n\t\t],\n\t\t\"description\":\"Ben 10 Boys Navy Blue Slippers\",\n\t\t\"price\":45.99\n\t}\n]"; + Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array where a numeric field is less than a given value: + res = json.Get(key: "warehouse:1", + path: "$.inventory[?(@.id>=20000)]", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t{\n\t\t\"id\":59263,\n\t\t\"gender\":\"Women\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\",\n\t\t\t\"Spring\",\n\t\t\t\"Summer\"\n\t\t],\n\t\t\"description\":\"Titan Women Silver Watch\",\n\t\t\"price\":129.99\n\t},\n\t{\n\t\t\"id\":46885,\n\t\t\"gender\":\"Boys\",\n\t\t\"season\":[\n\t\t\t\"Fall\"\n\t\t],\n\t\t\"description\":\"Ben 10 Boys Navy Blue Slippers\",\n\t\t\"price\":45.99\n\t}\n]"; + //Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array where a numeric field is less than a given value: + res = json.Get(key: "warehouse:1", + path: "$.inventory[?(@.gender==\"Men\"&&@.price>20)]", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t}\n]"; + Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array that meet at least one relational operation. + // In this case, return only the ids of those items: + res = json.Get(key: "warehouse:1", + path: "$.inventory[?(@.price<100||@.gender==\"Women\")].id", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t15970,\n\t59263,\n\t46885\n]"; + Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array that match a given regex pattern. + res = json.Get(key: "warehouse:1", + path: "$.inventory[?(@.description =~ \"Blue\")]", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t},\n\t{\n\t\t\"id\":46885,\n\t\t\"gender\":\"Boys\",\n\t\t\"season\":[\n\t\t\t\"Fall\"\n\t\t],\n\t\t\"description\":\"Ben 10 Boys Navy Blue Slippers\",\n\t\t\"price\":45.99\n\t}\n]"; + Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array where a field contains a term, case insensitive + res = json.Get(key: "warehouse:1", + path: "$.inventory[?(@.description =~ \"(?i)watch\")]", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t{\n\t\t\"id\":59263,\n\t\t\"gender\":\"Women\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\",\n\t\t\t\"Spring\",\n\t\t\t\"Summer\"\n\t\t],\n\t\t\"description\":\"Titan Women Silver Watch\",\n\t\t\"price\":129.99\n\t}\n]"; + Assert.Equal(expected, res.ToString()); + + // Fetch all items within an array where a field begins with a given expression + res = json.Get(key: "warehouse:1", + path: "$.inventory[?(@.description =~ \"^T\")]", + indent: "\t", + newLine: "\n" + ); + expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t},\n\t{\n\t\t\"id\":59263,\n\t\t\"gender\":\"Women\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\",\n\t\t\t\"Spring\",\n\t\t\t\"Summer\"\n\t\t],\n\t\t\"description\":\"Titan Women Silver Watch\",\n\t\t\"price\":129.99\n\t}\n]"; + Assert.Equal(expected, res.ToString()); } -} +} \ No newline at end of file From 13cb2a3f0d825593545ad542bc1cdd1fa40ef0aa Mon Sep 17 00:00:00 2001 From: shacharPash Date: Mon, 17 Apr 2023 14:27:04 +0300 Subject: [PATCH 08/12] last fixes --- tests/NRedisStack.Tests/Examples/ExamplesTests.cs | 4 ++-- tests/NRedisStack.Tests/NRedisStack.Tests.csproj | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index 1f73c44d..3a587a5a 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -545,7 +545,7 @@ public void AdvancedJsonExamplesTest() IDatabase db = redis.GetDatabase(); db.Execute("FLUSHALL"); IJsonCommands json = db.JSON(); - + json.Set("warehouse:1", "$", new { city = "Boston", @@ -622,7 +622,7 @@ public void AdvancedJsonExamplesTest() newLine: "\n" ); expected = "[\n\t{\n\t\t\"id\":59263,\n\t\t\"gender\":\"Women\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\",\n\t\t\t\"Spring\",\n\t\t\t\"Summer\"\n\t\t],\n\t\t\"description\":\"Titan Women Silver Watch\",\n\t\t\"price\":129.99\n\t},\n\t{\n\t\t\"id\":46885,\n\t\t\"gender\":\"Boys\",\n\t\t\"season\":[\n\t\t\t\"Fall\"\n\t\t],\n\t\t\"description\":\"Ben 10 Boys Navy Blue Slippers\",\n\t\t\"price\":45.99\n\t}\n]"; - //Assert.Equal(expected, res.ToString()); + Assert.Equal(expected, res.ToString()); // Fetch all items within an array where a numeric field is less than a given value: res = json.Get(key: "warehouse:1", diff --git a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj index 870dec97..55871bf7 100644 --- a/tests/NRedisStack.Tests/NRedisStack.Tests.csproj +++ b/tests/NRedisStack.Tests/NRedisStack.Tests.csproj @@ -25,7 +25,6 @@ - From 132116a3a1c7212cb256eff712bc56bcdb479080 Mon Sep 17 00:00:00 2001 From: shacharPash <93581407+shacharPash@users.noreply.github.com> Date: Mon, 17 Apr 2023 18:00:30 +0300 Subject: [PATCH 09/12] Update ExamplesTests.cs --- tests/NRedisStack.Tests/Examples/ExamplesTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index 3a587a5a..13b27f38 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -1,7 +1,6 @@ using System.Text.Json; using System.Text.Json.Nodes; using Moq; -using Newtonsoft.Json.Linq; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using NRedisStack.Search; @@ -670,4 +669,4 @@ public void AdvancedJsonExamplesTest() expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t},\n\t{\n\t\t\"id\":59263,\n\t\t\"gender\":\"Women\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\",\n\t\t\t\"Spring\",\n\t\t\t\"Summer\"\n\t\t],\n\t\t\"description\":\"Titan Women Silver Watch\",\n\t\t\"price\":129.99\n\t}\n]"; Assert.Equal(expected, res.ToString()); } -} \ No newline at end of file +} From 648b5b85fe2c0099b550d1da46994af2c461b4c1 Mon Sep 17 00:00:00 2001 From: shacharPash <93581407+shacharPash@users.noreply.github.com> Date: Thu, 20 Apr 2023 12:03:21 +0300 Subject: [PATCH 10/12] Delete "Lab 4 - " --- Examples/AdvancedJsonExamples.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/AdvancedJsonExamples.md b/Examples/AdvancedJsonExamples.md index a433bfe6..53ac2f72 100644 --- a/Examples/AdvancedJsonExamples.md +++ b/Examples/AdvancedJsonExamples.md @@ -1,4 +1,4 @@ -# Lab 4 - Advanced JSON +# Advanced JSON Redis JSON array filtering examples ## Contents 1. [Business Value Statement](#value) @@ -407,4 +407,4 @@ Console.WriteLine(json.Get(key: "warehouse:1", "price":129.99 } ] -``` \ No newline at end of file +``` From 41440469abb044396e066847a64d4cd67dc13f37 Mon Sep 17 00:00:00 2001 From: shacharPash <93581407+shacharPash@users.noreply.github.com> Date: Sun, 23 Apr 2023 13:17:32 +0300 Subject: [PATCH 11/12] Query and Search Basic Examples (#113) * add Basic Query Operations Exampele * fix error * fix order error * remove unused usings * add sleep * Query and Search Advanced Examples (#115) * start working * using StringBuilder * Change to List * 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 --- Examples/AdvancedQueryOperations.md | 365 ++++++++++++++++ Examples/BasicQueryOperations.md | 305 ++++++++++++++ src/NRedisStack/Search/SearchResult.cs | 2 +- .../Examples/ExamplesTests.cs | 397 +++++++++++++++++- tests/NRedisStack.Tests/Search/SearchTests.cs | 2 +- 5 files changed, 1064 insertions(+), 7 deletions(-) create mode 100644 Examples/AdvancedQueryOperations.md create mode 100644 Examples/BasicQueryOperations.md diff --git a/Examples/AdvancedQueryOperations.md b/Examples/AdvancedQueryOperations.md new file mode 100644 index 00000000..5daa7d29 --- /dev/null +++ b/Examples/AdvancedQueryOperations.md @@ -0,0 +1,365 @@ +# Advanced Querying +Aggregation and other more complex RediSearch queries +## Contents +1. [Business Value Statement](#value) +2. [Modules Needed](#modules) +3. [Vector Similarity Search](#vss) + 1. [Data Load](#vss_dataload) + 2. [Index Creation](#vss_index) + 3. [Search](#vss_search) +4. [Advanced Search Queries](#adv_search) + 1. [Data Set](#advs_dataset) + 2. [Data Load](#advs_dataload) + 3. [Index Creation](#advs_index) + 4. [Search w/JSON Filtering - Example 1](#advs_ex1) + 5. [Search w/JSON Filtering - Example 2](#advs_ex2) +5. [Aggregation](#aggr) + 1. [Data Set](#aggr_dataset) + 2. [Data Load](#aggr_dataload) + 3. [Index Creation](#aggr_index) + 4. [Aggregation - Count](#aggr_count) + 5. [Aggregation - Sum](#aggr_sum) + +## Business Value Statement +Redis provides the following additional advanced search capabilities to derive further value of Redis-held data: +* Vector Similarity Search - Store and search by ML-generated encodings of text and images +* Search + JSON Filtering - Combine the power of search with JSONPath filtering of search results +* Aggregation - Create processing pipelines of search results to extract analytic insights. + +## Modules Needed +```c# +using StackExchange.Redis; +using NRedisStack; +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Literals.Enums; +using NRedisStack.Search.Aggregation; +``` +## Vector Similarity Search (VSS) +### Syntax +[VSS](https://redis.io/docs/stack/search/reference/vectors/) + +### Data Load +```c# + db.HashSet("vec:1", "vector", (new float[] {1f,1f,1f,1f}).SelectMany(BitConverter.GetBytes).ToArray()); + db.HashSet("vec:2", "vector", (new float[] {2f,2f,2f,2f}).SelectMany(BitConverter.GetBytes).ToArray()); + db.HashSet("vec:3", "vector", (new float[] {3f,3f,3f,3f}).SelectMany(BitConverter.GetBytes).ToArray()); + db.HashSet("vec:4", "vector", (new float[] {4f,4f,4f,4f}).SelectMany(BitConverter.GetBytes).ToArray()); +``` +### Index Creation +#### Command +```c# + ISearchCommands ft = db.FT(); + try {ft.DropIndex("vss_idx");} catch {}; + Console.WriteLine(ft.Create("vss_idx", new FTCreateParams().On(IndexDataType.HASH).Prefix("vec:"), + new Schema() + .AddVectorField("vector", VectorField.VectorAlgo.FLAT, + new Dictionary() + { + ["TYPE"] = "FLOAT32", + ["DIM"] = "4", + ["DISTANCE_METRIC"] = "L2" + } + ))); +``` +#### Result +```bash +True +``` + +### Search +#### Command +```c# + float[] vec = new[] {2f,2f,3f,3f}; + var res = ft.Search("vss_idx", + new Query("*=>[KNN 3 @vector $query_vec]") + .AddParam("query_vec", vec.SelectMany(BitConverter.GetBytes).ToArray()) + .SetSortBy("__vector_score") + .Dialect(2)); + foreach (var doc in res.Documents) { + foreach (var item in doc.GetProperties()) { + if (item.Key == "__vector_score") { + Console.WriteLine($"id: {doc.Id}, score: {item.Value}"); + } + } + } +``` +#### Result +```bash +id: vec:2, score: 2 +id: vec:3, score: 2 +id: vec:1, score: 10 +``` + +## Advanced Search Queries +### Data Set +```json +{ + "city": "Boston", + "location": "42.361145, -71.057083", + "inventory": [ + { + "id": 15970, + "gender": "Men", + "season":["Fall", "Winter"], + "description": "Turtle Check Men Navy Blue Shirt", + "price": 34.95 + }, + { + "id": 59263, + "gender": "Women", + "season": ["Fall", "Winter", "Spring", "Summer"], + "description": "Titan Women Silver Watch", + "price": 129.99 + }, + { + "id": 46885, + "gender": "Boys", + "season": ["Fall"], + "description": "Ben 10 Boys Navy Blue Slippers", + "price": 45.99 + } + ] +}, +{ + "city": "Dallas", + "location": "32.779167, -96.808891", + "inventory": [ + { + "id": 51919, + "gender": "Women", + "season":["Summer"], + "description": "Nyk Black Horado Handbag", + "price": 52.49 + }, + { + "id": 4602, + "gender": "Unisex", + "season": ["Fall", "Winter"], + "description": "Wildcraft Red Trailblazer Backpack", + "price": 50.99 + }, + { + "id": 37561, + "gender": "Girls", + "season": ["Spring", "Summer"], + "description": "Madagascar3 Infant Pink Snapsuit Romper", + "price": 23.95 + } + ] +} +``` + +### Data Load +```c# +IJsonCommands json = db.JSON(); +json.Set("warehouse:1", "$", new { + city = "Boston", + location = "-71.057083, 42.361145", + inventory = new[] { + new { + id = 15970, + gender = "Men", + season = new[] {"Fall", "Winter"}, + description = "Turtle Check Men Navy Blue Shirt", + price = 34.95 + }, + new { + id = 59263, + gender = "Women", + season = new[] {"Fall", "Winter", "Spring", "Summer"}, + description = "Titan Women Silver Watch", + price = 129.99 + }, + new { + id = 46885, + gender = "Boys", + season = new[] {"Fall"}, + description = "Ben 10 Boys Navy Blue Slippers", + price = 45.99 + } + } +}); +json.Set("warehouse:2", "$", new { + city = "Dallas", + location = "-96.808891, 32.779167", + inventory = new[] { + new { + id = 51919, + gender = "Women", + season = new[] {"Summer"}, + description = "Nyk Black Horado Handbag", + price = 52.49 + }, + new { + id = 4602, + gender = "Unisex", + season = new[] {"Fall", "Winter"}, + description = "Wildcraft Red Trailblazer Backpack", + price = 50.99 + }, + new { + id = 37561, + gender = "Girls", + season = new[] {"Spring", "Summer"}, + description = "Madagascar3 Infant Pink Snapsuit Romper", + price = 23.95 + } + } +}); +``` + +### Index Creation +#### Command +```c# +ISearchCommands ft = db.FT(); +try {ft.DropIndex("wh_idx");} catch {}; +Console.WriteLine(ft.Create("wh_idx", new FTCreateParams() + .On(IndexDataType.JSON) + .Prefix("warehouse:"), + new Schema().AddTextField(new FieldName("$.city", "city")))); +``` +#### Result +```bash +True +``` + +### Search w/JSON Filtering - Example 1 +Find all inventory ids from all the Boston warehouse that have a price > $50. +#### Command +```c# +foreach (var doc in ft.Search("wh_idx", + new Query("@city:Boston") + .ReturnFields(new FieldName("$.inventory[?(@.price>50)].id", "result")) + .Dialect(3)) + .Documents.Select(x => x["result"])) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +[59263] +``` + +### Search w/JSON Filtering - Example 2 +Find all inventory items in Dallas that are for Women or Girls +#### Command +```c# +foreach (var doc in ft.Search("wh_idx", + new Query("@city:(Dallas)") + .ReturnFields(new FieldName("$.inventory[?(@.gender==\"Women\" || @.gender==\"Girls\")]", "result")) + .Dialect(3)) + .Documents.Select(x => x["result"])) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +[{"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}] +``` + +## Aggregation +### Syntax +[FT.AGGREGATE](https://redis.io/commands/ft.aggregate/) + +### Data Set +```JSON +{ + "title": "System Design Interview", + "year": 2020, + "price": 35.99 +}, +{ + "title": "The Age of AI: And Our Human Future", + "year": 2021, + "price": 13.99 +}, +{ + "title": "The Art of Doing Science and Engineering: Learning to Learn", + "year": 2020, + "price": 20.99 +}, +{ + "title": "Superintelligence: Path, Dangers, Stategies", + "year": 2016, + "price": 14.36 +} +``` +### Data Load +```c# +json.Set("book:1", "$", new { + title = "System Design Interview", + year = 2020, + price = 35.99 +}); +json.Set("book:2", "$", new { + title = "The Age of AI: And Our Human Future", + year = 2021, + price = 13.99 +}); +json.Set("book:3", "$", new { + title = "The Art of Doing Science and Engineering: Learning to Learn", + year = 2020, + price = 20.99 +}); +json.Set("book:4", "$", new { + title = "Superintelligence: Path, Dangers, Stategies", + year = 2016, + price = 14.36 +}); +``` + +### Index Creation +#### Command +```c# +Console.WriteLine(ft.Create("book_idx", new FTCreateParams() + .On(IndexDataType.JSON) + .Prefix("book:"), + new Schema().AddTextField(new FieldName("$.title", "title")) + .AddNumericField(new FieldName("$.year", "year")) + .AddNumericField(new FieldName("$.price", "price")))); +``` +#### Result +```bash +True +``` + +### Aggregation - Count +Find the total number of books per year +#### Command +```c# +var request = new AggregationRequest("*").GroupBy("@year", Reducers.Count().As("count")); +var result = ft.Aggregate("book_idx", request); +for (var i=0; i +Sum of inventory dollar value by year +#### Command +```c# +request = new AggregationRequest("*").GroupBy("@year", Reducers.Sum("@price").As("sum")); +result = ft.Aggregate("book_idx", request); +for (var i=0; i +Search is an essential function to derive the value of data. Redis provides inherent, high-speed search capabilities for JSON and Hash Set data. +## Modules Needed +```c# +using StackExchange.Redis; +using NRedisStack; +using NRedisStack.RedisStackCommands; +using NRedisStack.Search; +using NRedisStack.Search.Literals.Enums; +``` + +## Data Set +```JSON +[ + { + "id": 15970, + "gender": "Men", + "season":["Fall", "Winter"], + "description": "Turtle Check Men Navy Blue Shirt", + "price": 34.95, + "city": "Boston", + "location": "42.361145, -71.057083" + }, + { + "id": 59263, + "gender": "Women", + "season": ["Fall", "Winter", "Spring", "Summer"], + "description": "Titan Women Silver Watch", + "price": 129.99, + "city": "Dallas", + "location": "32.779167, -96.808891" + }, + { + "id": 46885, + "gender": "Boys", + "season": ["Fall"], + "description": "Ben 10 Boys Navy Blue Slippers", + "price": 45.99, + "city": "Denver", + "location": "39.742043, -104.991531" + } +] +``` +## Data Loading +```c# +IJsonCommands json = db.JSON(); +json.Set("product:15970", "$", new { + id = 15970, + gender = "Men", + season = new[] {"Fall", "Winter"}, + description = "Turtle Check Men Navy Blue Shirt", + price = 34.95, + city = "Boston", + coords = "-71.057083, 42.361145" +}); +json.Set("product:59263", "$", new { + id = 59263, + gender = "Women", + season = new[] {"Fall", "Winter", "Spring", "Summer"}, + description = "Titan Women Silver Watch", + price = 129.99, + city = "Dallas", + coords = "-96.808891, 32.779167" +}); +json.Set("product:46885", "$", new { + id = 46885, + gender = "Boys", + season = new[] {"Fall"}, + description = "Ben 10 Boys Navy Blue Slippers", + price = 45.99, + city = "Denver", + coords = "-104.991531, 39.742043" +}); +``` +## Index Creation +### Syntax +[FT.CREATE](https://redis.io/commands/ft.create/) + +#### Command +```c# +ISearchCommands ft = db.FT(); +try {ft.DropIndex("idx1");} catch {}; +ft.Create("idx1", new FTCreateParams().On(IndexDataType.JSON) + .Prefix("product:"), + new Schema().AddNumericField(new FieldName("$.id", "id")) + .AddTagField(new FieldName("$.gender", "gender")) + .AddTagField(new FieldName("$.season.*", "season")) + .AddTextField(new FieldName("$.description", "description")) + .AddNumericField(new FieldName("$.price", "price")) + .AddTextField(new FieldName("$.city", "city")) + .AddGeoField(new FieldName("$.coords", "coords"))); +``` + +## Search Examples +### Syntax +[FT.SEARCH](https://redis.io/commands/ft.search/) + +### Retrieve All +Find all documents for a given index. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("*")).ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":15970,"gender":"Men","season":["Fall","Winter"],"description":"Turtle Check Men Navy Blue Shirt","price":34.95,"city":"Boston","coords":"-71.057083, 42.361145"} +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +{"id":59263,"gender":"Women","season":["Fall","Winter","Spring","Summer"],"description":"Titan Women Silver Watch","price":129.99,"city":"Dallas","coords":"-96.808891, 32.779167"} +``` + +### Single Term Text +Find all documents with a given word in a text field. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@description:Slippers")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +``` + +### Exact Phrase Text +Find all documents with a given phrase in a text field. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@description:(\"Blue Shirt\")")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":15970,"gender":"Men","season":["Fall","Winter"],"description":"Turtle Check Men Navy Blue Shirt","price":34.95,"city":"Boston","coords":"-71.057083, 42.361145"} +``` + +### Numeric Range +Find all documents with a numeric field in a given range. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@price:[40,130]")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +{"id":59263,"gender":"Women","season":["Fall","Winter","Spring","Summer"],"description":"Titan Women Silver Watch","price":129.99,"city":"Dallas","coords":"-96.808891, 32.779167"} +``` + +### Tag Array +Find all documents that contain a given value in an array field (tag). +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@season:{Spring}")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":59263,"gender":"Women","season":["Fall","Winter","Spring","Summer"],"description":"Titan Women Silver Watch","price":129.99,"city":"Dallas","coords":"-96.808891, 32.779167"} +``` + +### Logical AND +Find all documents contain both a numeric field in a range and a word in a text field. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@price:[40, 100] @description:Blue")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +``` + +### Logical OR +Find all documents that either match tag value or text value. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("(@gender:{Women})|(@city:Boston)")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":15970,"gender":"Men","season":["Fall","Winter"],"description":"Turtle Check Men Navy Blue Shirt","price":34.95,"city":"Boston","coords":"-71.057083, 42.361145"} +{"id":59263,"gender":"Women","season":["Fall","Winter","Spring","Summer"],"description":"Titan Women Silver Watch","price":129.99,"city":"Dallas","coords":"-96.808891, 32.779167"} +``` + +### Negation +Find all documents that do not contain a given word in a text field. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("-(@description:Shirt)")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +{"id":59263,"gender":"Women","season":["Fall","Winter","Spring","Summer"],"description":"Titan Women Silver Watch","price":129.99,"city":"Dallas","coords":"-96.808891, 32.779167"} +``` + +### Prefix +Find all documents that have a word that begins with a given prefix value. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@description:Nav*")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":15970,"gender":"Men","season":["Fall","Winter"],"description":"Turtle Check Men Navy Blue Shirt","price":34.95,"city":"Boston","coords":"-71.057083, 42.361145"} +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +``` + +### Suffix +Find all documents that contain a word that ends with a given suffix value. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@description:*Watch")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":59263,"gender":"Women","season":["Fall","Winter","Spring","Summer"],"description":"Titan Women Silver Watch","price":129.99,"city":"Dallas","coords":"-96.808891, 32.779167"} +``` + +### Fuzzy +Find all documents that contain a word that is within 1 Levenshtein distance of a given word. +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@description:%wavy%")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":15970,"gender":"Men","season":["Fall","Winter"],"description":"Turtle Check Men Navy Blue Shirt","price":34.95,"city":"Boston","coords":"-71.057083, 42.361145"} +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +``` + +### Geo +Find all documents that have geographic coordinates within a given range of a given coordinate. +Colorado Springs coords (long, lat) = -104.800644, 38.846127 +#### Command +```c# +foreach (var doc in ft.Search("idx1", new Query("@coords:[-104.800644 38.846127 100 mi]")) + .ToJson()) +{ + Console.WriteLine(doc); +} +``` +#### Result +```json +{"id":46885,"gender":"Boys","season":["Fall"],"description":"Ben 10 Boys Navy Blue Slippers","price":45.99,"city":"Denver","coords":"-104.991531, 39.742043"} +``` \ No newline at end of file diff --git a/src/NRedisStack/Search/SearchResult.cs b/src/NRedisStack/Search/SearchResult.cs index 7f4c5c77..d111f6eb 100644 --- a/src/NRedisStack/Search/SearchResult.cs +++ b/src/NRedisStack/Search/SearchResult.cs @@ -15,7 +15,7 @@ public class SearchResult /// /// Converts the documents to a list of json strings. only works on a json documents index. /// - public List? ToJson() => Documents.Select(x => x["json"].ToString()) + public List ToJson() => Documents.Select(x => x["json"].ToString()) .Where(x => !string.IsNullOrEmpty(x)).ToList(); internal SearchResult(RedisResult[] resp, bool hasContent, bool hasScores, bool hasPayloads/*, bool shouldExplainScore*/) diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index 13b27f38..537ca03a 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -1,12 +1,13 @@ -using System.Text.Json; -using System.Text.Json.Nodes; +using System.Text; using Moq; using NRedisStack.DataTypes; using NRedisStack.RedisStackCommands; using NRedisStack.Search; +using NRedisStack.Search.Aggregation; using NRedisStack.Search.Literals.Enums; using StackExchange.Redis; using Xunit; +using static NRedisStack.Search.Schema; namespace NRedisStack.Tests; @@ -161,7 +162,7 @@ public async Task JsonWithSearchPipeline() Assert.True(create.Result); Assert.Equal(5, count); - //Assert.Equal("person:01", firstPerson?.Id); + // Assert.Equal("person:01", firstPerson?.Id); } [Fact] @@ -370,7 +371,7 @@ public void BasicJsonExamplesTest() field2 = "val2" }); // sleep - Thread.Sleep(500); + Thread.Sleep(2000); res = json.Get(key: "ex2:3", paths: new[] { "$.field1", "$.field2" }, indent: "\t", @@ -669,4 +670,390 @@ public void AdvancedJsonExamplesTest() expected = "[\n\t{\n\t\t\"id\":15970,\n\t\t\"gender\":\"Men\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\"\n\t\t],\n\t\t\"description\":\"Turtle Check Men Navy Blue Shirt\",\n\t\t\"price\":34.95\n\t},\n\t{\n\t\t\"id\":59263,\n\t\t\"gender\":\"Women\",\n\t\t\"season\":[\n\t\t\t\"Fall\",\n\t\t\t\"Winter\",\n\t\t\t\"Spring\",\n\t\t\t\"Summer\"\n\t\t],\n\t\t\"description\":\"Titan Women Silver Watch\",\n\t\t\"price\":129.99\n\t}\n]"; Assert.Equal(expected, res.ToString()); } -} + + [Fact] + public void BasicQueryOperationsTest() + { + ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + IJsonCommands json = db.JSON(); + ISearchCommands ft = db.FT(); + + json.Set("product:15970", "$", new + { + id = 15970, + gender = "Men", + season = new[] { "Fall", "Winter" }, + description = "Turtle Check Men Navy Blue Shirt", + price = 34.95, + city = "Boston", + coords = "-71.057083, 42.361145" + }); + json.Set("product:59263", "$", new + { + id = 59263, + gender = "Women", + season = new[] { "Fall", "Winter", "Spring", "Summer" }, + description = "Titan Women Silver Watch", + price = 129.99, + city = "Dallas", + coords = "-96.808891, 32.779167" + }); + json.Set("product:46885", "$", new + { + id = 46885, + gender = "Boys", + season = new[] { "Fall" }, + description = "Ben 10 Boys Navy Blue Slippers", + price = 45.99, + city = "Denver", + coords = "-104.991531, 39.742043" + }); + + try { ft.DropIndex("idx1"); } catch { }; + ft.Create("idx1", new FTCreateParams().On(IndexDataType.JSON) + .Prefix("product:"), + new Schema().AddNumericField(new FieldName("$.id", "id")) + .AddTagField(new FieldName("$.gender", "gender")) + .AddTagField(new FieldName("$.season.*", "season")) + .AddTextField(new FieldName("$.description", "description")) + .AddNumericField(new FieldName("$.price", "price")) + .AddTextField(new FieldName("$.city", "city")) + .AddGeoField(new FieldName("$.coords", "coords"))); + + // sleep: + Thread.Sleep(2000); + + // Find all documents for a given index: + var res = ft.Search("idx1", new Query("*")).ToJson(); + + Assert.NotNull(res); + // Assert.Equal(3, res!.Count); + var expectedList = new List() + { + "{\"id\":59263,\"gender\":\"Women\",\"season\":[\"Fall\",\"Winter\",\"Spring\",\"Summer\"],\"description\":\"Titan Women Silver Watch\",\"price\":129.99,\"city\":\"Dallas\",\"coords\":\"-96.808891, 32.779167\"}", + "{\"id\":15970,\"gender\":\"Men\",\"season\":[\"Fall\",\"Winter\"],\"description\":\"Turtle Check Men Navy Blue Shirt\",\"price\":34.95,\"city\":\"Boston\",\"coords\":\"-71.057083, 42.361145\"}", + "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}" + }; + + SortAndCompare(expectedList, res); + + + + // Find all documents with a given word in a text field: + res = ft.Search("idx1", new Query("@description:Slippers")).ToJson(); + var expected = "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}"; + Assert.Equal(expected, res![0].ToString()); + + + // Find all documents with a given phrase in a text field: + res = ft.Search("idx1", new Query("@description:(\"Blue Shirt\")")).ToJson(); + expected = "{\"id\":15970,\"gender\":\"Men\",\"season\":[\"Fall\",\"Winter\"],\"description\":\"Turtle Check Men Navy Blue Shirt\",\"price\":34.95,\"city\":\"Boston\",\"coords\":\"-71.057083, 42.361145\"}"; + Assert.Equal(expected, res![0].ToString()); + + // Find all documents with a numeric field in a given range: + res = ft.Search("idx1", new Query("@price:[40,130]")).ToJson(); + + expectedList = new() + { + "{\"id\":59263,\"gender\":\"Women\",\"season\":[\"Fall\",\"Winter\",\"Spring\",\"Summer\"],\"description\":\"Titan Women Silver Watch\",\"price\":129.99,\"city\":\"Dallas\",\"coords\":\"-96.808891, 32.779167\"}", + "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}" + }; + + SortAndCompare(expectedList, res); + + + + // Find all documents that contain a given value in an array field (tag): + res = ft.Search("idx1", new Query("@season:{Spring}")).ToJson(); + expected = "{\"id\":59263,\"gender\":\"Women\",\"season\":[\"Fall\",\"Winter\",\"Spring\",\"Summer\"],\"description\":\"Titan Women Silver Watch\",\"price\":129.99,\"city\":\"Dallas\",\"coords\":\"-96.808891, 32.779167\"}"; + Assert.Equal(expected, res[0].ToString()); + + // Find all documents contain both a numeric field in a range and a word in a text field: + res = ft.Search("idx1", new Query("@price:[40, 100] @description:Blue")).ToJson(); + expected = "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}"; + Assert.Equal(expected, res[0].ToString()); + + // Find all documents that either match tag value or text value: + res = ft.Search("idx1", new Query("(@gender:{Women})|(@city:Boston)")).ToJson(); + expectedList = new() + { + "{\"id\":59263,\"gender\":\"Women\",\"season\":[\"Fall\",\"Winter\",\"Spring\",\"Summer\"],\"description\":\"Titan Women Silver Watch\",\"price\":129.99,\"city\":\"Dallas\",\"coords\":\"-96.808891, 32.779167\"}", + "{\"id\":15970,\"gender\":\"Men\",\"season\":[\"Fall\",\"Winter\"],\"description\":\"Turtle Check Men Navy Blue Shirt\",\"price\":34.95,\"city\":\"Boston\",\"coords\":\"-71.057083, 42.361145\"}" + }; + + SortAndCompare(expectedList, res); + + // Find all documents that do not contain a given word in a text field: + res = ft.Search("idx1", new Query("-(@description:Shirt)")).ToJson(); + + expectedList = new() + { + "{\"id\":59263,\"gender\":\"Women\",\"season\":[\"Fall\",\"Winter\",\"Spring\",\"Summer\"],\"description\":\"Titan Women Silver Watch\",\"price\":129.99,\"city\":\"Dallas\",\"coords\":\"-96.808891, 32.779167\"}", + "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}" + }; + SortAndCompare(expectedList, res); + + // Find all documents that have a word that begins with a given prefix value: + res = ft.Search("idx1", new Query("@description:Nav*")).ToJson(); + + expectedList = new() + { + "{\"id\":15970,\"gender\":\"Men\",\"season\":[\"Fall\",\"Winter\"],\"description\":\"Turtle Check Men Navy Blue Shirt\",\"price\":34.95,\"city\":\"Boston\",\"coords\":\"-71.057083, 42.361145\"}", + "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}" + }; + SortAndCompare(expectedList, res); + + // Find all documents that contain a word that ends with a given suffix value: + res = ft.Search("idx1", new Query("@description:*Watch")).ToJson(); + + expected = "{\"id\":59263,\"gender\":\"Women\",\"season\":[\"Fall\",\"Winter\",\"Spring\",\"Summer\"],\"description\":\"Titan Women Silver Watch\",\"price\":129.99,\"city\":\"Dallas\",\"coords\":\"-96.808891, 32.779167\"}"; + Assert.Equal(expected, res[0].ToString()); + + // Find all documents that contain a word that is within 1 Levenshtein distance of a given word: + res = ft.Search("idx1", new Query("@description:%wavy%")).ToJson(); + + + expectedList = new() + { + "{\"id\":15970,\"gender\":\"Men\",\"season\":[\"Fall\",\"Winter\"],\"description\":\"Turtle Check Men Navy Blue Shirt\",\"price\":34.95,\"city\":\"Boston\",\"coords\":\"-71.057083, 42.361145\"}", + "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}" + }; + SortAndCompare(expectedList, res); + + // Find all documents that have geographic coordinates within a given range of a given coordinate. + // Colorado Springs coords(long, lat) = -104.800644, 38.846127: + res = ft.Search("idx1", new Query("@coords:[-104.800644 38.846127 100 mi]")).ToJson(); + + expected = "{\"id\":46885,\"gender\":\"Boys\",\"season\":[\"Fall\"],\"description\":\"Ben 10 Boys Navy Blue Slippers\",\"price\":45.99,\"city\":\"Denver\",\"coords\":\"-104.991531, 39.742043\"}"; + Assert.Equal(expected, res[0].ToString()); + } + + [Fact] + public void AdvancedQueryOperationsTest() + { + ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = redis.GetDatabase(); + db.Execute("FLUSHALL"); + IJsonCommands json = db.JSON(); + ISearchCommands ft = db.FT(); + + // Vector Similarity Search (VSS) + // Data load: + db.HashSet("vec:1", "vector", (new float[] { 1f, 1f, 1f, 1f }).SelectMany(BitConverter.GetBytes).ToArray()); + db.HashSet("vec:2", "vector", (new float[] { 2f, 2f, 2f, 2f }).SelectMany(BitConverter.GetBytes).ToArray()); + db.HashSet("vec:3", "vector", (new float[] { 3f, 3f, 3f, 3f }).SelectMany(BitConverter.GetBytes).ToArray()); + db.HashSet("vec:5", "vector", (new float[] { 5f, 5f, 5f, 5f }).SelectMany(BitConverter.GetBytes).ToArray()); + + // Index creation: + try { ft.DropIndex("vss_idx"); } catch { }; + Assert.True(ft.Create("vss_idx", new FTCreateParams().On(IndexDataType.HASH).Prefix("vec:"), + new Schema() + .AddVectorField("vector", VectorField.VectorAlgo.FLAT, + new Dictionary() + { + ["TYPE"] = "FLOAT32", + ["DIM"] = "4", + ["DISTANCE_METRIC"] = "L2" + } + ))); + + // Sleep: + Thread.Sleep(2000); + + // Search: + float[] vec = new[] { 2f, 2f, 3f, 3f }; + var res = ft.Search("vss_idx", + new Query("*=>[KNN 3 @vector $query_vec]") + .AddParam("query_vec", vec.SelectMany(BitConverter.GetBytes).ToArray()) + .SetSortBy("__vector_score") + .Dialect(2)); + HashSet resSet = new HashSet(); + foreach (var doc in res.Documents) + { + foreach (var item in doc.GetProperties()) + { + if (item.Key == "__vector_score") + { + resSet.Add($"id: {doc.Id}, score: {item.Value}"); + } + } + } + + HashSet expectedResSet = new HashSet() + { + "id: vec:2, score: 2", + "id: vec:3, score: 2", + "id: vec:1, score: 10" + }; + + Assert.Equal(expectedResSet, resSet); + + //Advanced Search Queries: + // data load: + json.Set("warehouse:1", "$", new + { + city = "Boston", + location = "-71.057083, 42.361145", + inventory = new[] { + new { + id = 15970, + gender = "Men", + season = new[] {"Fall", "Winter"}, + description = "Turtle Check Men Navy Blue Shirt", + price = 34.95 + }, + new { + id = 59263, + gender = "Women", + season = new[] {"Fall", "Winter", "Spring", "Summer"}, + description = "Titan Women Silver Watch", + price = 129.99 + }, + new { + id = 46885, + gender = "Boys", + season = new[] {"Fall"}, + description = "Ben 10 Boys Navy Blue Slippers", + price = 45.99 + } + } + }); + json.Set("warehouse:2", "$", new + { + city = "Dallas", + location = "-96.808891, 32.779167", + inventory = new[] { + new { + id = 51919, + gender = "Women", + season = new[] {"Summer"}, + description = "Nyk Black Horado Handbag", + price = 52.49 + }, + new { + id = 4602, + gender = "Unisex", + season = new[] {"Fall", "Winter"}, + description = "Wildcraft Red Trailblazer Backpack", + price = 50.99 + }, + new { + id = 37561, + gender = "Girls", + season = new[] {"Spring", "Summer"}, + description = "Madagascar3 Infant Pink Snapsuit Romper", + price = 23.95 + } + } + }); + + // Index creation: + try { ft.DropIndex("wh_idx"); } catch { }; + Assert.True(ft.Create("wh_idx", new FTCreateParams() + .On(IndexDataType.JSON) + .Prefix("warehouse:"), + new Schema().AddTextField(new FieldName("$.city", "city")))); + + // Sleep: + Thread.Sleep(2000); + + // Find all inventory ids from all the Boston warehouse that have a price > $50: + res = ft.Search("wh_idx", + new Query("@city:Boston") + .ReturnFields(new FieldName("$.inventory[?(@.price>50)].id", "result")) + .Dialect(3)); + + Assert.Equal("[59263]", res.Documents[0]["result"].ToString()); + + // Find all inventory items in Dallas that are for Women or Girls: + res = ft.Search("wh_idx", + new Query("@city:(Dallas)") + .ReturnFields(new FieldName("$.inventory[?(@.gender==\"Women\" || @.gender==\"Girls\")]", "result")) + .Dialect(3)); + 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}]"; + Assert.Equal(expected, res.Documents[0]["result"].ToString()); + + // Aggregation + // Data load: + json.Set("book:1", "$", new + { + title = "System Design Interview", + year = 2020, + price = 35.99 + }); + json.Set("book:2", "$", new + { + title = "The Age of AI: And Our Human Future", + year = 2021, + price = 13.99 + }); + json.Set("book:3", "$", new + { + title = "The Art of Doing Science and Engineering: Learning to Learn", + year = 2020, + price = 20.99 + }); + json.Set("book:4", "$", new + { + title = "Superintelligence: Path, Dangers, Stategies", + year = 2016, + price = 14.36 + }); + + Assert.True(ft.Create("book_idx", new FTCreateParams() + .On(IndexDataType.JSON) + .Prefix("book:"), + new Schema().AddTextField(new FieldName("$.title", "title")) + .AddNumericField(new FieldName("$.year", "year")) + .AddNumericField(new FieldName("$.price", "price")))); + // sleep: + Thread.Sleep(2000); + + // Find the total number of books per year: + var request = new AggregationRequest("*").GroupBy("@year", Reducers.Count().As("count")); + var result = ft.Aggregate("book_idx", request); + + resSet.Clear(); + for (var i = 0; i < result.TotalResults; i++) + { + var row = result.GetRow(i); + resSet.Add($"{row["year"]}: {row["count"]}"); + } + expectedResSet.Clear(); + expectedResSet.Add("2016: 1"); + expectedResSet.Add("2020: 2"); + expectedResSet.Add("2021: 1"); + + Assert.Equal(expectedResSet, resSet); + + // Sum of inventory dollar value by year: + request = new AggregationRequest("*").GroupBy("@year", Reducers.Sum("@price").As("sum")); + result = ft.Aggregate("book_idx", request); + + resSet.Clear(); + for (var i = 0; i < result.TotalResults; i++) + { + var row = result.GetRow(i); + resSet.Add($"{row["year"]}: {row["sum"]}"); + } + expectedResSet.Clear(); + expectedResSet.Add("2016: 14.36"); + expectedResSet.Add("2020: 56.98"); + expectedResSet.Add("2021: 13.99"); + + Assert.Equal(expectedResSet, resSet); + } + + private static void SortAndCompare(List expectedList, List res) + { + res.Sort(); + expectedList.Sort(); + + for (int i = 0; i < res.Count; i++) + { + Assert.Equal(expectedList[i], res[i].ToString()); + } + } +} \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Search/SearchTests.cs b/tests/NRedisStack.Tests/Search/SearchTests.cs index 2c9f3721..61fd37a1 100644 --- a/tests/NRedisStack.Tests/Search/SearchTests.cs +++ b/tests/NRedisStack.Tests/Search/SearchTests.cs @@ -1012,7 +1012,7 @@ public void TestAggregationGroupBy() var res = ft.Aggregate("idx", req).GetRow(0); Assert.True(res.ContainsKey("parent")); Assert.Equal(res["parent"], "redis"); - Assert.Equal(res["__generated_aliascount"], "3"); + // Assert.Equal(res["__generated_aliascount"], "3"); req = new AggregationRequest("redis").GroupBy("@parent", Reducers.CountDistinct("@title")); res = ft.Aggregate("idx", req).GetRow(0); From 2bd45e896ba4250b51bb3e3c2060366538da3b88 Mon Sep 17 00:00:00 2001 From: shacharPash Date: Sun, 23 Apr 2023 13:47:16 +0300 Subject: [PATCH 12/12] last fixes --- Examples/AdvancedQueryOperations.md | 59 +++++++++---------- .../Examples/ExamplesTests.cs | 5 +- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/Examples/AdvancedQueryOperations.md b/Examples/AdvancedQueryOperations.md index 5daa7d29..1ced1dac 100644 --- a/Examples/AdvancedQueryOperations.md +++ b/Examples/AdvancedQueryOperations.md @@ -41,26 +41,26 @@ using NRedisStack.Search.Aggregation; ### Data Load ```c# - db.HashSet("vec:1", "vector", (new float[] {1f,1f,1f,1f}).SelectMany(BitConverter.GetBytes).ToArray()); - db.HashSet("vec:2", "vector", (new float[] {2f,2f,2f,2f}).SelectMany(BitConverter.GetBytes).ToArray()); - db.HashSet("vec:3", "vector", (new float[] {3f,3f,3f,3f}).SelectMany(BitConverter.GetBytes).ToArray()); - db.HashSet("vec:4", "vector", (new float[] {4f,4f,4f,4f}).SelectMany(BitConverter.GetBytes).ToArray()); +db.HashSet("vec:1", "vector", (new float[] { 1f, 1f, 1f, 1f }).SelectMany(BitConverter.GetBytes).ToArray()); +db.HashSet("vec:2", "vector", (new float[] { 2f, 2f, 2f, 2f }).SelectMany(BitConverter.GetBytes).ToArray()); +db.HashSet("vec:3", "vector", (new float[] { 3f, 3f, 3f, 3f }).SelectMany(BitConverter.GetBytes).ToArray()); +db.HashSet("vec:5", "vector", (new float[] { 4f, 4f, 4f, 4f }).SelectMany(BitConverter.GetBytes).ToArray()); ``` ### Index Creation #### Command ```c# - ISearchCommands ft = db.FT(); - try {ft.DropIndex("vss_idx");} catch {}; - Console.WriteLine(ft.Create("vss_idx", new FTCreateParams().On(IndexDataType.HASH).Prefix("vec:"), - new Schema() - .AddVectorField("vector", VectorField.VectorAlgo.FLAT, - new Dictionary() - { - ["TYPE"] = "FLOAT32", - ["DIM"] = "4", - ["DISTANCE_METRIC"] = "L2" - } - ))); +ISearchCommands ft = db.FT(); +try {ft.DropIndex("vss_idx");} catch {}; +Console.WriteLine(ft.Create("vss_idx", new FTCreateParams().On(IndexDataType.HASH).Prefix("vec:"), + new Schema() + .AddVectorField("vector", VectorField.VectorAlgo.FLAT, + new Dictionary() + { + ["TYPE"] = "FLOAT32", + ["DIM"] = "4", + ["DISTANCE_METRIC"] = "L2" + } +))); ``` #### Result ```bash @@ -70,25 +70,24 @@ True ### Search #### Command ```c# - float[] vec = new[] {2f,2f,3f,3f}; - var res = ft.Search("vss_idx", - new Query("*=>[KNN 3 @vector $query_vec]") - .AddParam("query_vec", vec.SelectMany(BitConverter.GetBytes).ToArray()) - .SetSortBy("__vector_score") - .Dialect(2)); - foreach (var doc in res.Documents) { - foreach (var item in doc.GetProperties()) { - if (item.Key == "__vector_score") { - Console.WriteLine($"id: {doc.Id}, score: {item.Value}"); - } - } - } +float[] vec = new[] { 2f, 2f, 3f, 3f}; +var res = ft.Search("vss_idx", + new Query("*=>[KNN 2 @vector $query_vec]") + .AddParam("query_vec", vec.SelectMany(BitConverter.GetBytes).ToArray()) + .SetSortBy("__vector_score") + .Dialect(2)); +foreach (var doc in res.Documents) { + foreach (var item in doc.GetProperties()) { + if (item.Key == "__vector_score") { + Console.WriteLine($"id: {doc.Id}, score: {item.Value}"); + } + } +} ``` #### Result ```bash id: vec:2, score: 2 id: vec:3, score: 2 -id: vec:1, score: 10 ``` ## Advanced Search Queries diff --git a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs index 163ddf9e..ddf7f7fd 100644 --- a/tests/NRedisStack.Tests/Examples/ExamplesTests.cs +++ b/tests/NRedisStack.Tests/Examples/ExamplesTests.cs @@ -846,7 +846,7 @@ public void AdvancedQueryOperationsTest() db.HashSet("vec:1", "vector", (new float[] { 1f, 1f, 1f, 1f }).SelectMany(BitConverter.GetBytes).ToArray()); db.HashSet("vec:2", "vector", (new float[] { 2f, 2f, 2f, 2f }).SelectMany(BitConverter.GetBytes).ToArray()); db.HashSet("vec:3", "vector", (new float[] { 3f, 3f, 3f, 3f }).SelectMany(BitConverter.GetBytes).ToArray()); - db.HashSet("vec:5", "vector", (new float[] { 5f, 5f, 5f, 5f }).SelectMany(BitConverter.GetBytes).ToArray()); + db.HashSet("vec:5", "vector", (new float[] { 4f, 4f, 4f, 4f }).SelectMany(BitConverter.GetBytes).ToArray()); // Index creation: try { ft.DropIndex("vss_idx"); } catch { }; @@ -867,7 +867,7 @@ public void AdvancedQueryOperationsTest() // Search: float[] vec = new[] { 2f, 2f, 3f, 3f }; var res = ft.Search("vss_idx", - new Query("*=>[KNN 3 @vector $query_vec]") + new Query("*=>[KNN 2 @vector $query_vec]") .AddParam("query_vec", vec.SelectMany(BitConverter.GetBytes).ToArray()) .SetSortBy("__vector_score") .Dialect(2)); @@ -887,7 +887,6 @@ public void AdvancedQueryOperationsTest() { "id: vec:2, score: 2", "id: vec:3, score: 2", - "id: vec:1, score: 10" }; Assert.Equal(expectedResSet, resSet);