Создавать геопространственные приложения легко с Redis

Работа с геопространственными данными, как известно, сложна, поскольку широта и долгота — это числа с плавающей запятой и должны быть очень точными. Кроме того, кажется, что широту и долготу можно представить в виде сетки, но на самом деле это не так, просто потому что Земля не плоская, а математика сложная.

Например, чтобы определить расстояние большого круга между двумя точками на сфере на основе их широты и долготы, используется формула Гаверсина, которая выглядит следующим образом:

Другая распространенная задача, связанная с широтой и долготой, — это нахождение количества точек в радиусе на поверхности Земли. То есть дан большой шар (Земля) и вы пытаетесь найти точки в радиусе на этом шаре. Но Земля, на самом деле, не является идеальным шаром, это все же эллипсоид. Как вы уже догадались, математические расчеты для такой операции становятся довольно сложными.

В этой статье мы рассмотрим, как Redis может помочь нам минимизировать вычисления при работе с геопространственными данными.

Redis, что расшифровывается как Remote Dictionary Server, — это быстрое хранилище данных с открытым исходным кодом. Благодаря своей скорости Redis является популярным выбором для кэширования, управления сессиями, игр, аналитики, геопространственных данных и многого другого.

Давайте вернемся к геопространственным данным. Что такое геохэш?

Geohash — это система представления координат в виде строки. Геохеширование использует кодировку Base32 для преобразования широты и долготы в строку. Например, геохэш Дворцовой площади в Санкт-Петербурге будет выглядеть так: udtscze2chgq. Переменная длина геохэша представляет собой переменную точность местоположения, другими словами, чем короче геохэш, тем менее точными являются координаты, которые он представляет. То есть, более короткий геохэш будет представлять ту же геолокацию, но с меньшей точностью. Вы можете попробовать кодировать координаты в geohash на сайте http://geohash.org.

Как Redis хранит геопространственные данные?

Хранение геопространственных данных реализовано в Redis с использованием сортированных списков (ZSET) в качестве базовой структуры данных, но с кодированием и декодированием данных о местоположении «на лету» и новым API. Это означает, что индексирование, поиск и сортировка по определенному местоположению могут быть реализованы в Redis с помощью очень небольшого количества строк кода и минимальных усилий с использованием встроенных команд: GEOADD, GEODIST, GEORADIUS и GEORADIUSBYMEMBER (GEOSEARCH).

Geo Set является основой для работы с геопространственными данными в Redis — это структура данных, предназначенная для управления геопространственными индексами. Каждый Geo Set состоит из одного или нескольких элементов, каждый из которых состоит из уникального идентификатора и пары координат — долготы и широты.

Команды для работы с геопространственными данными

Чтобы добавить новый список (или новый элемент в существующий список) в хранилище Redis, используйте команду GEOADD. Для наглядности я приведу примеры команд в Redis, а также в Ruby-клиенте для работы с Redis:

# Redis example:
GEOADD "buses" -74.00020246342898 40.717855101298305 "Bus A"

# Ruby example:
RedisClient.geoadd("buses", -74.00020246342898, 40.717855101298305, "Bus A")
Войти в полноэкранный режим Выйти из полноэкранного режима

Эти команды добавляют в Geo Set с именем «buses» координаты местоположения автобуса «Bus A». Если Geo Set с таким именем еще не хранится в Redis, он будет создан. Новая запись будет добавлена в индекс только в том случае, если записи с таким же именем («Bus A») еще нет в списке. То есть Bus A — это уникальный идентификатор.

Также возможно добавление нескольких записей одновременно с помощью одного вызова GEOADD, что может помочь снизить нагрузку на сеть и базу данных. Идентификаторы записей должны быть уникальными:

# Redis example:
GEOADD "buses" -74.00020246342898 40.717855101298305 "Bus A" -73.99472237472686 40.725856700515855 "Bus B"

# Ruby example:
RedisClient.geoadd("buses", -74.00020246342898, 40.717855101298305, "Bus A",
                           -73.99472237472686, 40.725856700515855, "Bus B")
Войти в полноэкранный режим Выход из полноэкранного режима

Эта же команда используется для обновления индекса записи. Если GEOADD вызывается с записями, уже имеющимися в Geo Set, Redis просто обновляет данные для этих записей, как только автобус A начнет движение, его местоположение может быть обновлено:

# Redis example:
GEOADD "buses" -76.99265963484487 38.87275545298483 "Bus A"

# Ruby example:
RedisClient.geoadd("buses", -76.99265963484487, 38.87275545298483, "Bus A")
Войти в полноэкранный режим Выйти из полноэкранного режима

В дополнение к добавлению и обновлению, конечно же, записи могут быть удалены из индекса. Для удаления записи из Geo Set в Redis предусмотрена команда ZREM. ZREM принимает имя индекса, из которого нужно удалить записи, и идентификаторы записей, которые нужно удалить:

# Redis example:
ZREM buses "Bus A" "Bus B"

# Ruby example:
RedisClient.zrem("buses", "Bis A", "Bus B")
Войти в полноэкранный режим Выход из полноэкранного режима

Геоиндекс может быть удален полностью, а поскольку он хранится как ключ Redis, можно использовать команду DEL:

# Redis example:
DEL buses

# Ruby example:
RedisClient.del("buses")
Войти в полноэкранный режим Выйти из полноэкранного режима

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

# Redis example:
UNLINK buses

