Redis Geo

Redis Commands: Geography Edition

Note (version): I wrote this originally as a Redis module in early 2014, then Redis included a modified version directly into official Redis releases in 2016.

Note (implementation): The implementation included with Redis 3.2+ changed some parameter orderings from my original (intentional, well-thought-out) design and also removed some features listed on this page. YMMV.

Commands

geoadd geoset latitude longitude member

geoadd requires a minimum of four parameters: your geo set (spoiler: it’s just a zset), the lat/long you’re adding, and the member this lat/long belongs to. geoadd also accepts unlimited number of adds per run with repeating latitude longitude member.

Note: an API similar to this was added to Redis in mid-2015. The geo included in Redis broke this interface and rearranged parameters in a nonsensical order. Consult actual Redis documentation if you use built-in Redis geo.

Single add

Return value: 1 if member is new or 0 if member is updated.

Multi add

Return value: count of members submitted.

Browse underlying zset

georadius geoset latitude longitude radius units [withdistance] [withcoordinates] [withhash] [withgeojson] [withgeojsoncollection] [noproperties] [asc|desc]

georadius takes a minimum of five arguments and a maximum of twelve arguments. The radius argument is the search radius in units. Valid units are: m, km, ft, mi. You can return the distance from your latitude longitude by adding withdistance. The returned distance is in units of units.

Return value: array of members within radius units from your requested latitude longitude.

Search with distance returned

Return value: nested multi-bulk reply with each member in a different top-level multi-bulk reply.

Distance search with sorting

Return value: same as above, but now sorted. asc (or ascending) returns the closest to latitude longitude as the first entry, with distance increasing as you go down the result list. desc (or descending) returns the most distance from latitude longitude as the first entry, with distance decreasing for subsequent entries.

georadiusbymember geoset member radius units [withdistance] [withcoordinates] [withhash] [withgeojson] [withgeojsoncollection] [noproperties] [asc|desc]

georadiusbymember is exactly the same as georadius except your search is centered at coordinates for member in geoset.

Search radius by existing member

Search around existing member with distances returned

Search with results in descending distance

GeoJSON results

Return value: nested multi-bulk reply with each returned member having a GeoJSON feature returned too. Note: the distance, units, member name, and set name and all returned in the GeoJSON too. You may exclude properties in GeoJSON by adding noproperties to your arguments.

GeoJSON collection result

Return value: array of matching members with one entry for each result plus one final result for an aggregate GeoJSON FeatureCollection1. The FeatureCollection contains all radius matching points in one GeoJSON document. You can remove properties from the GeoJSON by adding noproperties to your arguments.

geoencode latitude longitude [radius units] [withgeojson]

Encode latitude and longitude to highest geohash accuracy

Return value: nested multi-bulk reply with 1: the 52-bit geohash integer for your latitude longitude, 2: The minimum corner of your geohash, 3: The maximum corner of your geohash, 4: The averaged center of your geohash.

Encode plus obtain result as GeoJSON

Return value: same as above, but with 5: GeoJSON of your encoded geohash center, 6: GeoJSON bounding box showing the extent of this geohash area. By default, we encode to the highest 52-bit geohash accuracy, which will always give us a bounding box containing a 2 ft (0.6 m) radius.

Encode geohash covering radius units area plus obtain result as GeoJSON

Return value: same as the GeoJSON encode above, except it encodes your coordinates to a geohash with bounding box radius units. You can use your GeoJSON Polygon to easily see the bounding box of this large geohash. Note: geohash bounding boxes are not centered at your requested coordinate. See How it Works for more details.

geodecode geohash [withgeojson]

Decode geohash

Return value: nested multi-bulk with 1: minimum decoded corner, 2: maximum decoded corner, 3: averaged center of bounding box.

Decode geohash and obtain GeoJSON result

Return value: same as above, but with 4: GeoJSON Point of your geohash center and 5: bounding box for this geohash. Note: we always decode to highest accuracy, so your decode GeoJSON Polygon bounding box will always contain a 2 ft (0.6 m) radius;

Other Operations

Removing members

