skip to Main Content

I have an application where the backend data store is Redis. This application (interface) provides users with a table that must support searching, pagination, sorting and filtering.

My Redis design includes the use of sorted sets and standard key:value pairs. As an example, consider a used car dealership. Lets say I have a set, toyota who’s members are a list of all available cars for sale, related to that brand. Each member is a combination of the car model, and some unique identifier that relates to a physical car.

toyota
  - corolla:100
  - corolla:200
  - corolla:300
  - sienna:100
  - sienna:200

Each member of that set, has a separate key, for example toyota:corolla:100, where the value is an object containing various information about that particular car:

{
  id: 100,
  brand: "Toyota",
  model: "Corolla",
  color: "red",
  cost: 15000
}

Understanding this basic data relationship, I find myself in a scenario where I want to offer the ability to sort data within this front-end table, by some property contained within the object of each key. Let’s say for example, the color of the car. Of course, in order to do that, I need to compare all of the objects.

My predicament, is how to implement that while taking pagination into consideration. In reality, my sets are not cars, and they can easily contain thousands of members. But the data relationship is the same concept. I do not want to have to fetch all of the keys in order to determine this sort, as it defeats the purpose of the pagination.

I’ll clarify that I am not artificially paginating the results in my API layer. I am achieving pagination by directly limiting the redis results through utilizing zrangebylex (to provide some basic ordering), along with a limit offset.

$results = [];
$cars = $redis->zRangeByLex("toyota", '-', '+', 0, 1);

foreach( $cars as $car ) {
    $results[] = json_decode($redis->get($car), true);
}

// example $cars return:
// [ "corolla:100", "corolla:200" ]

// example $results return:
// [
//   { id: 100, brand: "Toyota", model: "Corolla", color: "red", cost: 15000 },
//   { id: 200, brand: "Toyota", model: "Corolla", color: "blue", cost: 14000 },
// ]

I want to avoid artificially paginating results, as fetching thousands of records on each API call, then iterating through them, takes longer than is acceptable.

I’ll also note, when it comes to searching, I am searching against the sets by utilizing zscan — which isn’t ideal, as it means I am limited by the value of the members within each set.

$search = "corolla"; # user search term
$cars = []; # result container

$it = NULL; # iterator
$redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);

while($matches = $redis->zScan('toyota', $it, "*{$search}*")) {
    foreach($matches as $key => $score) {
        $cars[] = $key;
    }
}

// example $cars return:
// [ "corolla:100", "corolla:200", "corolla:300" ]

While I could redesign this application within a SQL environment and achieve all of these features with relative ease, I’m more interested in making this work using Redis. What would be a more appropriate Redis design/pattern look like, that would support all of the features I want to implement within this front-end table (sorting, pagination, searching, filtering)?

2

Answers


  1. Redis does not have the concept of indexes and you should thus build and maintain index-like structures like the ones you mentioned by yourself: a great and somewhat simple way to do that consists in maintaining one ZSET for each index you wish to sort and paginate by, with each ZSET member’s keys pointing to the final object key (e.g. corolla:100) and its score being the value you would use to sort and paginate the item with respect to the others.

    With this setup in place, you could use the ZRANGEBYSCORE command (or ZRANGE on Redis 6.2+) along with its LIMIT option to quickly obtain paginated subsets of the original ZSET.

    Here is how one can defina ZSET for an index-like structure of Toyota cars ordered by cost and later iterate through its sorted items, one page (each carrying just 2 items, for the sake of this example) at a time:

    ZADD cars-by-cost:toyota 15000 corolla:100
    ZADD cars-by-cost:toyota 12000 corolla:200
    ZADD cars-by-cost:toyota 16000 corolla:300
    ZADD cars-by-cost:toyota 13000 sienna:100
    ZADD cars-by-cost:toyota 15000 sienna:200
    
    ZRANGEBYSCORE cars-by-cost:toyota -inf +inf LIMIT 0 2
    ZRANGEBYSCORE cars-by-cost:toyota -inf +inf LIMIT 2 2
    ZRANGEBYSCORE cars-by-cost:toyota -inf +inf LIMIT 4 2
    

    On the performance side, ZRANGEBYSCORE has a time complexity of:

    O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned

    so it will be ideal for paging and sorting jobs.

    About searching: that really depends on how you wish to search and which entities / fields you need to search for; if you are happy with the pattern-matching offered by SCAN (and its companions HSCAN and ZSCAN) then I would suggest having and maintaining one ZSET for each searchable set of data you wish to offer to your users and sticking to ZSCAN.

    On the other hand, should you need a full-text-like search experience then the RediSearch module would really shine here: https://github.com/RediSearch/RediSearch

    And if you don’t want or cant use external modules in your Redis installation, then you may want to follow the SADD/SINTER approach mentioned here – which you could easily translate to a ZADD/ZINTER approach, should you need to.

    Login or Signup to reply.
  2. The Redisearch module is ideally suited to this use case.
    https://oss.redislabs.com/redisearch/

    You won’t need to use the sets because Redisearch indexes hashes directly. Redisearch is a secondary index engine which will enable you to easily index, query, filter, sort and paginate your data. You don’t have to use the full-text searching features, but maybe they might come in useful.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search