Skip to content

Commit b3d6940

Browse files
authored
groupBy / aggregate (#6)
* groupBy / aggregate * feat: reworked aggregate function
1 parent ecb4ed5 commit b3d6940

File tree

8 files changed

+220
-91
lines changed

8 files changed

+220
-91
lines changed

.babelrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"plugins": [
33
"transform-strict-mode",
4-
"transform-es2015-spread",
54
"transform-object-rest-spread"
65
]
76
}

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ const sortBy = 'priority';
3939
const expiration = 30000; // ms
4040

4141
// perform op
42+
const currentTime = Date.now();
43+
4244
redis
43-
.fsort('set-of-ids', 'metadata*', sortBy, 'DESC', filter, offset, limit, expiration)
45+
.fsort('set-of-ids', 'metadata*', sortBy, 'DESC', filter, currentTime, offset, limit, expiration)
4446
.then(data => {
4547
// how many items in the complete list
4648
// rest of the data is ids from the 'set-of-ids'
@@ -56,3 +58,17 @@ redis
5658
});
5759
});
5860
```
61+
62+
### Aggregate functions
63+
64+
1. use fsort to generate list of ids
65+
2. pass that id to aggregate function and receive results back
66+
67+
```js
68+
redis
69+
.fsortAggregate(ID_LIST_KEY, META_KEY_PATTERN, mod.filter({
70+
age: 'sum'
71+
}))
72+
.then(JSON.parse)
73+
.get('age')
74+
```

groupped-list.lua

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
-- cached id list key
2+
local idListKey = KEYS[1];
3+
-- meta key
4+
local metadataKey = KEYS[2];
5+
-- stringified [key]: [aggregateMethod] pairs
6+
local aggregates = ARGV[1];
7+
8+
-- local cache
9+
local rcall = redis.call;
10+
local tinsert = table.insert;
11+
12+
local jsonAggregates = cjson.decode(aggregates);
13+
local aggregateKeys = {};
14+
local result = {};
15+
16+
local function aggregateSum(value1, value2)
17+
value1 = value1 or 0;
18+
value2 = value2 or 0;
19+
return value1 + value2;
20+
end
21+
22+
local aggregateType = {
23+
sum = aggregateSum
24+
};
25+
26+
for key, method in pairs(jsonAggregates) do
27+
tinsert(aggregateKeys, key);
28+
result[key] = 0;
29+
30+
if type(aggregateType[method]) ~= "function" then
31+
return error("not supported op: " .. method);
32+
end
33+
end
34+
35+
local valuesToGroup = rcall("LRANGE", idListKey, 0, -1);
36+
37+
-- group
38+
for _, id in ipairs(valuesToGroup) do
39+
-- metadata is stored here
40+
local metaKey = metadataKey:gsub("*", id, 1);
41+
-- pull information about required aggregate keys
42+
-- only 1 operation is supported now - sum
43+
-- but we can calculate multiple values
44+
local values = rcall("HMGET", metaKey, unpack(aggregateKeys));
45+
46+
for i, aggregateKey in ipairs(aggregateKeys) do
47+
local aggregateMethod = aggregateType[jsonAggregates[aggregateKey]];
48+
local value = tonumber(values[i] or 0);
49+
result[aggregateKey] = aggregateMethod(result[aggregateKey], value);
50+
end
51+
end
52+
53+
return cjson.encode(result);

index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const snakeCase = require('lodash/snakeCase');
66

77
const lua = fs.readFileSync(path.join(__dirname, 'sorted-filtered-list.lua'));
88
const fsortBust = fs.readFileSync(path.join(__dirname, 'filtered-list-bust.lua'));
9+
const aggregateScript = fs.readFileSync(path.join(__dirname, 'groupped-list.lua'));
910

1011
// cached vars
1112
const regexp = /[\^\$\(\)\%\.\[\]\*\+\-\?]/g;
@@ -37,9 +38,11 @@ const fsortBustScript = luaWrapper(fsortBust);
3738
exports.attach = function attachToRedis(redis, _name, useSnakeCase = false) {
3839
const name = _name || 'sortedFilteredList';
3940
const bustName = (useSnakeCase ? snakeCase : camelCase)(`${name}Bust`);
41+
const aggregateName = (useSnakeCase ? snakeCase : camelCase)(`${name}Aggregate`);
4042

4143
redis.defineCommand(name, { numberOfKeys: 2, lua: fsortScript });
4244
redis.defineCommand(bustName, { numberOfKeys: 1, lua: fsortBustScript });
45+
redis.defineCommand(aggregateName, { numberOfKeys: 2, lua: aggregateScript });
4346
};
4447

4548
/**
@@ -96,4 +99,3 @@ exports.filter = function filter(obj) {
9699
* @type {Buffer}
97100
*/
98101
exports.script = lua;
99-

package.json

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
{
22
"name": "redis-filtered-sort",
3-
"version": "2.0.6",
3+
"version": "2.1.0",
44
"description": "Exports LUA script, which is able to perform multi filter operations, as well as sorts",
55
"main": "./lib",
6+
"engine": {
7+
"node": ">= 7.6.0"
8+
},
69
"scripts": {
710
"test": "./test/docker.sh",
811
"compile": "make all",
@@ -27,20 +30,18 @@
2730
},
2831
"homepage": "https://github.com/makeomatic/redis-filtered-sort#readme",
2932
"peerDependencies": {
30-
"ioredis": "1.x.x || 2.x.x"
33+
"ioredis": "2.x.x || 3.x.x"
3134
},
3235
"devDependencies": {
33-
"babel-register": "^6.16.3",
36+
"babel-cli": "^6.24.1",
37+
"babel-plugin-transform-object-rest-spread": "^6.23.0",
38+
"babel-plugin-transform-strict-mode": "^6.24.1",
39+
"babel-register": "^6.24.1",
3440
"chai": "^3.4.1",
35-
"faker": "^3.0.1",
41+
"faker": "^4.1.0",
3642
"ioredis": "^2.4.0",
3743
"lodash": "^4.16.3",
38-
"mocha": "^3.1.0",
39-
"babel-cli": "^6.14.0",
40-
"babel-plugin-syntax-async-functions": "^6.13.0",
41-
"babel-plugin-transform-es2015-spread": "^6.0.0",
42-
"babel-plugin-transform-object-rest-spread": "^6.0.0",
43-
"babel-plugin-transform-strict-mode": "^6.11.3"
44+
"mocha": "^3.3.0"
4445
},
4546
"dependencies": {
4647
"cluster-key-slot": "^1.0.6"

sorted-filtered-list.lua

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ local offset = tonumber(ARGV[5] or 0);
1717
local limit = tonumber(ARGV[6] or 10);
1818
-- caching time
1919
local expiration = tonumber(ARGV[7] or 30000);
20+
-- return the key only
21+
local returnKeyOnly = ARGV[8] or false;
2022

2123
-- local caches
2224
local tempKeysSet = getIndexTempKeys(idSet);
@@ -105,8 +107,17 @@ local function storeCacheBuster(key)
105107
end
106108

107109
local function updateExpireAndReturnWithSize(key)
110+
-- stores key for cache busting
108111
storeCacheBuster(key);
112+
113+
-- lengthens cache
109114
rcall("PEXPIRE", key, expiration);
115+
116+
-- returns either results or key where it's stored
117+
if returnKeyOnly ~= false then
118+
return key;
119+
end
120+
110121
local ret = rcall("LRANGE", key, offset, offset + limit - 1);
111122
tinsert(ret, rcall("LLEN", key));
112123
return ret;

test/index.js

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ const ld = require('lodash');
99
describe('filtered sort suite', function suite() {
1010
const idSetKey = 'id-set';
1111
const metaKeyPattern = '*-metadata';
12+
const keyPrefix = 'fsort-test:';
1213
const redis = new Redis({
1314
port: process.env.REDIS_PORT_6379_TCP_PORT,
1415
host: process.env.REDIS_PORT_6379_TCP_ADDR,
15-
keyPrefix: 'fsort-test:',
16+
keyPrefix,
1617
});
1718
const monitor = redis.duplicate();
1819
const mod = require('../index.js');
@@ -142,7 +143,7 @@ describe('filtered sort suite', function suite() {
142143

143144
describe('cache invalidation', function invalidationSuite() {
144145
it('should invalidate cache: sort', function test() {
145-
146+
146147
return redis.fsort(idSetKey, null, null, 'ASC', '{}', Date.now())
147148
.then(() => redis.zrangebyscore(`${idSetKey}::${mod.FSORT_TEMP_KEYSET}`, '-inf', '+inf'))
148149
.tap(keys => expect(keys.length).to.be.eq(1))
@@ -156,7 +157,7 @@ describe('filtered sort suite', function suite() {
156157
expect(keys).to.be.eq(0);
157158
});
158159
});
159-
160+
160161
it('should invalidate cache: sort + filter', function test() {
161162
const fieldName = 'fieldExists';
162163
const filter = mod.filter({
@@ -511,4 +512,34 @@ describe('filtered sort suite', function suite() {
511512
});
512513
});
513514
});
515+
516+
describe('aggregate filter', function suite() {
517+
it('sums up age and returns key', function test() {
518+
const filter = mod.filter({
519+
age: { gte: 10, lte: 40 },
520+
});
521+
522+
return redis
523+
.fsort(idSetKey, metaKeyPattern, 'age', 'DESC', filter, Date.now(), 0, 0, 30000, 1)
524+
.then((response) => {
525+
// key where the resulting data is stored
526+
expect(response).to.be.equal('fsort-test:id-set:DESC:fsort-test:*-metadata:age:{"age":{"gte":10,"lte":40}}');
527+
this.response = response;
528+
});
529+
});
530+
531+
it('returns aggregate', function test() {
532+
const aggregate = mod.filter({
533+
age: 'sum',
534+
});
535+
536+
return redis
537+
.fsortAggregate(this.response.slice(keyPrefix.length), metaKeyPattern, aggregate)
538+
.then(JSON.parse)
539+
.then((response) => {
540+
// this would average to 15000+ due to random ranges
541+
expect(response.age).to.be.gt(15000);
542+
});
543+
});
544+
});
514545
});

0 commit comments

Comments
 (0)