mirror of
https://github.com/redis/redis.git
synced 2026-05-28 04:02:46 -04:00
Add AGGREGATE COUNT option to ZUNION, ZINTER, ZUNIONSTORE, and ZINTERSTORE (#14892)
Some checks failed
CI / test-ubuntu-latest (push) Waiting to run
CI / test-sanitizer-address (push) Waiting to run
CI / build-debian-old (push) Waiting to run
CI / build-macos-latest (push) Waiting to run
CI / build-32bit (push) Waiting to run
CI / build-libc-malloc (push) Waiting to run
CI / build-centos-jemalloc (push) Waiting to run
CI / build-old-chain-jemalloc (push) Waiting to run
Codecov / code-coverage (push) Waiting to run
External Server Tests / test-external-standalone (push) Waiting to run
External Server Tests / test-external-cluster (push) Waiting to run
External Server Tests / test-external-nodebug (push) Waiting to run
Spellcheck / Spellcheck (push) Waiting to run
Reply-schemas linter / reply-schemas-linter (push) Has been cancelled
Some checks failed
CI / test-ubuntu-latest (push) Waiting to run
CI / test-sanitizer-address (push) Waiting to run
CI / build-debian-old (push) Waiting to run
CI / build-macos-latest (push) Waiting to run
CI / build-32bit (push) Waiting to run
CI / build-libc-malloc (push) Waiting to run
CI / build-centos-jemalloc (push) Waiting to run
CI / build-old-chain-jemalloc (push) Waiting to run
Codecov / code-coverage (push) Waiting to run
External Server Tests / test-external-standalone (push) Waiting to run
External Server Tests / test-external-cluster (push) Waiting to run
External Server Tests / test-external-nodebug (push) Waiting to run
Spellcheck / Spellcheck (push) Waiting to run
Reply-schemas linter / reply-schemas-linter (push) Has been cancelled
### Overview
This PR adds a new `COUNT` aggregation mode to the `ZUNIONSTORE`,
`ZINTERSTORE`, `ZUNION`, and `ZINTER` sorted set commands. When
`AGGREGATE COUNT` is specified, the resulting score for each element
reflects how many input sets contain it (optionally scaled by
`WEIGHTS`), rather than combining the actual scores of the elements.
This enables a common use case — counting set membership frequency —
directly at the command level, without application-side workarounds.
### Problem Statement
For developers who need to know **how many input sorted sets contain
each element**, there is no single-command solution today.
**Example:** given several game leaderboards, find how many leaderboards
each player appears in.
The existing aggregation modes (`SUM`, `MIN`, `MAX`) all operate on the
elements' scores. To ignore scores and just count set membership, you'd
currently need to copy each sorted set with all scores set to 1, then
run `ZUNIONSTORE`/`ZINTERSTORE` with `SUM` — requiring multiple round
trips, temporary keys, and application-level locking to avoid races.
A `COUNT` aggregation mode solves this directly.
### Solution
Introduces `AGGREGATE COUNT` as a fourth aggregation mode:
- `ZINTER numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE
<SUM | MIN | MAX | COUNT>] [WITHSCORES]`
- `ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight
...]] [AGGREGATE <SUM | MIN | MAX | COUNT>]`
- `ZUNION numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE
<SUM | MIN | MAX | COUNT>] [WITHSCORES]`
- `ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight
...]] [AGGREGATE <SUM | MIN | MAX | COUNT>]`
When `COUNT` is specified, **the scores in the input sets are ignored**.
Note that `WEIGHTS` is **not** ignored — each set contributes its weight
(default 1) per element, and the contributions are summed.
**Implementation details:**
A new helper function `zuiWeightedScore()` computes the per-set
contribution:
```c
inline static double zuiWeightedScore(double score, double weight, int aggregate) {
return (aggregate == REDIS_AGGR_COUNT) ? weight : weight * score;
}
```
The `zunionInterAggregate()` function treats `COUNT` identically to
`SUM` — it adds the per-set contributions. All four call sites where
`weight * score` was previously computed inline are updated to use
`zuiWeightedScore()`.
### Examples
```
> ZADD s1 1 foo 1 bar
> ZADD s2 2 foo 2 bar
> ZADD s3 3 foo
```
**With `SUM` (existing behavior, for comparison):**
```
> ZINTERSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE SUM
(integer) 1
> ZRANGE t1 0 -1 WITHSCORES
1) "foo"
2) "29"
> ZUNIONSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE SUM
(integer) 2
> ZRANGE t1 0 -1 WITHSCORES
1) "bar"
2) "20"
3) "foo"
4) "29"
```
**With `COUNT` and `WEIGHTS`:**
```
> ZINTERSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE COUNT
(integer) 1
> ZRANGE t1 0 -1 WITHSCORES
1) "foo"
2) "18"
> ZUNIONSTORE t1 3 s1 s2 s3 WEIGHTS 10 5 3 AGGREGATE COUNT
(integer) 2
> ZRANGE t1 0 -1 WITHSCORES
1) "bar"
2) "15"
3) "foo"
4) "18"
```
**With `COUNT` and no specified `WEIGHTS`** — resulting score equals the
number of input sorted sets containing the element:
```
> ZINTERSTORE t1 3 s1 s2 s3 AGGREGATE COUNT
(integer) 1
> ZRANGE t1 0 -1 WITHSCORES
1) "foo"
2) "3"
> ZUNIONSTORE t1 3 s1 s2 s3 AGGREGATE COUNT
(integer) 2
> ZRANGE t1 0 -1 WITHSCORES
1) "bar"
2) "2"
3) "foo"
4) "3"
```
### Backward Compatibility
This is a fully additive change. The new `COUNT` keyword is only
recognized after the `AGGREGATE` token in the four affected commands.
Existing commands, arguments, and default behavior (`AGGREGATE SUM`) are
completely unchanged. No new command is introduced, and no existing
response format is modified.
This commit is contained in:
parent
e1d35aca01
commit
80f1ebda88
7 changed files with 145 additions and 16 deletions
|
|
@ -9165,7 +9165,9 @@ struct COMMAND_ARG ZINCRBY_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZINTER history */
|
||||
#define ZINTER_History NULL
|
||||
commandHistory ZINTER_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9185,6 +9187,7 @@ struct COMMAND_ARG ZINTER_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZINTER argument table */
|
||||
|
|
@ -9192,7 +9195,7 @@ struct COMMAND_ARG ZINTER_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZINTER_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZINTER_aggregate_Subargs},
|
||||
{MAKE_ARG("withscores",ARG_TYPE_PURE_TOKEN,-1,"WITHSCORES",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
|
|
@ -9226,7 +9229,9 @@ struct COMMAND_ARG ZINTERCARD_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZINTERSTORE history */
|
||||
#define ZINTERSTORE_History NULL
|
||||
commandHistory ZINTERSTORE_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9246,6 +9251,7 @@ struct COMMAND_ARG ZINTERSTORE_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZINTERSTORE argument table */
|
||||
|
|
@ -9254,7 +9260,7 @@ struct COMMAND_ARG ZINTERSTORE_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZINTERSTORE_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZINTERSTORE_aggregate_Subargs},
|
||||
};
|
||||
|
||||
/********** ZLEXCOUNT ********************/
|
||||
|
|
@ -9894,7 +9900,9 @@ struct COMMAND_ARG ZSCORE_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZUNION history */
|
||||
#define ZUNION_History NULL
|
||||
commandHistory ZUNION_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9914,6 +9922,7 @@ struct COMMAND_ARG ZUNION_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZUNION argument table */
|
||||
|
|
@ -9921,7 +9930,7 @@ struct COMMAND_ARG ZUNION_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZUNION_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZUNION_aggregate_Subargs},
|
||||
{MAKE_ARG("withscores",ARG_TYPE_PURE_TOKEN,-1,"WITHSCORES",NULL,NULL,CMD_ARG_OPTIONAL,0,NULL)},
|
||||
};
|
||||
|
||||
|
|
@ -9929,7 +9938,9 @@ struct COMMAND_ARG ZUNION_Args[] = {
|
|||
|
||||
#ifndef SKIP_CMD_HISTORY_TABLE
|
||||
/* ZUNIONSTORE history */
|
||||
#define ZUNIONSTORE_History NULL
|
||||
commandHistory ZUNIONSTORE_History[] = {
|
||||
{"8.8.0","Added `COUNT` aggregate option."},
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef SKIP_CMD_TIPS_TABLE
|
||||
|
|
@ -9949,6 +9960,7 @@ struct COMMAND_ARG ZUNIONSTORE_aggregate_Subargs[] = {
|
|||
{MAKE_ARG("sum",ARG_TYPE_PURE_TOKEN,-1,"SUM",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("min",ARG_TYPE_PURE_TOKEN,-1,"MIN",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("max",ARG_TYPE_PURE_TOKEN,-1,"MAX",NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("count",ARG_TYPE_PURE_TOKEN,-1,"COUNT",NULL,"8.8.0",CMD_ARG_NONE,0,NULL)},
|
||||
};
|
||||
|
||||
/* ZUNIONSTORE argument table */
|
||||
|
|
@ -9957,7 +9969,7 @@ struct COMMAND_ARG ZUNIONSTORE_Args[] = {
|
|||
{MAKE_ARG("numkeys",ARG_TYPE_INTEGER,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
|
||||
{MAKE_ARG("key",ARG_TYPE_KEY,1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("weight",ARG_TYPE_INTEGER,-1,"WEIGHTS",NULL,NULL,CMD_ARG_OPTIONAL|CMD_ARG_MULTIPLE,0,NULL)},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=ZUNIONSTORE_aggregate_Subargs},
|
||||
{MAKE_ARG("aggregate",ARG_TYPE_ONEOF,-1,"AGGREGATE",NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=ZUNIONSTORE_aggregate_Subargs},
|
||||
};
|
||||
|
||||
/********** XACK ********************/
|
||||
|
|
@ -11988,9 +12000,9 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("zdiff","Returns the difference between multiple sorted sets.","O(L + (N-K)log(N)) worst case where L is the total number of elements in all the sets, N is the size of the first set, and K is the size of the result set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZDIFF_History,0,ZDIFF_Tips,0,zdiffCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZDIFF_Keyspecs,1,zunionInterDiffGetKeys,3),.args=ZDIFF_Args},
|
||||
{MAKE_CMD("zdiffstore","Stores the difference of multiple sorted sets in a key.","O(L + (N-K)log(N)) worst case where L is the total number of elements in all the sets, N is the size of the first set, and K is the size of the result set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZDIFFSTORE_History,0,ZDIFFSTORE_Tips,0,zdiffstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZDIFFSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,3),.args=ZDIFFSTORE_Args},
|
||||
{MAKE_CMD("zincrby","Increments the score of a member in a sorted set.","O(log(N)) where N is the number of elements in the sorted set.","1.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINCRBY_History,0,ZINCRBY_Tips,0,zincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZINCRBY_Keyspecs,1,NULL,3),.args=ZINCRBY_Args},
|
||||
{MAKE_CMD("zinter","Returns the intersect of multiple sorted sets.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTER_History,0,ZINTER_Tips,0,zinterCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZINTER_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZINTER_Args},
|
||||
{MAKE_CMD("zinter","Returns the intersect of multiple sorted sets.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTER_History,1,ZINTER_Tips,0,zinterCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZINTER_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZINTER_Args},
|
||||
{MAKE_CMD("zintercard","Returns the number of members of the intersect of multiple sorted sets.","O(N*K) worst case with N being the smallest input sorted set, K being the number of input sorted sets.","7.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTERCARD_History,0,ZINTERCARD_Tips,0,zinterCardCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZINTERCARD_Keyspecs,1,zunionInterDiffGetKeys,3),.args=ZINTERCARD_Args},
|
||||
{MAKE_CMD("zinterstore","Stores the intersect of multiple sorted sets in a key.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTERSTORE_History,0,ZINTERSTORE_Tips,0,zinterstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZINTERSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZINTERSTORE_Args},
|
||||
{MAKE_CMD("zinterstore","Stores the intersect of multiple sorted sets in a key.","O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZINTERSTORE_History,1,ZINTERSTORE_Tips,0,zinterstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZINTERSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZINTERSTORE_Args},
|
||||
{MAKE_CMD("zlexcount","Returns the number of members in a sorted set within a lexicographical range.","O(log(N)) with N being the number of elements in the sorted set.","2.8.9",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZLEXCOUNT_History,0,ZLEXCOUNT_Tips,0,zlexcountCommand,4,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZLEXCOUNT_Keyspecs,1,NULL,3),.args=ZLEXCOUNT_Args},
|
||||
{MAKE_CMD("zmpop","Returns the highest- or lowest-scoring members from one or more sorted sets after removing them. Deletes the sorted set if the last member was popped.","O(K) + O(M*log(N)) where K is the number of provided keys, N being the number of elements in the sorted set, and M being the number of elements popped.","7.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZMPOP_History,0,ZMPOP_Tips,0,zmpopCommand,-4,CMD_WRITE,ACL_CATEGORY_SORTEDSET,ZMPOP_Keyspecs,1,zmpopGetKeys,4),.args=ZMPOP_Args},
|
||||
{MAKE_CMD("zmscore","Returns the score of one or more members in a sorted set.","O(N) where N is the number of members being requested.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZMSCORE_History,0,ZMSCORE_Tips,0,zmscoreCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZMSCORE_Keyspecs,1,NULL,2),.args=ZMSCORE_Args},
|
||||
|
|
@ -12012,8 +12024,8 @@ struct COMMAND_STRUCT redisCommandTable[] = {
|
|||
{MAKE_CMD("zrevrank","Returns the index of a member in a sorted set ordered by descending scores.","O(log(N))","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZREVRANK_History,1,ZREVRANK_Tips,0,zrevrankCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZREVRANK_Keyspecs,1,NULL,3),.args=ZREVRANK_Args},
|
||||
{MAKE_CMD("zscan","Iterates over members and scores of a sorted set.","O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.","2.8.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZSCAN_History,0,ZSCAN_Tips,1,zscanCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZSCAN_Keyspecs,1,NULL,4),.args=ZSCAN_Args},
|
||||
{MAKE_CMD("zscore","Returns the score of a member in a sorted set.","O(1)","1.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZSCORE_History,0,ZSCORE_Tips,0,zscoreCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,ZSCORE_Keyspecs,1,NULL,2),.args=ZSCORE_Args},
|
||||
{MAKE_CMD("zunion","Returns the union of multiple sorted sets.","O(N)+O(M*log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNION_History,0,ZUNION_Tips,0,zunionCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZUNION_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZUNION_Args},
|
||||
{MAKE_CMD("zunionstore","Stores the union of multiple sorted sets in a key.","O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNIONSTORE_History,0,ZUNIONSTORE_Tips,0,zunionstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZUNIONSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZUNIONSTORE_Args},
|
||||
{MAKE_CMD("zunion","Returns the union of multiple sorted sets.","O(N)+O(M*log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","6.2.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNION_History,1,ZUNION_Tips,0,zunionCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,ZUNION_Keyspecs,1,zunionInterDiffGetKeys,5),.args=ZUNION_Args},
|
||||
{MAKE_CMD("zunionstore","Stores the union of multiple sorted sets in a key.","O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.","2.0.0",CMD_DOC_NONE,NULL,NULL,"sorted_set",COMMAND_GROUP_SORTED_SET,ZUNIONSTORE_History,1,ZUNIONSTORE_Tips,0,zunionstoreCommand,-4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,ZUNIONSTORE_Keyspecs,2,zunionInterDiffStoreGetKeys,5),.args=ZUNIONSTORE_Args},
|
||||
/* stream */
|
||||
{MAKE_CMD("xack","Returns the number of messages that were successfully acknowledged by the consumer group member of a stream.","O(1) for each message ID processed.","5.0.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XACK_History,0,XACK_Tips,0,xackCommand,-4,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XACK_Keyspecs,1,NULL,3),.args=XACK_Args},
|
||||
{MAKE_CMD("xackdel","Acknowledges and deletes one or multiple messages for a stream consumer group.","O(1) for each message ID processed.","8.2.0",CMD_DOC_NONE,NULL,NULL,"stream",COMMAND_GROUP_STREAM,XACKDEL_History,0,XACKDEL_Tips,0,xackdelCommand,-6,CMD_WRITE|CMD_FAST,ACL_CATEGORY_STREAM,XACKDEL_Keyspecs,1,NULL,4),.args=XACKDEL_Args},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -3,
|
||||
"function": "zinterCommand",
|
||||
"get_keys_function": "zunionInterDiffGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"READONLY"
|
||||
],
|
||||
|
|
@ -101,6 +107,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -4,
|
||||
"function": "zinterstoreCommand",
|
||||
"get_keys_function": "zunionInterDiffStoreGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM"
|
||||
|
|
@ -100,6 +106,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -3,
|
||||
"function": "zunionCommand",
|
||||
"get_keys_function": "zunionInterDiffGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"READONLY"
|
||||
],
|
||||
|
|
@ -101,6 +107,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@
|
|||
"arity": -4,
|
||||
"function": "zunionstoreCommand",
|
||||
"get_keys_function": "zunionInterDiffStoreGetKeys",
|
||||
"history": [
|
||||
[
|
||||
"8.8.0",
|
||||
"Added `COUNT` aggregate option."
|
||||
]
|
||||
],
|
||||
"command_flags": [
|
||||
"WRITE",
|
||||
"DENYOOM"
|
||||
|
|
@ -99,6 +105,12 @@
|
|||
"name": "max",
|
||||
"type": "pure-token",
|
||||
"token": "MAX"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"type": "pure-token",
|
||||
"token": "COUNT",
|
||||
"since": "8.8.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
24
src/t_zset.c
24
src/t_zset.c
|
|
@ -2653,6 +2653,15 @@ static int zuiCompareByRevCardinality(const void *s1, const void *s2) {
|
|||
#define REDIS_AGGR_SUM 1
|
||||
#define REDIS_AGGR_MIN 2
|
||||
#define REDIS_AGGR_MAX 3
|
||||
#define REDIS_AGGR_COUNT 4
|
||||
|
||||
/* Return the weighted contribution of a single sorted set member.
|
||||
* For COUNT aggregation the actual score is irrelevant — each member
|
||||
* contributes its set's weight (i.e. "one occurrence worth <weight>").
|
||||
* For all other aggregation modes the contribution is weight * score. */
|
||||
inline static double zuiWeightedScore(double score, double weight, int aggregate) {
|
||||
return (aggregate == REDIS_AGGR_COUNT) ? weight : weight * score;
|
||||
}
|
||||
|
||||
inline static void zunionInterAggregate(double *target, double val, int aggregate) {
|
||||
if (aggregate == REDIS_AGGR_SUM) {
|
||||
|
|
@ -2661,6 +2670,11 @@ inline static void zunionInterAggregate(double *target, double val, int aggregat
|
|||
* is +inf and the other is -inf. When these numbers are added,
|
||||
* we maintain the convention of the result being 0.0. */
|
||||
if (isnan(*target)) *target = 0.0;
|
||||
} else if (aggregate == REDIS_AGGR_COUNT) {
|
||||
*target += val;
|
||||
/* The val is zuiWeightedScore(…) == weight, which can be +inf/-inf,
|
||||
* so the NaN guard applies here. */
|
||||
if (isnan(*target)) *target = 0.0;
|
||||
} else if (aggregate == REDIS_AGGR_MIN) {
|
||||
*target = val < *target ? val : *target;
|
||||
} else if (aggregate == REDIS_AGGR_MAX) {
|
||||
|
|
@ -2962,6 +2976,8 @@ void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, in
|
|||
aggregate = REDIS_AGGR_MIN;
|
||||
} else if (!strcasecmp(c->argv[j]->ptr,"max")) {
|
||||
aggregate = REDIS_AGGR_MAX;
|
||||
} else if (!strcasecmp(c->argv[j]->ptr,"count")) {
|
||||
aggregate = REDIS_AGGR_COUNT;
|
||||
} else {
|
||||
zfree(src);
|
||||
addReplyErrorObject(c,shared.syntaxerr);
|
||||
|
|
@ -3018,17 +3034,17 @@ void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, in
|
|||
while (zuiNext(&src[0],&zval)) {
|
||||
double score, value;
|
||||
|
||||
score = src[0].weight * zval.score;
|
||||
score = zuiWeightedScore(zval.score, src[0].weight, aggregate);
|
||||
if (isnan(score)) score = 0;
|
||||
|
||||
for (j = 1; j < setnum; j++) {
|
||||
/* It is not safe to access the zset we are
|
||||
* iterating, so explicitly check for equal object. */
|
||||
if (src[j].subject == src[0].subject) {
|
||||
value = zval.score*src[j].weight;
|
||||
value = zuiWeightedScore(zval.score, src[j].weight, aggregate);
|
||||
zunionInterAggregate(&score,value,aggregate);
|
||||
} else if (zuiFind(&src[j],&zval,&value)) {
|
||||
value *= src[j].weight;
|
||||
value = zuiWeightedScore(value, src[j].weight, aggregate);
|
||||
zunionInterAggregate(&score,value,aggregate);
|
||||
} else {
|
||||
break;
|
||||
|
|
@ -3075,7 +3091,7 @@ void zunionInterDiffGenericCommand(client *c, robj *dstkey, int numkeysIndex, in
|
|||
zuiInitIterator(&src[i]);
|
||||
while (zuiNext(&src[i],&zval)) {
|
||||
/* Initialize value */
|
||||
score = src[i].weight * zval.score;
|
||||
score = zuiWeightedScore(zval.score, src[i].weight, aggregate);
|
||||
if (isnan(score)) score = 0;
|
||||
|
||||
/* Search for this element in the dict (which stores node pointers). */
|
||||
|
|
|
|||
|
|
@ -971,6 +971,26 @@ start_server {tags {"zset"}} {
|
|||
assert_equal {b 2 c 3} [r zinter 2 zseta{t} zsetb{t} aggregate max withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE with AGGREGATE COUNT - $encoding" {
|
||||
assert_equal 4 [r zunionstore zsetc{t} 2 zseta{t} zsetb{t} aggregate count]
|
||||
assert_equal {a 1 d 1 b 2 c 2} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNION/ZINTER with AGGREGATE COUNT - $encoding" {
|
||||
assert_equal {a 1 d 1 b 2 c 2} [r zunion 2 zseta{t} zsetb{t} aggregate count withscores]
|
||||
assert_equal {b 2 c 2} [r zinter 2 zseta{t} zsetb{t} aggregate count withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE with AGGREGATE COUNT and WEIGHTS - $encoding" {
|
||||
assert_equal 4 [r zunionstore zsetc{t} 2 zseta{t} zsetb{t} weights 2 3 aggregate count]
|
||||
assert_equal {a 2 d 3 b 5 c 5} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNION/ZINTER with AGGREGATE COUNT and WEIGHTS - $encoding" {
|
||||
assert_equal {a 2 d 3 b 5 c 5} [r zunion 2 zseta{t} zsetb{t} weights 2 3 aggregate count withscores]
|
||||
assert_equal {b 5 c 5} [r zinter 2 zseta{t} zsetb{t} weights 2 3 aggregate count withscores]
|
||||
}
|
||||
|
||||
test "ZINTERSTORE basics - $encoding" {
|
||||
assert_equal 2 [r zinterstore zsetc{t} 2 zseta{t} zsetb{t}]
|
||||
assert_equal {b 3 c 5} [r zrange zsetc{t} 0 -1 withscores]
|
||||
|
|
@ -1030,6 +1050,39 @@ start_server {tags {"zset"}} {
|
|||
assert_equal {b 2 c 3} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZINTERSTORE with AGGREGATE COUNT - $encoding" {
|
||||
assert_equal 2 [r zinterstore zsetc{t} 2 zseta{t} zsetb{t} aggregate count]
|
||||
assert_equal {b 2 c 2} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZINTERSTORE with AGGREGATE COUNT and WEIGHTS - $encoding" {
|
||||
assert_equal 2 [r zinterstore zsetc{t} 2 zseta{t} zsetb{t} weights 2 3 aggregate count]
|
||||
assert_equal {b 5 c 5} [r zrange zsetc{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE/ZINTERSTORE with AGGREGATE COUNT - 3 sets - $encoding" {
|
||||
r del s1{t} s2{t} s3{t} t1{t}
|
||||
r zadd s1{t} 1 foo 1 bar
|
||||
r zadd s2{t} 2 foo 2 bar
|
||||
r zadd s3{t} 3 foo
|
||||
|
||||
assert_equal 1 [r zinterstore t1{t} 3 s1{t} s2{t} s3{t} aggregate count]
|
||||
assert_equal {foo 3} [r zrange t1{t} 0 -1 withscores]
|
||||
|
||||
assert_equal 2 [r zunionstore t1{t} 3 s1{t} s2{t} s3{t} aggregate count]
|
||||
assert_equal {bar 2 foo 3} [r zrange t1{t} 0 -1 withscores]
|
||||
}
|
||||
|
||||
test "ZUNIONSTORE/ZINTERSTORE with AGGREGATE COUNT and WEIGHTS - 3 sets - $encoding" {
|
||||
assert_equal 1 [r zinterstore t1{t} 3 s1{t} s2{t} s3{t} weights 10 5 3 aggregate count]
|
||||
assert_equal {foo 18} [r zrange t1{t} 0 -1 withscores]
|
||||
|
||||
assert_equal 2 [r zunionstore t1{t} 3 s1{t} s2{t} s3{t} weights 10 5 3 aggregate count]
|
||||
assert_equal {bar 15 foo 18} [r zrange t1{t} 0 -1 withscores]
|
||||
|
||||
r del s1{t} s2{t} s3{t} t1{t}
|
||||
}
|
||||
|
||||
foreach cmd {ZUNIONSTORE ZINTERSTORE} {
|
||||
test "$cmd with +inf/-inf scores - $encoding" {
|
||||
r del zsetinf1{t} zsetinf2{t}
|
||||
|
|
|
|||
Loading…
Reference in a new issue