Това е до голяма степен опростена версия на функция, която използвам в приложение, създадено преди около 3 години. Адаптирано към разглеждания въпрос.
-
Намира местоположения в периметъра на точка с помощта на кутия . Човек може да направи това с кръг, за да получи по-точни резултати, но това е предназначено да бъде само приблизително като начало.
-
Пренебрегва факта, че светът не е плосък. Приложението ми беше предназначено само за местен регион, няколко 100 километра. А периметърът на търсене се простира само на няколко километра. Да направим света плосък е достатъчно добро за целта. (Задача:По-добро приближение за съотношението ширина/дължина в зависимост от геолокацията може да помогне.)
-
Работи с геокодове, каквито получавате от Google Maps.
-
Работи със стандартния PostgreSQL без разширение (не се изисква PostGis), тестван на PostgreSQL 9.1 и 9.2.
Без индекс би трябвало да се изчисли разстоянието за всеки ред в базовата таблица и да се филтрират най-близките. Изключително скъпо с големи маси.
Редактиране:
Проверих отново и текущата реализация позволява GisT индекс на точки (Postgres 9.1 или по-нова). Съответно опростете кода.
Основният трик е да се използва функционален GiST индекс на кутии , въпреки че колоната е само точка. Това прави възможно използването на съществуващата имплементация на GiST
.
С такова (много бързо) търсене можем да получим всички местоположения в кутия. Оставащият проблем:знаем броя на редовете, но не знаем размера на кутията, в която се намират. Това е все едно да знаем част от отговора, но не и въпроса.
Използвам подобно обратно търсене подход към описания по-подробно в този свързан отговор на dba.SE . (Само, че не използвам частични индекси тук - може също да работи).
Преминете през набор от предварително дефинирани стъпки за търсене, от много малки до „достатъчно големи, за да поберат поне достатъчно местоположения“. Означава, че трябва да изпълним няколко (много бързи) заявки, за да стигнем до размера на полето за търсене.
След това потърсете в базовата таблица с това поле и изчислете действителното разстояние само за няколкото реда, върнати от индекса. Обикновено ще има излишък, тъй като открихме, че кутията съдържа поне достатъчно места. Като вземаме най-близките, ефективно закръгляме ъглите на кутията. Бихте могли да принудите този ефект, като направите полето с един прорез по-голям (умножете radius
във функцията от sqrt(2), за да получите напълно точно резултати, но не бих давал всичко, тъй като това е приблизително като начало).
Това би било още по-бързо и по-просто с SP GiST индекс, наличен в най-новата версия на PostgreSQL. Но все още не знам дали това е възможно. Трябваше ни действително внедряване за типа данни и нямах време да се потопя в него. Ако намерите начин, обещайте да докладвате!
Предвид тази опростена таблица с някои примерни стойности (adr
.. адрес):
CREATE TABLE adr(adr_id int, adr text, geocode point);
INSERT INTO adr (adr_id, adr, geocode) VALUES
(1, 'adr1', '(48.20117,16.294)'),
(2, 'adr2', '(48.19834,16.302)'),
(3, 'adr3', '(48.19755,16.299)'),
(4, 'adr4', '(48.19727,16.303)'),
(5, 'adr5', '(48.19796,16.304)'),
(6, 'adr6', '(48.19791,16.302)'),
(7, 'adr7', '(48.19813,16.304)'),
(8, 'adr8', '(48.19735,16.299)'),
(9, 'adr9', '(48.19746,16.297)');
Индексът изглежда така:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
Ще трябва да настроите домашната зона, стъпките и коефициента на мащабиране според вашите нужди. Докато търсите в кутии от няколко километра около точка, плоската земя е достатъчно добро приближение.
Трябва да разбирате добре plpgsql, за да работите с това. Чувствам, че направих достатъчно тук.
CREATE OR REPLACE FUNCTION f_find_around(_lat double precision, _lon double precision, _limit bigint = 50)
RETURNS TABLE(adr_id int, adr text, distance int) AS
$func$
DECLARE
_homearea CONSTANT box := '(49.05,17.15),(46.35,9.45)'::box; -- box around legal area
-- 100m = 0.0008892 250m, 340m, 450m, 700m,1000m,1500m,2000m,3000m,4500m,7000m
_steps CONSTANT real[] := '{0.0022,0.003,0.004,0.006,0.009,0.013,0.018,0.027,0.040,0.062}'; -- find optimum _steps by experimenting
geo2m CONSTANT integer := 73500; -- ratio geocode(lon) to meter (found by trial & error with google maps)
lat2lon CONSTANT real := 1.53; -- ratio lon/lat (lat is worth more; found by trial & error with google maps in (Vienna)
_radius real; -- final search radius
_area box; -- box to search in
_count bigint := 0; -- count rows
_point point := point($1,$2); -- center of search
_scalepoint point := point($1 * lat2lon, $2); -- lat scaled to adjust
BEGIN
-- Optimize _radius
IF (_point <@ _homearea) THEN
FOREACH _radius IN ARRAY _steps LOOP
SELECT INTO _count count(*) FROM adr a
WHERE a.geocode <@ box(point($1 - _radius, $2 - _radius * lat2lon)
, point($1 + _radius, $2 + _radius * lat2lon));
EXIT WHEN _count >= _limit;
END LOOP;
END IF;
IF _count = 0 THEN -- nothing found or not in legal area
EXIT;
ELSE
IF _radius IS NULL THEN
_radius := _steps[array_upper(_steps,1)]; -- max. _radius
END IF;
_area := box(point($1 - _radius, $2 - _radius * lat2lon)
, point($1 + _radius, $2 + _radius * lat2lon));
END IF;
RETURN QUERY
SELECT a.adr_id
,a.adr
,((point (a.geocode[0] * lat2lon, a.geocode[1]) <-> _scalepoint) * geo2m)::int4 AS distance
FROM adr a
WHERE a.geocode <@ _area
ORDER BY distance, a.adr, a.adr_id
LIMIT _limit;
END
$func$ LANGUAGE plpgsql;
Обаждане:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Връща списък от $3
местоположения, ако има достатъчно в определената максимална зона за търсене.
Сортирани по действително разстояние.
Допълнителни подобрения
Създайте функция като:
CREATE OR REPLACE FUNCTION f_geo2m(double precision, double precision)
RETURNS point AS
$BODY$
SELECT point($1 * 111200, $2 * 111400 * cos(radians($1)));
$BODY$
LANGUAGE sql IMMUTABLE;
COMMENT ON FUNCTION f_geo2m(double precision, double precision)
IS 'Project geocode to approximate metric coordinates.
SELECT f_geo2m(48.20872, 16.37263) --';
(Буквално) глобалните константи 111200
и 111400
са оптимизирани за моя район (Австрия) от Дължина на градус дължина
и Дължината на градус географска ширина
, но по същество просто работят по целия свят.
Използвайте го, за да добавите мащабиран геокод към основната таблица, в идеалния случай генерирана колона както е описано в този отговор:
Как се прави математика за дата, която игнорира годината?
Вижте 3. Черна магическа версията където ви превеждам през процеса.
След това можете да опростите функцията още малко:мащабирайте входните стойности веднъж и премахнете излишните изчисления.