Skip to content

Commit 74d81f8

Browse files
committed
2 parents 4f7d02d + 1304b3c commit 74d81f8

File tree

21 files changed

+2231
-103
lines changed

21 files changed

+2231
-103
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
id: add-and-retrieve-objects
3+
title: Add and Retrieve Objects
4+
sidebar_label: Add and Retrieve Objects
5+
slug: /develop/dotnet/redis-om-dotnet/add-and-retrieve-objects
6+
---
7+
8+
The Redis OM library supports declarative storage and retrieval of objects from Redis. Without the RediSearch and RedisJson modules, this is limited to using hashes, and id lookups of objects in Redis. You will still use the `Document` Attribute to decorate a class you'd like to store in Redis. From there, all you need to do is either call `Insert` or `InsertAsync` on the `RedisCollection` or `Set` or `SetAsync` on the RedisConnection, passing in the object you want to set in Redis. You can then retrieve those objects with `Get<T>` or `GetAsync<T>` with the `RedisConnection` or with `FindById` or `FindByIdAsync` in the RedisCollection.
9+
10+
11+
```csharp
12+
public class Program
13+
{
14+
[Document(Prefixes = new []{"Employee"})]
15+
public class Employee
16+
{
17+
[RedisIdField]
18+
public string Id{ get; set; }
19+
20+
public string Name { get; set; }
21+
22+
public int Age { get; set; }
23+
24+
public double Sales { get; set; }
25+
26+
public string Department { get; set; }
27+
}
28+
29+
static async Task Main(string[] args)
30+
{
31+
var provider = new RedisConnectionProvider("redis://localhost:6379");
32+
var connection = provider.Connection;
33+
var employees = provider.RedisCollection<Employee>();
34+
var employee1 = new Employee{Name="Bob", Age=32, Sales = 100000, Department="Partner Sales"};
35+
var employee2 = new Employee{Name="Alice", Age=45, Sales = 200000, Department="EMEA Sales"};
36+
var idp1 = await connection.SetAsync(employee1);
37+
var idp2 = await employees.InsertAsync(employee2);
38+
39+
var reconstitutedE1 = await connection.GetAsync<Employee>(idp1);
40+
var reconstitutedE2 = await employees.FindByIdAsync(idp2);
41+
Console.WriteLine($"First Employee's name is {reconstitutedE1.Name}, they are {reconstitutedE1.Age} years old, " +
42+
$"they work in the {reconstitutedE1.Department} department and have sold {reconstitutedE1.Sales}, " +
43+
$"their ID is: {reconstitutedE1.Id}");
44+
Console.WriteLine($"Second Employee's name is {reconstitutedE2.Name}, they are {reconstitutedE2.Age} years old, " +
45+
$"they work in the {reconstitutedE2.Department} department and have sold {reconstitutedE2.Sales}, " +
46+
$"their ID is: {reconstitutedE2.Id}");
47+
}
48+
}
49+
```
50+
51+
The Code above will declare an `Employee` class, and allow you to add employees to Redis, and then retrieve Employees from Redis the output from this method will look like this:
52+
53+
54+
```text
55+
First Employee's name is Bob, they are 32 years old, they work in the Partner Sales department and have sold 100000, their ID is: 01FHDFE115DKRWZW0XNF17V2RK
56+
Second Employee's name is Alice, they are 45 years old, they work in the EMEA Sales department and have sold 200000, their ID is: 01FHDFE11T23K6FCJQNHVEF92F
57+
```
58+
59+
If you wanted to find them in Redis directly you could run `HGETALL Employee:01FHDFE115DKRWZW0XNF17V2RK` and that will retrieve the Employee object as a Hash from Redis. If you do not specify a prefix, the prefix will be the fully-qualified class name.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
id: apply-functions
3+
title: Apply Functions
4+
sidebar_label: Apply Functions
5+
slug: /develop/dotnet/redis-om-dotnet/aggregations/apply-functions
6+
---
7+
8+
Apply functions are functions that you can define as expressions to apply to your data in Redis. In essence, they allow you to combine your data together, and extract the information you want.
9+
10+
## Data Model
11+
12+
For the remainder of this article we will be using this data model:
13+
14+
```csharp
15+
[Document]
16+
public class Employee
17+
{
18+
[Indexed(Aggregatable = true)]
19+
public string Name { get; set; }
20+
21+
[Indexed]
22+
public GeoLoc? HomeLoc { get; set; }
23+
24+
[Indexed(Aggregatable = true)]
25+
public int Age { get; set; }
26+
27+
[Indexed(Aggregatable = true)]
28+
public double Sales { get; set; }
29+
30+
[Indexed(Aggregatable = true)]
31+
public double SalesAdjustment { get; set; }
32+
33+
[Searchable(Aggregatable = true)]
34+
public string Department { get; set; }
35+
36+
[Indexed(Aggregatable = true)]
37+
public long LastOnline { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
38+
}
39+
```
40+
41+
## Anatomy of an Apply Function
42+
43+
`Apply` is a method on the `RedisAggregationSet<T>` class which takes two arguments, each of which is a component of the apply function.
44+
45+
First it takes the expression that you want Redis to execute on every record in the pipeline, this expression takes a single parameter, an `AggregationResult<T>`, where `T` is the generic type of your `RedisAggregationSet`. This AggregationResult has two things we should think about, first it contains a `RecordShell` which is a placeholder for the generic type, and secondly it has an `Aggregations` property - which is a dictionary containing the results from your pipeline. Both of these can be used in apply functions.
46+
47+
The second component is the alias, that's the name the result of the function is stored in when the pipeline executes.
48+
49+
### Adjusted Sales
50+
51+
Our data model has two properties related to sales, `Sales`, how much the employee has sold, and `SalesAdjustment`, a figure used to adjust sales based off various factors, perhaps territory covered, experience, etc. . . The idea being that perhaps a fair way to analyze an employee's performance is a combination of these two fields rather than each individually. So let's say we wanted to find what everyone's adjusted sales were, we could do that by creating an apply function to calculate it.
52+
53+
```csharp
54+
var adjustedSales = employeeAggregations.Apply(x => x.RecordShell.SalesAdjustment * x.RecordShell.Sales,
55+
"ADJUSTED_SALES");
56+
foreach (var result in adjustedSales)
57+
{
58+
Console.WriteLine($"Adjusted Sales were: {result["ADJUSTED_SALES"]}");
59+
}
60+
```
61+
62+
## Arithmetic Apply Functions
63+
64+
Functions that use arithmetic and math can use the mathematical operators `+` for addition, `-` for subtraction, `*` for multiplication, `/` for division, and `%` for modular division, also the `^` operator, which is typically used for bitiwise exclusive-or operations, has been reserved for power functions. Additionally, you can use many `System.Math` library operations within Apply functions, and those will be translated to the appropriate methods for use by Redis.
65+
66+
### Available Math Functions
67+
68+
|Function|Type|Description|Example|
69+
|--------|----|-----------|-------|
70+
|Log10|Math|yields the 10 base log for the number|`Math.Log10(x["AdjustedSales"])`|
71+
|Abs|Math|yields the absolute value of the provided number|`Math.Abs(x["AdjustedSales"])`|
72+
|Ceil|Math|yields the smallest integer not less than the provided number|`Math.Ceil(x["AdjustedSales"])`|
73+
|Floor|Math|yields the smallest integer not greater than the provided number|`Math.Floor(x["AdjustedSales"])`|
74+
|Log|Math|yields the Log base 2 for the provided number|`Math.Log(x["AdjustedSales"])`|
75+
|Exp|Math|yields the natural exponent for the provided number (e^y)|`Math.Exp(x["AdjustedSales"])`|
76+
|Sqrt|Math|yields the Square root for the provided number|`Math.Sqrt(x["AdjustedSales"])`|
77+
78+
## String Functions
79+
80+
You can also apply multiple string functions to your data, if for example you wanted to create a birthday message for each employee you could do so by calling `String.Format` on your records:
81+
82+
```csharp
83+
var birthdayMessages = employeeAggregations.Apply(x =>
84+
string.Format("Congratulations {0} you are {1} years old!", x.RecordShell.Name, x.RecordShell.Age), "message");
85+
await foreach (var message in birthdayMessages)
86+
{
87+
Console.WriteLine(message["message"].ToString());
88+
}
89+
```
90+
91+
### List of String Functions:
92+
93+
|Function|Type|Description|Example|
94+
|--------|----|-----------|-------|
95+
|ToUpper|String|yields the provided string to upper case|`x.RecordShell.Name.ToUpper()`|
96+
|ToLower|String|yields the provided string to lower case|`x.RecordShell.Name.ToLower()`|
97+
|StartsWith|String|Boolean expression - yields 1 if the string starts with the argument|`x.RecordShell.Name.StartsWith("bob")`|
98+
|Contains|String|Boolean expression - yields 1 if the string contains the argument |`x.RecordShell.Name.Contains("bob")`|
99+
|Substring|String|yields the substring starting at the given 0 based index, the length of the second argument, if the second argument is not provided, it will simply return the balance of the string|`x.RecordShell.Name.Substring(4, 10)`|
100+
|Format|string|Formats the string based off the provided pattern|`string.Format("Hello {0} You are {1} years old", x.RecordShell.Name, x.RecordShell.Age)`|
101+
|Split|string|Split's the string with the provided string - unfortunately if you are only passing in a single splitter, because of how expressions work, you'll need to provide string split options so that no optional parameters exist when building the expression, just pass `StringSplitOptions.None`|`x.RecordShell.Name.Split(",", StringSplitOptions.None)`|
102+
103+
## Time Functions
104+
105+
You can also perform functions on time data in Redis. If you have a timestamp stored in a useable format, a unix timestamp or a timestamp string that can be translated from [strftime](http://strftime.org/), you can operate on them. For example if you wanted to translate a unix timestamp to YYYY-MM-DDTHH:MM::SSZ you can do so by just calling `ApplyFunctions.FormatTimestamp` on the record inside of your apply function. E.g.
106+
107+
```csharp
108+
var lastOnline = employeeAggregations.Apply(x => ApplyFunctions.FormatTimestamp(x.RecordShell.LastOnline),
109+
"LAST_ONLINE_STRING");
110+
111+
foreach (var employee in lastOnline)
112+
{
113+
Console.WriteLine(employee["LAST_ONLINE_STRING"].ToString());
114+
}
115+
```
116+
117+
### Time Functions Available
118+
119+
|Function|Type|Description|Example|
120+
|--------|----|-----------|-------|
121+
|ApplyFunctions.FormatTimestamp|time|transforms a unix timestamp to a formatted time string based off [strftime](http://strftime.org/) conventions|`ApplyFunctions.FormatTimestamp(x.RecordShell.LastTimeOnline)`|
122+
|ApplyFunctions.ParseTime|time|Parsers the provided formatted timestamp to a unix timestamp|`ApplyFunctions.ParseTime(x.RecordShell.TimeString, "%FT%ZT")`|
123+
|ApplyFunctions.Day|time|Rounds a unix timestamp to the beginning of the day|`ApplyFunctions.Day(x.RecordShell.LastTimeOnline)`|
124+
|ApplyFunctions.Hour|time|Rounds a unix timestamp to the beginning of current hour|`ApplyFunctions.Hour(x.RecordShell.LastTimeOnline)`|
125+
|ApplyFunctions.Minute|time|Round a unix timestamp to the beginning of the current minute|`ApplyFunctions.Minute(x.RecordShell.LastTimeOnline)`|
126+
|ApplyFunctions.Month|time|Rounds a unix timestamp to the beginning of the current month|`ApplyFunctions.Month(x.RecordShell.LastTimeOnline)`|
127+
|ApplyFunctions.DayOfWeek|time|Converts the unix timestamp to the day number with Sunday being 0|`ApplyFunctions.DayOfWeek(x.RecordShell.LastTimeOnline)`|
128+
|ApplyFunctions.DayOfMonth|time|Converts the unix timestamp to the current day of the month (1..31)|`ApplyFunctions.DayOfMonth(x.RecordShell.LastTimeOnline)`|
129+
|ApplyFunctions.DayOfYear|time|Converts the unix timestamp to the current day of the year (1..31)|`ApplyFunctions.DayOfYear(x.RecordShell.LastTimeOnline)`|
130+
|ApplyFunctions.Year|time|Converts the unix timestamp to the current year|`ApplyFunctions.Year(x.RecordShell.LastTimeOnline)`|
131+
|ApplyFunctions.MonthOfYear|time|Converts the unix timestamp to the current month (0..11)|`ApplyFunctions.MonthOfYear(x.RecordShell.LastTimeOnline)`|
132+
133+
## Geo Distance
134+
135+
Another useful function is the `GeoDistance` function, which allows you computer the distance between two points, e.g. if you wanted to see how far away from the office each employee was you could use the `ApplyFunctions.GeoDistance` function inside your pipeline:
136+
137+
```csharp
138+
var officeLoc = new GeoLoc(-122.064181, 37.377207);
139+
var distanceFromWork =
140+
employeeAggregations.Apply(x => ApplyFunctions.GeoDistance(x.RecordShell.HomeLoc, officeLoc), "DistanceToWork");
141+
await foreach (var element in distancesFromWork)
142+
{
143+
Console.WriteLine(element["DistanceToWork"].ToString());
144+
}
145+
```
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
id: groups
3+
title: Grouping and Reductions
4+
sidebar_label: Grouping and Reductions
5+
slug: /develop/dotnet/redis-om-dotnet/aggregations/groups/groups
6+
---
7+
8+
Grouping and reducing operations using aggregations can be extremely powerful.
9+
10+
## What Is a Group
11+
12+
A group is simply a group of like records in Redis.
13+
14+
e.g.
15+
16+
```json
17+
{
18+
"Name":"Susan",
19+
"Department":"Sales",
20+
"Sales":600000
21+
}
22+
23+
{
24+
"Name":"Tom",
25+
"Department":"Sales",
26+
"Sales":500000
27+
}
28+
```
29+
30+
If grouped together by `Department` would be one group. When grouped by `Name`, they would be two groups.
31+
32+
## Reductions
33+
34+
What makes groups so useful in Redis Aggregations is that you can run reductions on them to aggregate items within the group. For example, you can calculate summary statistics on numeric fields, retrieve random samples, distinct counts, approximate distinct counts of any aggregatable field in the set.
35+
36+
## Using Groups and Reductions with Redis OM .NET
37+
38+
You can run reductions against an `RedisAggregationSet` either with or without a group. If you run a reduction without a group, the result of the reduction will materialize immediately as the desired type. If you run a reduction against a group, the results will materialize when they are enumerated.
39+
40+
### Reductions without a Group
41+
42+
If you wanted to calculate a reduction on all the records indexed by Redis in the collection, you would simply call the reduction on the `RedisAggregationSet`
43+
44+
```csharp
45+
var sumSales = employeeAggregations.Sum(x=>x.RecordShell.Sales);
46+
Console.WriteLine($"The sum of sales for all employees was {sumSales}");
47+
```
48+
49+
### Reductions with a Group
50+
51+
If you want to build a group to run reductions on, e.g. you wanted to calculate the average sales in a department, you would use a `GroupBy` predicate to specify which field or fields to group by. If you want to group by 1 field, your lambda function for the group by will yield just the field you want to group by. If you want to group by multiple fields, `new` up an anonymous type in line:
52+
53+
```csharp
54+
var oneFieldGroup = employeeAggregations.GroupBy(x=>x.RecordShell.Department);
55+
56+
var multiFieldGroup = employeeAggregations.GroupBy(x=>new {x.RecordShell.Department, x.RecordShell.WorkLoc});
57+
```
58+
59+
From here you can run reductions on your groups. To run a Reduction, execute a reduction function. When the collection materializes the `AggregationResult<T>` will have the reduction stored in a formatted string which is the `PropertyName_COMMAND_POSTFIX`, see supported operations table below for postfixes. If you wanted to calculate the sum of the sales of all the departments you could:
60+
61+
```csharp
62+
var departments = employeeAggregations.GroupBy(x=>x.RecordShell.Department).Sum(x=>x.RecordShell.Sales);
63+
foreach(var department in departments)
64+
{
65+
Console.WriteLine($"The {department[nameof(Employee.Department)]} department sold {department["Sales_SUM"]}");
66+
}
67+
```
68+
69+
|Command Name|Command Postfix|Description|
70+
|------------|----------------|-----------|
71+
|Count|COUNT|number of records meeting the query, or in the group|
72+
|CountDistinct|COUNT_DISTINCT|Counts the distinct occurrences of a given property in a group|
73+
|CountDistinctish|COUNT_DISTINCTISH|Provides an approximate count of distinct occurrences of a given property in each group - less expensive computationally but does have a small 3% error rate |
74+
|Sum|SUM|The sum of all occurrences of the provided field in each group|b
75+
|Min|MIN|Minimum occurrence for the provided field in each group|
76+
|Max|MAX|Maximum occurrence for the provided field in each group|
77+
|Average|Avg|Arithmetic mean of all the occurrences for the provided field in a group|
78+
|StandardDeviation|STDDEV|Standard deviation from the arithmetic mean of all the occurrences for the provided field in each group|
79+
|Quantile|QUANTLE|The value of a record at the provided quantile for a field in each group, e.g., the Median of the field would be sitting at quantile .5|
80+
|Distinct|TOLIST|Enumerates all the distinct values of a given field in each group|
81+
|FirstValue|FIRST_VALUE|Retrieves the first occurrence of a given field in each group|
82+
|RandomSample|RANDOM_SAMPLE_{NumRecords}|Random sample of the given field in each group|
83+
84+
## Closing Groups
85+
86+
When you invoke a `GroupBy` the type of return type changes from `RedisAggregationSet` to a `GroupedAggregationSet`. In some instances you may need to close a group out and use its results further down the pipeline. To do this, all you need to do is call `CloseGroup` on the `GroupedAggregationSet` - that will end the group predicates and allow you to use the results further down the pipeline.

0 commit comments

Comments
 (0)