Closed7

Supabaseで地図や位置情報を扱うときの便利情報

タイラータイラー

位置情報をPostgresに保存したいとき、緯度経度をそのままそれぞれfloatとしてデータベースに保存することも可能だが、この手法だとデータセットが多くなったときにクエリーが遅くなる。Postgresには位置情報や地形情報を取り扱うのに優れたPostGISという拡張機能があるのでそれを使うのがベストプラクティス!

今回はPostGISを使ってどんな感じでSupabaseでデータのやり取りができるかをこちらの公式ガイドに沿ってフランクに書いていきます。

タイラータイラー

PostGISを使うときはまずSupabaseの管理画面から拡張機能を有効にしてあげる。管理画面左側のメニューのデータベースからExtensionsに入ってPostGISを有効化する。また、以下のSQLでも有効化できる。

-- これで有効化
create extension postgis with schema extensions;

-- これで無効化
drop extension if exists postgis;
タイラータイラー

PostGISには様々データタイプや関数、そしてIndexがついてくる。とりあえず、今回はPoint型を使った簡単なサンプルを用意してみる。Point型はシンプルな緯度経度のセットで、文字通り地図上に緯度経度で表される点を保存するための型。

まずはテーブル作成。今回は街中のレストラン一覧検索アプリ見たいなものを作る想定でレストラン情報を保存するテーブルを作る。このときlocationカラムがPoint型になっている。

create table if not exists public.restaurants (
	id int generated by default as identity primary key,
	name text not null,
	location geography(POINT) not null
);

また、テーブルができたら効率よくそのテーブルをクエリーできるようにインデックスを貼るべき。locationカラムにこのようにしてインデックスを貼ることができる。

create index restaurants_geo_index
  on public.restaurants
  using GIST (location);

また、Point以外にもPostGISには地図上の線や多角形を保存するための様々な型があるが、今回はそこまで深掘りしないので、深堀したいかたはこちらから。

タイラータイラー

テーブルができたらサンプルデータを挿入してみる。

SQLを使ってこんな感じでデータの挿入ができる。

insert into public.restaurants
(name, location)
values
  ('Supa Burger', st_point(-73.946823 40.807416)),
  ('Supa Pizza', st_point(-73.94581 40.807475)),
  ('Supa Taco', st_point(-73.945826 40.80629));

Supabaseのjsライブラリーを使ってデータを挿入することも可能。このときも上の注意事項と同じお作法。

const { error } = await supabase.from('restaurants').insert([
  {
    name: 'Supa Burger',
    location: 'POINT(-73.946823 40.807416)',
  },
  {
    name: 'Supa Pizza',
    location: 'POINT(-73.94581 40.807475)',
  },
  {
    name: 'Supa Taco',
    location: 'POINT(-73.945826 40.80629)',
  },
])
タイラータイラー

Supabaseの管理画面からテーブルを覗いてみるとlocationカラムの値がこんな感じの人には読めないフォーマットになっているが、これで正しく保存されている。

0101000020E6100000A4DFBE0E9C91614044FAEDEBC0494240

実際に今度これらの情報を読み込む際はこの人には読めないフォーマットで値が返ってくるので、それをまたPoint(X, Y)のフォーマットで返してくれるst_astext()というデータベース関数に通すことで解決。

タイラータイラー

データを特定の位置から近い順に並び替えて取得する

特定のデータを近い順に並び替えるのはよくあるユースケースだと思う。例えば、食べログ的なアプリを作っていて現在地から近い順に周辺のレストランを検索したい時など。

Supabaseでこのような複雑なクエリーをするときはデータベース関数を使う。実際の関数がこんな感じ。latとlongが緯度経度で、それをそれぞれ小数で渡せる形になっている。

create or replace function nearby_restaurants(lat float, long float)
returns setof record
language sql
as $$
  select id, name, st_astext(location) as location, st_distance(location, st_point(long, lat)::geography) as dist_meters
  from public.restaurants
  order by location <-> st_point(long, lat)::geography;
$$;

フロントエンドからはこのように呼び出せる。

const { data, error } = await supabase.rpc('nearby_restaurants', {
  lat: 40.807313,
  long: -73.946713,
})

そうするとこんな感じのデータが返ってくる。dist_metersにクエリーした地点からのメートルでの距離が入っている。

[
  {
    "id": 1,
    "name": "Supa Burger",
    "location": "POINT(-73.946823 40.807416)",
    "dist_meters": 14.73033739
  },
  {
    "id": 2,
    "name": "Supa Pizza",
    "location": "POINT(-73.94581 40.807475)",
    "dist_meters": 78.28980007
  },
  {
    "id": 3,
    "name": "Supa Taco",
    "location": "POINT(-73.945826 40.80629)",
    "dist_meters": 136.04329002
  }
]

綺麗に近い順に並び替えられていることがわかる。

タイラータイラー

四角で囲った場所の中のデータを取得してくる。

もう一つ地図を使ったユースケースでよく出てくるのが地図上の特定の四角で囲ったエリアにあるデータを引っ張ってくること。例えば、地図からレストランを検索する際に、ユーザーが地図をスワイプしたら新しくスワイプして表示されているエリアの中にあるレストラン一覧を引っ張ってきたいみたいなケース。

地図上の特定の四角

これもSupabaseで簡単に実装できる。まず、それ用のデータベース関数を用意。この関数には、検索したい四角いエリアの左下の点の緯度経度と右上の点の緯度経度をわたしている。

create or replace function restaurants_in_view(min_lat float, min_long float, max_lat float, max_long float)
returns setof record
language sql
as $$
	select id, name, st_astext(location) as location
	from public.restaurants
	where location && ST_SetSRID(ST_MakeBox2D(ST_Point(min_long, min_lat), ST_Point(max_long, max_lat)),4326)
$$;

フロントエンドから上の関数を.rpc()で呼んであげるだけでサクッとそのエリアにあるレストラン一覧を引っ張ってくることができる!

const { data, error } = await supabase.rpc('restaurants_in_view', {
  min_lat: 40.807,
  min_long: -73.946,
  max_lat: 40.808,
  max_long: -73.945,
})

返ってくるデータはこんな感じ

[
  {
    "id": 2,
    "name": "Supa Pizza",
    "location": "POINT(-73.94581 40.807475)"
  }
]
このスクラップは2023/01/05にクローズされました