1

現在、ユーザーがリストから場所を選択する必要があるモバイル アプリケーションを作成しています。すべての場所は、Play アプリから JPA を使用して Postgres データベースに保存されます。

私がやりたいことは、アプリでユーザーの場所を取得し、そのユーザーに最も近い最初の 20 または 50 の場所を取得するように要求することです。

これに独自のデータ構造を使用する場合は、KD-Tree を使用します。ただし、私は JPA/Play/PostgreSQL に非常に慣れていないため、データの永続性を手動で処理する方法がわかりません。

私の現在の知識で考えられる唯一のことは、各場所を見て距離を判断することですが、そのような巨大なデータベースでは信じられないほど遅くなります.

「この緯度と経度からの距離で並べ替えられた最初の X 件の結果を教えてください」と言うために実行できるクエリはありますか?

編集: 私は Heroku を使用しています。アプリケーションは開発の初期段階にあるため、アプリで PostGIS を使用する場合、Heroku に必要な月額 200 ドルを支払う必要はありません。

4

2 に答える 2

3

これは、約 3 年前に作成したアプリで使用する機能を大幅に簡略化したものです。当面の質問に適応。

  • ボックスを使用して、ポイントの周囲の位置を検索します。より正確な結果を得るために円を使用してこれを行うこともできますが、これは最初は概算にすぎません。

  • 世界が平らではないという事実を無視します。私のアプリケーションは、数 100 キロメートルのローカル リージョンのみを対象としていました。そして、捜索範囲はわずか数キロメートルしかありません。世界を平らにすることは、目的には十分です。(Todo: 位置情報に応じた緯度/経度比のより適切な概算が役立つ場合があります。)

  • Google マップから取得したようなジオコードで動作します。

  • PostgreSQL 9.1 および 9.2 でテストされた、拡張機能なし(PostGis は不要) の標準 PostgreSQLで動作します。

インデックスがなければ、ベース テーブルのすべての行の距離を計算し、最も近い行をフィルタリングする必要があります。大きなテーブルで非常に高価です。

編集:
再確認したところ、現在の実装ではポイントの GisT インデックスが許可されています (Postgres 9.1 以降)。それに応じてコードを簡素化しました。

なトリックは、列が単なるポイントであっても、ボックスの機能的なGiST インデックスを使用することです。これにより、既存のGiST 実装を使用できるようになります。

このような (非常に高速な) 検索を使用すると、ボックス内のすべての場所を取得できます。残りの問題: 行の数はわかっていますが、それらが入っているボックスのサイズはわかりません。これは、答えの一部を知っているが、質問を知らないようなものです。

dba.SE に関するこの関連する回答で詳しく説明されている方法と同様の逆引き参照アプローチを使用します。(ただし、ここでは部分インデックスを使用していません-実際にも機能する可能性があります)。

非常に小さいものから「少なくとも十分な場所を保持するのに十分な大きさ」まで、定義済みの検索ステップの配列を繰り返します。つまり、検索ボックスのサイズを取得するには、いくつかの (非常に高速な) クエリを実行する必要があります。

次に、このボックスでベース テーブルを検索し、インデックスから返された数行のみの実際の距離を計算します。ボックスには少なくとも十分な場所が含まれていることがわかったので、通常はいくらかの余剰があります。最も近いものを取得することで、ボックスの角を効果的に丸めます。ボックスを 1 ノッチ大きくすることで、この効果を強制することができます(完全に正確なradius結果を得るには、関数を sqrt(2) で乗算しますが、これは最初から概算であるため、全力を尽くすことはしません)。

これは、最新バージョンの PostgreSQL で利用可能なSP GiSTインデックスを使用すると、さらに高速かつ簡単になります。しかし、それが可能かどうかはまだわかりません。データ型を実際に実装する必要がありましたが、それについて詳しく説明する時間がありませんでした。方法を見つけたら、また報告することを約束してください!

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);

-> SQLfiddle

必要に応じて、ホーム エリア、ステップ、倍率を調整する必要があります。ポイントの周囲数キロメートルのボックスで検索する限り、平らな地球は十分な近似値です。

これを使用するには、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から私の地域(オーストリア)に最適化されていますが、基本的には世界中で機能します。

それを使用して、スケーリングされたジオコードをベース テーブルに追加します。理想的には、この回答で概説されているような生成された列です:
How do you do date math that ignores the year? プロセスを説明する3. ブラック マジック バージョン
を参照してください。 次に、関数をもう少し単純化できます。入力値を一度スケーリングし、冗長な計算を削除します。

于 2013-03-24T01:53:14.193 に答える
2

このために独自のデータ構造を作成する必要はありませが、幸いなことに PostgreSQL を使用しているので、幸運です。PostGISを使用します。合理的な時間内に構築できるものよりも桁違いに高速になります。

于 2013-03-23T15:25:01.713 に答える