Geospatial Data

Build location-based features with Redis GEO commands. Find nearby places, calculate distances, and implement geo-fencing.

Why Redis for Geospatial?#

  • Sub-millisecond queries — find 1000 nearby locations instantly
  • Built-in distance calculation — no external libraries needed
  • Sorted by distance — results ordered automatically
  • Memory efficient — uses sorted sets under the hood
  • Real-time updates — locations can move (delivery drivers, etc.)

Basic Location Storage#

Store and query locations using GEOADD and GEOSEARCH:

TypeScript
import Redis from 'ioredis'; const redis = new Redis(process.env.ARCTICKEY_URL); // Add locations: GEOADD key longitude latitude member await redis.geoadd('stores', 13.361389, 52.519444, 'store:berlin', -0.127758, 51.507351, 'store:london', 2.352222, 48.856614, 'store:paris', 18.068581, 59.329323, 'store:stockholm' ); // Find stores within 500km of a point const nearby = await redis.geosearch( 'stores', 'FROMMEMBER', 'store:berlin', 'BYRADIUS', 1000, 'km', 'WITHDIST', 'ASC', 'COUNT', 10 ); // Result: [['store:berlin', '0'], ['store:paris', '878.7']]

Find Nearby Locations#

TypeScript
interface Location { id: string; name: string; lat: number; lng: number; distance?: number; } class GeoService { private key: string; constructor(key: string) { this.key = key; } // Add a location async addLocation(id: string, lat: number, lng: number) { await redis.geoadd(this.key, lng, lat, id); } // Add multiple locations async addLocations(locations: Array<{ id: string; lat: number; lng: number }>) { const args: (string | number)[] = []; for (const loc of locations) { args.push(loc.lng, loc.lat, loc.id); } await redis.geoadd(this.key, ...args); } // Find nearby locations async findNearby( lat: number, lng: number, radius: number, unit: 'km' | 'm' | 'mi' = 'km', limit: number = 10 ): Promise<Array<{ id: string; distance: number }>> { const results = await redis.geosearch( this.key, 'FROMLONLAT', lng, lat, 'BYRADIUS', radius, unit, 'WITHDIST', 'ASC', 'COUNT', limit ); return results.map(([id, dist]) => ({ id: id as string, distance: parseFloat(dist as string), })); } // Get distance between two locations async getDistance(id1: string, id2: string, unit: 'km' | 'm' | 'mi' = 'km'): Promise<number | null> { const dist = await redis.geodist(this.key, id1, id2, unit); return dist ? parseFloat(dist) : null; } // Get coordinates of a location async getPosition(id: string): Promise<{ lat: number; lng: number } | null> { const pos = await redis.geopos(this.key, id); if (!pos || !pos[0]) return null; return { lng: parseFloat(pos[0][0]), lat: parseFloat(pos[0][1]), }; } // Remove a location async removeLocation(id: string) { await redis.zrem(this.key, id); } } // Usage: Find restaurants near user const restaurants = new GeoService('geo:restaurants'); await restaurants.addLocations([ { id: 'rest:1', lat: 59.3293, lng: 18.0686 }, { id: 'rest:2', lat: 59.3326, lng: 18.0649 }, { id: 'rest:3', lat: 59.3375, lng: 18.0711 }, ]); // User's location const userLat = 59.3310; const userLng = 18.0670; const nearbyRestaurants = await restaurants.findNearby(userLat, userLng, 2, 'km'); // [{ id: 'rest:2', distance: 0.31 }, { id: 'rest:1', distance: 0.21 }, ...]

Delivery Radius Check#

Check if an address is within delivery range:

TypeScript
class DeliveryService { private storesKey = 'geo:stores'; private maxDeliveryRadius = 10; // km async isDeliverable( storeLat: number, storeLng: number, customerLat: number, customerLng: number ): Promise<{ deliverable: boolean; distance: number }> { // Use a temporary key to calculate distance const tempKey = `temp:delivery:${Date.now()}`; await redis.geoadd(tempKey, storeLng, storeLat, 'store', customerLng, customerLat, 'customer' ); const dist = await redis.geodist(tempKey, 'store', 'customer', 'km'); await redis.del(tempKey); const distance = parseFloat(dist || '0'); return { deliverable: distance <= this.maxDeliveryRadius, distance, }; } // Find all stores that can deliver to a location async findDeliveringStores(customerLat: number, customerLng: number) { return redis.geosearch( this.storesKey, 'FROMLONLAT', customerLng, customerLat, 'BYRADIUS', this.maxDeliveryRadius, 'km', 'WITHDIST', 'ASC' ); } }

Real-Time Location Tracking#

Track moving entities (delivery drivers, vehicles):

TypeScript
class LiveTracking { private driversKey = 'geo:drivers:active'; private historyPrefix = 'driver:history:'; // Update driver location (call every few seconds) async updateLocation(driverId: string, lat: number, lng: number) { const timestamp = Date.now(); await redis.pipeline() // Update current position .geoadd(this.driversKey, lng, lat, driverId) // Store in history (for route replay) .zadd(`${this.historyPrefix}${driverId}`, timestamp, JSON.stringify({ lat, lng, t: timestamp })) // Keep only last hour of history .zremrangebyscore(`${this.historyPrefix}${driverId}`, 0, timestamp - 3600000) // Set expiry on driver position (remove if no update in 5 min) .expire(this.driversKey, 300) .exec(); } // Find drivers near a pickup location async findNearbyDrivers(lat: number, lng: number, radiusKm: number = 5) { const drivers = await redis.geosearch( this.driversKey, 'FROMLONLAT', lng, lat, 'BYRADIUS', radiusKm, 'km', 'WITHDIST', 'WITHCOORD', 'ASC', 'COUNT', 20 ); return drivers.map(([id, dist, coords]) => ({ driverId: id, distance: parseFloat(dist as string), lat: parseFloat((coords as string[])[1]), lng: parseFloat((coords as string[])[0]), })); } // Get driver's route history async getRouteHistory(driverId: string, lastMinutes: number = 30) { const since = Date.now() - (lastMinutes * 60 * 1000); const history = await redis.zrangebyscore( `${this.historyPrefix}${driverId}`, since, '+inf' ); return history.map(h => JSON.parse(h)); } // Mark driver as offline async driverOffline(driverId: string) { await redis.zrem(this.driversKey, driverId); } } // Usage const tracking = new LiveTracking(); // Driver app sends location updates await tracking.updateLocation('driver:123', 59.3293, 18.0686); // Customer app finds nearby drivers const nearby = await tracking.findNearbyDrivers(59.3310, 18.0670, 3);

Geo-Fencing#

Trigger actions when entities enter/exit areas:

TypeScript
interface GeoFence { id: string; name: string; lat: number; lng: number; radiusMeters: number; } class GeoFencing { private fencesKey = 'geo:fences'; async createFence(fence: GeoFence) { await redis.pipeline() .geoadd(this.fencesKey, fence.lng, fence.lat, fence.id) .hset(`fence:${fence.id}`, { name: fence.name, radius: fence.radiusMeters.toString(), lat: fence.lat.toString(), lng: fence.lng.toString(), }) .exec(); } async checkFences(entityId: string, lat: number, lng: number): Promise<{ entered: GeoFence[]; exited: GeoFence[]; inside: GeoFence[]; }> { const previousKey = `entity:fences:${entityId}`; // Get all fences const allFences = await redis.zrange(this.fencesKey, 0, -1); // Check which fences we're inside const inside: string[] = []; for (const fenceId of allFences) { const fenceData = await redis.hgetall(`fence:${fenceId}`); const distance = this.calculateDistance( lat, lng, parseFloat(fenceData.lat), parseFloat(fenceData.lng) ); if (distance <= parseFloat(fenceData.radius)) { inside.push(fenceId); } } // Get previous state const previouslyInside = await redis.smembers(previousKey); // Calculate changes const entered = inside.filter(f => !previouslyInside.includes(f)); const exited = previouslyInside.filter(f => !inside.includes(f)); // Update state if (inside.length > 0) { await redis.del(previousKey); await redis.sadd(previousKey, ...inside); } else { await redis.del(previousKey); } // Get full fence objects for results const getFenceDetails = async (ids: string[]) => { const fences: GeoFence[] = []; for (const id of ids) { const data = await redis.hgetall(`fence:${id}`); fences.push({ id, name: data.name, lat: parseFloat(data.lat), lng: parseFloat(data.lng), radiusMeters: parseFloat(data.radius), }); } return fences; }; return { entered: await getFenceDetails(entered), exited: await getFenceDetails(exited), inside: await getFenceDetails(inside), }; } private calculateDistance(lat1: number, lng1: number, lat2: number, lng2: number): number { const R = 6371000; // Earth's radius in meters const dLat = (lat2 - lat1) * Math.PI / 180; const dLng = (lng2 - lng1) * Math.PI / 180; const a = Math.sin(dLat/2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng/2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); } } // Usage const fencing = new GeoFencing(); // Create a geo-fence around office await fencing.createFence({ id: 'office', name: 'Company HQ', lat: 59.3293, lng: 18.0686, radiusMeters: 100, }); // Check when employee arrives const result = await fencing.checkFences('employee:123', 59.3294, 18.0685); if (result.entered.length > 0) { console.log('Employee arrived at:', result.entered.map(f => f.name)); // Trigger: clock in, disable alarm, etc. }

Store Locator API#

Complete example for a store locator:

TypeScript
import express from 'express'; const app = express(); const geo = new GeoService('geo:stores'); // Add a store app.post('/api/stores', async (req, res) => { const { id, name, lat, lng, ...metadata } = req.body; await redis.pipeline() .geoadd('geo:stores', lng, lat, id) .hset(`store:${id}`, { name, lat, lng, ...metadata }) .exec(); res.json({ success: true, id }); }); // Find nearby stores app.get('/api/stores/nearby', async (req, res) => { const { lat, lng, radius = 10, limit = 20 } = req.query; const results = await redis.geosearch( 'geo:stores', 'FROMLONLAT', parseFloat(lng as string), parseFloat(lat as string), 'BYRADIUS', parseFloat(radius as string), 'km', 'WITHDIST', 'ASC', 'COUNT', parseInt(limit as string) ); // Fetch store details const stores = await Promise.all( results.map(async ([id, distance]) => { const data = await redis.hgetall(`store:${id}`); return { id, distance: parseFloat(distance as string), ...data, }; }) ); res.json({ stores }); }); // Get store by ID app.get('/api/stores/:id', async (req, res) => { const data = await redis.hgetall(`store:${req.params.id}`); if (!data.name) return res.status(404).json({ error: 'Not found' }); const pos = await redis.geopos('geo:stores', req.params.id); res.json({ id: req.params.id, ...data, coordinates: pos[0] ? { lng: pos[0][0], lat: pos[0][1] } : null, }); });

Best Practices#

  1. Use appropriate precision — GPS is ~3-5m accurate, don't over-engineer
  2. Index strategically — separate keys for different entity types
  3. Clean up stale data — remove old locations, use TTLs
  4. Batch operations — use pipelines for bulk updates
  5. Consider privacy — hash or anonymize location data where possible
  6. Cache common queries — "stores in Stockholm" doesn't change often