Redisとredis-pyを使って、緯度経度での位置情報検索を実装する


レコメンドのプロジェクトで「緯度経度で最寄りの市区町村を検索し、その市区町村に紐づくアイテム(物件)を提案する(それを機械学習アルゴリズムで再ソートする)」ような実装が必要なことがありました。

ちなみに、元々はもっと複雑な特徴量を使って近似最近傍探索のようなロジックを考えていたそうなのですが、上記のような比較的シンプルな方法で早く試そうという方針に決まりました。(レコメンドアルゴリズムは別のメンバーが検証し、私は実装担当として関わっています)

その際に「最寄りの市区町村を検索する」アルゴリズム部分で、Redisの位置情報系の機能を使うと簡単に実装できたのでメモしておきます。…ここまでの情報で、あとはredis-pyのドキュメントを読めば実装できると思いますが、他メンバーの類似プロジェクトでも利用検討することがありそうなので、共有のために書いておきます。

Redisのクライアントライブラリ

Pythonではredis-pyというライブラリが用意されています。

import redis

client = redis.Redis(host='{Redisのエンドポイント}', port="6379", decode_responses=True)

ここで decode_responses=True のオプションをつけていないと、レスポンスが全て bytes 型で返ってしまい、それを .decode("utf-8") し続けるので大変になります。

アイテムの追加

このように追加します。第一引数(name)で登録先のkeyを指定します。

client.geoadd("restaurants", 139.741072, 35.684266, "LIFULL TABLE")

LIFULL Tableは弊社半蔵門オフィスにあるカフェです。弊社にいらっしゃった時はぜひいらっしゃってください。

緯度経度による検索

近い順で1件取得します。

response = client.georadius("restaurants", 139.741072, 35.684266, 10, "km", "ASC", count=1)

print(response)
# => [['LIFULL TABLE', 0.0002]]

同じ点なのに距離が出てしまっているのは、おそらく浮動小数点かなにかの誤差なんじゃないかと思います。

アイテムの削除

client.zrem("restaurants", "LIFULL TABLE")

また geodel コマンドじゃないのは、緯度経度の実態がSorted setで、そちらで用意されている zrem コマンドで十分だからのようです。公式ドキュメントでは次のように説明されています。

Note: there is no GEODEL command because you can use ZREM in order to remove elements. The Geo index structure is just a sorted set.

ちなみにSorted setを使えばリアルタイムのランキングの実装も簡単だそうです。

制限

Sorted setで、valueとして入れられる値は文字列のみに制限されているようです。そのため緯度経度で近い「レストラン名」は管理できても、これ単体でそれ以上(例えばレストランのメニューも入れてupdateさせるとか)はできないようでした。

実は「緯度経度で指定し、その最寄りのランドマーク周辺の json オブジェクト(最新の物件情報)をレスポンスとして返す」という実装をしたかったのですが、valueの値が物件情報が変わるたびに毎回変わり、挿入するたびに過去の値が消えずに残っていきます。そのため実は別の方法で実装しています。

また、極地付近のデータは利用できないようです。

The command takes arguments in the standard format x,y so the longitude must be specified before the latitude. There are limits to the coordinates that can be indexed: areas very near to the poles are not indexable.

参考