Jest to w dużej mierze uproszczona wersja funkcji, której używam w aplikacji zbudowanej około 3 lata temu. Dostosowane do aktualnego pytania.
-
Znajduje lokalizacje na obwodzie punktu za pomocą pudełka . Można to zrobić za pomocą koła, aby uzyskać dokładniejsze wyniki, ale na początku ma to być tylko przybliżenie.
-
Ignoruje fakt, że świat nie jest płaski. Moja aplikacja była przeznaczona tylko dla regionu o średnicy kilkuset kilometrów. A obwód poszukiwawczy obejmuje tylko kilka kilometrów. Wyrównywanie świata jest wystarczająco dobre do tego celu. (Do zrobienia:może pomóc lepsze przybliżenie stosunku lat/dług w zależności od geolokalizacji).
-
Działa z geokodami, tak jak w mapach Google.
-
Działa ze standardowym PostgreSQL bez rozszerzenia (nie wymaga PostGis), przetestowane na PostgreSQL 9.1 i 9.2.
Bez indeksu należałoby obliczyć odległość dla każdego wiersza w tabeli bazowej i przefiltrować najbliższe. Niezwykle drogie z dużymi stołami.
Edytuj:
Sprawdziłem ponownie i bieżąca implementacja pozwala na indeks GisT dla punktów (Postgres 9.1 lub nowszy). Odpowiednio uproszczono kod.
główna sztuczka jest użycie funkcjonalnego indeksu GiST pudełek , mimo że kolumna to tylko punkt. Umożliwia to korzystanie z istniejącej implementacji GiST
.
Przy takim (bardzo szybkim) wyszukiwaniu możemy uzyskać wszystkie lokalizacje wewnątrz pudełka. Pozostały problem:znamy liczbę rzędów, ale nie znamy rozmiaru pudełka, w którym się znajdują. To tak, jakby znać część odpowiedzi, ale nie pytanie.
Używam podobnego odwrotnego wyszukiwania podejście do opisanego bardziej szczegółowo w ta powiązana odpowiedź na dba.SE . (Tylko, że nie używam tutaj częściowych indeksów - może też działać).
Przejdź przez szereg wstępnie zdefiniowanych kroków wyszukiwania, od bardzo małych do „wystarczająco dużych, aby pomieścić co najmniej wystarczającą liczbę lokalizacji”. Oznacza to, że musimy uruchomić kilka (bardzo szybkich) zapytań, aby uzyskać rozmiar pola wyszukiwania.
Następnie przeszukaj tabelę bazową za pomocą tego pola i oblicz rzeczywistą odległość tylko dla kilku wierszy zwróconych z indeksu. Zwykle będzie trochę nadwyżki, ponieważ znaleźliśmy pudełko zawierające co najmniej wystarczająca liczba lokalizacji. Wybierając te najbliższe skutecznie zaokrąglamy rogi pudełka. Możesz wymusić ten efekt, powiększając pole o jeden oczko (pomnóż radius
w funkcji sqrt(2), aby uzyskać pełną dokładność wyniki, ale nie wyszedłbym na całość, ponieważ na początku jest to przybliżone).
Byłoby to jeszcze szybsze i prostsze dzięki SP GiST index, dostępny w najnowszej wersji PostgreSQL. Ale nie wiem, czy to jeszcze możliwe. Potrzebowalibyśmy rzeczywistej implementacji tego typu danych, a ja nie miałem czasu się w nią zagłębiać. Jeśli znajdziesz sposób, obiecaj się zgłosić!
Biorąc pod uwagę tę uproszczoną tabelę z kilkoma przykładowymi wartościami (adr
.. adres):
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)');
Indeks wygląda tak:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
Będziesz musiał dostosować obszar domu, stopnie i współczynnik skalowania do swoich potrzeb. Dopóki przeszukujesz pola o długości kilku kilometrów wokół punktu, płaska ziemia jest wystarczającym przybliżeniem.
Aby z tym pracować, musisz dobrze zrozumieć plpgsql. Czuję, że zrobiłem tutaj wystarczająco dużo.
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;
Zadzwoń:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Zwraca listę $3
lokalizacje, jeśli jest ich wystarczająco dużo w zdefiniowanym maksymalnym obszarze wyszukiwania.
Posortowane według rzeczywistej odległości.
Dalsze ulepszenia
Zbuduj funkcję taką jak:
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) --';
(dosłownie) globalne stałe 111200
i 111400
są zoptymalizowane dla mojego obszaru (Austria) na podstawie Długość stopnia długości geograficznej
oraz Długość stopnia szerokości geograficznej
, ale w zasadzie działa na całym świecie.
Użyj go, aby dodać skalowany geokod do tabeli podstawowej, najlepiej wygenerowaną kolumnę jak opisano w tej odpowiedzi:
Jak wykonać obliczenia na datach, które ignorują rok?
Patrz 3. Wersja czarnej magii gdzie przeprowadzę Cię przez ten proces.
Następnie możesz uprościć funkcję jeszcze bardziej:Skaluj wartości wejściowe raz i usuń zbędne obliczenia.