Advertisement
  1. Code
  2. JavaScript
  3. Web APIs

Crear un Buscador para tienda con Node.js y Redis

Scroll to top

() translation by (you can also view the original English article)

Visite el sitio web para cualquier cadena de restaurantes o tiendas y es probable que encuentre un "buscador de tienda": una pequeña página aparentemente simple donde ingresa su dirección postal o código postal y proporciona los lugares cerca de usted. Como cliente, es genial porque se puede encontrar lo que está cerca, y las implicaciones del negocio son obvias.

Construir un "buscador de tienda" es en realidad una tarea difícil. En este tutorial, cubriremos los conceptos básicos de cómo trabajar con datos geoespaciales en Node.js y Redis y construiremos un buscador rudimentario de tiendas.

Utilizaremos los comandos "geo" de Redis. Estos comandos se agregaron en la versión 3.2, por lo que tendrá que tener instalado en su máquina de desarrollo. Vamos a hacer un corto chequeo—enciende redis-cli y tipea GEOADD. Debería ver un mensaje de error similar a este:

1
(error) ERR wrong number of arguments for 'GEOADD' command

A pesar del mensaje de error, es una buena señal—muestra que tiene el comando GEOADD. Si ejecuta el comando y obtiene el siguiente error:

1
(error) ERR unknown command 'GEOADD'

Tendrá que descargar, crear e instalar una versión de Redis que admita los comandos geo antes de ir más lejos.

Ahora que tienes un servidor Redis compatible, vamos a hacer un recorrido por los comandos geo. Redis tiene seis comandos que están directamente involucrados en la indexación geoespacial: GEOADD, GEOHASH, GEOPOS, GEODIST, GEORADIUS y GEORADIUSBYMEMBER.

Empecemos con GEOADD. Este comando, como usted puede imaginar, agrega un elemento geoespacial. Tiene cuatro argumentos requeridos: clave, longitud, latitud y miembro. La clave es como un agrupamiento y representa un solo valor en el espacio de claves. Longitud y latitud son obviamente las coordenadas como flotadores; Tenga en cuenta el orden de estos valores, ya que es probable que se invierta de lo que está acostumbrado a ver. Por último, "miembro" es la forma en que va a identificar una ubicación. En redis-cli, vamos a ejecutar los siguientes comandos:

1
geoadd va-universities -76.493 37.063 christopher-newport-university
2
geoadd va-universities -76.706944 37.270833 college-of-william-and-mary
3
geoadd va-universities -78.868889 38.449444 james-madison-university
4
geoadd va-universities -78.395833 37.297778 longwood-university
5
geoadd va-universities -76.2625 36.8487 norfolk-state-university
6
geoadd va-universities -76.30522 36.88654 old-dominion-university
7
geoadd va-universities -80.569444 37.1275 radford-university
8
geoadd va-universities -77.475 38.301944 university-of-mary-washington
9
geoadd va-universities -78.478889 38.03 university-of-virginia
10
geoadd va-universities -82.576944 36.978056 uva-wise
11
geoadd va-universities -77.453255 37.546615 virginia-commonwealth-university
12
geoadd va-universities -79.44 37.79 virginia-military-institute
13
geoadd va-universities -77.425556 37.242778 virginia-state-university
14
geoadd va-universities -80.425 37.225 virginia-tech

Esta es la forma a largo plazo de agregar entradas múltiples, pero es bueno ver el patrón. Si quisieras acortar este proceso, podrías lograr lo mismo repitiendo la longitud, la latitud y el miembro para cada lugar adicional como más argumentos. Este es un ejemplo de la representación a mano corta de los dos últimos ítems:

1
geoadd  va-universities -77.425556 37.242778 virginia-state-university -80.425 37.225 virginia-tech

Internamente, estos elementos geográficos no son en realidad nada especial: son almacenados por Redis como un zset o conjunto ordenado. Para mostrar esto, vamos a ejecutar algunos comandos más en la llave va-universities:

1
TYPE va-universities

