GeoHash Indexing
GeoHash is the simplest spatial indexing system, encoding geographic coordinates into a sortable string that enables efficient DynamoDB BETWEEN queries.
How GeoHash Works
GeoHash divides the Earth into a grid of rectangular cells using a Z-order curve. Each character in the hash narrows the area by a factor of 32, providing progressively finer precision.
var sanFrancisco = new GeoLocation(37.7749, -122.4194);
string hash = sanFrancisco.ToGeoHash(7); // "9q8yy9r"
// Each character narrows the area:
// "9" → ~2500km × 5000km
// "9q" → ~630km × 1250km
// "9q8" → ~78km × 156km
// "9q8y" → ~20km × 39km
// "9q8yy" → ~2.4km × 4.9km
// "9q8yy9" → ~610m × 1.2km
// "9q8yy9r" → ~76m × 153m
Entity Definition
using Oproto.FluentDynamoDb.Attributes;
using Oproto.FluentDynamoDb.Geospatial;
[DynamoDbTable("stores")]
public partial class Store
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string StoreId { get; set; } = string.Empty;
[DynamoDbAttribute("location", GeoHashPrecision = 7)]
public GeoLocation Location { get; set; }
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
}
Precision Levels
| Precision | Cell Size | Use Case |
|---|---|---|
| 4 | ~20 km × 39 km | City-wide searches |
| 5 | ~2.4 km × 4.9 km | Neighborhood searches |
| 6 | ~610 m × 1.2 km | Default — District searches |
| 7 | ~76 m × 153 m | Street-level searches |
| 8 | ~19 m × 38 m | Building-level searches |
| 9 | ~4.8 m × 4.8 m | Precise location tracking |
Querying
Lambda Expression Queries
using Oproto.FluentDynamoDb.Geospatial.GeoHash;
var center = new GeoLocation(37.7749, -122.4194);
// Find stores within 5 kilometers
var nearbyStores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceKilometers(center, 5))
.ExecuteAsync();
// Sort by actual distance (DynamoDB can't sort by distance)
var sorted = nearbyStores
.OrderBy(s => s.Location.DistanceToKilometers(center))
.ToList();
All Distance Units
// Meters
var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceMeters(center, 5000))
.ExecuteAsync();
// Kilometers
var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceKilometers(center, 5))
.ExecuteAsync();
// Miles
var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceMiles(center, 3.1))
.ExecuteAsync();
Bounding Box Queries
var southwest = new GeoLocation(37.7, -122.5);
var northeast = new GeoLocation(37.8, -122.4);
var stores = await storeTable.Query
.Where<Store>(x => x.Location.WithinBoundingBox(southwest, northeast))
.ExecuteAsync();
Performance
GeoHash uses a single BETWEEN query regardless of search radius, making it the fastest option:
| Scenario | Query Count | Latency |
|---|---|---|
| Any radius, any precision | 1 query | ~50ms |
This is because GeoHash encodes to a sortable string, enabling a single range scan.
When to Use GeoHash
Best for:
- Simple proximity queries
- Low latency requirements (single query)
- Mid-latitude locations (between ±60°)
- Backward compatibility with existing GeoHash data
Limitations:
- Rectangular cells (not uniform area)
- Poor precision near poles (cells become elongated)
- Cell boundary issues (nearby items may be in different cells)
- Queries return rectangular areas, not circles
Post-Filtering for Exact Distances
GeoHash queries return rectangular bounding boxes. Always post-filter for exact circular distances:
var center = new GeoLocation(37.7749, -122.4194);
var radiusKm = 5.0;
// Query returns rectangular area
var candidates = await storeTable.Query
.Where<Store>(x => x.Location.WithinDistanceKilometers(center, radiusKm))
.ExecuteAsync();
// Post-filter for exact circular distance
var exactResults = candidates
.Where(s => s.Location.DistanceToKilometers(center) <= radiusKm)
.OrderBy(s => s.Location.DistanceToKilometers(center))
.ToList();
See Also
- Geospatial Overview — Installation and configuration
- S2 Cells — Better uniformity and polar coverage
- H3 Indexing — Hexagonal grid with uniform neighbors
- Query Optimization — Performance tuning