Advanced Topics

Geospatial Data

Master location-based queries with Cassandra using GeoHash, quadkeys, and spatial indexing for ride-sharing, delivery, and location services!

🌍 What is Geospatial Data?

The Ride-Sharing Problem 🚗

You're building a ride-sharing app like Uber. A user opens the app in San Francisco:

  • 📍 User location: 37.7749° N, 122.4194° W (lat/lon)
  • 🚕 Need to find: All available drivers within 5km
  • ⚡ Response time: < 100ms
  • 🔄 Updates: Every 10 seconds as drivers move
  • 📊 Scale: 10,000+ drivers in San Francisco alone

Challenge: How do you efficiently query "find all points within radius" in Cassandra?

Problem: ❌ Can't query by distance natively
Solution: ✅ Use GeoHash or QuadKeys to partition the world!

Geospatial Data Explained

Geospatial data = Location data (latitude/longitude) that requires proximity-based queries like "find nearby", "within radius", "along route".

Common Geospatial Queries:
  • 🔍 Proximity Search: "Find restaurants within 2km"
  • 📍 Nearest Neighbor: "Find closest 10 stores"
  • 🗺️ Bounding Box: "Show all pins on this map view"
  • 🛣️ Route Query: "Find gas stations along route"
  • 🏢 Containment: "Is location inside polygon?"

Geospatial Use Cases

🚗

Ride-Sharing

Uber, Lyft, Grab

  • Find nearby drivers
  • Real-time location tracking
  • Route optimization
  • Surge pricing zones
🍕

Food Delivery

DoorDash, UberEats

  • Restaurant proximity
  • Delivery zones
  • Driver dispatch
  • ETAs calculation
📱

Social Networks

Instagram, Snapchat

  • Nearby friends
  • Location-based posts
  • Geotagged stories
  • Event discovery
🏨

Travel & Hospitality

Airbnb, Booking.com

  • Property search
  • Map-based browsing
  • Nearby attractions
  • Availability zones

❓ The Geospatial Challenge in Cassandra

Why Geospatial is Hard in Cassandra

Cassandra has NO built-in geospatial functions. No ST_Distance, no spatial indexes, no geometry types.

What Doesn't Work

-- ❌ This is NOT possible in Cassandra: SELECT * FROM drivers WHERE ST_Distance(location, '37.7749,-122.4194') < 5000; -- ERROR: ST_Distance doesn't exist! SELECT * FROM restaurants WHERE latitude BETWEEN 37.7 AND 37.8 AND longitude BETWEEN -122.5 AND -122.4; -- ERROR: Range queries require clustering column! -- ❌ Can't query by coordinate ranges without partition key

The Core Problem

Why Coordinate Queries Fail

  • 🔑 Partition Key Required: Must include partition key in WHERE clause
  • 📊 No Range Queries: Can't do latitude BETWEEN without partition key
  • 🌍 2D Problem: Lat/lon are 2 dimensions, Cassandra thinks 1D (row order)
  • ⚡ Performance: Scanning all rows to check distance = SLOW!

The Solution: Spatial Partitioning

Convert 2D coordinates (lat, lon) into 1D partition keys that group nearby locations together!

This is what GeoHash and QuadKeys do - they divide the Earth into a grid and give each grid cell a unique string ID.

🔢 GeoHash - The Most Popular Approach

What is GeoHash?

GeoHash encodes latitude/longitude into a short string like 9q8yy that represents a geographic area.

How GeoHash Works

Visual Example:

San Francisco (37.7749, -122.4194) → GeoHash: 9q8yy9mf1h5v

Character Precision:

  • 9 (1 char) = ±2,500 km (whole region)
  • 9q (2 chars) = ±630 km
  • 9q8 (3 chars) = ±78 km
  • 9q8y (4 chars) = ±20 km
  • 9q8yy (5 chars) = ±2.4 km ← Good for city searches!
  • 9q8yy9 (6 chars) = ±610 m
  • 9q8yy9m (7 chars) = ±76 m ← Street-level precision
  • 9q8yy9mf (8 chars) = ±19 m

