Статьи

Построение магазина Finder с Node.js и Redis

Посетите веб-сайт любого сетевого ресторана или магазина, и вы, скорее всего, найдете «искатель магазина»: кажущуюся простой, небольшую страницу, где вы вводите свой адрес или почтовый индекс, и она предоставляет информацию о местах рядом с вами. Как клиент, это здорово, потому что вы можете найти то, что близко, и последствия для бизнеса очевидны.

Построение «магазина поиска» на самом деле является сложной задачей. В этом уроке мы рассмотрим основы работы с геопространственными данными в Node.js и Redis и создадим элементарный искатель хранилища.

Мы будем использовать «гео» команды Redis . Эти команды были добавлены в версии 3.2, поэтому вам необходимо установить их на компьютер для разработки. Давайте сделаем небольшую проверку — запустите redis-cli и наберите GEOADD . Вы должны увидеть сообщение об ошибке, которое выглядит так:

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

Несмотря на сообщение об ошибке, это хороший знак — он показывает, что у вас есть команда GEOADD . Если вы запустите команду, и вы получите следующую ошибку:

1
(error) ERR unknown command ‘GEOADD’

Вам нужно будет скачать , собрать и установить версию Redis, которая поддерживает гео-команды, прежде чем идти дальше.

Теперь, когда у вас есть поддерживаемый сервер Redis, давайте пройдемся по командам гео. Redis имеет шесть команд, которые напрямую связаны с геопространственной индексацией: GEOADD , GEOHASH , GEOPOS , GEODIST , GEORADIUS и GEORADIUSBYMEMBER .

Давайте начнем с GEOADD . Эта команда, как вы можете себе представить, добавляет геопространственный элемент. У него есть четыре обязательных аргумента: ключ, долгота, широта и член. Ключ похож на группировку и представляет одно значение в пространстве ключей. Долгота и широта — это, очевидно, координаты в виде поплавков; обратите внимание на порядок этих значений, так как они, скорее всего, обратны тому, что вы привыкли видеть. Наконец, «член» — это способ определения местоположения. В redis-cli давайте запустим следующие команды:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
geoadd va-universities -76.493 37.063 christopher-newport-university
geoadd va-universities -76.706944 37.270833 college-of-william-and-mary
geoadd va-universities -78.868889 38.449444 james-madison-university
geoadd va-universities -78.395833 37.297778 longwood-university
geoadd va-universities -76.2625 36.8487 norfolk-state-university
geoadd va-universities -76.30522 36.88654 old-dominion-university
geoadd va-universities -80.569444 37.1275 radford-university
geoadd va-universities -77.475 38.301944 university-of-mary-washington
geoadd va-universities -78.478889 38.03 university-of-virginia
geoadd va-universities -82.576944 36.978056 uva-wise
geoadd va-universities -77.453255 37.546615 virginia-commonwealth-university
geoadd va-universities -79.44 37.79 virginia-military-institute
geoadd va-universities -77.425556 37.242778 virginia-state-university
geoadd va-universities -80.425 37.225 virginia-tech

Это длинный способ добавления нескольких записей, но хорошо видеть шаблон. Если вы хотите сократить этот процесс, вы можете сделать то же самое, повторив долготу, широту и член для каждого дополнительного места в качестве дополнительных аргументов. Это пример краткого представления двух последних элементов:

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

Внутренне эти геоэлементы на самом деле не являются чем-то особенным — они хранятся в Redis как zset или отсортированный набор. Чтобы показать это, давайте запустим еще несколько команд в ключевых va-universities :

1
TYPE va-universities

Это возвращает zset , как и любой другой отсортированный набор. Теперь, что произойдет, если мы попытаемся вернуть все значения и включить оценки?

1
ZRANGE va-universities 0 -1 WITHSCORES

Это возвращает групповой ответ членов, введенных выше, с очень большим числом — 52-разрядным целым числом. Целое число на самом деле представляет собой геохэш , умную небольшую структуру, которая может представлять любое место на земном шаре. Позже мы углубимся немного глубже и на самом деле не будем взаимодействовать с геопространственными данными таким образом, но всегда полезно знать, как хранятся ваши данные.

