はじめに
Oracle AI Databaseでは地図情報を扱える。
聞いたことはあったものの、どうやってデータを取得して、どうやって保存して、どうやってアプリに実装するのかはよく分かっていませんでした。
そこで今回は、実際に地図アプリを作り、Oracle AI Databaseで地図情報を扱うとどんな感じなのか触ってみることにしました。
Oracle Autonomous AI Database(以降ADB)へカフェの場所を保存し、現在地の近くにあるカフェを表示するアプリです。
今回のデモでは、検索範囲を3kmに設定しています。
作り始める前は「緯度と経度をDBに保存して、画面にピンを置けば完成」くらいに考えていました。
実際に動かしてみると、地図データの構造も見えてきました。
この記事では、まず作ったアプリを紹介します。
後半は、地図データを初めて扱った自分向けのメモとして、今回初めて知った用語や仕組みを残しておきます。
作ったもの
外苑前駅、またはブラウザで取得した現在地を中心に、直線距離で3km以内にあるカフェを検索するアプリです。
検索結果は近い順に並びます。
せっかく地図アプリを作るので、画面は某RPGをイメージした探索地図風にしてみました。
カフェは宿屋風のアイコン、日本オラクル本社は城として表示しています。
カフェを選択すると、右側の「探索記録」で店名と距離を確認できます。
今回使用したものは、役割で分けると次のようになります。
| 役割 | 使用したもの | 今回やっていること |
|---|---|---|
| カフェ情報の取得元 | OpenStreetMapの検索API(Overpass API) | OpenStreetMapの中から、外苑前駅周辺のカフェだけを取得する |
| カフェ情報の保存と検索 | Oracle Autonomous AI Database | カフェ情報と場所をPointとして保存し、現在地から3km以内の店を検索する |
| ブラウザとDBの橋渡し | Python / FastAPI | ブラウザから現在地を受け取り、Oracle Autonomous AI Databaseの検索結果をブラウザへ返す |
| 背景地図と検索結果の描画 | MapLibre GL JS / OpenFreeMap | OpenFreeMapが配信するOSM由来のベクター地図を描画し、GeoJSONで返ってきたカフェを地図へ重ねる |
| マーカーと詳細表示 | app.js / DOM・SVG | 城、宿屋、クリスタルのマーカーを重ね、右側の「探索記録」を更新する |
最初に、カフェ情報を準備
まず、OpenStreetMapに登録されている地図データの中から、外苑前駅周辺のカフェをOpenStreetMapの検索APIで取得します。
取得した経度・緯度はSDO_GEOMETRYのPointへ変換し、OSM ID、名前、カテゴリとともにADBへ保存しておきます。
地図を開いた後に、現在地から検索する
利用者が地図を開くと、ブラウザは初期地点、検索半径、カテゴリを取得し、外苑前駅周辺のカフェを自動検索します。
道路や建物などの背景には、OpenFreeMapが配信するOSM由来のベクター地図を使い、MapLibre GL JSがブラウザ上で描画します。
その上に、app.jsが検索地点やカフェなどのマーカーを重ねます。
ADBから返った検索結果は、FastAPIが地図表示用のGeoJSON形式をブラウザへ返し、
app.jsはそのデータをMapLibre GL JSへ渡し、宿屋風のマーカーと右側の「探索記録」へ反映します。
OpenStreetMapの検索APIは多くの人が共有して使う公開サービスなので、画面を開くたびに呼び出さず、必要なデータだけを事前に取得する構成にしました。
Oracle AI Databaseで地図情報を扱ってみる
今回の出発点は、「Oracle AI Databaseでも地図情報を扱えるなら、実際にアプリを作って触ってみたい」というくらいのものでした。
地図情報を扱うといっても、最初は緯度経度ぐらいの情報かなと思ってたんですが、
調べてみると、Oracle AI Databaseには、場所を点として保存したり、道路を線、地域を面として保存したりする仕組みが用意されていました。
Oracle AI Databaseには、点・線・面などの位置や形を保存するSDO_GEOMETRYというデータ型があります。
今回は、カフェの場所を「点」として保存してます。
SDO_GEOMETRY(
2001,
4326,
SDO_POINT_TYPE(139.7178, 35.6708, NULL),
NULL,
NULL
)
この時点では2001や4326が何なのか、正直よく分かっていませんでした。
カフェの経度と緯度をADBへ保存し、次の関数へ検索地点と3kmという条件を渡すことで、指定範囲内のカフェを検索できます。
SDO_WITHIN_DISTANCE(
location,
検索地点,
'distance=3 unit=KM'
) = 'TRUE'
検索結果を距離順に並べ、Pythonからブラウザへ返すと、地図上にカフェを表示できました。
「位置情報を保存するために、Oracle AI Databaseだけで距離検索までできる」というのは、
実際に作ってみて便利に感じたところです。
ここまでのまとめ
地図アプリというと、最初は地図サービス側ですべて処理するものだと思っていました。
今回作ったものでは、地図の見た目は外部の地図サービスを利用し、
「現在地の近くにあるカフェを探す」という処理はADBが担当しています。
Oracle AI Databaseには位置や形を保存する型と、距離を条件に検索する機能があるため、普段使っている表やSQLの延長で地図情報も扱えました。
複雑な地理情報システムを作ったわけではありませんが、「近くの店舗を検索する」程度のアプリなら、思っていたよりも短いコードで実装できました。
ここから先は、今回のデモを作りながら初めて知ったことのメモです。
以降は自分用メモ:地図アプリを作って初めて知ったこと
どこからがブラウザ処理で、どこからがサーバ処理なのか
最初は、MapLibre GL JSをPythonが動かしているのか、それともブラウザが動かしているのかも分かっていませんでした。
今回の構成では、ブラウザとPythonサーバの境界は次のようになります。
水色の範囲がユーザーのブラウザ内です。index.htmlを受け取ったブラウザが、app.jsとMapLibre GL JSを読み込んで実行します。
緑色の範囲がPythonサーバです。FastAPIは最初にHTMLやJavaScriptを配り、その後はブラウザから届いた検索依頼を受け取ります。Oracle Autonomous AI Databaseへ問い合わせる処理もサーバ側で実行します。
つまり、Pythonが地図を描いているわけではありません。
Pythonは画面のファイルと検索結果を返し、実際に背景地図や検索結果を描くのはブラウザ内のMapLibre GL JSです。
城、宿屋、クリスタルなどのマーカーや「探索記録」は、app.jsがDOM・SVGを使って表示します。
地図に見えているものと、検索する店のデータは別だった
最初は、画面に表示されている地図の中から、そのままカフェを検索できるものだと思っていました。
しかし今回のアプリでは、次の二つは別物です。
- 道路、建物、地名などを表示する背景の地図
- Oracle Autonomous AI Databaseで検索するカフェのデータ
背景の地図は、OpenFreeMapが配信するOSM由来のベクター地図です。
一方、検索するカフェの名前と場所は、OpenStreetMapの検索APIから事前に取得してOracle Autonomous AI Databaseへ保存しています。
POIも背景地図も元データはOSMですが、取得経路と用途が異なります。背景地図はOpenFreeMapからブラウザへ配信され、検索対象のPOIは事前にOracle Autonomous AI Databaseへ保存されます。
つまり、背景地図にカフェの名前が見えていても、Oracle Autonomous AI Databaseがその画面を見て検索しているわけではありません。
OpenStreetMap(OSM)とは
記事やドキュメントを調べると「OSM」という略称がよく出てきました。
OSMはOpenStreetMapの略です。道路や建物、店舗などの地図データを、世界中の利用者が共同で作成・更新しています。
今回のアプリでは、カフェの名前と緯度・経度を取得する元データとして利用しました。
なお、OpenStreetMapのデータを使うことと、背景地図を配信することは別です。
今回はPOIをOpenStreetMapの検索APIから取得し、背景にはOpenFreeMapが配信するOSM由来のベクター地図を使用しています。
POIとは
地図データを調べていると「POI」という言葉も頻繁に出てきます。
POIはPoint of Interestの略で、カフェ、駅、病院、公園など、地図上で目印や検索対象になる場所のことでした。
最初は単なる緯度・経度だと思っていましたが、実際には次のような情報がまとまっています。
- 店の場所
- 店の名前
- カフェ、病院などの種類
- 元データ側で付けられた番号
今回のカフェであれば、「緯度・経度に店名や種類を付けたデータ」と考えると、自分には分かりやすかったです。
SDO_GEOMETRYとは
Oracle Spatialで位置や形状を保存するためのオブジェクト型がMDSYS.SDO_GEOMETRYです。
カフェの位置を通常のNUMBER列へ緯度・経度として分けて保存することもできます。
しかし、SDO_WITHIN_DISTANCEなどの空間演算子や空間索引の対象として扱うには、形状をSDO_GEOMETRY型の列へ格納します。
今回のPLACESテーブルでは、カフェの位置を保存するLOCATION列を次のように定義しています。
location MDSYS.SDO_GEOMETRY NOT NULL
SDO_GEOMETRYはPoint専用ではありません。
Point、Line、Polygon、それらを複数まとめた形状などを、共通して格納できます。
オブジェクト型としては、次の5属性を持っています。
MDSYS.SDO_GEOMETRY(
SDO_GTYPE,
SDO_SRID,
SDO_POINT,
SDO_ELEM_INFO,
SDO_ORDINATES
)
| 属性 | データ型 | 役割 |
|---|---|---|
SDO_GTYPE |
NUMBER |
次元数、LRSの測定次元、形状種別を表す |
SDO_SRID |
NUMBER |
座標参照系を識別するSRID |
SDO_POINT |
SDO_POINT_TYPE |
単純なPointをX・Y・Zで保持する |
SDO_ELEM_INFO |
SDO_ELEM_INFO_ARRAY |
座標配列をどのような要素・形状として解釈するかを定義する |
SDO_ORDINATES |
SDO_ORDINATE_ARRAY |
形状を構成する座標値を配列で保持する |
今回格納するのは、WGS 84の経度・緯度で表した2次元のPointです。そのため、各属性には次の値を設定しています。
MDSYS.SDO_GEOMETRY(
2001, -- SDO_GTYPE:2次元のPoint
4326, -- SDO_SRID:WGS 84の地理座標系
MDSYS.SDO_POINT_TYPE(
139.7178, -- X:経度
35.6708, -- Y:緯度
NULL -- Z:3次元目の座標
),
NULL, -- SDO_ELEM_INFO
NULL -- SDO_ORDINATES
)
SDO_GTYPE = 2001
SDO_GTYPEはDLTTという4桁の形式です。
-
D:次元数 -
L:LRS(Linear Referencing System)で測定値を保持する次元。使用しない場合は0 -
TT:形状種別。01はPoint、02はLineまたはCurve、03はPolygon
したがって2001は、次のように分解できます。
2 0 01
│ │ └─ Point
│ └─── LRSの測定次元なし
└───── 2次元
カフェを地図上の2次元Pointとして格納するため、今回は2001です。線なら2002、面なら2003というように、格納する形状に応じて値が変わります。
SDO_SRID = 4326
SDO_SRIDは、その座標をどの座標参照系で解釈するかを示します。値はOracle AI DatabaseのMDSYS.CS_SRSに登録されているSRIDと対応します。
4326はWGS 84の地理座標系です。今回取得したカフェの経度・緯度をWGS 84として格納するため、SDO_SRIDへ4326を設定しています。
なお、SRIDを指定しても座標値が自動変換されるわけではありません。異なる座標参照系のデータを格納する場合は、座標変換を行ってから同じSRIDへ揃える必要があります。
SDO_POINT
SDO_POINT属性の型はSDO_POINT_TYPEで、X、Y、Zの3属性を持ちます。
SRID 4326を使用する今回のPointでは、Xへ経度、Yへ緯度を設定します。SDO_GTYPEが2次元を表す2001なので、3次元目のZはNULLです。
SDO_ELEM_INFOとSDO_ORDINATES
SDO_ELEM_INFOは、座標配列の開始位置、要素種別、解釈方法を3つの値の組で定義します。SDO_ORDINATESは、その形状を構成する座標値を並べた配列です。
LineやPolygonなど、複数の座標から形状を構成する場合は、主にこの2属性を使用します。
今回のような単純なPointは、座標をSDO_POINTへ直接格納できます。そのため、座標配列とその構造定義は必要なく、SDO_ELEM_INFOとSDO_ORDINATESはNULLです。
NULLの場所 |
対象属性 | 今回NULLとなる理由 |
|---|---|---|
SDO_POINT_TYPEの3番目 |
Z |
SDO_GTYPEが2次元なので、3次元目の座標を持たない |
SDO_GEOMETRYの4番目 |
SDO_ELEM_INFO |
Pointの座標をSDO_POINTへ直接格納するため、座標配列の構造定義が不要 |
SDO_GEOMETRYの5番目 |
SDO_ORDINATES |
Pointの座標をSDO_POINTへ直接格納するため、別の座標配列が不要 |
緯度と経度の順番
普段は「緯度・経度」と言うので、緯度を先に書きたくなります。
今回Oracle Autonomous AI Databaseへ点を保存するときは、次の順番でした。
経度, 緯度
東京周辺であれば、次のようになります。
139.7178, 35.6708
ブラウザへ位置情報を渡すときに使ったGeoJSONという形式でも、座標は「経度、緯度」の順です。
GeoJSONとは
Oracle Autonomous AI Databaseの検索結果を、そのままブラウザの地図へ表示できるわけではありません。
そこで、地図上の点や線などをJSONで表す「GeoJSON」という形式へ変換しました。
例えば、カフェの場所は次のようになります。
{
"type": "Point",
"coordinates": [139.7178, 35.6708]
}
Oracle AI Databaseには、保存した位置情報をGeoJSONへ変換するSDO_UTIL.TO_GEOJSONという関数もあります。
今回はDB側で変換してからPythonへ返したため、Python側でOracle独自の位置情報を一から変換せずに済みました。
FastAPIは、複数のカフェをGeoJSONのFeatureCollectionとしてまとめて返します。各カフェは一つのFeatureとなり、geometryにPoint、propertiesに店名・カテゴリ・距離などが入ります。
空間索引とは
普通のDB検索では、店の番号や名前などに索引を作ります。
位置情報の場合は、「この地点から3km以内」のような条件で探すための空間索引があります。
今回のデモはカフェが数百件なので、索引がなくても大きな差は感じにくいかもしれません。それでも、位置情報を検索する基本的な構成として空間索引を作成しました。
CREATE INDEX places_location_sidx
ON places(location)
INDEXTYPE IS MDSYS.SPATIAL_INDEX_V2;
索引内部の細かい仕組みまでは、今回の記事では扱いません。
さいごに
最初は「緯度と経度を保存すれば作れる」と考えていましたが、実際には背景地図、検索する店のデータ、座標のルールなどが別々に存在していました。
地図データの役割を整理すると、アプリ全体は次のシンプルな流れで動いています。
- OpenStreetMapからカフェ情報を取得する
- Oracle AI Databaseへ場所と店名を保存する
- Oracle AI Databaseで指定範囲内を検索する
- 検索結果をブラウザで表示できる形へ変換する
- 地図上に表示する
Oracle AI Databaseで位置情報まで扱えることを、実際に動くアプリとして確認できました。
同じように「Oracle AI Databaseで地図情報を扱うと何ができるのだろう」と思っている方の参考になれば幸いです。
最後までお付き合いいただき、ありがとうございます。

