Skip to content

Commit 71b940c

Browse files
authored
Top k/commands (#6)
* Starting TopK * Add TOPK Commands + Tests
1 parent 0c883f3 commit 71b940c

File tree

9 files changed

+324
-3
lines changed

9 files changed

+324
-3
lines changed

src/NRedisStack.Core/Auxiliary.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using StackExchange.Redis;
2+
3+
namespace NRedisStack.Core
4+
{
5+
public static class Auxiliary
6+
{
7+
public static List<object> MergeArgs(RedisKey key, RedisValue[] items)
8+
{
9+
var args = new List<object> { key };
10+
foreach (var item in items) args.Add(item);
11+
return args;
12+
}
13+
}
14+
}

src/NRedisStack.Core/CountMinSketch/CmsCommands.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public bool Merge(RedisValue destination, long numKeys, RedisValue[] source, lon
120120
/// <param name="key">The name of the sketch</param>
121121
/// <param name="items">One or more items for which to return the count.</param>
122122
/// <returns>Array with a min-count of each of the items in the sketch</returns>
123-
/// <remarks><seealso href="https://redis.io/commands/cms.merge"/></remarks>
123+
/// <remarks><seealso href="https://redis.io/commands/cms.query"/></remarks>
124124
public long[]? Query(RedisKey key, RedisValue[] items) //TODO: Create second version of this function using params for items input
125125
{
126126
if (items.Length < 1)

src/NRedisStack.Core/ModulPrefixes.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public static class ModulPrefixes
1313
static bool cmsCreated = false;
1414
static CmsCommands cmsCommands;
1515

16+
static bool topKCreated = false;
17+
static TopKCommands topKCommands;
18+
1619
static bool searchCreated = false;
1720
static SearchCommands searchCommands;
1821

@@ -55,6 +58,17 @@ static public CmsCommands CMS(this IDatabase db)
5558
return cmsCommands;
5659
}
5760

61+
static public TopKCommands TOPK(this IDatabase db)
62+
{
63+
if (!topKCreated)
64+
{
65+
topKCommands = new TopKCommands(db);
66+
topKCreated = true;
67+
}
68+
69+
return topKCommands;
70+
}
71+
5872
static public SearchCommands FT(this IDatabase db)
5973
{
6074
if (!searchCreated)

src/NRedisStack.Core/ResponseParser.cs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using NRedisStack.Core.Bloom.DataTypes;
88
using NRedisStack.Core.CuckooFilter.DataTypes;
99
using NRedisStack.Core.CountMinSketch.DataTypes;
10+
using NRedisStack.Core.TopK.DataTypes;
1011

1112
namespace NRedisStack.Core
1213
{
@@ -31,9 +32,9 @@ public static bool[] ToBooleanArray(RedisResult result)
3132
return boolArr;
3233
}
3334

34-
public static RedisResult[] ToArray(RedisResult result)
35+
public static RedisResult[]? ToArray(RedisResult result)
3536
{
36-
return (RedisResult[])result;
37+
return (RedisResult[]?)result;
3738
}
3839

3940
public static long ToLong(RedisResult result)
@@ -295,6 +296,42 @@ public static IReadOnlyList<TimeSeriesRule> ToRuleArray(RedisResult result)
295296
return new CmsInformation(width, depth, count);
296297
}
297298

299+
public static TopKInformation? ToTopKInfo(RedisResult result) //TODO: Think about a different implementation, because if the output of CMS.INFO changes or even just the names of the labels then the parsing will not work
300+
{
301+
long k, width, depth;
302+
double decay;
303+
304+
k = width = depth = -1;
305+
decay = -1.0;
306+
307+
RedisResult[]? redisResults = (RedisResult[]?)result;
308+
309+
if (redisResults == null) return null;
310+
311+
for (int i = 0; i < redisResults.Length; ++i)
312+
{
313+
string? label = redisResults[i++].ToString();
314+
315+
switch (label)
316+
{
317+
case "k":
318+
k = (long)redisResults[i];
319+
break;
320+
case "width":
321+
width = (long)redisResults[i];
322+
break;
323+
case "depth":
324+
depth = (long)redisResults[i];
325+
break;
326+
case "decay":
327+
decay = (double)redisResults[i];
328+
break;
329+
}
330+
}
331+
332+
return new TopKInformation(k, width, depth, decay);
333+
}
334+
298335
public static TimeSeriesInformation ToTimeSeriesInfo(RedisResult result)
299336
{
300337
long totalSamples = -1, memoryUsage = -1, retentionTime = -1, chunkSize = -1, chunkCount = -1;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace NRedisStack.Core.TopK.DataTypes
2+
{
3+
/// <summary>
4+
/// This class represents the response for CMS.INFO command.
5+
/// This object has Read-only properties and cannot be generated outside a CMS.INFO response.
6+
/// </summary>
7+
public class TopKInformation
8+
{
9+
public long K { get; private set; }
10+
public long Width { get; private set; }
11+
public long Depth { get; private set; }
12+
public double Decay { get; private set; }
13+
14+
15+
internal TopKInformation(long k, long width, long depth, double decay)
16+
{
17+
K = k;
18+
Width = width;
19+
Depth = depth;
20+
Decay = decay;
21+
}
22+
}
23+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace NRedisStack.Core.Literals
2+
{
3+
internal class TopKArgs
4+
{
5+
//public static string WEIGHTS => "WEIGHTS";
6+
// public static string CAPACITY => "CAPACITY";
7+
// public static string EXPANSION => "EXPANSION";
8+
// public static string NOCREATE => "NOCREATE";
9+
// public static string ITEMS => "ITEMS";
10+
// public static string BUCKETSIZE => "BUCKETSIZE";
11+
// public static string MAXITERATIONS => "MAXITERATIONS";
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace NRedisStack.Core.Literals
2+
{
3+
internal class TOPK
4+
{
5+
public static string RESERVE => "TOPK.RESERVE";
6+
public static string ADD => "TOPK.ADD";
7+
public static string INCRBY => "TOPK.INCRBY";
8+
public static string QUERY => "TOPK.QUERY";
9+
public static string COUNT => "TOPK.COUNT";
10+
public static string LIST => "TOPK.LIST";
11+
public static string INFO => "TOPK.INFO";
12+
}
13+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using NRedisStack.Core.TopK.DataTypes;
2+
using NRedisStack.Core.Literals;
3+
using StackExchange.Redis;
4+
namespace NRedisStack.Core
5+
{
6+
7+
public class TopKCommands //TODO: Finish this
8+
{
9+
IDatabase _db;
10+
public TopKCommands(IDatabase db)
11+
{
12+
_db = db;
13+
}
14+
15+
/// <summary>
16+
/// Increases the count of item by increment.
17+
/// </summary>
18+
/// <param name="key">The name of the sketch.</param>
19+
/// <param name="item">Item to be added.</param>
20+
/// <returns>Array of simple-string-reply - if an element was dropped from the TopK list, null otherwise</returns>
21+
/// <remarks><seealso href="https://redis.io/commands/topk.add"/></remarks>
22+
public RedisResult[]? Add(RedisKey key, RedisValue item)
23+
{
24+
return ResponseParser.ToArray(_db.Execute(TOPK.ADD, key, item));
25+
}
26+
27+
/// <summary>
28+
/// Increases the count of item by increment.
29+
/// </summary>
30+
/// <param name="key">The name of the sketch.</param>
31+
/// <param name="items">Items to be added</param>
32+
/// <returns>Array of simple-string-reply - if an element was dropped from the TopK list, null otherwise</returns>
33+
/// <remarks><seealso href="https://redis.io/commands/topk.add"/></remarks>
34+
public RedisResult[]? Add(RedisKey key, params RedisValue[] items)
35+
{
36+
var args = Auxiliary.MergeArgs(key, items);
37+
38+
return (RedisResult[]?)_db.Execute(TOPK.ADD, args);
39+
}
40+
41+
/// <summary>
42+
/// Returns count for an item.
43+
/// </summary>
44+
/// <param name="key">Name of sketch where item is counted</param>
45+
/// <param name="item">Item to be counted.</param>
46+
/// <returns>count for responding item.</returns>
47+
/// <remarks><seealso href="https://redis.io/commands/cf.count"/></remarks>
48+
public long Count(RedisKey key, RedisValue item)
49+
{
50+
return ResponseParser.ToLong(_db.Execute(TOPK.COUNT, key, item));
51+
}
52+
53+
/// <summary>
54+
/// Returns count for an items.
55+
/// </summary>
56+
/// <param name="key">Name of sketch where item is counted</param>
57+
/// <param name="item">Items to be counted.</param>
58+
/// <returns>count for responding item.</returns>
59+
/// <remarks><seealso href="https://redis.io/commands/cf.count"/></remarks>
60+
public long[]? Count(RedisKey key, params RedisValue[] items)
61+
{
62+
var args = Auxiliary.MergeArgs(key, items);
63+
return ResponseParser.ToLongArray(_db.Execute(TOPK.COUNT, args));
64+
}
65+
66+
67+
/// <summary>
68+
/// Increase the score of an item in the data structure by increment.
69+
/// </summary>
70+
/// <param name="key">Name of sketch where item is added.</param>
71+
/// <param name="itemIncrements">Tuple of The items which counter is to be increased
72+
/// and the Amount by which the item score is to be increased.</param>
73+
/// <returns>Score of each item after increment.</returns>
74+
/// <remarks><seealso href="https://redis.io/commands/topk.incrby"/></remarks>
75+
public RedisResult[]? IncrBy(RedisKey key, params Tuple<RedisValue, long>[] itemIncrements)
76+
{
77+
if (itemIncrements.Length < 1)
78+
throw new ArgumentException(nameof(itemIncrements));
79+
80+
List<object> args = new List<object> { key };
81+
foreach (var pair in itemIncrements)
82+
{
83+
args.Add(pair.Item1);
84+
args.Add(pair.Item2);
85+
}
86+
return ResponseParser.ToArray(_db.Execute(TOPK.INCRBY, args));
87+
}
88+
89+
// //TODO: information about what?
90+
/// <summary>
91+
/// Return TopK information.
92+
/// </summary>
93+
/// <param name="key">Name of the key to return information about.</param>
94+
/// <returns>TopK Information.</returns>
95+
/// <remarks><seealso href="https://redis.io/commands/topk.info"/></remarks>
96+
public TopKInformation? Info(RedisKey key)
97+
{
98+
var info = _db.Execute(TOPK.INFO, key);
99+
return ResponseParser.ToTopKInfo(info);
100+
}
101+
102+
/// <summary>
103+
/// Return full list of items in Top K list.
104+
/// </summary>
105+
/// <param name="key">The name of the sketch.</param>
106+
/// <param name="withcount">return Count of each element is returned.</param>
107+
/// <returns>Full list of items in Top K list</returns>
108+
/// <remarks><seealso href="https://redis.io/commands/topk.list"/></remarks>
109+
public RedisResult[]? List(RedisKey key, bool withcount = false)
110+
{
111+
var result = (withcount) ? _db.Execute(TOPK.LIST, key, "WITHCOUNT")
112+
: _db.Execute(TOPK.LIST, key);
113+
return ResponseParser.ToArray(result);
114+
}
115+
116+
/// <summary>
117+
/// Returns the count for one or more items in a sketch.
118+
/// </summary>
119+
/// <param name="key">The name of the sketch</param>
120+
/// <param name="item">Item to be queried.</param>
121+
/// <returns><see langword="true"/> if item is in Top-K, <see langword="false"/> otherwise/></returns>
122+
/// <remarks><seealso href="https://redis.io/commands/topk.query"/></remarks>
123+
public bool? Query(RedisKey key, RedisValue item)
124+
{
125+
return _db.Execute(TOPK.QUERY, key, item).ToString() == "1";
126+
}
127+
128+
/// <summary>
129+
/// Returns the count for one or more items in a sketch.
130+
/// </summary>
131+
/// <param name="key">The name of the sketch</param>
132+
/// <param name="items">Items to be queried.</param>
133+
/// <returns>Bolean Array where <see langword="true"/> if item is in Top-K, <see langword="false"/> otherwise/></returns>
134+
/// <remarks><seealso href="https://redis.io/commands/topk.query"/></remarks>
135+
public bool[]? Query(RedisKey key, params RedisValue[] items)
136+
{
137+
if (items.Length < 1)
138+
throw new ArgumentNullException(nameof(items));
139+
140+
var args = Auxiliary.MergeArgs(key, items);
141+
142+
return ResponseParser.ToBooleanArray(_db.Execute(TOPK.QUERY, args));
143+
}
144+
145+
/// <summary>
146+
/// Initializes a TopK with specified parameters.
147+
/// </summary>
148+
/// <param name="key">Key under which the sketch is to be found.</param>
149+
/// <param name="topk">Number of top occurring items to keep.</param>
150+
/// <param name="width">Number of counters kept in each array. (Default 8)</param>
151+
/// <param name="depth">Number of arrays. (Default 7)</param>
152+
/// <param name="decay">The probability of reducing a counter in an occupied bucket. (Default 0.9)</param>
153+
/// <returns><see langword="true"/> if executed correctly, error otherwise/></returns>
154+
/// <remarks><seealso href="https://redis.io/commands/topk.reserve"/></remarks>
155+
public bool? Reserve(RedisKey key, long topk, long width = 7, long depth = 8, double decay = 0.9)
156+
{
157+
return ResponseParser.ParseOKtoBoolean(_db.Execute(TOPK.RESERVE, key, topk, width, depth, decay));
158+
}
159+
}
160+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Xunit;
2+
using StackExchange.Redis;
3+
using NRedisStack.Core.RedisStackCommands;
4+
using Moq;
5+
6+
namespace NRedisStack.Tests.TopK;
7+
8+
public class TopKTests : AbstractNRedisStackTest, IDisposable
9+
{
10+
Mock<IDatabase> _mock = new Mock<IDatabase>();
11+
private readonly string key = "TOPK_TESTS";
12+
public TopKTests(RedisFixture redisFixture) : base(redisFixture) { }
13+
14+
public void Dispose()
15+
{
16+
redisFixture.Redis.GetDatabase().KeyDelete(key);
17+
}
18+
19+
[Fact]
20+
public void CreateTopKFilter()
21+
{
22+
IDatabase db = redisFixture.Redis.GetDatabase();
23+
db.Execute("FLUSHALL");
24+
25+
db.TOPK().Reserve("aaa", 30, 2000, 7, 0.925);
26+
27+
var res = db.TOPK().Add("aaa", "bb", "cc");
28+
Assert.True(res[0].IsNull && res[1].IsNull);
29+
30+
Assert.Equal(db.TOPK().Query("aaa", "bb", "gg", "cc"), new bool[] { true, false, true });
31+
32+
Assert.Equal(db.TOPK().Count("aaa", "bb", "gg", "cc"), new long[] { 1, 0, 1 });
33+
34+
var res2 = db.TOPK().List("aaa");
35+
Assert.Equal(res2[0].ToString(), "bb");
36+
Assert.Equal(res2[1].ToString(), "cc");
37+
38+
var tuple = new Tuple<RedisValue, long>("ff", 10);
39+
var del = db.TOPK().IncrBy("aaa", tuple);
40+
Assert.True(db.TOPK().IncrBy("aaa", tuple)[0].IsNull);
41+
42+
res2 = db.TOPK().List("aaa");
43+
Assert.Equal(res2[0].ToString(), "ff");
44+
Assert.Equal(res2[1].ToString(), "bb");
45+
Assert.Equal(res2[2].ToString(), "cc");
46+
}
47+
}

0 commit comments

Comments
 (0)