Теперь, когда у нас есть некоторые данные, давайте посмотрим на команду GEODIST . С помощью этой команды вы можете определить расстояние между двумя точками, которые вы ранее ввели под тем же ключом. Итак, давайте найдем расстояние между членами virginia-tech и christopher-newport-university :

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

Это должно вывести 349054.2554687438, или расстояние между двумя местами в метрах. Вы также можете указать третий аргумент в виде единиц mi (миль), km (километров), ft (футов) или m (метров по умолчанию). Давайте получим расстояние в милях:

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

Который должен ответить «216.89279795987412».

Прежде чем идти дальше, давайте поговорим о том, почему вычисление расстояния между двумя геопространственными точками — это не просто геометрическое вычисление. Земля круглая (или почти), поэтому, когда вы уходите от экватора, расстояние между линиями долготы начинает сходиться, и они «встречаются» на полюсах. Итак, чтобы рассчитать расстояние, нужно учесть глобус.

К счастью, Redis защищает нас от этой математики (если вам интересно, есть пример реализации чистого JavaScript ). Одно замечание: Redis делает предположение, что земля — ​​идеальная сфера (формула Хаверсайна), и она может вносить ошибку до 0,5%, что достаточно для большинства приложений, особенно для чего-то вроде поиска магазина.

В большинстве случаев мы хотим, чтобы все точки находились в пределах определенного радиуса местоположения, а не только расстояние между двумя точками. Мы можем сделать это с GEORADIUS команды GEORADIUS . Команда GEORADIUS ожидает, по крайней мере, ключ, долготу, широту, расстояние и единицу измерения. Итак, давайте найдем все университеты в наборе данных в пределах 100 миль от этой точки.

1
GEORADIUS va-universities -78.245278 37.496111 100 mi

Который возвращает:

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

GEORADIUS есть несколько вариантов. Скажем, мы хотели получить расстояние между нашей указанной точкой и всеми локациями. Мы можем сделать это, добавив аргумент WITHDIST в конце:

1
GEORADIUS va-universities -78.245278 37.496111 100 mi WITHDIST

Это возвращает массовый ответ с указанием местоположения и расстояния (в указанном блоке):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
1) 1) «longwood-university»
   2) «16.0072»
2) 1) «virginia-state-university»
   2) «48.3090»
3) 1) «virginia-commonwealth-university»
   2) «43.5549»
4) 1) «university-of-virginia»
   2) «39.0439»
5) 1) «university-of-mary-washington»
   2) «69.7595»
6) 1) «college-of-william-and-mary»
   2) «85.9017»
7) 1) «virginia-military-institute»
   2) «68.4639»
8) 1) «james-madison-university»
   2) “74.1314″

Другой необязательный аргумент — WITHCOORD , который, как вы уже догадались, возвращает вам координаты долготы и широты. Вы также можете смешать это с аргументом WITHDIST . Давайте попробуем это:

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

Результирующий набор становится немного сложнее:

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

Обратите внимание, что расстояние идет перед координатами, несмотря на обратный порядок в наших аргументах. Redis не волнует, в каком порядке вы указываете аргумент WITH* , но он будет возвращать расстояние до координат. Есть еще один аргумент ( WITHHASH ), но мы рассмотрим это в следующем разделе — просто знайте, что он будет последним в вашем ответе.

Коротко о вычислениях, которые здесь происходят — если вы думаете о математике, которую мы ранее рассмотрели в том, как работает GEODIST , давайте подумаем о радиусе. Так как радиус — это круг, мы должны думать о круге, наложенном на сферу, который сильно отличается от простого круга, наложенного на плоскую плоскость. И снова Redis делает все эти расчеты за нас (к счастью).

