A todos nos ha pasado. Eliges una solución NoSQL como Google Cloud Datastore o Firestore por su flexibilidad schema-less y su escalado automático. Entonces, cae el requisito inevitable: “Necesitamos una funcionalidad de ‘Buscar tiendas cerca de mí’.”
Si estuvieras ejecutando PostgreSQL, simplemente habilitarías PostGIS y seguirías adelante. Sin embargo, en el ecosistema NoSQL, específicamente con implementaciones antiguas de Datastore o patrones puros de key-value, el soporte geoespacial nativo suele ser limitado, prohibitivamente costoso o completamente inexistente.
Me encontré con este mismo cuello de botella mientras optimizaba un servicio de localización de alto tráfico. Nuestra implementación “ingenua” inicial obtenía conjuntos de datos (datasets) enteros a nivel de estado y los filtraba en memoria. A medida que nuestros datos crecieron a más de 10.000 registros por región, los timeouts se convirtieron en la norma en lugar de la excepción. No necesitábamos una nueva base de datos; necesitábamos un algoritmo más inteligente.
En este post, compartiré cómo resolvimos esto usando Geohashes. Este patrón probado en batalla te permite consultar datos espaciales 2D utilizando búsquedas de strings 1D, reduciendo drásticamente los costos de consulta mientras potencias el rendimiento.
El Problema: Por qué falla el filtrado “Ingenuo”
Cuando construyes un localizador por primera vez, el conjunto de datos suele ser manejable. Es tentador obtener todos los registros para una región genérica (como un código postal o un Estado) y calcular las distancias en el cliente.
Sin embargo, a medida que tu servicio escala, este enfoque choca contra un muro:
- Latencia de Red: Traer 5.000 registros solo para mostrar los 10 más cercanos es un desperdicio masivo de ancho de banda.
- Overhead de Memoria: Parsear megabytes de JSON puede congelar las interfaces de celular (móvil) o tumbar los servicios de backend.
- Costo: Muchas bases de datos NoSQL cobran por lectura (read). Filtrar del lado del cliente significa que pagas por miles de lecturas que el usuario nunca llega a ver.
Necesitábamos un enfoque quirúrgico: consultar solo el bounding box relevante para el usuario, confiando en las capacidades de consulta limitadas de un document store (específicamente, filtros de igualdad y comparaciones de rango).
La Solución: Geohashes
La solución es el Geohashing.
Para los que no estén familiarizados, un Geohash codifica una ubicación geográfica (latitud/longitud) en una cadena corta de letras y dígitos. Funciona dividiendo recursivamente el mundo en una cuadrícula (grid):
gcubre un pedazo masivo del planeta.gces un pedazo más pequeño dentro deg.gcpes aún más pequeño.
Por qué esto funciona para NoSQL
La elegancia de los Geohashes radica en su proximidad basada en prefijos: los prefijos compartidos indican cercanía física. Si dos puntos comparten un prefijo largo (por ejemplo, dr5ru...), son vecinos.
Esto transforma una consulta espacial compleja en dos dimensiones en una simple query de rango de strings, una tarea que las bases de datos NoSQL manejan con una eficiencia increíble.
Guía de Implementación
Utilizamos la librería geofire-common (disponible vía npm). Esta abstrae la matemática pesada requerida para calcular los bounding boxes.
1. El Path de Escritura: Almacenando el Hash
Cuando creas o actualizas tu registro de “Tienda” o “Ubicación de Servicio”, debes calcular el hash y guardarlo junto con las coordenadas.
import { geohashForLocation } from "geofire-common";
const lat = 40.7128;
const lng = -74.006;
const hash = geohashForLocation([lat, lng]);
// Guardar 'hash', 'lat', y 'lng' en tu documento NoSQL
// Usamos el hash para queries y lat/lng para comparaciones precisas de distancia
await db.collection("locations").doc("nyc-store").update({
geohash: hash,
lat: lat,
lng: lng,
});
2. El Path de Lectura: Calculando los Límites (Bounds)
Aquí es donde ocurre la optimización. Cuando un usuario solicita “Tiendas en un radio de 8 km (5 millas)”, no escaneamos toda la tabla. En su lugar, calculamos los “bounds” del Geohash para ese radio específico.
Debido a la alineación de la cuadrícula, un área de búsqueda circular podría superponerse hasta con 9 cuadrados diferentes de la cuadrícula de geohash.
import { geohashQueryBounds, distanceBetween } from "geofire-common";
const center = [lat, lng];
const radiusInMeters = 8 * 1000; // 8 km (aprox 5 millas)
// Obtener las claves de inicio/fin para los cuadrados de la cuadrícula relevantes
const bounds = geohashQueryBounds(center, radiusInMeters);
3. Consultando y Filtrando
Probablemente necesitarás ejecutar algunas queries en paralelo, una para cada límite (bound), y fusionar los resultados.
Nota Crucial: Los Geohashes son rectangulares, pero tu radio de búsqueda es circular. Encontrarás “falsos positivos” (resultados en la esquina del cuadrado que están fuera de tu círculo). Debes filtrar estos del lado del cliente usando un cálculo de distancia preciso.
Optimizando la UX: El Patrón de “Anillo Expansivo”
Simplemente consultar un radio estático a menudo resulta en una mala experiencia de usuario. En una ciudad densa, 8 km podrían arrojar 50 resultados, mientras que en una zona rural, podrían ser cero.
Para resolver esto sin martillar la base de datos, implementamos un algoritmo de anillo expansivo:
- Intento 1: Consultar los 25 registros más cercanos dentro de 8 km.
- Check: ¿Obtuvimos menos de 25 resultados?
- Intento 2: Si es así, duplicar el radio a 16 km y consultar de nuevo.
- Repetir: Continuar expandiendo (32, 64, 128 km) hasta que se cumpla el umbral o se alcance un límite máximo.
Esto asegura que los usuarios urbanos obtengan resultados rápidos sin traer datos de más (over-fetching), mientras que los usuarios rurales igual encuentran lo que necesitan.
Casos Borde del Mundo Real
Implementar esto en producción reveló un par de “detalles” que la mayoría de los tutoriales omiten.
1. La Trampa del Punto Flotante
Nunca confíes ciegamente en las coordenadas proporcionadas por cargas de archivos (ficheros) CSV o sistemas legacy. Como discutí en mi artículo anterior, “El error de los 24 kilómetros”, truncar la precisión flotante en tu latitud/longitud hace que el Geohash sea inexacto.
- Si tu Base de Datos almacena
41.8en lugar de41.88203, tu Geohash apuntará a un cuadrado de la cuadrícula completamente diferente. - Regla: Siempre sanitiza y valida la precisión de las coordenadas antes de generar el hash.
2. Datos Obsoletos (Staleness)
La información de ubicación de las tiendas puede cambiar. Si dependes de APIs de terceros para obtener tus ubicaciones, necesitas una estrategia para actualizaciones incrementales.
- Verificamos el timestamp de “última actualización” en las lecturas.
- Si un registro está “obsoleto” (ej. > 6 meses), disparamos un background job para volver a obtener la geodata de la fuente de la verdad (como Google Maps o la API del proveedor) para asegurar que el geohash siga siendo preciso.
Conclusión
No siempre necesitas migrar a una base de datos nativa geoespacial para desbloquear funcionalidades de ubicación de alto rendimiento. Aprovechando los Geohashes y librerías como geofire-common para el ecosistema JavaScript o thephpleague/geotools para PHP, puedes construir un servicio de localización robusto directamente sobre Datastore, Firestore o cualquier otra base de datos NoSQL.
Es eficiente, rentable y escala sin esfuerzo con tu tráfico.
Si estás luchando con datos de coordenadas desordenados, te recomiendo encarecidamente leer mi análisis sobre errores de Precisión Flotante.
¿Has implementado Geohashing en una DB no estándar? Cuéntame tus anécdotas o historias de guerra en los comentarios.