skip to content

Redis geospatial commands are often described as a thin layer over another core Redis type. Which type actually backs GEOADD, how is a longitude/latitude pair encoded into it, and what practical consequences does that have for how you operate the key?

level: middleimportance: should knowfreq 30%

answer

  1. TYPE geo:key → zset
  2. 26 bits/axis, interleaved → 52-bit score
  3. 52 < 53-bit double mantissa → exact
  4. 9 cells scanned, then exact distance filter
  5. ZADD by hand = corrupted index

basics

~20 s

A sorted set. GEOADD interleaves 26 bits of longitude and 26 bits of latitude into one 52-bit geohash integer and stores it as the member's score. So ZREM, ZSCORE, ZCARD, ZRANGE, DUMP and EXPIRE all work on a geo key, and nearby-search is a set of score-range scans.

solid answer

~50 s

`GEOADD` writes a **sorted set**. Each coordinate pair is quantised to 26 bits per axis and bit-interleaved into a single 52-bit integer — a geohash — which becomes the member's score. 52 bits fit exactly in a double's 53-bit mantissa, so scores are exact integers, not approximations. Consequences: - Every sorted-set command works: `ZREM` to delete, `ZSCORE` to see the raw hash, `ZCARD` to count, `ZRANGE` to page through, `ZUNIONSTORE`-style plumbing if you must. - Persistence, replication, `DUMP`/`RESTORE`, memory reporting and encoding all follow the sorted set — there is nothing geo-specific in RDB or AOF. - TTL is per key, never per member; there is no per-point expiry. - Because interleaved geohashes are a space-filling curve, points close on the map usually have close scores. `GEOSEARCH` exploits that: it range-scans the covering cell plus its eight neighbours, then filters candidates by real distance. - Writing your own score with `ZADD` corrupts the index — geo reads will return nonsense.

code

text · 10 lines
text
GEOADD cities -122.4194 37.7749 SF
TYPE cities                 # => zset
ZSCORE cities SF            # => "1367354011224948"  (52-bit geohash)
ZCARD cities                # => 1
ZRANGE cities 0 -1          # => 1) "SF"
ZREM cities SF              # the only way to delete a point

# corrupting the index by writing your own score:
ZADD cities 42 BOGUS
GEOPOS cities BOGUS         # decodes 42 into a meaningless coordinate

go deeper

for a junior

Recall that a geo key is a sorted set whose score is an encoded geohash, so ZREM/ZCARD/ZSCORE work on it.

for a middle

Explain the 26-bits-per-axis interleaving, why 52 bits fits a double exactly, and that a search is score-range scans plus an exact distance filter.

for a senior

Draw out the operational consequences: density-driven query cost, no per-member TTL so sweeps are explicit, short member names for memory, and never hand-writing scores.

for a principal

Position it as a one-dimensional index over a space-filling curve — cheap and in-memory, but circle/box only, one attribute per key, and a dense region becomes a hot single-shard key.

