R7.06 · Data Modeling & Memory — module hub
Geospatial patterns.
Store locations and query by radius, distance, or bounding box using GEOADD, GEOSEARCH, and GEODIST commands built on geohash-encoded Sorted Sets.
The GEO command family encodes 2-D coordinates into a single ZSET score via a 52-bit interleaved geohash. Select a command below; the readout names what it does and its key gotcha.
Adding locations & retrieving coordinates
geospatial.md.txt · Adding Locations / Retrieving CoordinatesGEOADD stores a named location in a Sorted Set encoded as a geohash. The argument order is
longitude first, then latitude — the GeoJSON convention, not the everyday
latitude-first habit. Getting the order wrong silently maps every point to the wrong hemisphere; there is
no runtime error.
GEOADD locations -122.4194 37.7749 "san_francisco" # lon FIRST, then lat GEOADD locations -73.9857 40.7484 "new_york" GEOADD locations -0.1276 51.5074 "london" GEOPOS locations "san_francisco" # → [[lon, lat]] (the stored value, ±0.6 m) GEODIST locations "san_francisco" "new_york" km # → 4129.09 (km)
GEOPOS returns coordinates in the same longitude-first order. The returned values may differ
by a few microdegrees from the inputs because coordinates are stored as 52-bit geohash integers
(precision ±0.6 m).
GEODIST accepts four units: m (metres), km (kilometres),
mi (miles), ft (feet).
GEOADD, GEOPOS,
GEODIST, GEOSEARCH, GEOSEARCHSTORE, and GEOHASH are all
core Valkey commands — present since Redis 3.2 (radius family) and Valkey 9. No
extension module is required. Cite:
valkey.io/commands/geoadd ·
valkey.io/commands/geosearch.The lon-before-lat order is consistent with GeoJSON but reverses the everyday habit — it is the single most common source of a GEO correctness bug, and there is no error to catch it. State the order once at the boundary and enforce it everywhere.
Searching by radius & bounding box
geospatial.md.txt · Searching by Radius / Search Options / Searching by Bounding BoxGEOSEARCH supersedes the deprecated GEORADIUS and
GEORADIUSBYMEMBER (pre-6.2 forms). Two search origins: FROMMEMBER centres the
query on a stored member; FROMLONLAT centres it on arbitrary coordinates. Two shapes:
BYRADIUS draws a circle; BYBOX draws a rectangle (width × height centred on the origin).
# search from a stored member, 500 km radius GEOSEARCH locations FROMMEMBER "san_francisco" BYRADIUS 500 km # search from coordinates, with output options GEOSEARCH locations FROMLONLAT -122.4 37.8 BYRADIUS 50 km WITHCOORD WITHDIST COUNT 10 ASC # bounding box: 1000 km × 1000 km centred on san_francisco GEOSEARCH locations FROMMEMBER "san_francisco" BYBOX 1000 1000 km
Output options: WITHCOORD includes [lon, lat] per result ·
WITHDIST includes the distance from the origin · WITHHASH includes the raw
geohash integer · COUNT N limits to N results · ASC / DESC
sorts by distance.
Persist results to a new key with GEOSEARCHSTORE. The output is a Sorted Set with geohash
scores, ready for further range queries.
GEOSEARCHSTORE nearby_cache locations FROMLONLAT -122.4 37.8 BYRADIUS 50 km
Combine FROMLONLAT + BYRADIUS + COUNT 20 ASC for the
classic “find the 20 nearest” query. Add WITHDIST when the result order needs to be
communicated to the client; sort by the caller otherwise.
Location with metadata & real-time tracking
geospatial.md.txt · Location with Metadata / Real-Time Location TrackingA GEO member carries only a name. There is no room in the Sorted Set score for ratings, cuisines, or
availability. Store extra data in a separate Hash keyed by the same id; GEOSEARCH
returns the ids, then HGETALL fetches the rows.
# store location + metadata in separate structures GEOADD restaurants -122.4194 37.7749 "restaurant:123" HSET restaurant:123 name "Joe's Diner" cuisine "American" rating "4.5" # find nearby, then fetch each row GEOSEARCH restaurants FROMLONLAT -122.4 37.8 BYRADIUS 5 km HGETALL restaurant:123
The same pattern applies to real-time tracking. On each GPS ping, update the driver's position with a
new GEOADD (calling GEOADD on an existing member replaces the coordinates). A
separate status Hash records the timestamp and availability flag; query the GEO index for candidates, then
filter by the status Hash.
GEOADD drivers -122.4 37.8 "driver:456" # GPS ping
HSET driver:456:status last_update 1706648400 available 1
GEOSEARCH drivers FROMLONLAT -122.4 37.8 BYRADIUS 5 km COUNT 20
GEO + Hash is the standard idiom: the Sorted Set answers “what is near here?” and returns ids; the Hash answers “what is the full record?” Two structures, one query shape.
Geofencing & geohash
geospatial.md.txt · Geofencing / Geohash / Removing LocationsValkey does not provide built-in geofencing events. The pattern is application-side: store fence
definitions as GEO entries (or in a Hash), check each position update with GEOSEARCH, and
compare current state to previous state to detect enter or exit transitions. Broadcast transitions via
Pub/Sub to interested subscribers.
The GEOHASH command returns the human-readable geohash string for a member. Longer strings
encode finer precision; the string is the same encoding stored as the ZSET score (decoded to a 52-bit
integer). Useful for debugging and for exporting to external geospatial tools.
GEOHASH locations "san_francisco" # → ["9q8yy9mfry0"] ZREM locations "san_francisco" # remove: it's a ZSET member
Removing a location uses ZREM, not a dedicated GEOREMOVE, because GEO data is
stored in a standard Sorted Set.
Geofencing is application logic over the GEO index, not a server feature. The primitives are
GEOSEARCH (am I inside?) and a state machine (was I inside before?). Pub/Sub carries the
transition event.
The grounding — ZSET-as-sortable-scalar family
geospatial.md.txt · figure 17/18/19GEOADD locations -122.4194 37.7749 "san_francisco" # lon FIRST, then lat GEOPOS locations "san_francisco" # → [lon, lat] GEODIST locations "san_francisco" "new_york" km # units: m | km | mi | ft GEOSEARCH locations FROMMEMBER "san_francisco" BYRADIUS 500 km GEOSEARCH locations FROMLONLAT -122.4 37.8 BYRADIUS 50 km WITHCOORD WITHDIST COUNT 10 ASC GEOSEARCH locations FROMMEMBER "san_francisco" BYBOX 1000 1000 km GEOSEARCHSTORE nearby locations FROMLONLAT -122.4 37.8 BYRADIUS 50 km GEOHASH locations "san_francisco" # the geohash string
The structure reveal (figure 18): a GEO set is a Sorted Set. The source states it three
times: “Coordinates are stored as geohashes in a Sorted Set” (line 5) · “The results
are stored as a Sorted Set with geohash scores” (line 71) · “Since GEO data is stored in
a Sorted Set, use ZREM” (line 125). GEOADD encodes (lon, lat)
into a 52-bit interleaved geohash integer stored as the ZSET score; GEOSEARCH
is a bounded range-scan over that score space.
codemojex has no geospatial surface (figure 19). It is a location-agnostic Telegram emoji-guessing game — players guess emoji; the bus rotates service across players; nothing is placed in space. Unlike some other modules in the R7 family, geospatial does not map onto codemojex’s domain. The standalone store-locator and ride-share examples above are the natural home for GEO commands.
Encode 2-D space into one sortable scalar so a ZSET range-scan answers “what is near here?” — the geohash packs place into a ZSET score.
The same move: a BCS branded id packs time into a sortable id (a ZSET sorts
by birth); R4’s delayed-job score packs fire-time so ZRANGEBYSCORE sweeps
due jobs. A geohash, a branded snowflake, and a delayed-job score are one idea: a structured value
flattened to a sortable scalar.
A sortable scalar is the trick that makes an ordered structure answer a structured query. Geospatial, time-based ids, and scheduled jobs are all the same pattern at different problem shapes.
The three dives
geoadd-and-geosearch → radius-and-box-queries → the-geohash-sorted-setEach dive takes one slice of the geospatial pattern in depth. Read them in order: the add-and-query commands first, the shape options second, the underlying structure third.
GEOADD and GEOSEARCH
Adding locations (lon-before-lat), GEOSEARCH FROMMEMBER|FROMLONLAT BYRADIUS,
GEOPOS/GEODIST + units, WITH*/COUNT/ASC
output options. Two interactives: the command surface + a radius-search visual.
Radius and box queries
BYRADIUS vs BYBOX + GEOSEARCHSTORE + the
metadata-in-a-separate-Hash pattern + real-time tracking + geofencing via Pub/Sub.
The geohash sorted set
The structure reveal: a GEO set is a ZSET, the score is a 52-bit interleaved geohash
integer (Z-order curve). GEOHASH/WITHHASH, ZREM to delete,
precision vs proximity, the R4 ZSET-as-scalar parallel.
Begin with GEOADD and GEOSEARCH. Return to the Data Modeling & Memory chapter to continue.
References
sources · relatedSources
- Valkey — GEOADD — add members with lon/lat; lon-before-lat argument order.
- Valkey — GEOSEARCH — radius and box queries; supersedes GEORADIUS (deprecated 6.2.0).
- Valkey — GEODIST — distance between two members in m/km/mi/ft.
- Valkey — GEOHASH — the geohash string representation; precision and interoperability.
Related in this course
- R7.06.1 · GEOADD and GEOSEARCHcommands, lon-before-lat, radius search, output options
- R7.05 · Vectors & similarity searchthe preceding modeling-family module
- R7.04 · Bitmapsboolean flags at 1 bit per entity
- R7.01 · Redis as a primary databasethe modeling family chapter hub
- R4 · Time, delay & prioritythe sorted-set-as-a-clock family — the same encode-into-a-ZSET-score move