Каков наилучший подход для поиска всех адресов, находящихся на определенном расстоянии до выбранной точки
Я разрабатываю приложение, которое должно показывать адреса, находящиеся на определенном расстоянии от местоположения. Я знаю, как найти расстояние между двумя точками, но проблема в том, что я не уверен, какой был бы лучший подход с точки зрения производительности.
Один из способов - получить все адреса и проверить их один за другим по выбранному адресу в фоновом режиме, но есть ли способ минимизировать количество элементов, которые я извлекаю из базы данных, а не использовать память? Каков наилучший подход для этого и как?
Представьте, что у меня есть 300 000 записей, я должен их загрузить и рассчитать их расстояние до выбранной точки? Поскольку Джеймс предположил, что у меня могут быть записи в разных регионах и рассчитать расстояние, то какой метод будет хорош, отслеживать расстояние через запрос или Java?
public class Address{
long Id;
Double latitude;
Double longitude;
..
}
Расчет
public static double distFrom(double lat1, double lng1, double lat2, double lng2) {
double earthRadius = 3958.75;
double dLat = Math.toRadians(lat2-lat1);
double dLng = Math.toRadians(lng2-lng1);
double sindLat = Math.sin(dLat / 2);
double sindLng = Math.sin(dLng / 2);
double a = Math.pow(sindLat, 2) + Math.pow(sindLng, 2)
* Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2));
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
double dist = earthRadius * c;
return dist;
}
Этот вопрос и этот предлагают методы вычисления расстояния через mysql, но каким образом лучше Java или mysql Я очень смущен.
Ответы
Ответ 1
Когда я реализовал это в MySQL (для хранения мест в сплющенной сфере, которая в основном является землей (я предполагаю, что вы говорите о земле!)), я сохранил как можно больше предварительно рассчитанной информации в базы данных. Итак, для строки, которая хранит latitude
и longitude
, я также вычисляю при вводе время следующие поля:
-
radiansLongitude
(Math.toRadians(longitude)
)
-
sinRadiansLatitude
(Math.sin(Math.toRadians(latitude)
)
-
cosRadiansLatitude
(Math.cos(Math.toRadians(latitude)
)
Затем, когда я ищу места, которые находятся внутри единиц X latitude
/longitude
, мой подготовленный оператор выглядит следующим образом:
from Location l where
acos(
sin(:latitude) * sinRadiansLatitude +
cos(:latitude) * cosRadiansLatitude *
cos(radiansLongitude - :longitude)
) * YYYY < :distance
and l.latitude>:minimumSearchLatitude
and l.latitude<:maximumSearchLatitude
and l.longitude>:minimumSearchLongitude
and l.longitude<:maximumSearchLongitude
order by acos(
sin(:latitude) * sinRadiansLatitude +
cos(:latitude) * cosRadiansLatitude *
cos(radiansLongitude - :longitude)
) * YYYY asc
Где YYYY
= 3965 дает расстояние в милях или YYYY
= 6367 можно использовать для расстояний в км.
Наконец, я использовал параметры maximumSearchLatitude
/maximumSearchLongitude
/minimumSearchLongitude
/maximumSearchLongitude
, чтобы исключить большинство точек из набора результатов до того, как база данных выполнит какие-либо вычисления. Вы можете или не нуждаться в этом. Если вы будете использовать это, вам понадобятся, какие значения вы выберете для этих параметров, так как это будет зависеть от того, что вы ищете.
Очевидно, потребуются разумные приложения индексов в базе данных.
Преимущество использования этого подхода заключается в том, что информация, которая никогда не изменяется, но необходима каждый раз, вычисляется только один раз, тогда как вычисление значений radiansLongitude
, sinRadiansLatitude
, cosRadiansLatitude
для каждой строки каждый раз, когда вы выполняете поиск будет очень дорогим очень быстро.
Другой вариант - использовать геопространственный индекс, что означает, что все это берется для вас базой данных. Я не знаю, насколько хорошо Hibernate интегрируется с этим.
Отказ от ответственности: я долго смотрел на это, и я не специалист по ГИС!
Ответ 2
Вы можете выполнять расчетную серверную часть в самом запросе, а не на стороне клиента, получая при этом только результаты расчета. Здесь (ссылка на архив для потомков) является примером Haversine- (извините, статья просто слишком длинна для меня, чтобы скопировать + вставить или суммировать здесь, хотя это отличная статья и простое чтение).
В качестве альтернативы вы можете разделить свою базу данных на регионы (например, четырехъядерное дерево с полярными координатами) и получить только области рядом с точкой, что даст вам меньшее подмножество для тестирования на стороне клиента. Аналогично, вы можете рассчитать приблизительную шкалу ширины и долготы на основе расстояния, с индексом базы данных по широте и долготе, и выбрать только адреса в этом диапазоне для рассмотрения в ваших расчетах.
Подход запросов - это более простой и понятный подход, хотя и с хорошей производительностью из-за начальной фильтрации расстояния. Я бы применил только подход к региону, если первое из вас не может быть реализовано по какой-то причине.
Ответ 3
Я бы сказал, что подход к базе данных является лучшим, поскольку вам не нужно иметь огромную память. Вы можете использовать следующий код для извлечения их через спящий режим.
@Transactional
public List<Double> getAllPoisAroundUser(double longitude, double latitude, int page) {
Query query = getSessionFactory().getCurrentSession().createSQLQ uery("SELECT (6371 * 2 * ASIN(SQRT(POWER(SIN((:ulatitude - abs(latitude)) * pi()/180 / 2),2) +" +
"COS(:ulatitude * pi()/180 ) * COS(abs(latitude) * pi()/180) *" +
"POWER(SIN((:ulongitude - longitude) * pi()/180 / 2), 2))))*1000 as distance " +
"FROM poi HAVING distance < 5000 ORDER BY distance");
query.setParameter("ulongitude", longitude);
query.setParameter("ulatitude", latitude);
query.setFirstResult((page-1)*10);
query.setMaxResults(10);
return (List<Double>) query.list();
}
Ответ 4
Я использую спящий режим и делаю это следующим образом:
public List<Tour> searchTours(double lat, double lon, double distance) {
Session session = getSession();
Criteria criteria = session.createCriteria(Tour.class, "tour");
//
// 1 Grad lat = 111 km
// 1 grad lon = cos(lat) * 111
//
final double KM_IN_ONE_LAT = 111.0;
double t1 = distance / Math.abs(Math.cos(Math.toRadians(lat)) * KM_IN_ONE_LAT);
double t2 = distance / KM_IN_ONE_LAT;
double lonA = lon - t1;
double lonB = lon + t1;
double latA = lat - t2;
double latB = lat + t2;
Criterion c1 = Restrictions.between("longitude", lonA, lonB);
Criterion c2 = Restrictions.between("latitude", latA, latB);
criteria.add(c1);
criteria.add(c2);
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return criteria.list();
}
Проверьте эту статью для получения дополнительной информации: Geo (proximity) Поиск с MySQL
Ответ 5
Насколько вы точны. Использование индекса gg postgres или индекса r-дерева может быть полезным в качестве отправной точки. Затем выполните запрос ограничивающей рамки. Затем выполните радиальное расстояние на клиенте. Таким образом, математика FP не выполняется центральным сервером (затухающая масштабируемость). Моя проблема заключается в том, что ГИС и rtrees являются самыми медленными типами индексов (ориентированы только на индексы FTS). Поэтому я обычно выбирал 1D-индексы, такие как geohash. Если у вас есть данные о точках, просто сохраните все в общем GSD (Ground Sample Distance), например, 10 метров или 1 метр или что-вы-вы.. Вы строите ' string '(обычно с кодировкой base-64), который является lat-long (каждый бит чередует lat и long). Точки хранятся в виде простого индекса строки в БД (очень эффективны для индексирования и хранения). Затем для запросов вы должны создать ограничительную рамку из точки поиска по всему интересующему вас гео-хэшу... Если у вас очень большие радиусы, это должно сузить результаты поиска... Сделайте окончательная фильтрация в клиенте (или использование одного из методов, перечисленных другим для предварительно рассчитанных значений триггера).
Проблема, однако, в том, что просеивание через 1М точек происходит быстро. Сделать 1000 случайных дисков доступ непригодным. Так что даже если у вас хороший гео-хэш, если у него много случайных точек; это не сработает.
То, что я обычно делал, это bin на диске все соответствующие блоки данных. Таким образом, гео-поиск дает вам конечный набор дисковых расположений... Затем вы загружаете ВСЕ данные (несколько десятков МБ) до 4 дисковых нагрузок. Затем просеиваем всю геометрию. Это может быть на 1000 раз быстрее в лучшем случае (vs .s.000 rand access). Но, очевидно, имеет серьезные ограничения на то, как вы сначала сохранили эти данные в сетках (полностью переписывая или фиксируя размер ваших ящиков).
Очевидно, если у вас достаточно ОЗУ для кэширования всей БД, тогда запустите его. Алгоритм не будет иметь большого значения. Сначала подумайте о шаблонах доступа к диску. Затем шаблоны доступа к процессору (вы можете масштабировать процессоры, но трудно поддерживать дубликаты данных вашего диска).
Ответ 6
План A: Поскольку у вас есть 300K строк, INDEX (lat) является не стартером, с точки зрения производительности, даже с ограничением на полосу: AND lat BETWEEN 65 AND 69
. INDEX(lat, lng)
не лучше, потому что оптимизатор не будет использовать оба столбца, даже с AND lng BETWEEN...
План B: Следующий выбор будет включать lat и lng, плюс подзапрос. И версия 5.6 была бы полезна. Это что-то вроде этого (после включения INDEX(lat, lng, id)
):
SELECT ... FROM (
SELECT id FROM tbl
WHERE lat BETWEEN...
AND lng BETWEEN... ) x
JOIN tbl USING (id)
WHERE ...;
По разным причинам Plan B немного лучше, чем Plan A.
План C: если вам понадобятся миллионы строк, вам понадобится мой алгоритм pizza-салона. Это включает в себя хранимую процедуру для многократного зондирования, ища достаточно строк. Он также включает PARTITION
ing для получения грубого 2D-индекса.
Планы A и B O(sqrt(N))
; План C - O(1)
. То есть для планов A и B, если вы в четыре раза увеличиваете количество строк, вы удваиваете время. План C не замедляется по мере увеличения N.
Ответ 7
Вы можете использовать необработанный запрос для выбора списка идентификаторов формы таблицы адресов в спящем режиме.
public List<Long> getNearByLocations(float latitude, float longitude,
float distance) {
Session sess = getSession();
String queryString = "SELECT id, (6371 * acos (cos(radians("
+ latitude
+ ")) * cos(radians(latitude)) * cos(radians(longitude) - radians("
+ longitude
+ ")) + sin(radians("
+ latitude
+ ")) * sin(radians(latitude)))) AS distance FROM Address HAVING distance < "
+ distance + " ORDER BY distance";
Query qry = sess.createSQLQuery(queryString);
List<Object[]> list = null;
list = qry.list();
List<Long> idList = new ArrayList<>();
for (Object[] obj : list) {
Long id = (Long) obj[0];
idList.add(id);
}
return idList;
}
Ответ 8
Он не эффективен и не масштабируется для запроса всей таблицы базы данных. Рассмотрите возможность использования R-tree для повышения производительности.