はじめに
これまでRedisは、キャッシュサーバとしてしか認識していませんでしたが、Redisの地理空間インデックスという機能があるとのことで利用してみました。
Redisの地理空間インデックスは、位置情報データを効率的に格納・検索するための機能で特定の半径内や境界ボックス内にあるオブジェクトを高速に検索できます。
Google Maps上にアイコンを表示することを想定し、 地図上の中心から円形の領域内の情報を取得するコードを試してみました。
使用したもの
- google Map
- Rust 1.81.0
- React 18
- Redis Cloud
データの追加
データをRedisの地理空間インデックスに追加するためのコードは以下の通りです。コードはRustで作成しました。
use redis::geo::{ RadiusOptions, RadiusOrder, Unit, Coord };
use redis::AsyncCommands;
// 中間のコードは省略
let _: () = con.geo_add(
"user_locations",
(Coord::lon_lat(payload.location.lng, payload.location.lat), &payload.name)
).await.unwrap();
以下の処理を行っています。
- con.geo_add() メソッドを使用して、Redisの地理空間データ構造にデータを追加しています. conは、Redisへのコネクションです
- "user_locations" は、位置情報を格納するためのキー名です。データベースのテーブル名のような役割で、位置情報をこの名前の下に保存します
- データとして追加されるのは以下の情報です:
位置情報:Coord::lon_lat(payload.location.lng, payload.location.lat) で経度と緯度を指定
識別子:&payload.name でID(プログラムではユーザ名)を指定
payload
のデータ構造は以下のように定義しました。
#[derive(Deserialize, Serialize)]
pub struct Location {
lat: f64,
lng: f64,
}
#[derive(Deserialize, Serialize)]
pub struct UserData {
name: String,
location: Location
}
payload(UserData)は、クライアントから送信されたデータや、アプリケーション内で生成されたユーザーデータを表現しました。この構造体は:
- name: ユーザーの名前(文字列)識別子として使用
- location: ユーザーの位置情報(Location構造体)
- lat: 緯度(浮動小数点数)
- lng: 経度(浮動小数点数)
を含めています。
データ検索
クライアント側で検索用のデータを作成し、サーバ側でRedisの地理空間インデックスを活用して位置情報に基づくデータ検索を行います。
クライアント側
JavaScriptで、Google Maps APIを使用して以下のように検索用のデータを作成しました:
const center = event.map.getCenter() as google.maps.LatLng;
const bounds = event.map.getBounds();
const ne = bounds?.getNorthEast() as google.maps.LatLng;
const radius = google.maps.geometry.spherical.computeDistanceBetween(center, ne);
const locationQuery = {
location: {
lat: center.lat(),
lng: center.lng()
},
radius: radius
};
以下の処理を行っています。
- 地図の中心点(center)を取得
- 地図の表示範囲(bounds)を取得
- 表示範囲の北東の角(ne)を取得
- 半径(radius)を計算。 中心点から北東の角までの距離
中心点の緯度・経度と計算された半径をlocationQueryオブジェクトにまとめ、サーバー側に送信し、位置情報に基づく検索を行っています
※中心点から北東の角までの距離を半径としているので、Mapよりも検索範囲は広くなります。
サーバ側
クライアントから受け取った値を使用して、Redisから検索します。
use redis::geo::{ RadiusOptions, RadiusOrder, Unit, Coord };
use redis::AsyncCommands;
// 中間のコードは省略
let opts = RadiusOptions::default().order(RadiusOrder::Asc).limit(200);
let user_names: Vec<String> = con.geo_radius(
"user_locations",
payload.location.lng,
payload.location.lat,
payload.radius,
Unit::Meters,
opts
).await.unwrap();
以下の処理を行っています。
- RadiusOptionsの設定:
RadiusOptions::default().order(RadiusOrder::Asc).limit(200) で検索オプションを設定しました。
結果を距離の昇順(近い順)で並べ替え、最大200件のユーザーを取得するよう指定しています。 - geo_radiusコマンドの実行:
con.geo_radius()メソッドを使用して、Redisの地理空間検索を実行します。conは、Redisへのコネクションです。- パラメータ:
- "user_locations": 検索対象の地理空間インデックスのキー名
- payload.location.lng, payload.location.lat: 検索の中心点の経度と緯度
- payload.radius: 検索半径(メートル単位)
- Unit::Meters: 距離の単位をメートルに指定
- opts: 上記で設定した検索オプション
- パラメータ:
- 結果の取得:
検索結果はVec型で返され、これは条件に合致するIDのリストです。
クライアント側にはJsonに変換して返します。
payloadのデータ型は、クライアントと同じになるよう、次のように定義しています。
#[derive(Deserialize, Serialize)]
pub struct Location {
lat: f64,
lng: f64,
}
#[derive(Deserialize, Serialize)]
pub struct LocationQuery {
location: Location,
radius: f64,
}
おわりに
個人的にですが、これまで地図の表示範囲(boundary)の緯度・経度の範囲内にあるデータを取得するために、緯度・経度を条件式に含めて検索したり、またはフィルタをかけたりしていました。Redisの地理空間インデックスを使用することで、位置情報検索がより簡単かつ効率的に行えました。ただし、位置情報以外のデータ管理には別の方法を併用する必要があり、工夫が必要だと思いました。