Key Insight: Locations with the same GeoHash prefix are geographically close!
9q8yyxyz and 9q8yyabc are ~100m apart.

Implementing GeoHash in Cassandra

-- Table design with GeoHash partition key CREATE TABLE drivers_by_location ( geohash text, ← Partition key (5 chars = ~2.4km) driver_id uuid, ← Clustering key latitude double, longitude double, driver_name text, vehicle_type text, available boolean, last_updated timestamp, PRIMARY KEY ((geohash), driver_id) ); -- Index for quick status filtering CREATE INDEX ON drivers_by_location (available);

Python Implementation

import pygeohash as pgh from cassandra.cluster import Cluster # Connect to Cassandra cluster = Cluster(['localhost']) session = cluster.connect('rideshare') # 1. INSERT: Update driver location def update_driver_location(driver_id, lat, lon): # Encode lat/lon to geohash (5 chars = ~2.4km grid) geohash = pgh.encode(lat, lon, precision=5) session.execute(""" INSERT INTO drivers_by_location (geohash, driver_id, latitude, longitude, available, last_updated) VALUES (%s, %s, %s, %s, %s, toTimestamp(now())) """, (geohash, driver_id, lat, lon, True)) print(f"Driver {driver_id} at GeoHash: {geohash}") # Example: Driver in San Francisco update_driver_location( driver_id='550e8400-e29b-41d4-a716-446655440000', lat=37.7749, lon=-122.4194 ) # Output: Driver ... at GeoHash: 9q8yy # 2. QUERY: Find drivers near user def find_nearby_drivers(user_lat, user_lon, radius_km=5): # Get user's geohash user_geohash = pgh.encode(user_lat, user_lon, precision=5) # Get neighboring geohashes (covers edge cases) neighbors = pgh.get_adjacent(user_geohash) geohashes_to_check = [user_geohash] + neighbors drivers = [] for gh in geohashes_to_check: # Query Cassandra for each geohash result = session.execute(""" SELECT * FROM drivers_by_location WHERE geohash = %s AND available = true """, (gh,)) for row in result: # Calculate actual distance distance = haversine_distance( user_lat, user_lon, row.latitude, row.longitude ) if distance <= radius_km: drivers.append({ 'driver_id': row.driver_id, 'distance_km': distance, 'location': (row.latitude, row.longitude) }) # Sort by distance drivers.sort(key=lambda d: d['distance_km']) return drivers # Haversine formula for distance def haversine_distance(lat1, lon1, lat2, lon2): from math import radians, sin, cos, sqrt, atan2 R = 6371 # Earth radius in km lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) dlat = lat2 - lat1 dlon = lon2 - lon1 a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 c = 2 * atan2(sqrt(a), sqrt(1-a)) return R * c

GeoHash Precision Guide

Precision Cell Size Use Case
4 chars ±20 km Country/regional search
5 chars ±2.4 km City-wide (Uber drivers, restaurants)
6 chars ±610 m Neighborhood search
7 chars ±76 m Street-level (building search)
8 chars ±19 m Precise location (parking spots)

GeoHash Edge Cases

  • ⚠️ Border Problem: Points 1m apart can have different geohashes if on grid border
  • ⚠️ Solution: Always check neighboring geohashes (8 neighbors)
  • ⚠️ Polar Regions: Grid cells become smaller near poles
  • ⚠️ Date Line: Special handling for longitude ±180°

📐 QuadKeys - Microsoft's Alternative

What are QuadKeys?

QuadKeys are Microsoft's geospatial indexing system used in Bing Maps. Similar to GeoHash but uses quadtree division.

QuadKey vs GeoHash

GeoHash

  • Encoding: Base32 (0-9, a-z)
  • Example: 9q8yy9mf
  • Grid: Alternating lat/lon bits
  • Pro: Shorter strings
  • Con: Complex edge cases

