CROSS MEというアプリのサーバーサイドを担当している川田です。
iOSの記事の続編的立ち位置で書かせていただきます。
サービス概要
CROSS MEは、すれ違いを恋のきっかけにしてもらうためのカップリングサービスです。
このアプリでは、すれ違いがサービスのキモなので、極力ユーザーにストレスのないすれ違い体験をしていただけるように設計しています。
本記事では、CROSS MEのメイン機能のすれ違い機能の概要を紹介させていただきます。
CROSS MEにおける「すれ違い」の定義
まずはCROSS MEにおける「すれ違い」の定義を説明します。
CROSS MEではすれ違い機能を実現するためにGPSを採用しています。
一般的にすれ違いと聞くと、2ユーザー間の、時間軸に連続的な位置の情報を使って、一定距離の幅を持たせたときにぶつかった場所をすれ違ったポイントとすることを想像します。
一般的にすれ違いというと、上記のようなイメージを持つのではないでしょうか。
UserAとUserBにそれぞれ幅を持たせて、ぶつかった場所(赤い部分)がすれ違いポイントとなります。
しかしGPSのデータは常に送信されているわけではなく、特定の間隔で送信されるので、時間・緯度・経度がセットになった点のデータになります。
点のデータのみでは上記方法は実現できないため、別の方法を採用する必要があります。
いくつかやり方が考えられますが、CROSS MEでは、最後に送った位置情報の地点に一定時間滞在しているものとし、時間、距離の2軸について一定の幅を持たせてすれ違い判定をしています。
CROSS MEにおけるすれ違いのイメージは上の図のようになっています。
UserCとUserDはそれぞれ、特定のタイミングで位置情報を送信します。 UserCは15:30以降位置情報を送信していませんが、しばらくその地点に居続けると推定し、15:45にUserDが位置情報を送ったタイミングですれ違い発生としています(時間は分かりやすく入れただけなので、実際の挙動は例とは異なります)。
すれ違い処理概要
それではCROSS MEのすれ違い処理の概要を説明していきます。
構成としては、大きく3つのフェーズに分かれています。
- 位置情報取得: Kinesisで位置情報を受け取る
- すれ違い判定: Lambdaですれ違ったかどうか判定
- すれ違い履歴の保存: DBにすれ違った履歴を保存
という構成ですれ違い判定を行っています。
すれ違い処理部分を簡素化した図です。
それぞれをもう少し詳しく説明します。
位置情報取得: Kinesisで位置情報を受け取る
クライアントから、特定のタイミングで位置情報を送信してもらっています。 Kinesisは複数シャードで運用しており、CROSS MEアカウントとKinesisシャードの紐付きは変わらないように設計しています。
シャード分割について、投げるデータと割り当てられるシャードの関係が分かりづらかったので、別の記事で自分なりにまとめてみました。興味がある方は見てみてください。
Kinesisの役割は、クライアントに投げてもらった位置情報を時系列に保持しておくだけです。
以下はKinesisで受け取ったレコード数を時系列に並べたグラフです。
平日にKinesisが受け取ったレコードと、
休日にKinesisが受け取ったレコードです。
CROSS MEでは移動したときと、アプリを使用しているときに位置情報を送信する頻度が高くなります。
平日は朝の通勤時、ランチ時間、帰宅時間に人の移動かアプリの使用率が高くなるのに対し、休日は朝の動きはほとんどなく、夕方にかけて徐々に送信数が増えていることが分かります。
だから何だという話がある訳ではないんですが、こういうデータは眺めてると楽しいですよね。
すれ違い判定: Lambdaですれ違ったかどうか判定
Kinesisから送られてきた位置情報は、Lambdaが定期的に拾って処理してくれます。
Lambdaでは、送られてきた位置情報が有効かどうかをチェックし、その後有効な位置情報のみをElasticsearchに対してBulkでユーザーごとにユニークに保存しています。
その後、送られてきた位置情報と一定時間内、距離内にある他ユーザーの位置情報をElasticsearchからgeo_distanceフィルタを使って検索し、すれ違ったユーザーを特定します。
位置情報を保存するindexには、以下のようにtypeをgeo_pointとしてmappingを定義して緯度経度を保存することで、geo関連の検索ができるようになります。
{
"properties": {
"location": { "type": "geo_point" }
}
}
上記geo_pointで定義したデータに対して、
{
'geo_distance': {
'distance': distance,
'location': [ location.lon, location.lat ],
'distance_type': distance_type
}
}
のようなクエリを投げることで、一定距離内ユーザーを検索しています。
Elasticsearchは、mappingを手動で定義しなくても、データを投げれば自動的にmappingを判断してくれますが、geo_distanceなどの特定のtypeは手動で定義しないと別の型(今回の場合はdouble)と認識され、距離の検索ができなくなるので注意が必要です。
すれ違い履歴の保存: DBにすれ違った履歴を保存
Lambdaで得られたすれ違ったユーザー情報を、DBに保存します。
このとき、一定時間同じユーザーとはすれ違わない仕様のため、直前にすれ違った時間を見て、一定時間内だった場合は情報の更新を行わないようにしています。
すれ違った場所の住所情報は、Elasticsearchに緯度経度から住所を検索するためのIndexを作って叩いて取得しています。
いくつかのサービスを検討したのですが、利用料が割と高いことと、国土交通省が配布している位置参照情報から手軽に作れそうだったので、自前で用意することにしました。
取得したCSVのデータを
{
"location": {"lat": 35.657241,"lon": 139.697849},
"prefId": "13",
"pref": "東京都",
"cityId": "13113",
"city": "渋谷区",
"streetId": "131130026001",
"street": "道玄坂一丁目"
}
のような形でjsonに変換してElasticsearchへBulkでimportし、検索時に
{
'sort': {
'_geo_distance': {
'location': [ lon, lat ],
'order': 'asc'
}
}
}
と、距離を昇順で並び替えて最も近い1件のみを取得することで、緯度経度に対応する住所を検索しています。
これらの一連の処理が、位置情報が送られる度に(正確にはLambdaがKinesisからデータを拾う度に)走り、すれ違い情報を生成しています。
現状、位置情報が送信されてからすれ違い発生まではほぼリアルタイムに処理されています。
ちなみに、CROSS MEではユーザーごとに最新の位置情報しか保持していないので、データを見ても「このユーザーはここで位置情報をたくさん送っているから家がこのあたりなんじゃないか」みたいな推測はできないようになっています。
また緯度経度の情報は、すれ違いだけでなく、すれ違いカバー画像(すれ違い一覧ページの上部にある風景画像)にも使用しています。
すれ違いカバー画像は、ユーザーのいる地点によって切り替わるようになっています。
住所に紐づくものと、こちらであらかじめ指定した特定範囲内の特別な画像の2種類が存在し、都内だけでも56種類の画像が用意されています。
ぜひCROSS MEを使って、全種類のカバー画像を見つけてみてください。
以上、ざっくりCROSS MEのすれ違い機能の紹介でした。