Esto, devuelve zset, igual que cualquier otro conjunto ordenado. Ahora, ¿qué pasa si intentamos recuperar todos los valores e incluir las puntuaciones?

1
ZRANGE va-universities 0 -1 WITHSCORES

Esto devuelve una respuesta masiva de los miembros introducidos anteriormente, con un número muy grande—un entero de 52 bits. El entero es en realidad una representación de un geohash, una pequeña estructura inteligente que puede representar cualquier lugar del globo. Vamos a bucear un poco más profundamente más adelante y realmente no estarán interactuando con los datos geoespaciales de esta manera, pero siempre es bueno saber cómo se almacenan sus datos.

Ahora que tenemos algunos datos para jugar, veamos el comando GEODIST. Con este comando, puede determinar la distancia entre dos puntos que ha introducido previamente bajo la misma clave. Así que, vamos a encontrar la distancia entre los miembros virginia-tech y christopher-newport-university:

1
GEODIST va-universities virginia-tech christopher-newport-university

Esto debería producir 349054.2554687438, o la distancia entre los dos lugares en metros. También puede proporcionar un tercer argumento como una unidad mi (millas), km (kilómetros), pies (pies), o m (metros, el valor predeterminado). Vamos a obtener la distancia en millas:

1
GEODIST va-universities virginia-tech christopher-newport-university mi

Que debe responder con "216.89279795987412."

Antes de ir más lejos, vamos a hablar de por qué el cálculo de la distancia entre dos puntos geoespaciales no es sólo un simple cálculo geométrico. La tierra es redonda (o casi), de modo que al alejarse del ecuador, la distancia entre las líneas de longitud comienza a converger y "se encuentran" en los polos. Por lo tanto, para calcular la distancia, es necesario tener en cuenta el mundo.

Afortunadamente, Redis nos protege de esta matemática (si estás interesado, hay un ejemplo de una implementación de JavaScript puro). Una nota, Redis hace la suposición de que la tierra es una esfera perfecta (la fórmula de Haversine), y puede introducir un error de hasta el 0,5%, que es lo suficientemente bueno para la mayoría de las aplicaciones, especialmente para algo parecido a un buscador de tienda.

La mayor parte del tiempo vamos a querer todos los puntos dentro de un cierto radio de un lugar, no sólo la distancia entre dos puntos. Podemos hacer esto con el comando GEORADIUS. El comando GEORADIUS espera, al menos, la clave, la longitud, la latitud, la distancia y una unidad. Así que, vamos a encontrar todas las universidades en el conjunto de datos dentro de 100 millas de este punto.

1
GEORADIUS va-universities -78.245278 37.496111 100 mi

Que devuelve:

1
1) "longwood-university"
2
2) "virginia-state-university"
3
3) "virginia-commonwealth-university"
4
4) "university-of-virginia"
5
5) "university-of-mary-washington"
6
6) "college-of-william-and-mary"
7
7) "virginia-military-institute"
8
8) "james-madison-university”

GEORADIUS tiene algunas opciones. Digamos que queríamos obtener la distancia entre nuestro punto especificado y todas las ubicaciones. Podemos hacer esto añadiendo el argumento WITHDIST al final:

1
GEORADIUS va-universities -78.245278 37.496111 100 mi WITHDIST

Esto devuelve una respuesta masiva con el miembro de ubicación y la distancia (en la unidad especificada):

1
1) 1) "longwood-university"
2
   2) "16.0072"
3
2) 1) "virginia-state-university"
4
   2) "48.3090"
5
3) 1) "virginia-commonwealth-university"
6
   2) "43.5549"
7
4) 1) "university-of-virginia"
8
   2) "39.0439"
9
5) 1) "university-of-mary-washington"
10
   2) "69.7595"
11
6) 1) "college-of-william-and-mary"
12
   2) "85.9017"
13
7) 1) "virginia-military-institute"
14
   2) "68.4639"
15
8) 1) "james-madison-university"
16
   2) “74.1314"

Otro argumento opcional es WITHCOORD, que, como habrás podido adivinar, te devuelve las coordenadas de longitud y latitud. También puede mezclar esto con el argumento WITHDIST. Intentemos esto:

