For this tutorial on the new Redis GEO commands, we’re going a bit out of the box. Normally you might expect to see a tutorial showing you how to match users with local resources such as restaurants or hotels. For this we’re going to track runners in a marathon. This will showcase some of the more dynamic things you can do with Redis’ Geo support.
One of the nice aspects to Redis’ geo support is the ability to have Redis to the math and coordinate based operations such as determining the distance between two points or finding all registered points with a given radius of another point. For our hypothetical marathon we are going to track the location of each runner, some waystations/aid stations, and approximate runner pace. For this marathon we are equipping our runners with a device which transmits their current location every 60 seconds along with a timestamp to a service API. This API will interact with our Redis pod and will be where we focus our attention.
When interacting with our Redis, we will be using the following Redis Geo commands:
GEOPOS, GEOADD, GEODIST, and GEORADIUSBYMEMBER. For recording basic information we will also be making use of the following non-Geo Redis commands:
ZCARD. As various libraries have differing ways to call Redis commands, I’ll be using the Redis native command pattern here as opposed to direct sample code. A second aspect is that with Geo being new to Redis there isn’t direct universal support of Geo commands among client libraries, so providing the Redis commands will help you to understand the “what” which needs done, and your particular library’s documentation will provide the “how".
Our service needs to perform various tasks, so let us walk through those. First we need to be able to add a runner into our system and associate them with an ID. Most running events have a unique number associated with each runner - usually seen on their bib. We will keep this custom and use that as their ID, though we could just as easily use some other unique identifier such as email address, an account number, or account ID provided they are unique. For our system we will assume the identifiers are assigned outside of the logging system so we won’t generate them.
We will also assume that the runner’s device is “enabled” as they cross the starting line automatically. Thus we will only store events for runners who have crossed the starting line. But we will still need to store other entities on our course log. These could be first aid stations, water stations, or just mile markers - or a combination thereof. We will also need to update every runner's current location as well as pull this information in various ways.
To add a station we will need its longitude and latitude, and an ID. For this example we will have “aid stations” and “mile markers” on the course in addition to runners. To add a station we use the
GEOADD command. Our Redis key which contains our course and all of it’s markers will be called
on-course. Thus to add “mile marker 1” we would call
GEOADD on-course <long> <lat> mile-marker-1 - where
<lat> are replaced by the respective longitude and latitude and ‘mile-marker-1’ is the name or identifier for the station.
This is what it would look like from the redis-cli:
127.0.0.1:6379> geoadd on-course 105.1 12.35 mile-marker-1
On the initial add Redis will return a “1” indicating the number of items added. While we are only adding a single location at a time in this tutorial, the
GEOADD command will allow you to add multiple in a single call so the number it returns is the number of new items added to the set.
Our runner’s tracking device makes a call to our service with the runner’s ID, current location, and a timestamp. We will need to make a few calls to Redis whenever we add a runner’s current location in order to get the various tracking we need. Fortunately we can get away with this because of Redis’ sheer speed.
When we update a location we will need to set their current location in our course set. The course set contains the current location of all tracked objects from runners to stations and waypoints. To update the runner’s current location we call
GEOADD on-course long lat <id> - just as we did for course stations.
In addition to the runner’s current location we want to track per-runner information. To do this we use a per-runner key in Redis. Thus on every location update we also run
GEOADD runner:<id>:log long lat timestamp where
<id> is replaced with the runner’s ID. This provides us with a geo set specific to the runner which contains a running log of locations. Because each location in the log needs a unique identifier, we are leveraging this by storing the timestamp of the update as reported by the tracking device.
In order to use these timestamps we will need to know what they are. Obviously we won’t know ahead of time so we need a way to either pull them or store them. I’ll list both ways here to show a finer point of Redis’ Geo command implementation. If you’ve done much indexing in Redis the first route may feel natural or familiar. For the first route we take advantage of knowing the timestamp when recorded so we create a Redis set which contains all timestamps for a given runner. To do this we would call
SADD runner:<id>:timestamps <timestamp>. With this we could pull a list of all known timestamps using
SMEMBERS runner:<id>:timestamps and do whatever we needed to with that information.
For example you could take the first and last timestamps and calculate the time on course for the runner, or you could take two given timestamps and determine their speed between two points. It is important to note in this case that we are talking straight line geo distance - not on-course distance. To do that you would need to run a finer grained timing interval and then do some basic summing to determine average speed.
However, we can also take advantage of the implementation of Geo support in Redis. Redis’ Geo support uses the Redis Sorted Set data structure to implement it. When you call
GEOADD <key> <long> <lat> <name> Redis creates a sorted set known as
<key>. As a result we can use all the normal sorted set operations on our Geo data structures. Thus to get a list of timestamps from a runner’s log we can call
ZRANGE runner<id>:log 0 -1. We can also use
ZCARD runner:<id>:log to get the number of entries.
Sorted set operations are some of the most complex in Redis, so which one to use depends on the size of the runner’s log. If you are recording every second you probably would be better off with the indexing due to sheer size of the sorted set. If you are using a lower resolution as we are here this may not be much of a concern. In our case we are running a resolution of one minute. Thus we can expect under a thousand entries per runner, still resulting in a fast operation in Redis.
Get Runners' Current Location
To get a runner’s current location we look it up from the course key using
GEOPOS on-course <runnerId> whereupon Redis will return the longitude and latitude. If we wanted to pull the current location of all runners we could either iterate through the list of runner Ids or we could take advantage of the variadic nature of the GEOPOS command and make a single request with all entries.
In either case we need to generate the list of runners. Because we used a Redis counter key we can simply query it using
get runnerCount and then use a basic for loop to build a list of runners. While we could leverage the sorted set implementation this would also add some complexity by also returning the “current” location of all waypoints, aid stations, etc. we added earlier. If we used strings for identifying those we can easily distinguish between the two. Which method to use is dependent on what you’re trying to pull out.
If, for example, you were displaying the current location of all components on the course, using a
ZRANGE call followed by
GEOPOS call with all entries listed would be the most efficient - assuming you don’t have thousands of entries on the single call. If you do I would recommend breaking it up into a series of GEOPOS calls. Otherwise your display could block updates! Here is a sample redis-cli invocation of the
GEOPOS command which retrieves the location of "mile-marker-1":
Find Nearby Runners
Let us assume our race is being broadcast in some way with broadcasters. Broadcasters tend to enjoy being able to “zoom in” on the action. To support this we want to have the ability to show all runners near a given point tor runner. Fortunately Redis provides us with
GEORADIUSBYMEMBER to do just that. It takes the Redis key, two member names such as runner IDs, and a distance with units and returns the list of all members in that radius.
GEORADIUSBYMEMBER command also has some interesting and useful parameters you can pass to it. Among them the ability to limit the number of returned members, include their coordinates, and even calculate distance from the reference point. For example we want to return the list of all runners within one mile of "aid-station-one” and how far from the station they are. To do this we can call
GEORADIUSBYMEMBER on-course aid-station-one 1 mi WITHDIST. When using
WITHDIST, it is important to note that the distance returned is in the units you used in the command. Thus if you specified “100 ft” the distance listed for each nearby runner would be as measured in feet.
Assuming we had a pair of runners nearby, here is what it could look like from redis-cli:
Notice the first item, with zero distance, is always the member you are centering your radius on. What follows is the runnerId and their distance - in miles in this example. If we specify feet instead we can see the change in units:
Now we can see that "runner1" is about three and a half feet from the mile marker, whereas "runner2" is around 18.5 feet. This is much easier to grok than "0.0007" miles.
Only The Beginning
The above tasks are really just the beginnings of what is possible using Redis’ Geo command set and support. With this base of tools at your disposal you can do a wide range of things which before would require writing the code to determine yourself. You could build in some basic “safety” checks to better aid the event and runners. For example you could, when updating a runner’s location use
GEORADIUSBYMEMBER to see if they are still within a given distance from their last entry, and if so perhaps raise an alert to the nearest aid station or wandering safety crew. You can go fine-grained and have near-realtime updates on a Google Map. You could integrate with the GeoHash site to add a little Geocaching to the run by using Redis’ native
While this tutorial was focused on an event with a mix of static and dynamic components, it should give you plenty of ideas on how you could use this for a wide array of capabilities. Using a variant of the above, for example, you could implement a treasure hunt or an orienteering course. You could give your mobile app location awareness such as finding “nearby hotels”, or you could even implement hike logging. Redis now gives you the ability to do it with minimal coding and a fast implementation, now go forth and build!