How can I sort a leaderboard on multiple fields?

Last updated 22, Mar 2024

Question

How can I perform multi-dimensional sort operations?

Answer

In a leaderboard, if you need to sort on different dimensions: e.g. sort players based on their scores, if the score is the same, then sort by their ranking, then sort by the time to complete the task, it is possible to model the scores using two methods.

Encoding the scores in a sorted set

This method uses the sorted set with a custom score encoding the different fields. We will encode the three scores as a double-precision number composed of three parts:

  1. The principal score, variable and unbounded
  2. Ranking, which has an upper bound. 999, as an example
  3. Time, which has an upper bound. 99999, as an example

So we would encode the overall score of this entry:

HSET user:score:11 score 10 ranking 15 time 1000

As follows, concatenating the following characters, left to right:

"10" + "015" + "01000" = 1001501000

The leaderboard can now be modeled using the sorted set.

ZADD leaderboard 1001501000 user:score:11
ZADD leaderboard 5000902000 user:score:12
ZADD leaderboard 1002002000 user:score:13
ZADD leaderboard 1002502000 user:score:14
ZADD leaderboard 1003002000 user:score:15
ZADD leaderboard 1003502000 user:score:16

Now the users are naturally sorted by the three scores.

ZREVRANGE leaderboard 0 -1
1) "user:score:12"
2) "user:score:16"
3) "user:score:15"
4) "user:score:14"
5) "user:score:13"
6) "user:score:11"

Indexing and aggregating with FT.AGGREGATE

With a different data model, where every user object stores the fields to be used for ordering, you can model scores as NUMERIC SORTABLE fields and request SORTBY a list of properties. Check the following example.

HSET user:score:11 user user:11 score 10 ranking 15 time 1000
HSET user:score:12 user user:12 score 50 ranking 9 time 2000
HSET user:score:13 user user:13 score 10 ranking 20 time 2000
HSET user:score:14 user user:14 score 10 ranking 25 time 2000
HSET user:score:15 user user:15 score 10 ranking 30 time 2000
HSET user:score:16 user user:16 score 10 ranking 35 time 2000

FT.CREATE user_score_idx ON HASH PREFIX 1 user:score SCHEMA user AS user TEXT score AS score NUMERIC SORTABLE ranking AS ranking NUMERIC SORTABLE time AS time NUMERIC SORTABLE

FT.AGGREGATE user_score_idx * SORTBY 6 @score DESC @ranking DESC @time DESC 
1) (integer) 6
2) 1) "score"
   2) "50"
   3) "ranking"
   4) "9"
   5) "time"
   6) "2000"
3) 1) "score"
   2) "10"
   3) "ranking"
   4) "35"
   5) "time"
   6) "2000"
4) 1) "score"
   2) "10"
   3) "ranking"
   4) "30"
   5) "time"
   6) "2000"
5) 1) "score"
   2) "10"
   3) "ranking"
   4) "25"
   5) "time"
   6) "2000"
6) 1) "score"
   2) "10"
   3) "ranking"
   4) "20"
   5) "time"
   6) "2000"
7) 1) "score"
   2) "10"
   3) "ranking"
   4) "15"
   5) "time"
   6) "1000"

References

Check the documentation for FT.AGGREGATE.