1
GEORADIUS va-universities -78.245278 37.496111 100 mi WITHCOORD WITHDIST

El conjunto de resultados se vuelve un poco más complicado:

1
1) 1) "longwood-university"
2
   2) "16.0072"
3
   3) 1) "-78.395833075046539"
4
      2) "37.297776773137613"
5
2) 1) "virginia-state-university"
6
   2) "48.3090"
7
   3) 1) "-77.425554692745209"
8
      2) "37.242778393422277"
9
3) 1) "virginia-commonwealth-university"
10
   2) "43.5549"
11
   3) 1) "-77.453256547451019"
12
      2) "37.546615418792236"
13
4) 1) "university-of-virginia"
14
   2) "39.0439"
15
   3) 1) "-78.478890359401703"
16
      2) "38.029999417483971"
17
5) 1) "university-of-mary-washington"
18
   2) "69.7595"
19
   3) 1) "-77.474998533725739"
20
      2) "38.301944581227126"
21
6) 1) "college-of-william-and-mary"
22
   2) "85.9017"
23
   3) 1) "-76.706942617893219"
24
      2) "37.27083268721384"
25
7) 1) "virginia-military-institute"
26
   2) "68.4639"
27
   3) 1) "-79.440000951290131"
28
      2) "37.789999344511962"
29
8) 1) "james-madison-university"
30
   2) "74.1314"
31
   3) 1) "-78.868888914585114"
32
      2) "38.449445074931383"

Observe que la distancia viene antes de las coordenadas, a pesar del orden invertido en nuestros argumentos. Redis no le importa qué orden especifique el argumento WITH*, pero devolverá la distancia antes de las coordenadas. Hay uno más con argumentos (WITHHASH), pero lo cubriremos en una sección posterior—sólo sé que vendrá en último lugar en tu respuesta.

Un pequeño comentario sobre los cálculos que se están llevando a cabo aquí—Si piensas en las matemáticas que ya cubrimos en cómo funciona GEODIST, pensemos en un radio. Puesto que un radio es un círculo, tenemos que pensar en un círculo que se coloca sobre una esfera, que es muy diferente de un simple círculo aplicado sobre un plano llano. Una vez más, Redis hace todos estos cálculos para nosotros (afortunadamente).

Ahora, vamos a cubrir un comando relacionado a GEORADIUS, GEORADIUSBYMEMBER. GEORADIUSBYMEMBER funciona exactamente igual que el GEORADIUS, pero en lugar de especificar una longitud y una latitud en los argumentos, puede especificar un miembro ya en su clave. Así, por ejemplo, esto devolverá a todos los miembros dentro de 100 millas dela miembro university-of-virginia.

1
GEORADIUSBYMEMBER va-universities university-of-virginia 100 mi

Puede utilizar las mismas unidades y WITH* argumentos y unidades en GEORADIUSBYMEMBER como podría hacerlo en GEORADIUS.

Anteriormente, cuando ejecutamos ZRANGE en nuestra clave, es posible que se haya preguntado cómo recuperar las coordenadas de una posición que agregó con GEOADD, lo podemos lograr con el comando GEOPOS. Al suministrar la llave y un miembro, podemos recuperar las coordenadas:

1
GEOPOS va-universities university-of-virginia

Que debe producir un resultado de:

1
1) 1) "-78.478890359401703"
2
   2) “38.029999417483971"

Si nos fijamos en cuando se agregó el valor de university-of-virginia, los números son un poco diferentes, aunque redondean a la misma cantidad. Esto se debe a cómo Redis almacena las coordenadas en el formato geohash. Una vez más, esto es muy cercano y lo suficientemente bueno para la mayoría de las aplicaciones—en el ejemplo anterior, la distancia real diferencia entre la entrada y la salida de GEOPOS es de 5,5 pulgadas / 14 cm.

