Гео-команды: координаты и радиус

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

Geo-команды (GEOADD, GEOSEARCH, GEODIST, GEOPOS) — это надстройка над sorted set: координаты (долгота, широта) кодируются в одно число (geohash), которое становится score, а имя объекта — членом множества.

«Найди рестораны рядом», «покажи свободные самокаты в радиусе 300 метров», «какие курьеры ближе всего к точке выдачи» — за всеми этими фичами стоит одна операция: поиск объектов вокруг координаты. Redis умеет это из коробки, без отдельной геобазы, и отвечает за доли миллисекунды. В этом уроке разберём команды и заодно поймём, почему гео в Redis — это, по сути, тот же sorted set, что вы уже знаете.

GEOADD: добавляем объекты с координатами

GEOADD key longitude latitude member добавляет объект. Важен порядок: сначала долгота (longitude), потом широта (latitude) — частая путаница. Заведём несколько кафе в центре Петербурга.

GEOADD cafes 30.3209 59.9410 "Кофе у Дома"
GEOADD cafes 30.3146 59.9398 "Эрмитаж-кафе"
GEOADD cafes 30.4500 59.8900 "Дальний угол"

Под капотом это обычный ZADD: член — название кафе, а score — число, в которое упакованы обе координаты. Поэтому к гео-ключу применимы и команды sorted set: ZCARD cafes вернёт число объектов, ZREM cafes "Дальний угол" удалит точку.

GEODIST: расстояние между объектами

GEODIST key member1 member2 [unit] возвращает расстояние между двумя сохранёнными объектами. Единицы: m (по умолчанию), km, mi, ft.

GEODIST cafes "Кофе у Дома" "Эрмитаж-кафе" m    # => "около 350"
GEODIST cafes "Кофе у Дома" "Дальний угол" km   # => "около 8"

GEOSEARCH: поиск «рядом»

Главная команда. GEOSEARCH ищет объекты вокруг центра — либо вокруг координаты (FROMLONLAT), либо вокруг уже сохранённого объекта (FROMMEMBER), в радиусе (BYRADIUS) или в прямоугольнике (BYBOX). Модификаторы WITHCOORD, WITHDIST, ASC/DESC, COUNT управляют выводом.

# ближайшие кафе в радиусе 1 км от точки (я стою тут), с расстоянием, по возрастанию
GEOSEARCH cafes FROMLONLAT 30.3158 59.9398 BYRADIUS 1000 m ASC WITHDIST
# 1) "Эрмитаж-кафе"  67.xxxx
# 2) "Кофе у Дома"   314.xxxx
# "Дальний угол" не попал — он дальше 1 км

В современных Redis именно GEOSEARCH заменяет устаревшие GEORADIUS и GEORADIUSBYMEMBER — у новой команды единый, более гибкий интерфейс.

Как это работает под капотом

Координаты кодируются алгоритмом geohash: пространство рекурсивно делится пополам по долготе и широте, и из чередующихся бит складывается одно 52-битное целое. Соседние по этому числу значения — это, как правило, географически близкие точки. Это число и есть score в sorted set, а sorted set хранит элементы отсортированными по score. Поэтому поиск «в радиусе» сводится к выборке диапазонов соседних score (что sorted set делает мгновенно) и финальной проверке честного расстояния.

Само расстояние Redis считает по формуле гаверсинусов (haversine) — расстоянию по дуге сферы. Воспроизведём фильтр «в радиусе 1 км, отсортировать по близости» на чистом Python, чтобы увидеть, что делает GEOSEARCH ... BYRADIUS после выборки кандидатов:

from math import radians, sin, cos, asin, sqrt

def haversine(lon1, lat1, lon2, lat2):
    R = 6372797.56                        # радиус Земли в метрах, как в Redis
    lon1, lat1, lon2, lat2 = map(radians, (lon1, lat1, lon2, lat2))
    dlon, dlat = lon2 - lon1, lat2 - lat1
    a = sin(dlat/2)**2 + cos(lat1)*cos(lat2)*sin(dlon/2)**2
    return 2 * R * asin(sqrt(a))

me = (30.3158, 59.9398)                   # центр поиска (lon, lat)
cafes = [
    ("Кофе у Дома",  30.3209, 59.9410),
    ("Эрмитаж-кафе", 30.3146, 59.9398),
    ("Дальний угол", 30.4500, 59.8900),
]
found = []
for name, lon, lat in cafes:
    d = haversine(me[0], me[1], lon, lat)
    if d <= 1000:                          # BYRADIUS 1000 m
        found.append((round(d), name))
found.sort()                              # ASC по расстоянию
print("Кафе в радиусе 1 км (ASC):")
for dist, name in found:
    print(f"  {name:14} {dist} м")

Вывод:

Кафе в радиусе 1 км (ASC):
  Эрмитаж-кафе   67 м
  Кофе у Дома    314 м

«Дальний угол» отсеялся — он за пределами километра. Redis делает то же самое, только кандидатов отбирает не перебором всех точек, а по диапазонам geohash в sorted set, поэтому работает быстро даже на миллионах объектов.

Частые ошибки

  • Перепутать порядок координат. В GEOADD и GEOSEARCH FROMLONLAT сначала долгота, потом широта. Если поменять местами — точки окажутся «в океане».
  • Выход за пределы координат. Долгота должна быть в диапазоне примерно ±180, широта — примерно ±85.05 (ограничение проекции). Координаты вне диапазона Redis отвергнет ошибкой.
  • Огромный радиус на большом множестве. BYRADIUS в тысячи км вернёт почти весь набор; всегда ограничивайте выдачу через COUNT.
  • Полагаться на устаревшие команды. GEORADIUS объявлены устаревшими; в новом коде используйте GEOSEARCH и GEOSEARCHSTORE.
  • Ждать точности до сантиметра. Geohash — приближение: на 52 битах ошибка кодирования в пределах метра. Для городской логистики это незаметно, для геодезии — нет.

Итоги

  • Гео в Redis — это sorted set, где score — закодированные координаты (geohash), а member — имя объекта.
  • GEOADD добавляет точку (долгота, затем широта!), GEODIST меряет расстояние, GEOSEARCH ищет в радиусе или прямоугольнике.
  • Поиск «рядом» быстр, потому что сводится к диапазонам соседних score в sorted set плюс проверка честного расстояния по haversine.
  • Используйте GEOSEARCH вместо устаревших GEORADIUS, ограничивайте выдачу через COUNT и не путайте порядок координат.
Проверьте себя
1. На какой структуре данных Redis реализованы гео-команды?
AНа хэше (Hash), где поля — широта и долгота
BНа sorted set, где score — это закодированные в geohash координаты
CНа отдельном пространственном индексе (R-дерево)
DНа списке (List) пар координат
2. В каком порядке GEOADD принимает координаты?
AСначала широта, потом долгота
BСначала долгота, потом широта
CПорядок не важен, Redis определит сам
DВ формате одной строки 'lat,lon'
3. Какую команду стоит использовать в новом коде для поиска объектов в радиусе вокруг точки?
AGEORADIUS — она для этого и создана
BGEOSEARCH с FROMLONLAT и BYRADIUS
CGEODIST с большим расстоянием
DZRANGEBYSCORE по сырому geohash