Посетите веб-сайт любого сетевого ресторана или магазина, и вы, скорее всего, найдете «искатель магазина»: кажущуюся простой, небольшую страницу, где вы вводите свой адрес или почтовый индекс, и она предоставляет информацию о местах рядом с вами. Как клиент, это здорово, потому что вы можете найти то, что близко, и последствия для бизнеса очевидны.
Построение «магазина поиска» на самом деле является сложной задачей. В этом уроке мы рассмотрим основы работы с геопространственными данными в 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 — посмотрите на эти ссылки и посмотрите, как координаты и карта удаляются от исходного местоположения:
- http://geohash.org/dqb0q5jkv30 (очень точно)
- http://geohash.org/dqb0q5jkv3
- http://geohash.org/dqb0q5jkv
- http://geohash.org/dqb0q5jk
- http://geohash.org/dqb0q5j
- http://geohash.org/dqb0q5
- http://geohash.org/dqb0q
- http://geohash.org/dqb0
- http://geohash.org/dqb
- http://geohash.org/dq
- http://geohash.org/d (очень неточно)
Нам нужно рассмотреть еще одну функцию, и если вы уже знакомы с отсортированными наборами Redis, вы уже знаете это. Поскольку ваши геопространственные данные на самом деле просто хранятся в zset, мы можем удалить элемент с помощью ZREM :
|
1
|
ZREM va-universities university-of-virginia
|
Store Finder Server
Теперь, когда у нас есть основы для использования команд 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
Чтобы интегрировать 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, для предоставления встроенных карт и направлений.