Теперь давайте рассмотрим соответствующую команду для GEORADIUS , GEORADIUSBYMEMBER . GEORADIUSBYMEMBER работает точно так же, как и GEORADIUS , но вместо указания долготы и широты в аргументах вы можете указать элемент, уже указанный в вашем ключе. Таким образом, это, например, вернет всех участников в радиусе 100 миль от university-of-virginia .

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

В GEORADIUSBYMEMBER вы можете использовать те же единицы измерения и аргументы WITH* и в GEORADIUS .

Ранее, когда мы запускали ZRANGE для нашего ключа, вы, возможно, задавались вопросом, как вернуть координаты из позиции, которую вы добавили с помощью GEOADD мы можем сделать это с GEOPOS команды GEOPOS . Предоставив ключ и член, мы можем получить координаты:

1
GEOPOS va-universities university-of-virginia

Который должен дать результат:

1
2
1) 1) «-78.478890359401703»
  2) “38.029999417483971″

Если вы посмотрите назад, когда мы добавили значение для university-of-virginia , цифры немного отличаются, хотя они округляются до одинакового количества. Это связано с тем, как Redis хранит координаты в формате геохэш. Опять же, это очень близко и достаточно хорошо для большинства приложений — в приведенном выше примере фактическая разница расстояний между входом и выходом GEOPOS составляет 5,5 дюймов / 14 см.

Это приводит нас к нашей последней команде Redis GEO: GEOHASH . Это вернет значение geohash, используемое для хранения координат. Как упоминалось ранее, это умная система, основанная на сетке, и ее можно представить различными способами — в Redis используется 52-разрядное целое число, но более распространенным представлением является строка из base-32. Используя команду GEOHASH с ключом и членом, Redis вернет строку base-32, которая представляет это местоположение. Если мы запустим команду:

1
GEOHASH va-universities university-of-virginia

Вы вернетесь:

1
1) «dqb0q5jkv30»

Это строковое представление geohash base-32. Строки Geohash имеют удобное свойство: если вы удаляете символы справа от строки, вы постепенно снижаете точность координат. Это можно проиллюстрировать на веб-сайте geohash — посмотрите на эти ссылки и посмотрите, как координаты и карта удаляются от исходного местоположения:

Нам нужно рассмотреть еще одну функцию, и если вы уже знакомы с отсортированными наборами Redis, вы уже знаете это. Поскольку ваши геопространственные данные на самом деле просто хранятся в zset, мы можем удалить элемент с помощью ZREM :

1
ZREM va-universities university-of-virginia

Теперь, когда у нас есть основы для использования команд Redis GEO, давайте в качестве примера создадим сервер поиска хранилища на основе Node.js. Мы собираемся использовать данные сверху, так что, я думаю, технически это поиск университета, а не поиска магазина, но концепция идентична. Прежде чем начать, убедитесь, что у вас установлены Node.js и npm. Создайте каталог для вашего проекта и переключитесь на этот каталог в командной строке. В командной строке введите:

1
npm init

Это создаст ваш файл package.json , задав вам несколько вопросов. После того, как вы инициализировали свой проект, мы установим четыре модуля. Снова из командной строки выполните следующие четыре команды:

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

Первый модуль — это Express.js , модуль веб-сервера. Для работы с сервером нам также потребуется установить систему шаблонов. Для этого проекта мы будем использовать мопса (формально известного как Jade). Pug прекрасно интегрируется с Express и позволит нам создать базовый шаблон страницы всего за несколько строк. Мы также установили node_redis , который управляет соединением между Node.js и сервером Redis. Наконец, нам понадобится другой модуль для обработки интерпретации значений HTTP POST: body-parser .

Для нашего первого шага мы просто собираемся настроить сервер так, чтобы он мог принимать запросы HTTP и заполнять шаблон значениями.

01
02
03
04
05
06
07
08
09
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
var
  bodyParser = require(‘body-parser’),
  express = require(‘express’),
   
  app = express();
   
app.set(‘view engine’, ‘pug’);
 
app.get( // method «get»
  ‘/’, // the route, aka «Home»
  function(req, res) {
    res.render(‘index’, { //you can pass any value to the template here
      pageTitle: ‘University Finder’
    });
  }
);
 