QuadKey

  • Encoding: Base4 (0, 1, 2, 3)
  • Example: 02301012133
  • Grid: Recursive quadtree
  • Pro: Map tile aligned
  • Con: Longer strings

Python QuadKey Implementation

import pyquadkey2 as pqk # Convert lat/lon to quadkey quadkey = pqk.from_geo((37.7749, -122.4194), level=12) print(quadkey) # Output: '023010121330' # Cassandra table with QuadKey """ CREATE TABLE drivers_by_quadkey ( quadkey text, driver_id uuid, latitude double, longitude double, PRIMARY KEY ((quadkey), driver_id) ); """

🎨 Data Modeling for Geospatial Queries

Pattern 1: Single GeoHash Table (Simple)

CREATE TABLE places_by_location ( geohash text, ← Partition by grid cell place_id uuid, name text, category text, ← restaurant, cafe, etc latitude double, longitude double, rating decimal, PRIMARY KEY ((geohash), place_id) ); -- Pro: Simple, fast proximity queries -- Con: Hot partitions in dense areas

Pattern 2: Multi-Level GeoHash (Advanced)

CREATE TABLE places_by_precision ( precision int, ← 4, 5, 6, 7 char geohash geohash text, place_id uuid, name text, latitude double, longitude double, PRIMARY KEY ((precision, geohash), place_id) ); -- Use different precisions for different zoom levels: -- Zoom out (country view) → precision 4 -- City view → precision 5 -- Street view → precision 7

Pattern 3: Hybrid Bucketing (Production)

CREATE TABLE drivers_location ( city text, ← Coarse bucket (san_francisco) geohash text, ← Fine-grained grid (5 chars) driver_id uuid, latitude double, longitude double, available boolean, last_updated timestamp, PRIMARY KEY ((city, geohash), driver_id) ) WITH CLUSTERING ORDER BY (driver_id ASC); -- Benefits: -- 1. Prevents global hot partitions -- 2. City-level distribution -- 3. Easy to add/remove cities

🔍 Common Query Patterns

Query 1: Find Nearby (Proximity Search)

# Python - Find restaurants within 3km def find_nearby_restaurants(user_lat, user_lon, radius_km=3): # 1. Calculate user's geohash precision = 6 # 610m cells user_gh = pgh.encode(user_lat, user_lon, precision) # 2. Get geohash + 8 neighbors (3x3 grid) search_area = [user_gh] + pgh.get_adjacent(user_gh) # 3. Query each geohash restaurants = [] for gh in search_area: rows = session.execute(""" SELECT * FROM places_by_location WHERE geohash = %s AND category = 'restaurant' """, (gh,)) for row in rows: dist = haversine_distance( user_lat, user_lon, row.latitude, row.longitude ) if dist <= radius_km: restaurants.append({ 'name': row.name, 'distance': dist, 'rating': row.rating }) # 4. Sort by distance return sorted(restaurants, key=lambda r: r['distance'])[:10]

Query 2: Bounding Box Search

# Find all places in map viewport def get_places_in_bounds(north, south, east, west): # Get all geohashes covering bounding box geohashes = get_geohashes_in_bbox(north, south, east, west, precision=5) places = [] for gh in geohashes: rows = session.execute(""" SELECT * FROM places_by_location WHERE geohash = %s """, (gh,)) places.extend(rows) return places

Query 3: K-Nearest Neighbors

# Find 10 closest coffee shops def find_nearest_coffee(user_lat, user_lon, k=10): precision = 6 user_gh = pgh.encode(user_lat, user_lon, precision) # Start with immediate area, expand if needed candidates = [] search_radius = 1 # geohash radius while len(candidates) < k * 2: # Get 2x for filtering geohashes = get_geohashes_at_radius(user_gh, search_radius) for gh in geohashes: rows = session.execute(""" SELECT * FROM places_by_location WHERE geohash = %s AND category = 'coffee' """, (gh,)) candidates.extend(rows) search_radius += 1 if search_radius > 5: # Safety limit break # Calculate distances and return k nearest for c in candidates: c.distance = haversine_distance(user_lat, user_lon, c.latitude, c.longitude) return sorted(candidates, key=lambda c: c.distance)[:k]

