【Supabase + PostGIS】サーバーレスでも位置情報を扱いたい
はじめに
こんにちは!
「愛犬との毎日を楽しく便利にするアプリ オトとりっぷ」でエンジニアしています、足立です!
最近アプリ内で位置情報を扱うための諸々を整備しているのですが、サーバーレス構成での位置情報とデータベースに関する情報が少なく苦労しました。
AWS がメイン環境で、VPN レスのサーバーレス構成という Amazon Aurora Serverless も利用できない環境でいかに位置情報を扱うか?の備忘録を残すととともに、同じような疑問を抱えている方の一助になればと思います。
位置情報とデータベース
GeoJson
そもそも、位置情報とはどのような形をしているのでしょうか?
大きく分けて 3 つの形があります。
- ポイント座標(Point):1 次元の点情報
- ライン座標(LineString):2 次元の線情報
- ポリゴン座標(Polygon):3 次元の面情報
これらの情報を必要に応じて使い分けるわけです。
例えば、現在地点から目的地までの直線距離の情報が欲しい場合はポイント座標の情報が、現在地点から目的地までの道程の情報が欲しい場合はライン座標が、目的地の所在地の広さの情報が欲しい場合はポリゴン座標が、必要になるわけですね。
そして次元の異なる情報を扱うためのデータ構造として、GeoJson という規格があります。
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [102.0, 0.5]
}
}
LineString と Polygon
[
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[102.0, 0.0],
[103.0, 1.0],
[104.0, 0.0],
[105.0, 1.0]
]
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
]
]
}
}
]
データベースには、これら 1 次元〜3 次元の情報を上手く格納できなければならないわけです。
位置情報の検索性
データベースに格納するということは、何かしらの条件でデータを抽出し利用したいからです。
例えば近くのお店を検索する
などですね。その場合、近くのお店というのをどのように検索すべきなのでしょうか?
いくつか手法は考えられるのですが、「SQL の機能を利用する方法」「Elasticsearch のような検索エンジンを利用する方法」「GeoHash を利用する方法」の 3 つを整理してみます。
まず考えられるのが、SQL を利用する方法です。データベースに直接問い合わせる形なので、一番シンプルな方法だといえます。
SQL の機能を利用する場合、PostgreSQL には PostGIS という拡張機能が、MySQL には標準で Geometry 型が存在します。
その機能の違いについては、以下の記事が非常に参考になります。
次に検索エンジンを利用する方法です。
例えば Amazon OpenSearch Service を利用して位置情報を扱うことができます。
詳細な利用方法については、以下の記事が非常に参考になります。
とはいえあくまで検索エンジンなので、こちらを利用する場合であってもデータベースは別に用意する必要がありそうです。
なので検索する
という目的はこちらで対応可能かもしれませんが、それ以外のデータベースに関する機能をどうするか?を別途考える必要がありそうです。
最後に GeoHash を利用する方法です。
そもそも GeoHash とは何ぞや?については、以下の記事が非常に参考になります。
本文から引用すると、
一言で言うと、世界地図を緯度経度で 2 分割していき、北(東)を 1,南(西)0 を当てた二進数を 32 進数化したものです。
だそうです。緯度経度情報を文字列にエンコードしたものってことですね。
これの面白いところは、エンコードされた文字列を利用して検索可能な点です。詳細は先ほどの記事にありますのでそちらをご覧いただくのがいいのですが、要するに似たような場所は似たような文字列になるからです。
難点は、あくまでエンコードされてしまった情報なので精度の問題を抱える点です。データ量が多すぎて逆に精度を落としたい場合であれば別ですが、そうでなければ少し使い方を工夫する必要がありそうです。
マネージドなデータベースサービス の選択
AWS で VPN レスのサーバーレス環境を志向した場合、データベースの選択肢はマネージドな DB サービスになります。VPN レスなので Amazon Aurora Serverless も利用できません。
例えば Amazon DynamoDB は、VPN レスのマネージドな NoSQL データベースです。しかし DynamoDB は位置情報を扱うための方法が豊富ではなく、前述の GeoHash を用いた手法などで戦わなければなりません。
そこで、AWS 内部のサービスではなく外部のマネージドなデータベースサービスと連携して構築を目指します。マネージドな DB サービスとして、有名なものに以下のようなものがあります。
- MySQL 系
- TiDB
- PostgreSQL 系
- Supabase
- Neon
この中から選択していくわけですが、まず TiDB は Geometry 型が対応していません。(参考)
ですので、使いたくてもまだちょっと難しそうです。
次に Neon ですが、日本リージョンがありません。(参考)
リージョンにそこまでこだわりが深いわけではありませんが、一方で日本リージョンがないということは日本へのサポートを心配してしまいます。困った時に日本語で対応できないのは地味に辛いです。
というわけで、今回は Supabase を利用してみます。
Supabase の PostGIS 拡張機能
導入方法
公式ドキュメントがあります。
こちらを参考に Table 作成からデータ入力まで全て SQL で済ませていきます。
まずは、PostGIS 拡張機能を許可します。
-- Example: enable the "postgis" extension
create extension postgis with schema "extensions";
次に、Table を作成します。
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
);
インデックスも貼っておきます。
create index restaurants_geo_index
on public.restaurants
using GIST (location);
次に、Table に Item を追加します。
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));
注意点として、st_point は経度、緯度の順番です。
無事に Table に Item が追加されましたね。
location データは、0101000020E6100000A4DFBE0E9C91614044FAEDEBC0494240
のような形で入力されています。クライアントサイドから location データを取得すると、そのまま上記の文字列が渡されるだけで、残念ながら Point 型に自動で変換してくれません。
そこで、位置情報を取得できるように Database Functions なるものを作成します。
こちらは、近いお店を取得する Database Functions
を作成するための SQL です。
create or replace function nearby_restaurants(lat float, long float)
returns table (id public.restaurants.id%TYPE, name public.restaurants.name%TYPE, lat float, long float, dist_meters float)
language sql
as $$
select id, name, st_y(location::geometry) as lat, st_x(location::geometry) as long, st_distance(location, st_point(long, lat)::geography) as dist_meters
from public.restaurants
order by location <-> st_point(long, lat)::geography;
$$;
以上、重要な点として、
- PostGIS 機能は拡張機能を別途許可することで利用可能になる
- location データは
0101000020E6100000A4DFBE0E9C91614044FAEDEBC0494240
みたいなエンコードされた文字列データの状態で保存される - デコードされたデータの取得には Database Functions を利用して SQL を書く必要がある
です。データを取り回すのに、Database Functions っていうのが必要なんですね。
Database Functions
Database Functions をクライアントサイドから利用するには、 RPC (Remote Procedure Call)で呼び出す必要があります。
RPC の呼び出し方は簡単で、例えば JavaScript クライアントからだと、以下のようになります。
const { data, error } = await supabase.rpc('nearby_restaurants', {
lat: 40.807313,
long: -73.946713,
})
簡単ですね。
注意点
クライアントライブラリでは自由な SQL を記述できるわけではないので、位置情報取得の全てに Database Functions を書く必要があるということですね。
なので、取得自由度の高い SQL を書いてクライアントサイドでデータ整形するのか、それとも全ての取得パターンの Database Functions を書くのか、運用上のお悩みポイントが生まれそうです。
最後に
ここまで読んでいただきありがとうございました。
私は、位置情報および PostgreSQL に関しては完全な初心者でしたが、Supabase を利用して非常に簡単に位置情報データベースを導入することができました。
もし犬専用の音楽アプリに興味を持っていただけたら、ぜひダウンロードしてみてください!
参考
Discussion