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
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 (orZRANGE
on Redis 6.2+) along with itsLIMIT
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:
On the performance side,
ZRANGEBYSCORE
has a time complexity of: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 companionsHSCAN
andZSCAN
) then I would suggest having and maintaining one ZSET for each searchable set of data you wish to offer to your users and sticking toZSCAN
.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 aZADD
/ZINTER
approach, should you need to.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.