A geoset is actually just a Redis zset. To remove a member from your geoset, use the ZREM command: ZREM _geoset_ _member_.

Counting members

Since a geoset is just a zset, you can use ZCARD _geoset_ to count your set size.

Mult-set operations

You can also use ZUNIONSTORE and ZINTERSTORE on your geoset, but be careful to avoid the default behavior of summing scores (adding two geohash scores together results in meaningless output).

How to Add Geo Commands to Redis

Geo commands are implemented as modular Redis commands for Dynamic Redis.

Dynamic Redis is a simple set of patches on top of regular Redis allowing new commands to load using shared libraries (no need to patch and recompile Redis to add new commands!).

The geo module is distributed as part of Redis Module ToolKit.

Cloning, compiling, and loading geo.so

Below is a set of commands to:

  • clone the current development branch of Dynamic Redis
  • the current Redis Module ToolKit
  • compile the geo.so module
  • and load geo.so into a Dynamic Redis server running on the default Redis port

If you would like to run a Dynamic Redis based on a released Redis version, use a stable branch listed at Dynamic Redis.

Quick Validation Test

You can check if geo.so loaded successfully by one or more of:

  • redis-cli geoencode 0 0
  • redis-cli INFO modules
  • or just watch your redis-server terminal (or log) for successful module addition output

Note: If INFO modules fails, you are not running a Dynamic Redis. You need to run a Dynamic Redis to load modules.

Origin Story

Location is important. Everything exists somewhere. Knowing where things are is more and more important in the world. Every mobile phone is a complex location tracking device these days, but how are we using all the data we have access to now?

Location information presents a very basic problem. Location information is inherently four dimesional: (latitude, longitude, elevation) at a given time. For ease of data processing, we tend to ignore elevation and time when presenting location data and only focus on latitude/longitude grid coordinates.

Things exist

Things exist at a location (a latitude and a longitude). We ignore elevation for now. Discovering if one (latitude, longitude) pair is near another requires a two dimensional index. Two dimensional indexing is largely a solved problem, but not as widely implemented as very simple one-dimensional indexing using range searches.

What we need is a way to represent a two dimensional space in only one dimension. For integers, we can use simple space filling curves. We can represent a pair of integer (x,y) coordinates in only one integer, but geographic data isn’t only integers.

In 2008, a public domain implementation for encoding latitude/longitude pairs down to one dimension showed up as geohash. Now we can easily encode our geographic information (minus elevation and time) into one range-searchable entity.

The Geohash

A Geohash is traditionally represented as a base-32 encoded string so locations can be easily shared with a short, easy to type string. Most databases have a way of running range searches over strings, so you can get a rough ability to search based on location, but because it uses a string encoding, each character represents between 1 and 5 bits. It’s not an efficient or accurate encoding for doing range searches.

The Improved Geohash

Nothing about using a Geohash requires you encode it as an ASCII string. And, actually, using the Geohash in raw binary form is much more accurate and precise than a base-32 conversion.

The accuracy of a Geohash depends on its length. A 32-bit Geohash represents an area with an average error of about 600 meters. A 64-bit geohash represents an area with an average error of 18 centimeters. A 128-bit geohash represents an area with an average error of the width of a strand of DNA.

Obviously a 128-bit Geohash is overkill. We don’t need to target DNA with femtodrones (yet). But, going up the scale, a 32-bit Geohash isn’t nearly accurage enough. A 42-bit Geohash gives us an accuracy error of 20 meters. That’s better, but still not great—there would be too much ancillary damage when targeting a person in a 20 meter radius.

A happy a compromise is a 52-bit Geohash. 52-bits gives us accuracy down to 0.6 m (2 ft), which, unless you’re on crowded public transport or at a concert, is about the amount of space a human-sized person takes up in the world.

How it Works

Geohash Implementation

Ardb, an extended Redis clone written in C++, recently released their integer-only Geohash implementation used in their 2D Spatial Index.

Upon seeing the brilliant non-text-Geohash, I just had to port Geo Indexing back to Redis. (Cross polination of open source projects is great, isn’t it?)