Esto nos lleva a nuestro comando Redis GEO final: GEOHASH. Esto devolverá el valor geohash utilizado para mantener las coordenadas. Mencionado anteriormente, este es un sistema inteligente que se basa en una rejilla y puede ser representado en una variedad de maneras—Redis utiliza un entero de 52 bits, pero una representación más comúnmente vista es una cadena de base-32. Utilizando el comando GEOHASH con la clave y un miembro, Redis devolverá la cadena base-32 que representa esta ubicación. Si ejecutamos el comando:

1
GEOHASH va-universities university-of-virginia

Obtendras:

1
1) "dqb0q5jkv30"

Esta es la representación de la cadena geohash base-32. Las cadenas de Geohash tienen una propiedad limpia que si quita caracteres de la derecha de la cadena, reduce progresivamente la precisión de las coordenadas. Esto puede ilustrarse con el sitio web de geohash: mire estos enlaces y vea cómo las coordenadas y el mapa se alejan de la ubicación original:

Hay una función más que necesitaremos cubrir, y si ya estás familiarizado con los conjuntos clasificados de Redis ya lo sabes. Dado que sus datos geoespaciales sólo se almacenan en un zset, podemos eliminar un elemento con ZREM:

1
ZREM va-universities university-of-virginia

Servidor del Buscador de tienda

Ahora que tenemos los fundamentos abajo para usar los comandos de Redis GEO, construyamos buscador de tienda basado en Node.js como ejemplo en servido. Vamos a utilizar los datos de arriba, así que supongo que esto es técnicamente un buscador de universidades en lugar de un buscador de tiendas, pero el concepto es idéntico. Antes de empezar, asegúrese de tener tanto Node.js como npm instalados. Haga un directorio para su proyecto y cambie a ese directorio en su línea de comandos. En la línea de comandos, escriba:

1
npm init

Esto creará su archivo package.json haciéndole algunas preguntas. Después de haber inicializado su proyecto, instalaremos cuatro módulos. De nuevo, desde la línea de comandos, ejecute los siguientes cuatro comandos:

1
npm install express --save
2
npm install pug --save
3
npm install redis --save
4
npm install body-parser --save

El primer módulo es Express.js, un módulo de servidor web. Para ir junto con el servidor, también tendremos que instalar un sistema de plantillas. Para este proyecto usaremos pug (conocido formalmente como Jade). Pug se integra muy bien con Express y nos permitirá crear una plantilla de página básica en sólo unas pocas líneas. También instalamos node_redis, que gestiona la conexión entre Node.js y el servidor Redis. Por último, necesitaremos otro módulo para manejar la interpretación de los valores HTTP POST: body-parser.

Para nuestro primer paso, sólo vamos a levantar el servidor hasta el punto de que puede aceptar peticiones HTTP y rellenar la plantilla con valores.

1
var 
2
  bodyParser  = require('body-parser'), 
3
  express     = require('express'),
4
  
5
  app = express();
6
  
7
app.set('view engine', 'pug'); //this associates the pug module with the res.render function

8
9
app.get(  // method "get"

10
  '/',    // the route, aka "Home"

11
  function(req, res) {
12
    res.render('index', { //you can pass any value to the template here

13
      pageTitle: 'University Finder' 
14
    });
15
  }
16
);
17
18
app.post( // method "post"

19
  '/', 
20
  bodyParser.urlencoded({ extended : false }), // this allows us to parse the values POST'ed from the form

21
  function(req,res) {
22
    var
23
      latitude  = req.body.latitude,    // req.body contains the post values

24
      longitude = req.body.longitude;
25
      
26
    res.render('index', { 
27
      pageTitle : 'University Finder Results',
28
      latitude  : latitude,
29
      longitude : longitude,
30
      results   : []                  // we'll populate it later

31
    });
32
  }
33
);
34
35
app.listen(3000, function () {
36
  console.log('Sample store finder running on port 3000.');
37
});

Este servidor solo servirá con éxito la página de nivel superior ('/') y sólo si el cliente HTTP (a.k.a. navegador) solicita con un método GET o POST.