app.post( // method «post»
  ‘/’,
  bodyParser.urlencoded({ extended : false }), // this allows us to parse the values POST’ed from the form
  function(req,res) {
    var
      latitude = req.body.latitude, // req.body contains the post values
      longitude = req.body.longitude;
       
    res.render(‘index’, {
      pageTitle : ‘University Finder Results’,
      latitude : latitude,
      longitude : longitude,
      results : [] // we’ll populate it later
    });
  }
);
 
app.listen(3000, function () {
  console.log(‘Sample store finder running on port 3000.’);
});

Этот сервер будет успешно обслуживать только страницу верхнего уровня (‘/’) и только если HTTP-клиент (он же браузер) запрашивает метод GET или POST .

Нам понадобится простой шаблон — достаточно, чтобы показать заголовок, форму и (позже) показать результаты. Pug — очень лаконичный язык шаблонов с соответствующими пробелами. Таким образом, при вложении тега отступа первым словом строки после отступа является тег (а закрывающие теги выводятся синтаксическим анализатором), и мы интерполируем значения с помощью #{} . Это требует некоторого привыкания, но вы можете создать много HTML с минимальными символами — взгляните на веб-сайт мопса, чтобы узнать больше. Обратите внимание, что на момент написания этой статьи официальный сайт Pug не обновлялся. Вот официальный билет GitHub относительно проблемы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
//- Anything that starts with «//-» is a non-rendered comment
//- add the doctype for HTML 5
doctype html
//- the HTML tag with the attribute «lang» equal to «en»
html(lang=»en»)
  head
    //- 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
    title= pageTitle
  body
    h1 University Finder
    form(action=»/» method=»post»)
      div
        label(for=»#latitude») Latitude
        //- «value=» will pull in the ‘latitude’ variable in from the server, ignoring it if the variable doesn’t exist
        input#latitude(type=»text» name=»latitude» value= latitude)
      div
        label(for=»#longitude») Longitude
        input#longitude(type=»text» name=»longitude» value= longitude)
      button(type=»submit») Find
    //- «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
    if results
      h2 Showing Results for #{latitude}, #{longitude}

Мы можем попробовать наш магазин поиска, запустив сервер в командной строке:

1
node app.js

Затем укажите ваш браузер на http://localhost:3000/ .

Вы должны увидеть простую, не стилизованную страницу с большим заголовком с надписью «University Finder» и форму с несколькими текстовыми полями. Так как обычный запрос страницы браузером — это запрос GET, эта страница генерируется функцией в аргумент для app.get .

Скриншот базовой формы

Если вы введете значения в учебники по широте и долготе и нажмете «найти», вы увидите, что эти результаты отображаются и отображаются в строке с надписью «Отображение результатов для…». На данный момент у вас не будет никаких результатов, поскольку мы еще не интегрировали Redis.

Форма со значениями после нажатия на скриншот

Чтобы интегрировать Redis, сначала нужно немного настроить. В объявлении переменной включите модуль и переменную (пока не определенную) для клиента.

1
2
3
4
   redis = require(‘redis’),
   client,
   …

После объявления переменной нам нужно создать соединение с Redis. В нашем примере мы предполагаем подключение к локальному хосту через порт по умолчанию и без аутентификации (в производственной среде обязательно защитите свой сервер Redis ).

1
client = redis.createClient();

Отличная особенность node_redis заключается в том, что клиент будет ставить команды в очередь во время установления соединения, поэтому вам не нужно беспокоиться об ожидании установления соединения с сервером Redis.

Теперь, когда у нашего экземпляра узла есть клиент Redis, который может принимать соединения, давайте работать над сердцем нашего поиска магазина. Мы возьмем широту и долготу пользователя и применим их к команде GEORADIUS . Наш пример использует радиус 100 миль. Мы также хотим узнать расстояние и координаты этих результатов.

В обратном вызове мы обрабатываем любые ошибки, если они возникнут. Если ошибок не обнаружено, сопоставьте результаты, чтобы сделать их более значимыми и проще интегрировать в шаблон. Эти результаты затем вводятся в шаблон.

01
02
03
04
05
06
07
08
09
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
42
43
44
45
46
47
48
app.post( // method «post»
  ‘/’,
  bodyParser.urlencoded({ extended : false }), // this allows us to parse the values POST’ed from the form
  function(req,res,next) {
    var
      latitude = req.body.latitude, // req.body contains the post values
      longitude = req.body.longitude;
  
     client.georadius(
      ‘va-universities’, //va-universities is the key where our geo data is stored
      longitude, //the longitude from the user
      latitude, //the latitude from the user
      ‘100’, //radius value
      ‘mi’, //radius unit (in this case, Miles)
      ‘WITHCOORD’, //include the coordinates in the result
      ‘WITHDIST’, //include the distance from the supplied latitude & longitude
      ‘ASC’, //sort with closest first
      function(err, results) {
        if (err) { next(err);
          //the results are in a funny nested array.
          //1) «longwood-university» [0]
          //2) «16.0072» [1]
          //3) 1) «-78.395833075046539» [2][0]
          // 2) «37.297776773137613» [2][1]
          //by using the `map` function we’ll turn it into a collection (array of objects)
          results = results.map(function(aResult) {
            var
              resultObject = {
                key : aResult[0],
                distance : aResult[1],
                longitude : aResult[2][0],
                latitude : aResult[2][1]
              };
               
            return resultObject;
          })
          res.render(‘index’, {
            pageTitle : ‘University Finder Results’,
            latitude : latitude,
            longitude : longitude,
            results : results
          });
        }
      }
    );
     
  }
);

В шаблоне нам нужно обработать набор результатов. Мопс имеет бесшовную итерацию по массивам (с почти словесным синтаксисом). Это вопрос определения этих значений для одного результата; шаблон будет обрабатывать все остальное.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
each result in results
   div
     h3 #{result.key}
     div
       strong Distance:
       |
       |
     div
       strong Coordinates:
       |
       |
       |
       |
       a(href=»https://www.openstreetmap.org/#map=18/»+result.latitude+»/»+result.longitude) Map
       |

После того, как вы получили окончательный шаблон и код узла, снова запустите сервер app.js и наведите браузер на http: // localhost: 3000 / .

Если вы введете в поля широту 38,904722 и долготу -77,016389 (координаты Вашингтона, округ Колумбия, на северной границе Вирджинии) и нажмете «Найти», вы получите три результата. Если вы измените значения на широту 37,533333 и долготу -77,466667 (Ричмонд, Вирджиния, столица штата и центральная / восточная часть штата), вы увидите десять результатов.

На данный момент у вас есть основные части поиска магазина, но вам нужно настроить его в соответствии с вашим собственным проектом.

  • Большинство пользователей не думают с точки зрения координат, поэтому вам нужно рассмотреть более удобный для пользователя подход, такой как:
    1. Использование клиентского JavaScript для определения местоположения с помощью API геолокации
    2. Использование службы геолокации на основе IP
    3. Запросите у пользователя почтовый индекс или адрес и используйте сервис геокодирования, который преобразует либо в координаты. На рынке представлено множество различных услуг геокодирования, поэтому выберите тот, который хорошо подходит для вашей целевой области.

  • Этот скрипт не проверяет форму. Если вы покинете поля ввода широты и долготы, вам нужно убедиться, что вы проверяете свои данные и избегаете сообщения об ошибке.

  • Разверните ключ местоположения в более полезную информацию. Если вы используете Redis для хранения дополнительной информации о каждом местоположении, рассмотрите возможность сохранения этой информации в хешах с ключом, который соответствует вашим возвращенным членам GEORADIUS . Вам нужно будет сделать дополнительные звонки в Redis.

  • Более тесная интеграция с картографическим сервисом, таким как Google Maps , OpenStreetMap или Bing Maps, для предоставления встроенных карт и направлений.