## The type underneath Redis exposes geospatial commands (`GEOADD`, `GEOSEARCH`, `GEODIST`, `GEOPOS`, `GEOHASH`) but no geospatial *type*. `TYPE mykey` on a geo index answers `zset`. All the geo commands do is compute or decode a score for an ordinary sorted set — a structure of unique members each carrying a numeric score, kept in score order. ## How a coordinate becomes a score The encoding is a **geohash**, a space-filling curve built by recursive bisection: 1. Longitude is mapped over [-180, 180] and latitude over roughly [-85.05112878, 85.05112878] (the Mercator-style latitude cut-off, which is why the poles cannot be stored). 2. Each axis is bisected 26 times. Each bisection emits one bit: 0 if the value falls in the lower half, 1 if the upper half. That gives 26 bits per axis. 3. The two bit strings are **interleaved** — longitude bit, latitude bit, longitude bit, … — into one 52-bit integer. 4. That integer is stored as the member's score. 52 bits matters: an IEEE-754 double holds integers exactly up to 2^53, so a 52-bit geohash survives as a sorted-set score with no rounding. The lossy step is the quantisation itself: 26 bits per axis leaves cells small enough that the worst-case positional error is under a metre, which is why `GEOPOS` returns something very slightly different from what you wrote. The interleaving is what makes proximity search possible. Two points in the same small map cell share a long common prefix of interleaved bits, so their 52-bit integers are numerically close. A cell of a chosen precision is therefore a contiguous **score range**, and "everything in this cell" is one `ZRANGEBYSCORE`-style scan. ## How GEOSEARCH uses it Given a centre and a radius, Redis picks the geohash precision whose cells are at least as wide as the search shape, computes the cell containing the centre, and takes its eight neighbours as well (a point near a cell edge has close neighbours in the adjacent cell — the classic space-filling-curve seam problem). It range-scans those nine ranges, then computes the exact great-circle distance for each candidate and discards the ones outside the shape. Hence the documented complexity O(N + log(M)): logarithmic to seek into the ranges, plus work proportional to how many members sit in the scanned area. This also explains a real operational property: the cost of a geo query depends on **density**, not on total key size. A 2 km search in Manhattan touches far more members than the same search in the desert, even in the same key. ## What this buys you - **All sorted-set commands apply.** `ZREM` is the delete (there is no `GEODEL`). `ZCARD` counts indexed points. `ZSCORE` shows the raw hash. `ZSCAN` iterates without blocking. `ZRANGEBYSCORE` even lets you hand-roll cell queries. - **Nothing geo-specific in the storage layer.** RDB, AOF, `DUMP`/`RESTORE`, replication, `MEMORY USAGE`, `OBJECT ENCODING` (listpack for small sets, skiplist + hash table above the configured thresholds) all treat it as a zset. Memory per point is the sorted-set entry cost — a member string plus a double — so member names should be short IDs, not JSON blobs. - **The key is the unit of everything.** TTL, replication, cluster slot and rebalancing all operate on the whole index. There is no per-member expiry, so stale points must be swept with `ZREM`, typically driven by a parallel last-seen sorted set scored by timestamp. ## What this costs you - **You can corrupt it.** `ZADD geo:key 42 member` inserts a member whose score is not a valid geohash; `GEOPOS` will decode it into a meaningless coordinate and `GEOSEARCH` will return garbage. Treat geo keys as write-only through `GEO*`. - **One dimension only.** The score is fully consumed by the coordinate, so you cannot also rank by rating, price or recency inside the same key. Secondary attributes live in a hash or a second sorted set and are joined client-side. - **The index is 2-D and shape-limited.** Circles and axis-aligned boxes are all Redis offers; polygons, route distance, nearest-road snapping or altitude need a real GIS engine. Redis is the fast candidate filter in front of it. - **A hot area is a hot key.** Because a key is a single slot on a single shard, a dense city in a single index concentrates both writes and query CPU on one core.

  • Why 52 bits and not 64?
    Sorted-set scores are IEEE-754 doubles, which represent integers exactly only up to 2^53. Capping the geohash at 52 bits (26 per axis) keeps every score an exact integer, so encode/decode round-trips are stable. It still leaves cells small enough that the quantisation error is under about a metre.
  • Why does GEOSEARCH scan nine cells instead of one?
    A geohash cell is a rectangle, and a search centred near an edge overlaps neighbouring rectangles. Scanning the containing cell plus its eight neighbours guarantees no candidate near the boundary is missed. Redis then computes the exact great-circle distance for each candidate and discards those outside the requested radius or box.
  • Can you store an extra attribute, like driver rating, in the same geo key?
    No — the score is entirely consumed by the coordinate, and the only other slot is the member name. Attributes live elsewhere: a hash per member, or a second sorted set scored by the attribute, and you join client-side (or in a Lua script) after GEOSEARCH returns the candidate IDs.

The interleaved geohash is like an address written most-significant-digit first: everything in the same postcode shares a prefix, so 'find nearby' becomes 'scan the block of addresses with this prefix', plus the adjacent blocks for anyone just over the boundary.

saying these in an interview costs you the question

  • Claiming Redis has a dedicated geo type with its own persistence format
  • Writing scores into a geo key with ZADD and expecting GEO* reads to still work
  • Thinking the 52-bit score is lossy because doubles are imprecise (it is exact; the quantisation is the lossy part)
  • Assuming query cost scales with total key size rather than with point density in the searched area
  • Expecting per-member expiry because sorted sets are 'just keys'

context