Фильтрация объектов по координатам (широте и долготе)

В настоящее время  часто встречается задача —  на карте расположено множество точек  и необходимо выделить их подмножество (отфильтровать) по критерию  расстояния  от некоторой «особой» точки. В качестве примеров укажем сайты маркетплейсов, где точки пункты выдачи, в сетевых компаниях это офисы на карте, логистические компании отслеживают передвижение транспортных средств, операторы такси находят на карте ближайшие автомобили и можно перечислить еще много областей применения. При кажущейся простоте постановки задачи её решение требует значительных ресурсов, так как запросы к базе данных, как правило, очень тяжелые. Здесь на примере задачи «Пункты выдачи» с фильтрацией по расстоянию (радиусу) от накопительного узла товаров показывается как можно сократить количество обработанных строк в запросе. Реализация задачи на БД mySQL.

Координаты исходной точки могут быть получены несколькими способами, первый если устройство пользователя оборудовано GPS и пользователь разрешил передачу своих координат, то мы получаем координаты пользователя без затруднений, второй способ менее точный мы получаем координаты пользователя из его IP, для этого используется библиотека maxmind.

В БД mySQL 5.7 появилась встроенная функция ST_Distance_Sphere для расчета (евклидово) расстояния между двумя точками. Данная функция принимает широту и долготу двух точек, а возвращает расстояние между ними в метрах, далее мы производим сравнение с критической величиной радиуса и определяем попадает ли данная точка в круг  с этим радиусом.

WHERE (ST_Distance_Sphere(point(longitude, latitude), point(longitude2, latitude2)) ) <= radius

43ddbf92b76ef3979d76e594ad958e8a.png

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

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

1a9dbb5fbb9d2bd901824a399071191c.png

Вычисляем координаты вершин квадрата

Сторона квадрата будет равняться диаметру окружности, то есть радиус*2.

Формула вычисления координат вершин квадрата:

Distance — искомая нами дистанция в километрах (радиус).

Экваториальная окружность Земли составляет около 40 075 км (не будем использовать меридианную 40 008).

Окружность 360 градусов.

Для полного понимания данных расчетов необходимо понимать:

Широта и долгота измеряются в градусах:

— градусом географической широты является 1/180 часть меридиана (latitude — широта значения от -90 до 90)

— градусом географической долготы является 1/360 часть экватора (longitude — долгота значения от -180 до 180)

Небольшое отхождение от темы. Данная формула не работает для тех кто считает что земля плоская))), а таких с каждым днем все больше (((

e544f3851f3a449eec548c01e739e705.png

Итак, мы вычислили координаты вершин квадрата,  в который вписан наш круг и можем применить промежуточную фильтрацию + основную. Фильтрация по WHERE between работает быстрее, чем встроенная фильтрация по ST_Distance_Sphere

WHERE longitude between longitudeMin and longitudeMax

and latitude between latitudeMin and latitudeMax

and (ST_Distance_Sphere(point(longitude, latitude), point(longitude2, latitude2)) ) <= radius // radius в метрах

В качестве оптимизации добавляем индекс на столбцы: longitude, latitude

 CREATE INDEX coordinate ON table_name (latitude, longitude);

Данная оптимизация позволила без тормозов обрабатывать 2 млн. записей.

Но было решено на этом не останавливаться и произвести еще одну оптимизацию.

В связи с тем что пользователи чаще всего используют фильтр с радиусом 0–5 км, было решено добавить еще один столбец в таблицу для фильтрации, этот столбец будет целочисленными и его значение будет от -90 000 до 90000.

ALTER TABLE table_name ADD COLUMN latitudeInt INT DEFAULT NULL

Заполнять этот столбцы мы будем по формуле: latitudeInt = Math.round (latitude*1000). Итак, что же мы делаем, по факту имея в БД значение latitude хх.хххххххх мы его приводим к целочисленному виду и округляем до 5 знаков ххххх. При текущей фильтрации с радиусом 0–5 км пяти знаков достаточно для произведения фильтрации, тем самым мы упрощаем процесс фильтрации, тем более фильтрацию с помощью функции ST_Distance_Sphere мы оставляем. 

Представим, что координаты исходной точки latitude = 10.111 и longitude = 41.000 по формуле выше вычисляем:

longitude min — 40.98951994951971

longitude max — 41.01048005048029

latitude min — 10.10051684341859

latitude max — 10.12131565814199

По latitude min и latitude max вычисляем искомый диапазон latitudeInt.

getRange(latMin, latMax) {

    const latitudeMinInt = Math.floor(latMin * 1000)

    const latitudeMaxInt = Math.ceil(latMax * 1000)

    const mas = [latitudeMinInt];

    for (let index = 1; index < latitudeMaxInt - latitudeMinInt + 1; index++) {

      mas.push(latitudeMinInt+index)   

    }

    return mas;

}

В нашем случае диапазон будет:

[10102,10103,10104,10105,10106,10107,10108,10109,10110,10111,10112,10113,10114,10115,10116,10117,10118,10119,10120,10121,10122]

В случае если несколько пунктов выдачи попали например, в диапазон 10107 — 10108, то фактически мы их склеили одним значением поля latitudeInt

62f5cbedd44fd3c6fbf7e42450b8bd1e.png

Теперь фильтры будут представлены следующим образом:

latitudeIntMas = [10102,10103,10104,10105,10106,10107,10108,10109,10110,10111,10112, 10113,10114,10115,10116,10117,10118,10119,10120,10121,10122]

longitudeMin — 40.98951994951971

longitudeMax -41.01048005048029

WHERE latitudeInt in (latitudeIntMas )

AND longitude between longitudeMin and longitudeMax

AND (ST_Distance_Sphere(point(longitude, latitude), point(longitude2, latitude2)) ) <= radius // radius в метрах

Так же добавляем индекс

CREATE INDEX latitudeInt ON table_name (latitudeInt);

Заметим, что данные расчеты не учитывают кривизну земли, но для основного запроса потребителя 0–5 км погрешность не является  существенной.

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

© Habrahabr.ru