こんにちは。サーバサイドエンジニアの川口です。 Applibot Lifestyleで提供しているマッチングサービス「Qunme」で、5/2にリリースした「ちょこっとトーク」のバックエンドで採用した技術について紹介します。
「Qunme」はシンプルで直感的な使い心地と、プロフィールなどをもとに相性がよさそうな異性とのマッチングを自動で行い、相手と5分間のリアルタイムチャットができる「ちょこっとトーク」 機能により、”探さない系恋活アプリ”として、新たなユーザー体験を可能にしたマッチングサービスです。
「Qunme」のクライアントサイドエンジニアが先日執筆したブログ「Swift is NOT Kotlin…?」も良かったらご覧ください。
「ちょこっとトーク」とは?
「Qunme」で新リリースした「ちょこっとトーク」は、ユーザがワンタップで、異性とトークを開始することができる機能です。 毎日21:00~25:00の時間限定で利用できる機能で、「Qunme」が独自のアルゴリズムでマッチングを行い、1人につき5分間の時間限定でリアルタイムチャットを行うことができます。
従来のマッチングサービスでは、プロフィールをもとに異性を探して、お互いが「いいね」をしたら会話ができるというものが主流です。 そんな中、「ちょこっとトーク」は、「1タップで誰でも必ず、トークができる」、「5分間会話した上で、その後もやりとりを続けるかどうか選択できる」という点が特徴で、ユーザは気軽に異性とコミュニケーションを取ることができるようになっています。「ちょこっとトーク」は、以下のような機能要件となります。
- 自分の近くにいる異性のユーザと、独自に算出したスコアを元にマッチングできる
- マッチングしたユーザとリアルタイムチャットができる
1について、自分の近くにいる異性というだけではマッチングの精度が良くありません。精度を上げるために相性を加味し、ユーザプロフィールから独自に算出したスコアに基づいて、マッチングを行う必要がありました。
2について、「ちょこっとトーク」は、マッチングした相手とすぐやりとりができることがポイントなので、リアルタイムチャットである必要がありました。
Elasticsearchによるマッチング
「Qunme」では、ユーザデータ管理のために、データストアとしてMySQLを利用しています。要件として、物理的な距離やユーザプロフィールなど、複合的な条件によって算出されたスコアを元にマッチングする必要がありました。 MySQLの場合、複雑なクエリを書くことで実現できましたが、現実的な応答時間ではありませんでした。
そこで要件を満たすために、Elasticsearchを導入しました。
- フィールドの種類にgeo_pointを指定することで、位置情報による検索を容易に行うことができる
- スコアの算出の際に、各フィールドに重み付けができる
- 検索結果を算出されたスコアを元にソートし、スコア順に取得できる
上記のような検索を簡単に下記に例を載せておきますので、イメージがつかない方は見てみてください。
Elasticsearchを用いた検索例
要件:共通点が多いユーザを抽出したい
※出身地が一致:+50、居住地が一致:+100、年齢が+-1: + 30の重み付けで検索
ユーザ名
|
出身地
|
居住地
|
年齢
|
スコア
|
たくや
|
東京
|
東京
|
26
|
130
|
まい
|
神奈川
|
東京
|
23
|
150
|
さとし
|
長野
|
東京
|
21
|
100
|
あきと
|
山梨
|
神奈川
|
22
|
0
|
検索するユーザが「”神奈川”出身、”東京”在住、”25歳”」の場合
このユーザ情報の場合、検索ユーザにとって最もスコアが高いのは”まい”となります。
(計算式:出身地が一致→ +50, 居住地が一致→ +100, 年齢不一致→ +0 合計スコア:150)
Elasticsearchではこのように、検索する際、各フィールド毎に条件にしたがって重み付けをし、スコアによって結果をソートして取り出すことが可能です。
|
位置情報による検索
位置情報による検索を行いたいフィールドに対して、geo_pointという型を指定しておきます。以下が、型定義の登録をするためのJSON定義になります。
{
"mappings" : {
"entry_info" : {
"properties" : {
"residence" : { "type" : "integer"},
"gender" : { "type" : "integer"},
"birthday" : { "type" : "date", "format" : "yyyy-MM-dd HH:mm:ss"},
"entry_time" : { "type" : "long"},
"location": { "type" : "geo_point"}
}
}
}
}
「Qunme」はサーバの実装にJavaを用いていますが、ElasticsearchのクライアントにはJestを用いています。
Jestでの位置情報による距離検索は以下のとおりです。
// 距離検索クエリの生成 (半径5km内を検索する例)
QueryBuilder disatanceQueryBuilder = QueryBuilders.geoDistanceQuery("location")
.point(latitude, longitude)
.distance("5km");
各フィールドへの重み付けした検索
スコアの算出の際に重みをつける際は、以下のようにしたら実現できます。
指定のクエリの条件にあてはまったら、指定した重みを加算するようになっています。
.add構文は後ろに続けて定義できるので、重みづけを定義したい条件の数だけ羅列することができます。
QueryBuilder queryBuilder = QueryBuilders.functionScoreQuery()
// 全てのweightの合計を使用する指定
.scoreMode("sum")
// 定常のscoreを使用せず,ここで計算したscoreを使用する指定
.boostMode("replace")
// 距離(近い距離にいたら重み:100fを加算)
.add(distanceQuerybuilder, ScoreFunctionBuilders.weightFactorFunction(100f))
.add(..., ...);
リアルタイムチャットの実装
今回、メッセージ部分をリアルタイムチャットとしてFirebaseに決めた理由は
- リリースまでの開発期間が短い
- 文献が他よりも多い
- クライアントが使用するSDK, NDKの使いやすさ
- ユーザが増えた際に、スケールするアーキテクチャになっている
などの理由によるものです。
サーバ側でやっていることは以下の3つです。
- カスタムトークンの発行
- メッセージの取得
- メッセージの削除
1について、ユーザIDを元に一意なカスタムトークンを発行します。それをクライアントに渡しFirebaseとの通信に使用しています。
2について、チャット終了後に、やりとりしていた内容をユーザに引き継ぐためにメッセージを取得しています。
3について、Firebaseでは自動でデータが削除されることはなく、今回契約したプランでは容量上限もあるので、こちらで明示的に削除する必要がありました。
そのため、チャットは終了して一定時間が経った内容は消すようにしています。
実装自体はとてもシンプルで、公式通りで出来るのでそちらを見て頂ければと思います。 Firebase周りでのサーバの実作業はおよそ2日(早い人だと1日もかからなさそう)だったので、 こんなに簡単かつリーズナブル(自分達でチャットサーバ運用することを考えたら安いくらい)にチャット機能が導入出来て素晴らしいなと思いました。
まとめ
ElasticsearchはJestというJavaクライアントがあるのでそこそこ使いやすいですが、 Firebaseは、使用しやすいクライアントなどもなく自分で書く必要があったり、レスポンスがJSON解釈出来ない形式で来たりと、使い辛さを少し感じていました。
とはいえ、一度書いてしまえば、リアルタイムチャットをこちらの手を煩うこともなく使用でき、尚且つそこでのやりとりのデータをimportからdeleteまで自前サーバ側でできるのは、とても便利というか、どんなサービスにとっても凄くありがたいのではないでしょうか。
ElasticsearchもFirebaseにも、サービスで採用するにあたって、文献などを探しても、「使ってみた」系の記事がほとんどだったため、英語の文献をあたることが多かったです。 この記事がこれから触る人の一助になれれば幸いです。
「ちょっこっとトーク」を実装するにあたって、Firebase以外のデータストアやサービスについても調査・検証を行ったので、機会があれば記事を書きたいと思います。
この記事へのコメントはありません。