Daniel Ancuta es un ingeniero de software con varios años de experiencia utilizando diferentes tecnologías. Es un gran fan de "El Zen de Python", que intenta aplicar no sólo en su código sino también en su vida privada. Puedes encontrarle en Twitter: @daniel_ancuta
Consultas geoespaciales: Uso de Python para buscar ciudades
La información de geolocalización se utiliza a diario en casi todos los aspectos de nuestra interacción con los ordenadores. Ya sea un sitio web que quiere enviarnos notificaciones personalizadas basadas en la ubicación, mapas que nos muestran la ruta más corta posible o simplemente tareas que se ejecutan en segundo plano y comprueban los lugares que hemos visitado.
Hoy me gustaría presentarles a consultas geoespaciales que se utilizan en Couchbase. Consultas geoespaciales permiten buscar documentos en función de su localización geográfica.
Juntos escribiremos una herramienta en Python que utilice consultas geoespaciales con API REST de Couchbase y Búsqueda de texto completo en Couchbaseque nos ayudará a buscar en una base de datos de ciudades.
Requisitos previos
Dependencias
En este artículo he utilizado Couchbase Edición Enterprise 5.1.0 build 5552 y Python 3.6.4.
Para ejecutar fragmentos de este artículo debe instalar Couchbase 2.3 (estoy usando 2.3.4) a través de pip.
Couchbase
- Crear un cubo de ciudades
- Cree una búsqueda de ciudades con un campo geográfico de tipo geopunto. Puede leer sobre ello en el Insertar un campo hijo parte de la documentación.
Debería parecerse a la imagen de abajo:
Rellenar Couchbase con datos
En primer lugar, necesitamos datos para nuestro ejercicio. Para ello, utilizaremos una base de datos de ciudades de geonames.org.
GeoNames contiene dos bases de datos principales: lista de ciudades y lista de códigos postales.
Todos están agrupados por países con la información correspondiente como nombre, coordenadas, población, zona horaria, código de país, etc. Ambos están en formato CSV.
Para este ejercicio, utilizaremos la lista de ciudades. He utilizado PL.zip pero siéntase libre de elegir el que prefiera del lista de ciudades.
Modelo de datos
La clase City será nuestra representación de una única ciudad que utilizaremos en toda la aplicación. Al encapsularla en un modelo, unificamos la API y no necesitamos depender de fuentes de datos de terceros (por ejemplo, un archivo CSV) que podrían cambiar.
La mayoría de nuestros fragmentos se encuentran (hasta que se diga lo contrario) en el archivo core.py. Así que recuerda actualizarlo (especialmente cuando añadas nuevas importaciones) y no anular todo el contenido.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# core.py clase Ciudad: def __init__(auto, geonameid, clase_característica, nombre, población, lat, lon): auto.geonameid = geonameid auto.clase_característica = clase_característica auto.nombre = nombre auto.población = población auto.lat = lat auto.lon = lon @método de clase def from_csv_row(cls, fila): devolver cls(fila[0], fila[7], fila[1], fila[12], fila[4], fila[5]) |
Iterador CSV para procesar ciudades
Como ya tenemos una clase modelo, es hora de preparar un iterador que nos ayude a leer las ciudades del fichero CSV.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# core.py importar csv de colecciones importar Iterador clase CiudadesCsvIterator(Iterador): def __init__(auto, ruta): auto.Camino = ruta auto._fp = Ninguno auto._csv_reader = Ninguno def __enter__(auto): auto._fp = abra(auto.Camino, 'r') auto._csv_reader = csv.lector(auto._fp, delimitador='\t') devolver auto def __exit__(auto, exc_tipo, exc_val, exc_tb): auto._fp.cerrar() def __next__(auto): devolver Ciudad.from_csv_row(siguiente(auto._csv_reader)) |
Insertar ciudades en el bucket de Couchbase
Hemos unificado la forma de representar una ciudad, y tenemos un iterador que leería las del archivo csv.
Es hora de poner estos datos en nuestra fuente de datos principal, Couchbase.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# core.py importar registro importar sys de couchbase.grupo importar Grupo, PasswordAuthenticator registrador = registro.getLogger() registrador.setLevel(registro.DEBUG) registrador.addHandler(registro.StreamHandler(sys.stdout)) def get_bucket(nombre de usuario, contraseña, cadena_de_conexión=couchbase://localhost): grupo = Grupo(cadena_de_conexión) autentificador = PasswordAuthenticator(nombre de usuario, contraseña) grupo.autentifique(autentificador) devolver grupo.open_bucket(ciudades) clase CiudadesServicio: def __init__(auto, cubo): auto._bucket = cubo def cargar_desde_csv(auto, ruta): con CiudadesCsvIterator(ruta) como iterador_ciudades: para ciudad en iterador_ciudades: si ciudad.clase_característica no en (PPL, PPLA, PPLA2, PPLA3, PPLA4, PPLC): continuar registrador.información(f'Insertando {ciudad.geonameid}') auto._bucket.upsert( ciudad.geonameid, { nombre: ciudad.nombre, clase_característica: ciudad.clase_característica, población: ciudad.población, geo: {"lat: float(ciudad.lat), "lon: float(ciudad.lon)} } ) |
Para comprobar si todo lo que hemos escrito hasta ahora funciona, vamos a cargar contenido CSV en Couchbase.
1 2 3 4 5 |
# core.py cubo = get_bucket('admin', prueba123456) servicio_ciudades = CiudadesServicio(cubo) servicio_ciudades.cargar_desde_csv(~/directorio-con-ciudades/PL/PL.txt, cubo) |
En este punto deberías tener las ciudades cargadas en tu bucket de Couchbase. El tiempo que tarde dependerá del país que haya elegido.
Buscar ciudades
Ya tenemos nuestro bucket listo con los datos, así que es hora de volver a CitiesService y preparar unos cuantos métodos que nos ayuden en la búsqueda de ciudades.
Pero antes de empezar, tenemos que modificar ligeramente la clase City, añadiendo el siguiente método:
1 2 3 4 5 6 7 8 9 10 11 12 |
# core.py @método de clase def from_couchbase_dict(cls, fila): campos = fila[campos] devolver cls(fila[id], campos[clase_característica], campos[nombre], campos[población], campos[geo][1], campos[geo][0]) |
Esa es la lista de métodos que implementaremos en CitiesService:
- get_by_name(name, limit=10), devuelve las ciudades por su nombre
- get_by_coordinates(lat, lon), devuelve la ciudad por coordenadas
- get_nearest_to_city(ciudad, distancia='10', unidad='km', limit=10), devuelve la ciudad más cercana
get_by_name
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# core.py de couchbase.texto completo importar TermQuery ÍNDICE_NOMBRE = ciudades def get_by_name(auto, nombre, límite=10): resultado = auto._bucket.busque en(auto.NOMBRE_DEL_ÍNDICE, TermQuery(nombre.inferior(), campo=nombre), límite=límite, campos='*') para c_ciudad en resultado: rendimiento Ciudad.from_couchbase_dict(c_ciudad) |
get_by_coordinates
1 2 3 4 5 6 7 8 9 10 11 12 |
# core.py de couchbase.texto completo importar GeoDistanceQuery ÍNDICE_NOMBRE = ciudades def get_by_coordinates(auto, lat, lon): resultado = auto._bucket.busque en(auto.NOMBRE_DEL_ÍNDICE, GeoDistanceQuery(1 km, (lon, lat)), campos='*') para c_ciudad en resultado: rendimiento Ciudad.from_couchbase_dict(c_ciudad) |
get_nearest_to_city
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# core.py de couchbase.texto completo importar RawQuery, SortRaw ÍNDICE_NOMBRE = ciudades def get_nearest_to_city(auto, ciudad, distancia='10', unidad=km, límite=10): consulta = RawQuery({ ubicación: { "lon: ciudad.lon, "lat: ciudad.lat }, distancia: str(distancia) + unidad, campo: geo }) ordenar = SortRaw([{ "por: geo_distancia, campo: geo, unidad: unidad, ubicación: { "lon: ciudad.lon, "lat: ciudad.lat } }]) resultado = auto._bucket.busque en(auto.NOMBRE_DEL_ÍNDICE, consulta, ordenar=ordenar, campos='*', límite=límite) para c_ciudad en resultado: rendimiento Ciudad.from_couchbase_dict(c_ciudad) |
Como podrás notar en este ejemplo, usamos las clases RawQuery y SortRaw. Lamentablemente, la API couchbase-python-client no funciona correctamente con las nuevas búsquedas de Couchbase y geo.
Métodos de llamada
Como ya tenemos todos los métodos listos, ¡podemos llamarlo!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# core.py cubo = get_bucket('admin', prueba123456) servicio_ciudades = CiudadesServicio(cubo) # cities_service.load_from_csv('/mi-ruta/PL/PL.txt') imprimir(get_by_name) ciudades = servicio_ciudades.get_by_name(Poznań) para ciudad en ciudades: imprimir(ciudad.__dict__) imprimir('get_by_coordinates') ciudades = servicio_ciudades.get_by_coordinates(52.40691997632544, 16.929929926276657) para ciudad en ciudades: imprimir(ciudad.__dict__) imprimir(get_nearest_to_city) ciudades = servicio_ciudades.get_nearest_to_city(ciudad) para ciudad en ciudades: imprimir(ciudad.__dict__) |
¿Qué hacer a partir de ahora?
Creo que esta introducción le permitirá trabajar en algo más avanzado.
Hay algunas cosas que puedes hacer:
- Tal vez utilizar una herramienta CLI o REST API para servir estos datos... Mejorar la forma en que cargamos los datos, ya que podría no ser super performante si queremos cargar TODAS las ciudades de TODOS los países.
Puedes encontrar el código completo de core.py en gist github.
Si tienes alguna pregunta, no dudes en tuitearme @daniel_ancuta.
Este post forma parte del Programa de escritura comunitaria