Databases 19 min read

How Redis Implements “Nearby People” with GEOADD and GEORADIUS: A Source‑Code Deep Dive

This article explains how Redis uses its Geo module, combining ordered sets and Geohash encoding, to efficiently implement the “nearby people” feature, covering command usage, detailed source‑code analysis of GEOADD and GEORADIUS, algorithmic steps, and complexity evaluation.

Programmer DD
Programmer DD
Programmer DD
How Redis Implements “Nearby People” with GEOADD and GEORADIUS: A Source‑Code Deep Dive

Commands

Since Redis 3.2 the Geo module provides six commands for geographic data handling: GEOADD , GEOPOS , GEODIST , GEOHASH , GEORADIUS , and GEORADIUSBYMEMBER .

GEOADD – add a member with longitude, latitude and name to a key.

GEOPOS – return the stored longitude and latitude of one or more members.

GEODIST – return the distance between two members.

GEOHASH – return the Geohash representation of one or more members.

GEORADIUS – return all members within a given radius of a coordinate.

GEORADIUSBYMEMBER – same as GEORADIUS but the center is an existing member.

Combining GEOADD and GEORADIUS implements the basic “add” and “query” functions required for a nearby‑people service.

GEOADD

Usage

GEOADD key longitude latitude member [longitude latitude member ...]

The command stores each member in a Redis ordered set (zset) where the score is a 52‑bit Geohash value derived from the supplied longitude and latitude.

Source‑code analysis

void geoaddCommand(client *c) {
    // argument validation
    if ((c->argc - 2) % 3 != 0) {
        addReplyError(c, "syntax error. Try GEOADD key [lon lat name] ...");
        return;
    }
    int elements = (c->argc - 2) / 3;
    int argc = 2 + elements * 2; // ZADD key score member ...
    robj **argv = zcalloc(argc * sizeof(robj*));
    argv[0] = createRawStringObject("zadd", 4);
    argv[1] = c->argv[1];
    incrRefCount(argv[1]);
    for (int i = 0; i < elements; i++) {
        double xy[2];
        if (extractLongLatOrReply(c, (c->argv+2)+(i*3), xy) == C_ERR) {
            // cleanup omitted for brevity
            return;
        }
        GeoHashBits hash;
        geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash);
        GeoHashFix52Bits bits = geohashAlign52Bits(hash);
        robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits));
        robj *val = c->argv[2 + i*3 + 2];
        argv[2 + i*2] = score;
        argv[3 + i*2] = val;
        incrRefCount(val);
    }
    replaceClientCommandVector(c, argc, argv);
    zaddCommand(c);
}

The implementation validates arguments, converts latitude/longitude to a 52‑bit Geohash, builds a ZADD command vector, and finally inserts the member into the ordered set.

double precision provides 52 bits; the Geohash encodes about a 0.6 m × 0.6 m cell, resulting in a theoretical positional error of roughly 0.424 m.

Algorithm summary

GEOADD performs three main steps: (1) argument extraction and validation, (2) conversion of coordinates to a 52‑bit Geohash score, and (3) insertion of the member and its score into the target zset via ZADD.

GEORADIUS

Usage

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count] [STORE key] [STOREDIST key]

The command returns all members whose distance from the given coordinate does not exceed the specified radius. Optional flags control the returned data and sorting.

WITHDIST – also return the distance.

WITHCOORD – also return longitude and latitude.

WITHHASH – also return the raw 52‑bit Geohash score.

ASC / DESC – sort results by distance.

COUNT – limit the number of returned members.

STORE / STOREDIST – store results in a key (makes the command a write operation).

Because STORE and STOREDIST turn GEORADIUS into a write command, it is routed to the master instance; read‑only variants GEORADIUS_RO and GEORADIUSBYMEMBER_RO were added in Redis 3.2.10 and 4.0.0.

Source‑code analysis

void georadiusGeneric(client *c, int flags) {
    robj *key = c->argv[1];
    robj *storekey = NULL;
    int stoRedis = 0; // 0 for STORE, 1 for STOREDIST
    // lookup the ordered set
    robj *zobj = lookupKeyReadOrReply(c, key, shared.null[c->resp]);
    if (!zobj || checkType(c, zobj, OBJ_ZSET)) return;
    // parse center point (coordinates or member) and radius
    double xy[2] = {0};
    if (flags & RADIUS_COORDS) {
        // extract longitude/latitude from arguments
    }
    double radius_meters = 0, conversion = 1;
    if ((radius_meters = extractDistanceOrReply(c, c->argv + base_args - 2, &conversion)) < 0) return;
    // parse optional flags (WITHDIST, WITHHASH, WITHCOORD, ASC/DESC, COUNT, STORE)
    // compute geohash area covering the radius
    GeoHashRadius georadius = geohashGetAreasByRadiusWGS84(xy[0], xy[1], radius_meters);
    // search the 9 neighboring geohash boxes
    geoArray *ga = geoArrayCreate();
    membersOfAllNeighbors(zobj, georadius, xy[0], xy[1], radius_meters, ga);
    if (ga->used == 0 && storekey == NULL) {
        addReplyNull(c);
        geoArrayFree(ga);
        return;
    }
    // build and send reply (omitted for brevity)
    geoArrayFree(ga);
}

The core steps are: (1) validate parameters, (2) compute the bounding box and the highest‑resolution Geohash grid that fully covers the search circle, (3) generate nine neighboring Geohash boxes, (4) for each box retrieve members from the ordered set using the skip‑list range query, and (5) filter the candidates by exact spherical distance.

int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, double radius, geoArray *ga) {
    GeoHashBits neighbors[9];
    neighbors[0] = n.hash;
    // fill neighbors[1]..neighbors[8] with n.neighbors.*
    for (int i = 0; i < 9; i++) {
        if (HASHISZERO(neighbors[i])) continue;
        count += membersOfGeoHashBox(zobj, neighbors[i], ga, lon, lat, radius);
    }
    return count;
}

Each membersOfGeoHashBox call determines the minimum and maximum Geohash values for the box, then uses the ordered set’s skip‑list to locate the first element in that range and iterates forward, checking the exact distance with geohashGetDistanceIfInRadiusWGS84.

Algorithm summary

GEORADIUS works by (1) extracting and validating arguments, (2) converting the center point and radius into a Geohash bounding box of the appropriate precision, (3) generating a 3×3 grid of boxes that fully covers the circle, (4) scanning each box via ordered‑set range queries, and (5) discarding points whose true distance exceeds the radius.

The overall time complexity can be expressed as O(N + log M), where N is the number of members inside the radius and M is the number of members examined in the nine‑grid search.

Geo algorithm illustration
Geo algorithm illustration
Geohash grid illustration
Geohash grid illustration
Skiplist structure
Skiplist structure
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

algorithmredisGeoHashSpatial IndexGEO
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.