🎯 Real-World Use Cases

Use Case 1: Ride-Sharing Driver Matching

Uber/Lyft Architecture

-- Table: Driver locations updated every 10s CREATE TABLE active_drivers ( city text, ← SF, NYC, LA geohash text, ← 5-char grid (~2.4km) driver_id uuid, latitude double, longitude double, car_type text, ← UberX, UberXL, etc current_status text, ← available, busy, offline last_ping timestamp, PRIMARY KEY ((city, geohash), driver_id) ) WITH default_time_to_live = 300; ← Auto-expire after 5min -- Query: User requests ride in San Francisco -- 1. Calculate user's geohash: 9q8yy -- 2. Query geohash + neighbors (9 cells) -- 3. Filter by car_type and status -- 4. Calculate actual distance -- 5. Return 5 closest available drivers

Result: Sub-100ms driver search even with 100,000+ active drivers!

Use Case 2: Food Delivery Zone Assignment

-- DoorDash/UberEats delivery zones CREATE TABLE delivery_zones ( city text, geohash text, ← 5-char grid zone_id text, restaurants list, ← Restaurants in this zone drivers list, ← Assigned drivers avg_delivery_time int, ← Minutes surge_multiplier decimal, PRIMARY KEY ((city, geohash)) ); -- Benefits: -- • Automatic zone assignment by location -- • Easy to calculate coverage gaps -- • Dynamic surge pricing per zone

Use Case 3: Social "Nearby Friends"

-- Snapchat/Instagram location sharing CREATE TABLE user_locations ( geohash text, ← 6-char for privacy (~600m) user_id uuid, latitude double, longitude double, shared_with set, ← Friends who can see expires_at timestamp, PRIMARY KEY ((geohash), user_id) ) WITH default_time_to_live = 3600; ← 1 hour expiry -- Query: Show nearby friends -- 1. Get user's geohash -- 2. Query geohash + neighbors -- 3. Filter by shared_with (privacy!) -- 4. Return friends within 5km

✅ Geospatial Best Practices

✅ DO These

  • Use GeoHash 5-6 chars for cities
  • Check neighboring geohashes
  • Calculate actual distance (Haversine)
  • Use TTL for moving objects
  • Add city/region bucket
  • Index status fields (available)
  • Monitor hot partitions
  • Cache frequent queries

❌ DON'T Do These

  • Store only lat/lon (no geohash)
  • Use too high precision (8+)
  • Forget edge cases (borders)
  • Skip distance calculation
  • Create global hot spots
  • Update location too frequently
  • Assume geohash = distance
  • Ignore privacy concerns

Precision Selection Guide

  • ✅ Ride-sharing: 5-6 chars (city to neighborhood)
  • ✅ Restaurant search: 6-7 chars (street level)
  • ✅ Social nearby: 5-6 chars (privacy balance)
  • ✅ Delivery zones: 4-5 chars (regional)
  • ✅ Asset tracking: 7-8 chars (building level)

🎯 Geospatial Summary

You now understand geospatial queries in Cassandra!

📚 Key Takeaways:

  • 🌍 Cassandra has NO native geospatial - need GeoHash/QuadKeys
  • 🔢 GeoHash converts lat/lon → string partition key
  • 📏 5 chars = ~2.4km (perfect for city searches)
  • 🎯 Always check 9 cells (geohash + 8 neighbors)
  • 📐 Calculate actual distance with Haversine formula
  • 🏙️ Add city bucket to prevent hot partitions
  • ⚡ Real use: Uber, DoorDash, Instagram, Airbnb

GeoHash + Cassandra = Fast proximity queries at any scale! 🌍🚀

Advertisement

Responsive Ad