Vamos a necesitar una plantilla de huesos, lo suficiente para poder mostrar un encabezado, el formulario y (más adelante) mostrar los resultados. El barro amasado es un lenguaje templating muy terso con espacios en blanco relevantes. Por lo tanto, con la indentación de etiquetas de anidación, la primera palabra de una línea después de la indentación es la etiqueta (y las etiquetas de cierre son inferidas por el analizador) y estamos interpolando los valores con #{}. Esto requiere cierto tiempo para acostumbrarse, pero puede crear una gran cantidad de HTML con caracteres mínimos: eche un vistazo al sitio web de pug para obtener más información. Nota al momento de este artículo, el sitio web oficial de Pug no ha sido actualizado. . Aquí está el boleto oficial de GitHub con respecto al problema

1
//- Anything that starts with "//-" is a non-rendered comment
2
//- add the doctype for HTML 5
3
doctype html
4
//- the HTML tag with the attribute "lang" equal to "en"
5
html(lang="en")        
6
  head
7
    //- this produces a title tag and the "=" means to assign the entire value of pageTitle (passed from our server) between the opening and closing tag
8
    title= pageTitle
9
  body
10
    h1 University Finder
11
    form(action="/" method="post")
12
      div 
13
        label(for="#latitude") Latitude  
14
        //- "value=" will pull in the 'latitude' variable in from the server, ignoring it if the variable doesn't exist
15
        input#latitude(type="text" name="latitude" value= latitude)
16
      div
17
        label(for="#longitude") Longitude  
18
        input#longitude(type="text" name="longitude" value= longitude)
19
      button(type="submit") Find
20
    //- "if" is a reserved word in Pug - anything that follows and is indented one more level will only be rendered if the 'results' variable is present
21
    if results
22
      h2 Showing Results for #{latitude}, #{longitude}

Podemos probar nuestro buscador de tienda iniciando el servidor en la línea de comandos:

1
node app.js

Luego ingrese en su navegador ahttp://localhost:3000/.

Debería ver una página sencilla, sin trama, con un encabezado grande que dice "University Finder" y un formulario con un par de cuadros de texto, Ya que una solicitud de página normal por un navegador es una solicitud GET, esta página está siendo generada por la función en El argumento para app.get.

Basic form screenshotBasic form screenshotBasic form screenshot

Si introduce valores en los libros de texto de Latitud y Longitud y hace clic en "Buscar", verá que esos resultados se representan y se muestran en la línea que dice "Mostrar resultados para ..." En este punto, no obtendrá ningún resultado, Como todavía no hemos integrado Redis.

Form with values after click screenshotForm with values after click screenshotForm with values after click screenshot

Integración de Redis

Para integrar Redis, primero tendremos que hacer una pequeña configuración. En la declaración de variables, incluya tanto el módulo como una variable (aún no definida) para el cliente.

1
    ...
2
    redis       = require('redis'),
3
    client,
4
    ...

Después de la declaración de variables, tendremos que crear la conexión a Redis. En nuestro ejemplo, asumiremos una conexión localhost en el puerto predeterminado y sin autenticación (en un entorno de producción, asegúrese de proteger su servidor Redis).

1
client = redis.createClient();

Una característica interesante de node_redis es que el cliente hará cola de comandos mientras se establece una conexión, por lo que no hay necesidad de preocuparse por esperar para establecer una conexión con el servidor Redis.

Ahora que nuestra instancia de nodo tiene un cliente Redis que puede aceptar conexiones, vamos a trabajar en el corazón de nuestro buscador de tiendas. Tomaremos la latitud y longitud del usuario y lo aplicaremos al comando GEORADIUS. Nuestro ejemplo es usar un radio de 100 millas. También vamos a querer obtener la distancia y las coordenadas de esos resultados.

En la devolución de llamada, manejamos cualquier error, si surge. Si no se encuentran errores, muestre los resultados para que sean más significativos y más fáciles de integrar en la plantilla. Estos resultados se introducen en la plantilla.

