Repository: redis/redis

Problem statement:
[NEW] zrank with score option
**The problem/use-case that the feature addresses**

Our user wants to return `score` when querying the ranking through `zrank`, instead of querying through `zscore` again.

Currently, we can recommend users to use `pipeline` to solve this problem (avoid network waits), but I think this scenario should become a **built-in** feature of Redis, as it is so common.

Possible command formats:

1，
```
z[rev]rank key member [withscore]
```
2，
```
z[rev]rankwithscore key
```

**Why not pipeline**

For many clients, using pipeline not only requires accepting a new concept, but also requires adjustment in code, new pipeline class or other.


Hints:
I prefer the first format.
I think the first option is preferred since it is inline with other commands like ZRANGE.
speaking of ZRANGE, isn't it plain and simple to use `ZRANGE WITHSCORES` to get one items instead of ZRANK?
> speaking of ZRANGE, isn't it plain and simple to use ZRANGE WITHSCORES to get one items instead of ZRANK?

We just want to get the rank of a member by its name, so `ZRANK` might be better.
I like the idea. However, the 1st option will lock us into single-key ZRANK forever.
> I like the idea. However, the 1st option will lock us into single-key ZRANK forever.

@itamarhaber Are you thinking of a command like `zmrank`? Is it right? Will it support multiple `member` instead of multiple `key`? Like the following:

````
ZMRANK key member [member ...]
````
we are not likely to ever add a ZMRANK that works on multiple keys.
but letting ZRANK work on multiple members (like HSET does today), is a good point, and indeed the `withscore` option blocks it.
both can be solved by a plain pipeline of command, and both will require repeated lookup of the key, so i'm not sure which of them should be prioritized, arguably we can have both, by either two separate commands, or better yet, one command that can handle multiple members with or without score, we just need to get the syntax right, like:
```
ZGET key <count> member-1, member-2 ... member-n [withscore / withrank]
```
@yangbodong22011 sorry, I meant single-element instead of single-key.

@oran we can also go full-ugly (a la `MIGRATE`) so we don't add a new command and do

    ZRANK <key | numkeys key [key...] [WITHSCORE]>
not sure if it's as ugly as MIGRATE, or maybe uglier?
in migrate we add an empty string as key, here you suggest we choose a flavor by checking the number of arguments. right?
i'm not sure i'm happy with that, sounds like something we're gonna regret.
@guybe7 ?
OMG, don't add another ugly command as `MIGRATE`, `ZRANK key member [WITHSCORE]` is good enough.
Definitely not anything that resembles MIGRATE
I think implementing two commands is **easy to understand** and **friendly to client developers**:
````
z[rev]rank key member [withscore]
zm[rev]rank key 3 member1 member2 member2 [withscore]
````
well in theory it could also be `z[m][rev]rand[withscore]`, but i'd rather not add so many commands, and instead i'd rather the `rev` and `withscore` be arguments.
so maybe just the variadic one can be a separate command.
so the existing `zrank`, will get `rev` and `withscore` arguments.
and we'll someday (or today` add a similar `zmrank`
Agreed, it looks like we have two features that I or someone else will start implementing once approved by the @redis/core-team
```
zrank key member [withscore] [rev]
zmrank key num-member member [member ...] [withscores] [rev]
```
Looking at this again, trying to decide if it justifies special commands (we can add hundreds of them if we open that door), i wanna ask again why / if that combination is considered common.

Do you have any examples or numbers to show that this is a common use case that deserves a special command?

