Redis sorted set with multiple score "columns" - redis

I have a need for a leaderboard where users are stored with a score AND a percentage of level completeness. I only need to be able to sort by score and get ranks by score.
Using Redis' sorted sets a can easily store users' scores like so:
ZADD leaderboard:gamemode1 100 user1
ZADD leaderboard:gamemode1 300 user2
however, I am struggelig to figure out, how to best store the percentage values belonging to the scores of 100 and 300.
Should I do something like this:
ZADD leaderboard:gamemode1 100 user1:29.45
where the :29.45 is the percentage for user1's score of 100 in gamemode1? I think this will make it complicated to update user1's score for gamemode1 later.
Or should I instead create a hashtable as a book-keeping mechanism, that stores all users' scores and percentages for all gamemodes?
My concern about the hashtable approach is, that if I want to show a leaderboard of ~50 users with their score and percentage values, I will then have to query that hashtable 50 times to get the percentages, after pulling the top50 scores with ZRANGE.
Does anyone have any input on how to construct this in a good way?

When working with Sorted Sets, keep in mind that it has member string unicity.
That means:
ZADD game1 100 user1
ZADD game1 90 user1
Leaves you with only 90 user1.
Just a heads-up, I think you've covered that.
For storing the percentage, you could use the decimal part of the score:
ZADD leaderboard:gamemode1 100.0995 user1
ZADD leaderboard:gamemode1 90.0850 user2
Above, I use the decimal part for the percentage like this: 0.0995 = 99.5%.
You can also use the score for score only, and serialize the member string:
ZADD leaderboard:gamemode1 100 [serialized dictionary/dataset]
ZADD leaderboard:gamemode1 90 [serialized dictionary/dataset]
If you use JSON or MsgPack, you can always use server-side scripting to update some data within the serialized string.
I would not recommend bookkeeping for this simple scenario, at least not at this stage. You can always implement that when needed.
Hope this helps, TW

Related

Redis ZRANGEBYLEX matching secondary indexes

I have an index for each user which represents their current score.
I want to find all users with the same score.
Provided that the input is as follows, how can I retrieve only users 1 and 2?
ZADD users 0 1:10
ZADD users 0 2:10
ZADD users 0 3:5
I would expect to do something like mentioned in the docs but I cannot use the key as my index since it is too large (for the post I changed to 1,2,3 so it is not an issue), which is why I set it to 0.
I thought the command could be something like ZRANGEBYLEX users [0:10 (:10 but that returns all 3.
1) "1:10"
2) "2:10"
3) "3:5"
I was able to come up with a solution for this problem, which can further refine as:
Retrieve all users with a given score or retrieve a single score for a single user.
What I determined was that using the namespace users is not a sufficient descriptor for both the score and user id. When using 2 different keys, it is clearly possible to get either a range or a single value. The downside is twice the writes I had hoped, but that is overall not a lot of overhead for me and the only way I found to properly retrieve both score and user.
ZADD score:users 0 10:1
ZADD score:users 0 10:2
ZADD score:users 0 5:3
ZADD users:score 0 1:10
ZADD users:score 0 2:10
ZADD users:score 0 3:5
ZRANGEBYLEX users:score [1: [1:\xff # "1:10"
ZRANGEBYLEX score:users [10: [10:\xff # "10:1" "10:2"

Redis sorted set leader board ranking on same score

I'm using Redis sorted set to implement the leaderboard of my game, where I show the user ranking in descending order. I'm stuck in a case where two or more users have the same score. So in this case, I want the higher ranking of the user who gets the score first. For example, I'm adding the following entries in Redis.
127.0.0.1:6379> zadd testing-key 5 a
(integer) 1
127.0.0.1:6379> zadd testing-key 4 b
(integer) 1
127.0.0.1:6379> zadd testing-key 5 c
(integer) 1
and when I'm querying for the rank in reverse order, I'm getting this
127.0.0.1:6379> zrevrange testing-key 0 10
1) "c"
2) "a"
3) "b"
but in my case, the ranking should be like
1) "a"
2) "c"
3) "b"
So is there any provision in Redis to give higher precedence to the entity which entered first in the set with the same score?
I found one solution to this problem. In my case, the score is an integer so I converted it into decimal and added Long.MAX_VALUE - System.nanoTime() after decimal. So the final score code will be like
double finalScore = score.(Long.MAX_VALUE - System.nanoTime());
So the final score of the player who scored first would be higher than the second one. Please let me know if you have any better solution.
If your leaderboard's scores are "small" enough, you may get away with using a combination of the score and the timestamp (e.g. 123.111455234, where 123 is the score). However, since the Sorted Set score is a double floating point, you may lose precision.
Alternatively, keep two Sorted Sets - one with each player's leaderboard score and the other with each player's score timestamp, and use both to determine the order.
Or, use a single sorted set for the leader board, encode the timestamp as part of the member and rely on lexicographical ordering.