So, I forked their Geohash implementation, bikesheded the heck out of it (formatting fixes, coding convention fixes, friendiler interfaces, some logic improvements) and built Redis Geo Commands around my heavily modified 52-bit Geohash implementation originally provided by Ardb.

Why 52-bits?

Redis stores all scores of sorted set (zset) as type double. Doubles are 64-bit floating point representations—but—they have a special property: 52-bit integers are perfectly represented with no loss of accuracy (as seen in double precision floats have direct representations for 52-bit integers).

By using 52-bit integers for our Geohash, we can attach a two dimensional location to every zset member.

The Geohash encoding guarantees, within known bounds, a range search between a lower range Geohash and a higher range Geohash will include all coordinates between. We get free two dimensional indexing with a simple range search. Awesome.

(Yes, we also cover the 8 + 1 (neighbors + center) search scenarios too.)

Investigate Geohash Storage in Redis

When using georadius or georadiusbymember, you can request the underlying Geohash too by adding withhash to your command.

You can use zset commands directly to investigate the structure of your geoset too:

You could even implement GEOADD yourself by encoding your coordinates with GEOENCODE then using the returned Geohash as the score in a ZADD _zset_ _score_ _member_, but GEOADD provides a nicer interface, and GEOADD enables the special use case of our next section.

Bonus: Real Time Streaming Location Updates

Big data real time streaming microservices. (Billion dollars nao, plz.)

Everything is real time. Imagine you have users pushing high frequency location updates. Maybe they are on a train and updating their location once a second. How do you show their location updates in a live view?

Thanks to the magic of Geo Commands combined with Redis PubSub, you can get notified of every location update by subscribing to a geo-themed Redis PubSub channel.

Redis Geo PubSub Channel

All values set by GEOADD generate a Redis PUBLISH event on channel __geo:[geoset]:[member].

Track ALL THE THINGS

Subscribe to all location updates: PSUBSCRIBE __geo:*

Subscribe to location updates for a specific geoset: PSUBSCRIBE __geo:[geoset]:*

Subscribe to location updates for a specific (geoset, member): SUBSCRIBE __geo:[geoset]:[member]

(Usage note: PSUBSCRIBE is “pattern subscribe” and matches anything after *SUBSCRIBE matches only the exact channel you specify.)

Trivial Use Case

You can imagine a scenario where one user is tracking another in real-time by SUBSCRIBE __geo:members:5331 to track user 5531 in the members geoset.

Bulk PubSub Location Tracking Example

Let’s do a bulk GEOADD while also being subscribed to all geoset PubSub channels in another terminal.

Update locations in Terminal 1:

Real-time updates received live in Terminal 2:

(Note: Terminal 2 is configured to psubscribe __geo:* before the GEOADD ran. Redis PubSub only sends live results. If you are not subscribed when an event happens, the event cannot be sent to you.)

Benchmarking

Raw Geohashing

A simple speed test is distributed with the geo commands: geo-test:

Encode throughput: 4.6 million encodes per second. Decode throughput: 5.5 million decodes per second.

These numbers are for translating latitude/longitude to and from 52-bit Geohashes. Redis has network overhead, internal data structure overhead, and other “server processing” features, so we don’t get 5.5 million operations per second, but we still get enough.

Redis Geo Commands

Our performance is pretty good. There is room for improvement in a few areas, but geo commands are perfectly usable for high performance applications today.

This is a series of benchmarks as of the first public geo commands release (0.3).

All benchmarks were run on my 2013 i7 OS X laptop (which is typically faster than my 2008 i7 server). There’s also a good chance these commands were run with redis-server being simultaneously executed by a debugger and introspected by a memory leak checker. Your performance will be better on a non-laptop.

You can easily increase your performance by running multiple redis-server processes on one multi-core server (or on multiple servers). If your server has 8 cores, you can fully utilize it by running 6 to 10 redis-server processes (depending on your workload).

Bulk Benchmark Sample Pipeline of 32 Requests

Individual Benchmarks for Single Requests


  1. Pretty formatted version of the FeatureCollection. You can paste it directly into geojson.io or GeoJSONLint to see all the features on one map.