Also, i wonder if we have any performance concerns against pipeline (in which case i'd like to see a benchmark), or is it just about convenience for people who find it hard to use pipeline?
This is from our customers, if you like I can invite them to discuss here directly.

Anyway, get the rank and the score from a sorted set is a very useful and common case, for example, in game ranking list players wanna know who is the top3 players and their score at the same time.

> Also, i wonder if we have any performance concerns against pipeline (in which case i'd like to see a benchmark), or is it just about convenience for people who find it hard to use pipeline?

In this theory pipeline can solve everything, but it's not a good way, not only from the performance perspective but also from the convenience perspective. And pipeline cannot support atomicity.

And I remember that when adding GETEX/GETDEL commands I asked the same question, but you said new commands are better(https://github.com/redis/redis/pull/8327#issuecomment-762801080), I don't know why you have different opinions on the same type of requests.
> And I remember that when adding GETEX/GETDEL commands I asked the same question, but you said new commands are better(https://github.com/redis/redis/pull/8327#issuecomment-762801080), I don't know why you have different opinions on the same type of requests.

It's not that i have different opinions, it's the same concern: on one hand i'd like common things to be easy to use, and on the other hand i don't wanna add hundreds of commands for every combination of commands.

I guess at the time i thought that GET+DEL is a common use case, and i'm asking here about RANK+SCORE since it seemed less obvious to me.
I have heard of an AWS customer asking for `zmrank with scores`. It was, stereotypically, a game company who wanted to fetch your score + friends scores. They wanted both the rank as well as the actual scores. They currently multi-exec it, but they wanted something simpler. Generally I like the two outlined commands/updates:

```
zrank key member [withscore] [rev]
zmrank key num-member member [member ...] [withscores] [rev]
```

Patch:
diff --git a/src/commands.c b/src/commands.c
index 5ee012c7911..2b5ec230fe1 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -5890,7 +5890,10 @@ struct redisCommandArg ZRANGESTORE_Args[] = {
 /********** ZRANK ********************/
 
 /* ZRANK history */
-#define ZRANK_History NULL
+commandHistory ZRANK_History[] = {
+{"7.2.0","Added the optional `WITHSCORE` argument."},
+{0}
+};
 
 /* ZRANK tips */
 #define ZRANK_tips NULL
@@ -5899,6 +5902,7 @@ struct redisCommandArg ZRANGESTORE_Args[] = {
 struct redisCommandArg ZRANK_Args[] = {
 {"key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE},
 {"member",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE},
+{"withscore",ARG_TYPE_PURE_TOKEN,-1,"WITHSCORE",NULL,NULL,CMD_ARG_OPTIONAL},
 {0}
 };
 
@@ -6040,7 +6044,10 @@ struct redisCommandArg ZREVRANGEBYSCORE_Args[] = {
 /********** ZREVRANK ********************/
 
 /* ZREVRANK history */
-#define ZREVRANK_History NULL
+commandHistory ZREVRANK_History[] = {
+{"7.2.0","Added the optional `WITHSCORE` argument."},
+{0}
+};
 
 /* ZREVRANK tips */
 #define ZREVRANK_tips NULL
@@ -6049,6 +6056,7 @@ struct redisCommandArg ZREVRANGEBYSCORE_Args[] = {
 struct redisCommandArg ZREVRANK_Args[] = {
 {"key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE},
 {"member",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE},
+{"withscore",ARG_TYPE_PURE_TOKEN,-1,"WITHSCORE",NULL,NULL,CMD_ARG_OPTIONAL},
 {0}
 };
 
@@ -7345,7 +7353,7 @@ struct redisCommand redisCommandTable[] = {
 {"zrangebylex","Return a range of members in a sorted set, by lexicographical range","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).","2.8.9",CMD_DOC_DEPRECATED,"`ZRANGE` with the `BYLEX` argument","6.2.0",COMMAND_GROUP_SORTED_SET,ZRANGEBYLEX_History,ZRANGEBYLEX_tips,zrangebylexCommand,-4,CMD_READONLY,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZRANGEBYLEX_Args},
 {"zrangebyscore","Return a range of members in a sorted set, by score","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).","1.0.5",CMD_DOC_DEPRECATED,"`ZRANGE` with the `BYSCORE` argument","6.2.0",COMMAND_GROUP_SORTED_SET,ZRANGEBYSCORE_History,ZRANGEBYSCORE_tips,zrangebyscoreCommand,-4,CMD_READONLY,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZRANGEBYSCORE_Args},
 {"zrangestore","Store a range of members from sorted set into another key","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements stored into the destination key.","6.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZRANGESTORE_History,ZRANGESTORE_tips,zrangestoreCommand,-5,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_OW|CMD_KEY_UPDATE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}},{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={2},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZRANGESTORE_Args},
-{"zrank","Determine the index of a member in a sorted set","O(log(N))","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZRANK_History,ZRANK_tips,zrankCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZRANK_Args},
+{"zrank","Determine the index of a member in a sorted set","O(log(N))","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZRANK_History,ZRANK_tips,zrankCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZRANK_Args},
 {"zrem","Remove one or more members from a sorted set","O(M*log(N)) with N being the number of elements in the sorted set and M the number of elements to be removed.","1.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZREM_History,ZREM_tips,zremCommand,-3,CMD_WRITE|CMD_FAST,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RW|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREM_Args},
 {"zremrangebylex","Remove all members in a sorted set between the given lexicographical range","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.","2.8.9",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZREMRANGEBYLEX_History,ZREMRANGEBYLEX_tips,zremrangebylexCommand,4,CMD_WRITE,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RW|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREMRANGEBYLEX_Args},
 {"zremrangebyrank","Remove all members in a sorted set within the given indexes","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZREMRANGEBYRANK_History,ZREMRANGEBYRANK_tips,zremrangebyrankCommand,4,CMD_WRITE,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RW|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREMRANGEBYRANK_Args},
@@ -7353,7 +7361,7 @@ struct redisCommand redisCommandTable[] = {
 {"zrevrange","Return a range of members in a sorted set, by index, with scores ordered from high to low","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.","1.2.0",CMD_DOC_DEPRECATED,"`ZRANGE` with the `REV` argument","6.2.0",COMMAND_GROUP_SORTED_SET,ZREVRANGE_History,ZREVRANGE_tips,zrevrangeCommand,-4,CMD_READONLY,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREVRANGE_Args},
 {"zrevrangebylex","Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).","2.8.9",CMD_DOC_DEPRECATED,"`ZRANGE` with the `REV` and `BYLEX` arguments","6.2.0",COMMAND_GROUP_SORTED_SET,ZREVRANGEBYLEX_History,ZREVRANGEBYLEX_tips,zrevrangebylexCommand,-4,CMD_READONLY,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREVRANGEBYLEX_Args},
 {"zrevrangebyscore","Return a range of members in a sorted set, by score, with scores ordered from high to low","O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).","2.2.0",CMD_DOC_DEPRECATED,"`ZRANGE` with the `REV` and `BYSCORE` arguments","6.2.0",COMMAND_GROUP_SORTED_SET,ZREVRANGEBYSCORE_History,ZREVRANGEBYSCORE_tips,zrevrangebyscoreCommand,-4,CMD_READONLY,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREVRANGEBYSCORE_Args},
-{"zrevrank","Determine the index of a member in a sorted set, with scores ordered from high to low","O(log(N))","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZREVRANK_History,ZREVRANK_tips,zrevrankCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREVRANK_Args},
+{"zrevrank","Determine the index of a member in a sorted set, with scores ordered from high to low","O(log(N))","2.0.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZREVRANK_History,ZREVRANK_tips,zrevrankCommand,-3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZREVRANK_Args},
 {"zscan","Incrementally iterate sorted sets elements and associated scores","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,COMMAND_GROUP_SORTED_SET,ZSCAN_History,ZSCAN_tips,zscanCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZSCAN_Args},
 {"zscore","Get the score associated with the given member in a sorted set","O(1)","1.2.0",CMD_DOC_NONE,NULL,NULL,COMMAND_GROUP_SORTED_SET,ZSCORE_History,ZSCORE_tips,zscoreCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}},.args=ZSCORE_Args},
 {"zunion","Add 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,COMMAND_GROUP_SORTED_SET,ZUNION_History,ZUNION_tips,zunionCommand,-3,CMD_READONLY,ACL_CATEGORY_SORTEDSET,{{NULL,CMD_KEY_RO|CMD_KEY_ACCESS,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_KEYNUM,.fk.keynum={0,1,1}}},zunionInterDiffGetKeys,.args=ZUNION_Args},
diff --git a/src/commands/zrank.json b/src/commands/zrank.json
index 6d5154715c2..a15d96c5953 100644
--- a/src/commands/zrank.json
+++ b/src/commands/zrank.json
@@ -4,8 +4,14 @@
         "complexity": "O(log(N))",
         "group": "sorted_set",
         "since": "2.0.0",
-        "arity": 3,
+        "arity": -3,
         "function": "zrankCommand",
+        "history": [
+            [
+                "7.2.0",
+                "Added the optional `WITHSCORE` argument."
+            ]
+        ],
         "command_flags": [
             "READONLY",
             "FAST"
@@ -42,7 +48,13 @@
             {
                 "name": "member",
                 "type": "string"
+            },
+            {
+                "name": "withscore",
+                "token": "WITHSCORE",
+                "type": "pure-token",
+                "optional": true
             }
         ]
     }
-}
+}
\ No newline at end of file
diff --git a/src/commands/zrevrank.json b/src/commands/zrevrank.json
index bcd48769547..7d5aa795bf3 100644
--- a/src/commands/zrevrank.json
+++ b/src/commands/zrevrank.json
@@ -4,8 +4,14 @@
         "complexity": "O(log(N))",
         "group": "sorted_set",
         "since": "2.0.0",
-        "arity": 3,
+        "arity": -3,
         "function": "zrevrankCommand",
+        "history": [
+            [
+                "7.2.0",
+                "Added the optional `WITHSCORE` argument."
+            ]
+        ],
         "command_flags": [
             "READONLY",
             "FAST"
@@ -42,6 +48,12 @@
             {
                 "name": "member",
                 "type": "string"
+            },
+            {
+                "name": "withscore",
+                "token": "WITHSCORE",
+                "type": "pure-token",
+                "optional": true
             }
         ]
     }
diff --git a/src/server.h b/src/server.h
index 2ec7dc7ea63..08da9843efa 100644
--- a/src/server.h
+++ b/src/server.h
@@ -2859,7 +2859,7 @@ void zsetConvertToListpackIfNeeded(robj *zobj, size_t maxelelen, size_t totelele
 int zsetScore(robj *zobj, sds member, double *score);
 unsigned long zslGetRank(zskiplist *zsl, double score, sds o);
 int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore);
-long zsetRank(robj *zobj, sds ele, int reverse);
+long zsetRank(robj *zobj, sds ele, int reverse, double *score);
 int zsetDel(robj *zobj, sds ele);
 robj *zsetDup(robj *o);
 void genericZpopCommand(client *c, robj **keyv, int keyc, int where, int emitkey, long count, int use_nested_array, int reply_nil_when_empty, int *deleted);
diff --git a/src/t_zset.c b/src/t_zset.c
index 4016fc92549..7bb7db016aa 100644
--- a/src/t_zset.c
+++ b/src/t_zset.c
@@ -1511,7 +1511,7 @@ int zsetDel(robj *zobj, sds ele) {
  * the one with the lowest score. Otherwise if 'reverse' is non-zero
  * the rank is computed considering as element with rank 0 the one with
  * the highest score. */
-long zsetRank(robj *zobj, sds ele, int reverse) {
+long zsetRank(robj *zobj, sds ele, int reverse, double *output_score) {
     unsigned long llen;
     unsigned long rank;
 
@@ -1535,6 +1535,8 @@ long zsetRank(robj *zobj, sds ele, int reverse) {
         }
 
         if (eptr != NULL) {
+            if (output_score) 
+                *output_score = zzlGetScore(sptr);
             if (reverse)
                 return llen-rank;
             else
@@ -1554,6 +1556,8 @@ long zsetRank(robj *zobj, sds ele, int reverse) {
             rank = zslGetRank(zsl,score,ele);
             /* Existing elements always have a rank. */
             serverAssert(rank != 0);
+            if (output_score)
+                *output_score = score;
             if (reverse)
                 return llen-rank;
             else
@@ -3763,17 +3767,43 @@ void zrankGenericCommand(client *c, int reverse) {
     robj *key = c->argv[1];
     robj *ele = c->argv[2];
     robj *zobj;
+    robj* reply;
     long rank;
+    int opt_withscore = 0;
+    double score;
 
-    if ((zobj = lookupKeyReadOrReply(c,key,shared.null[c->resp])) == NULL ||
-        checkType(c,zobj,OBJ_ZSET)) return;
-
-    serverAssertWithInfo(c,ele,sdsEncodedObject(ele));
-    rank = zsetRank(zobj,ele->ptr,reverse);
+    if (c->argc > 4) {
+        addReplyErrorArity(c);
+        return;
+    }
+    if (c->argc > 3) {
+        if (!strcasecmp(c->argv[3]->ptr, "withscore")) {
+            opt_withscore = 1;
+        } else {
+            addReplyErrorObject(c, shared.syntaxerr);
+            return;
+        }
+    }
+    reply = opt_withscore ? shared.nullarray[c->resp] : shared.null[c->resp];
+    if ((zobj = lookupKeyReadOrReply(c, key, reply)) == NULL || checkType(c, zobj, OBJ_ZSET)) {
+        return;
+    }
+    serverAssertWithInfo(c, ele, sdsEncodedObject(ele));
+    rank = zsetRank(zobj, ele->ptr, reverse, opt_withscore ? &score : NULL);
     if (rank >= 0) {
-        addReplyLongLong(c,rank);
+        if (opt_withscore) {
+            addReplyArrayLen(c, 2);
+        }
+        addReplyLongLong(c, rank);
+        if (opt_withscore) {
+            addReplyDouble(c, score);
+        }
     } else {
-        addReplyNull(c);
+        if (opt_withscore) {
+            addReplyNullArray(c);
+        } else {
+            addReplyNull(c);
+        }
     }
 }
 


Test patch:
diff --git a/tests/unit/type/zset.tcl b/tests/unit/type/zset.tcl
index 0e245751653..1ad036dd838 100644
--- a/tests/unit/type/zset.tcl
+++ b/tests/unit/type/zset.tcl
@@ -438,17 +438,33 @@ start_server {tags {"zset"}} {
             assert_equal 0 [r zrank zranktmp x]
             assert_equal 1 [r zrank zranktmp y]
             assert_equal 2 [r zrank zranktmp z]
-            assert_equal "" [r zrank zranktmp foo]
             assert_equal 2 [r zrevrank zranktmp x]
             assert_equal 1 [r zrevrank zranktmp y]
             assert_equal 0 [r zrevrank zranktmp z]
-            assert_equal "" [r zrevrank zranktmp foo]
+            r readraw 1
+            assert_equal {$-1} [r zrank zranktmp foo]
+            assert_equal {$-1} [r zrevrank zranktmp foo]
+            r readraw 0
+
+            # withscores
+            assert_equal {0 10} [r zrank zranktmp x withscore]
+            assert_equal {1 20} [r zrank zranktmp y withscore]
+            assert_equal {2 30} [r zrank zranktmp z withscore]
+            assert_equal {2 10} [r zrevrank zranktmp x withscore]
+            assert_equal {1 20} [r zrevrank zranktmp y withscore]
+            assert_equal {0 30} [r zrevrank zranktmp z withscore]
+            r readraw 1
+            assert_equal {*-1} [r zrank zranktmp foo withscore]
+            assert_equal {*-1} [r zrevrank zranktmp foo withscore]
+            r readraw 0
         }
 
         test "ZRANK - after deletion - $encoding" {
             r zrem zranktmp y
             assert_equal 0 [r zrank zranktmp x]
             assert_equal 1 [r zrank zranktmp z]
+            assert_equal {0 10} [r zrank zranktmp x withscore]
+            assert_equal {1 30} [r zrank zranktmp z withscore]
         }
 
         test "ZINCRBY - can create a new sorted set - $encoding" {