Search code examples
redisjedis

How to update sorted set by another sorted set in Redis?


I am new to Redis, and now I need to update the a sorted set if the key exists in another sorted set.

I think it may be clearer to explain by an example, lets say that there are two sorted sets like the following:

set_1
{key_1:val_1, key_2:val_2, key_3:val_3}

set_2
{key_1:val_new_1, key_3:val_new_3, key_4:val_new_4}

Now I am trying to update the first set if the key exists in the second set, so the result should be:

set_1
{key_1:val_new_1, key_2:val_2, key_3:val_new_3}

I have been reading the Redis documentation for a while, and it seems using he SET command with XX option may help:

The SET command supports a set of options that modify its behavior: XX -- Only set the key if it already exist.

But is it possible to avoid running this on each entry in the first set? Maybe using something like zunionstore?


Solution

  • The SET command only works for regular keys, not for sorted sets.

    In sorted sets, you have score-member pairs, so the key-value pair nomenclature of your example is a little confusing. I'll assume key_1, key_2, key_3, ... are members and val_1, val_2, ... are scores.

    Let's create the sorted sets as follows to review the solution:

    > ZADD set_1 1 key_1 2 key_2 3 key_3
    (integer) 3
    > ZADD set_2 1001 key_1 1003 key_3 1004 key_4
    (integer) 3
    

    The default AGGREGATE is SUM, it's what we will use all across.

    We'll create two sorted sets with the intersection of both, one with the scores of set_1 and one with the scores of set_2.

    > ZINTERSTORE intersect_set_1 2 set_1 set_2 WEIGHTS 1 0
    (integer) 2
    > ZINTERSTORE intersect_set_2 2 set_1 set_2 WEIGHTS 0 1
    (integer) 2
    

    Now, we create a middle-step set for set_1, where we set the score to zero for those that are in set_2 as well:

    > ZUNIONSTORE pre_set_1 2 set_1 intersect_set_1 WEIGHTS 1 -1
    (integer) 3
    

    Now we are ready to update set_1, doing a union of:

    • pre_set_1: all set_1 but with those also in set_2 set to zero score.
    • intersect_set_2: the intersection of set_1 and set_2, with the scores of set_2.

    Here is the final command:

    > ZUNIONSTORE set_1 2 pre_set_1 intersect_set_2
    (integer) 3
    

    Let's see the result:

    > ZRANGE set_1 0 -1 WITHSCORES
    1) "key_2"
    2) "2"
    3) "key_1"
    4) "1001"
    5) "key_3"
    6) "1003"
    

    Don't forget to clean up:

    > UNLINK pre_set_1 intersect_set_1 intersect_set_2
    

    This solution is not optimal as it uses multiple middle-steps, there is a risk for members added to the original sets in between and it uses more memory than necessary.

    The optimal solution would be a Lua script:

    local set2 = redis.call('ZRANGE', KEYS[1], '0', '-1', 'WITHSCORES')
    local set2length = table.getn(set2)
    for i=1,set2length,2 do redis.call('ZADD', KEYS[2], 'XX', set2[i+1], set2[i]) end
    return set2length/2
    

    This loops through set_2, updating set_1. Note the use of XX in the ZADD command, to only update if it exists.

    Use as:

    EVAL "local set2 = redis.call('ZRANGE', KEYS[1], '0', '-1', 'WITHSCORES') \n local set2length = table.getn(set2) \n for i=1,set2length,2 do print(1) redis.call('ZADD', KEYS[2], 'XX', set2[i+1], set2[i]) end \n return set2length/2" 2 set_2 set_1
    

    The Lua script is atomic due to the single-threaded nature of Redis.