1
app.post( // method "post"

2
  '/', 
3
  bodyParser.urlencoded({ extended : false }), // this allows us to parse the values POST'ed from the form

4
  function(req,res,next) {
5
    var
6
      latitude  = req.body.latitude,    // req.body contains the post values

7
      longitude = req.body.longitude;
8
 
9
     client.georadius(
10
      'va-universities',    //va-universities is the key where our geo data is stored

11
      longitude,            //the longitude from the user

12
      latitude,             //the latitude from the user

13
      '100',                //radius value

14
      'mi',                 //radius unit (in this case, Miles)

15
      'WITHCOORD',          //include the coordinates in the result

16
      'WITHDIST',           //include the distance from the supplied latitude & longitude

17
      'ASC',                //sort with closest first

18
      function(err, results) {
19
        if (err) { next(err); } else { //if there is an error, we'll give it back to the user

20
          //the results are in a funny nested array. Example:

21
          //1) "longwood-university"        [0]

22
          //2) "16.0072"                    [1]

23
          //3)  1) "-78.395833075046539"    [2][0]

24
          //    2) "37.297776773137613"     [2][1]

25
          //by using the `map` function we'll turn it into a collection (array of objects)

26
          results = results.map(function(aResult) {
27
            var
28
              resultObject = {
29
                key       : aResult[0],
30
                distance  : aResult[1],
31
                longitude : aResult[2][0],
32
                latitude  : aResult[2][1]
33
              };
34
              
35
            return resultObject;
36
          })
37
          res.render('index', { 
38
            pageTitle : 'University Finder Results',
39
            latitude  : latitude,
40
            longitude : longitude,
41
            results   : results
42
          });
43
        }
44
      }
45
    );
46
    
47
  }
48
);

En la plantilla, necesitamos manejar el conjunto de resultados. Pug tiene iteración sin fisuras sobre matrices (con una sintaxis casi verbal). Se trata de extraer esos valores para un solo resultado; La plantilla se encargará de todo lo demás.

1
each result in results
2
    div
3
      h3 #{result.key}
4
      div
5
        strong Distance: 
6
        | #{result.distance}
7
        |  miles
8
      div
9
        strong Coordinates: 
10
        | #{result.latitude}
11
        | , 
12
        | #{result.longitude}
13
        | (
14
        a(href="https://www.openstreetmap.org/#map=18/"+result.latitude+"/"+result.longitude) Map
15
        | )

Después de que tenga su plantilla final y código de nodo en su lugar, reinicie su servidor app.js y apunte su navegador de nuevo a http://localhost:3000/.

Si introduce una latitud de 38.904722 y una longitud de -77.016389 (las coordenadas de Washington, DC, en la frontera norte de Virginia) en los cuadros y haga clic en buscar, obtendrá tres resultados. Si cambia los valores a una latitud de 37.533333 y una longitud de -77.466667 (Richmond, Virginia, la capital del estado y en la parte central / oriental del estado), verá diez resultados.

En este punto, usted tiene las partes básicas de un buscador de tienda, pero tendrá que ajustarlo para adaptarse a su propio proyecto.

  •     La mayoría de los usuarios no piensa en términos de coordenadas, por lo que tendrá que considerar un enfoque más fácil de usar, tales como:
    1. Uso del JavaScript del cliente para detectar la ubicación mediante la API de geolocalización
    2. Uso de un servicio geolocalizador basado en IP
    3. Solicite al usuario un código postal o dirección y utilice un servicio de geocodificación que se convierta en coordenadas. Muchos servicios de geocodificación diferentes están en el mercado, así que escoja uno que funcione bien para su área de destino.

  • Este script no realiza validación de formularios. Si deja los cuadros de entrada de latitud y longitud, debe asegurarse de que está validando sus datos y evitando un mensaje de error.

  • Expanda la clave de ubicación en información más útil. Si está utilizando Redis para almacenar más información sobre cada ubicación, considere almacenar esa información en hashes con una clave que coincida con los miembros devueltos de GEORADIUS. Tendrá que hacer llamadas adicionales a Redis.

  • Más estrechamente integrar con un servicio de cartografía como Google Maps, OpenStreetMap o Bing Maps para proporcionar mapas integrados y direcciones.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.