option for lexicographical order in zrange?

When i add a score for a key using zincrby, it increases the score and puts the element in lexicographical order.
Can i get this list in the order, in which the elements are updated or added ?
e.g>
If I execute
zincrby A 100 g
zincrby A 100 a
zincrby A 100 z
and then
zrange A 0 -1
then the result is
a->g->z
where, i want the result in order the entries are made so,
g->a->z
As score is same for all, redis is placing the elements in lexicographical order. Is there any way to prevent it ?
I don't think it is possible, but if you want to keep the order of insertion with scores, you should manipulate something like this:
<score><timestamp>
instead of
<score>
You will have to define a good time record (millis should be ok). Then you can use
zincrby A 100 * (10^nbdigitsformillis)
For instance:
Score = 100 and timestamps is 1381377600 seconds
That gives: 1001381377600
You incr by 200 the score: 1001381377600 + 200 * 10 = 3001381377600
Be careful with zset as it stores scores with double values (64 bits, but only 52 available for int value) so don't store more than 15-17 digits.
If you can't do that (need for great timestamp precision, and great score precision), you will have to manage two zsets (one for actual score, one for timestamp) and managing your ranking manual with the two values.

REDIS - Get value of multiple keys

How do I get the value of multiple keys from redis using a sorted set?
zadd Users 0 David
zadd Users 5 John
zadd Users 15 Linda
zrevrange Users 0 -1 withscores
This will have two users in it.
How can I retrieve the users with key 'David' and 'Linda' in one query?
You can use Redis MGET
redis> MGET key1 key2 nonexisting
1) "Hello"
2) "World"
3) (nil)
More here
http://redis.io/commands/mget
There are multiple ways to do it without introducing a new command in Redis.
For instance, you can fill a temporary set with the names you are interested in, then calculate the intersection between the temporary set and the zset:
multi
sadd tmp David Linda ... and more ...
zinterstore res 2 tmp Users weights 0 1
zrange res 0 -1 withscores
del tmp res
exec
With pipelining, this will only generate one roundtrip and you can fill an arbitrary number of input parameters in tmp.
With Redis 2.6, you can also wrap these lines into a server-side Lua script to finally get a command accepting an input list and returning the result you want:
eval "redis.call( 'sadd', 'tmp', unpack(KEYS) );
redis.call( 'zinterstore', 'res', 2, 'tmp', 'Users', 'weights', 0, 1 );
local res = redis.call( 'zrange', 'res', 0, -1, 'withscores' );
redis.call( 'del', 'res', 'tmp' ) ;
return res
" 2 David Linda
You can safely assume no new command will be added to Redis if it can easily been implemented using scripting.
One uses a sorted set because you want to deal with items that are sorted. What you are asking for is to not use a sorted set as a sorted set. If you don't care about sort order, then perhaps a sorted set is not what you are looking for. You already can retrieve multiple keys, but not arbitrary ones.
If your primary goal is to retrieve multiple arbitrary keys, use a hash and hmget. If your primary need is to access a sorted set, use sorted set and either go the scripting route or pipeline a series of zscore calls.
You cannot get this with one command. The closest you can do to get it in one response:
MULTI
ZSCORE Users David
ZSCORE Users Linda
EXEC
EDIT: Alternatively, you can maintain a parallel hash with users' scores, and query it with
HMGET UserScores David Linda

Redis: How to intersect a "normal" set with a sorted set?

Assume I have a set (or sorted set or list if that would be better) A of 100 to 1000 strings.
Then I have a sorted set B of many more strings, say one million.
Now C should be the intersection of A and B (of the strings of course).
I want to have every tuple (X, SCORE_OF_X_IN_B) where X is in C.
Any Idea?
I got two ideas:
Interstore
store A a sorted set with every score being 0
interstore to D
get every item of D
delete D
Simple loop in client
loop over A in my client programm
get zscore for every string
While 1. has way too much overhead on the redis side (Has to write for example. The redis page states quite a high time complexity, too http://redis.io/commands/zinterstore), 2. would have |A| database connections and won't be a good choice.
Maybe I could write a redis/lua script which will work like zscore but with an arbitrary number of strings, but I'm not sure if my hoster allows scripts...
So I just wanted to ask SO, if there is an elegant and fast solution available without scripting!
There is a simple solution to your problem: ZINTERSTORE will work with a SET and a ZSET. Try:
redis> sadd foo a
(integer) 1
redis> zadd bar 1 a
(integer) 1
redis> zadd bar 2 b
(integer) 1
redis> zinterstore baz 2 foo bar AGGREGATE MAX
(integer) 1
redis> zrange baz 0 -1 withscores
1) "a"
2) "1"
Edit: I added AGGREGATE MAX above, since redis will give each member of the (non-sorted) set foo a default score of 1, and SUM that with whatever score it has in the (sorted) set bar.