Toto je do značné míry zjednodušená verze funkce, kterou používám v aplikaci vytvořené asi před 3 lety. Přizpůsobeno aktuální otázce.
-
Vyhledá umístění v obvodu bodu pomocí boxu . Dalo by se to udělat pomocí kruhu, abyste získali přesnější výsledky, ale to je pro začátek pouze přibližné.
-
Ignoruje skutečnost, že svět není plochý. Moje žádost byla určena pouze pro místní region o průměru několika 100 kilometrů. A hledaný obvod má jen několik kilometrů napříč. Udělat svět plochým je pro tento účel dost dobré. (Úkol:Může pomoci lepší přiblížení poměru šířky/délky v závislosti na geolokaci.)
-
Funguje s geokódy, které získáte z map Google.
-
Funguje se standardním PostgreSQL bez rozšíření (nevyžaduje se PostGis), testováno na PostgreSQL 9.1 a 9.2.
Bez indexu bychom museli vypočítat vzdálenost pro každý řádek v základní tabulce a filtrovat ty nejbližší. Extrémně drahé s velkými stoly.
Upravit:
Znovu jsem to zkontroloval a současná implementace umožňuje index GisT v bodech (Postgres 9.1 nebo novější). Zjednodušte odpovídajícím způsobem kód.
hlavní trik je použít funkční GiST index boxů , i když sloup je jen bod. Díky tomu je možné použít stávající implementaci GiST
.
S takovým (velmi rychlým) vyhledáváním můžeme dostat všechna místa do krabice. Zbývající problém:známe počet řádků, ale neznáme velikost krabice, ve které jsou. To je jako znát část odpovědi, ale ne otázku.
Používám podobné zpětné vyhledávání přístup k tomu, který je podrobněji popsán v tato související odpověď na dba.SE . (Pouze zde nepoužívám částečné indexy – ve skutečnosti by také mohly fungovat).
Procházejte řadou předdefinovaných vyhledávacích kroků, od velmi malých až po „tak akorát velké, aby obsahovaly alespoň dostatek míst“. Znamená to, že musíme spustit několik (velmi rychlých) dotazů, abychom se dostali k velikosti vyhledávacího pole.
Poté pomocí tohoto pole prohledejte základní tabulku a vypočítejte skutečnou vzdálenost pouze pro několik řádků vrácených z indexu. Obvykle tam bude nějaký přebytek, protože jsme našli krabici obsahující nejméně dostatek míst. Tím, že vezmeme ty nejbližší, efektně zakulatíme rohy krabice. Tento efekt můžete vynutit tak, že rámeček uděláte o stupeň větší (vynásobte radius
ve funkci pomocí sqrt(2), abyste byli zcela přesní výsledky, ale nešel bych do toho, protože to je pro začátek přibližné).
To by bylo ještě rychlejší a jednodušší s SP GiST index, dostupný v nejnovější verzi PostgreSQL. Ale nevím, jestli je to ještě možné. Potřebovali bychom skutečnou implementaci pro datový typ a neměl jsem čas se do toho ponořit. Pokud najdete způsob, slibte, že se ohlásíte!
Vzhledem k této zjednodušené tabulce s některými příklady hodnot (adr
.. adresa):
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)');
Index vypadá takto:
CREATE INDEX adr_geocode_gist_idx ON adr USING gist (geocode);
Budete muset upravit oblast domova, kroky a měřítko podle svých potřeb. Dokud budete hledat v rámečcích o několika kilometrech kolem bodu, rovná země je dostatečně dobrá aproximace.
Abyste s tím mohli pracovat, musíte dobře rozumět plpgsql. Mám pocit, že jsem toho tady udělal docela dost.
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;
Volejte:
SELECT * FROM f_find_around (48.2, 16.3, 20);
Vrátí seznam $3
místa, pokud je jich dostatek v definované maximální prohledávané oblasti.
Seřazeno podle skutečné vzdálenosti.
Další vylepšení
Vytvořte funkci jako:
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) --';
(doslova) globální konstanty 111200
a 111400
jsou optimalizovány pro mou oblast (Rakousko) z délky stupně zeměpisné délky
a Délka stupně zeměpisné šířky
, ale v podstatě jen pracovat po celém světě.
Použijte jej k přidání změněného geokódu do základní tabulky, ideálně vygenerovaného sloupce jak je uvedeno v této odpovědi:
Jak se dělá matematika data, která ignoruje rok?
Viz 3. Verze černé magie kde vás provedu celým procesem.
Pak můžete funkci ještě zjednodušit:Změňte jednou měřítko vstupních hodnot a odstraňte nadbytečné výpočty.