# Ruby example:
RedisClient.unlink("buses")
Войти в полноэкранный режим Выход из полноэкранного режима

Помните, что Redis имеет механизм истечения срока действия индексов, если вы не укажете дату истечения для индекса, то он никогда не истечет и будет съедать память. Чтобы этого не произошло, необходимо использовать команду EXPIRE, передав имя индекса и количество секунд для истечения срока действия:

# Redis example:
EXPIRE buses 1000

# Ruby example:
RedisClient.expire("buses", 1000)
Войти в полноэкранный режим Выход из полноэкранного режима

Redis использует механизм полуленивого истечения срока действия, это означает, что срок действия индекса не истек, пока он не прочитан, если во время операции чтения выясняется, что срок действия истек, то результат не возвращается, а сам объект удаляется из хранилища. То есть, пока мы не запросим Geo Set, он будет храниться в памяти неограниченно долго.

Однако у Redis есть второй уровень истечения срока действия — активный и случайный. Это сборщик мусора, который случайным образом считывает различные ключи, и когда ключ считывается, происходит стандартный механизм проверки истечения срока действия.

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

Что насчет чтения и поиска по геопространственным данным?
Существует несколько способов чтения записей из индекса. Для начала можно воспользоваться командами ZRANGE и ZSCAN. Эти команды выполняют итерацию по всем записям в индексе. Например, чтобы вернуть все записи в индексе:

# Redis example:
ZRANGE buses 0 -1

# Ruby example:
RedisClient.zrange("buses", 0, -1)
Войти в полноэкранный режим Выйти из полноэкранного режима

Что касается геопространственных данных, есть две команды для получения местоположения записи из индекса. Первая — GEOPOS команда возвращает координаты записи в индексе:

# Redis example:
GEOPOS buses "Bus A"

# Ruby example:
RedisClient.geopos("buses", "Bus A")
Войти в полноэкранный режим Выйти из полноэкранного режима

Вторая команда — GEOHASH возвращает координаты записи, закодированной в геохэше:

# Redis example:
GEOHASH buses "Bus A"

# Ruby example:
RedisClient.geohash("buses", "Bus A")
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтобы получить расстояние между двумя записями в индексе, можно использовать команду GEODIST:

# Redis example:
GEODIST buses "Bus A" "Bus B"

# Ruby example:
RedisClient.geodist("buses", "Bus A", "Bus B", "km")
Войти в полноэкранный режим Выйти из полноэкранного режима

Результат команды по умолчанию будет возвращен в метрах. Вы можете указать необходимые единицы измерения, передав команде четвертый аргумент, например: km — для километров, m — для метров, mi — для миль, ft — для футов.

Для поиска в индексе также используются команды GEORADIUS и GEORADIUSBYMEMBER (для версий Redis меньше 6.2) или GEOSEARCH (для версий старше 6.2).

GEORADIUS и GEORADIUSBYMEMBER принимают параметры WITHDIST (отображение результатов + расстояние от указанной точки/записи) и WITHCOORD (отображение результатов + координаты записи), а также опцию сортировки ASC или DESC (сортировка по расстоянию от точки):

# Redis example:
GEORADIUS buses -73 40 200 km WITHDIST

# returns:
1) 1) "Bus A"
  2) "190.4424"
2) 1) "Bus B"
  2) "56.4413"

GEORADIUS buses -73 40 200 km WITHCOORD

# returns:
1) 1) "Bus A"
  2) 1) "-74.00020246342898"
     2) "40.717855101298305"
2) 1) "Bus B"
  2) 1) "-73.99472237472686
     2) "40.725856700515855"

GEORADIUS buses -73 40 200 km WITHDIST WITHCOORD

# returns:
1) 1) "Bus A"
  2) "190.4424"
  3) 1) "-74.00020246342898"
     2) "40.717855101298305"
2) 1) "Bus B"
  2) "56.4413"
  3) 1) "-73.99472237472686
     2) "40.725856700515855"

# Redis example:
GEORADIUSBYMEMBER buses "Bus A" 100 km

# returns:
1) “Bus B”

# Ruby example:
RedisClient.georadiusbymember("buses", "Bus A", 100, "km")
Войти в полноэкранный режим Выход из полноэкранного режима

Команда GEOSEARCH для новых версий Redis имеет похожий синтаксис и делает то же самое. Синтаксис команды выглядит следующим образом:

# Redis examples:
GEOSEARCH buses FROMMEMBER "Bus A" BYRADIUS 100 km ASC WITHCOORD WITHDIST WITHHASH
# returns all entries in 100km radius from Bus A with coordinates, distances and geohashes

GEOSEARCH buses FROMLONLAT -74.00020246342898 40.717855101298305" BYRADIUS 200 mi DESC COUNT 2
# returns maximum 2 entries sorted from the farest to the closest within 200 miles from the center
# with given coordinates
Войти в полноэкранный режим Выйти из полноэкранного режима

Заключение

Простота реализации приложений для определения местоположения с геопространственными данными в Redis не только облегчает работу с большими объемами геопространственных данных, но и позволяет реализовать некоторую сложную обработку данных. Например, запрос записей в радиусе может помочь вам реализовать поиск точек интереса поблизости, предоставляя пользователю только ближайшие к нему варианты. Если ваше приложение каким-либо образом использует геопространственные данные, подумайте о переносе сложных вычислений в Redis, это может повысить эффективность вашего приложения.

Оцените статью
devanswers.ru
Добавить комментарий