トップ20のリーダーボードを表示する必要があるプロジェクトがあります。リーダーボードにいないユーザーは、現在のランキングで21位に表示されます。
これに効率的な方法はありますか?
Cloud Firestoreをデータベースとして使用しています。 MongoDBの代わりに選択するのは間違いだったと思いますが、私はプロジェクトの途中にいるので、Cloud Firestoreでそれをしなければなりません。
アプリは3万人のユーザーが使用します。 3万人のユーザー全員を獲得せずにそれを行う方法はありますか?
this.authProvider.afs.collection('profiles', ref => ref.where('status', '==', 1)
.where('point', '>', 0)
.orderBy('point', 'desc').limit(20))
これはトップ20を取得するために行ったコードですが、トップ20にいない場合に現在のログインユーザーランクを取得するためのベストプラクティスは何ですか?
リーダーボードで任意のプレーヤーのランクを見つけることは、スケーリングの方法でデータベースの一般的な難しい問題です。
次のような、選択する必要のあるソリューションを推進する要因がいくつかあります。
典型的な単純なアプローチは、SELECT count(id) FROM players WHERE score > {playerScore}
など、より高いスコアを持つすべてのプレーヤーをカウントすることです。
この方法は低規模で機能しますが、プレーヤーのベースが大きくなると、すぐに遅くなり、リソースが高価になります(MongoDBとCloud Firestoreの両方で)。
Cloud Firestoreは、スケーラブルでない操作であるため、count
をネイティブにサポートしていません。返されたドキュメントを単純に数えることにより、クライアント側で実装する必要があります。または、Cloud Functions for Firebaseを使用してサーバー側で集計を行い、ドキュメントを返すことによる余分な帯域幅を回避できます。
ライブランキングを提供するのではなく、1時間ごとなど、頻繁に更新するように変更します。たとえば、Stack Overflowのランキングを見ると、毎日更新されるだけです。
このアプローチでは、実行に540秒以上かかる場合は 関数のスケジュール 、または App Engineのスケジュール を使用できます。この関数は、ladder
コレクションのようにプレーヤーのリストを書き出し、新しいrank
フィールドにプレーヤーのランクが設定されます。プレイヤーが今はしごを見ると、上位Xとプレイヤー自身のランクをO(X)時間で簡単に取得できます。
さらに良いことに、上位Xを単一のドキュメントとしてさらに最適化して明示的に書き出すことができるため、ラダーを取得するには、トップXとプレーヤーの2つのドキュメントを読むだけで済み、お金を節約し、高速化できます。
このアプローチは、帯域外で行われるため、任意の数のプレーヤーと任意の書き込み速度で実際に機能します。あなたが支払う意思に応じて成長するにつれて、頻度を調整する必要があるかもしれません。最適化を行わない限り、毎時3万人のプレーヤーは1時間あたり0.072ドル(1日あたり1.73ドル)になります(たとえば、最後に結ばれていることがわかっているため0スコアのプレーヤーをすべて無視します)。
この方法では、いくらかの逆索引を作成します。この方法は、プレーヤーの数に応じて著しく小さい制限されたスコア範囲がある場合に機能します(例:0〜999のスコアと3万人のプレーヤー)。また、一意のスコアの数がプレーヤーの数よりもかなり少ない無制限のスコア範囲でも機能します。
「スコア」と呼ばれる個別のコレクションを使用すると、_player_count
_というフィールドを持つ個々のスコア(そのスコアを持っている人がいない場合は存在しません)ごとにドキュメントが作成されます。
プレーヤーが新しい合計スコアを取得したら、scores
コレクションで1-2回の書き込みを行います。書き込みの1つは、新しいスコアに対して_player_count
_に+1することです。それが初めてでない場合は、古いスコアに対して-1にします。このアプローチは、「最新のスコアが現在のスコアである」と「最高のスコアが現在のスコアである」スタイルのラダーの両方で機能します。
プレーヤーの正確なランクを見つけることは、SELECT sum(player_count)+1 FROM scores WHERE score > {playerScore}
のようなものと同じくらい簡単です。
Cloud Firestoreはsum()
をサポートしていないため、上記を行いますが、クライアント側で合計します。 +1は、合計があなたの上のプレイヤーの数であるため、1を追加するとそのプレイヤーのランクが得られます。
このアプローチを使用すると、最大999のドキュメントを読み、平均500ishでプレーヤーのランクを取得する必要がありますが、実際には、プレーヤーがいないスコアを削除するとこれは少なくなります。
新しいスコアの書き込み速度は、平均で2秒ごとに1回しか更新できないため、理解することが重要です*。完全に分散されたスコア範囲が0〜999の場合、1秒あたり500の新しいスコアを意味します**。これを増やすには、各スコアに distributed counters を使用します。
*各スコアが2つの書き込みを生成するため、2秒ごとに1つの新しいスコアのみ
**平均ゲーム時間を2分とすると、500の新しいスコア/秒は、分散カウンターなしで60000人の同時プレイヤーをサポートできます。 「最高スコアが現在のスコアである」を使用している場合、これは実際にははるかに高くなります。
これは断然最も難しいアプローチですが、すべてのプレーヤーに対してより高速でリアルタイムのランキング位置を確保することができます。上記の逆索引アプローチの読み取り最適化バージョンと考えることができますが、上記の逆索引アプローチはこれの書き込み最適化バージョンです。
適用可能な一般的なアプローチについては、 'Datastoreでの高速で信頼性の高いランキング' の関連記事を参照してください。このアプローチでは、制限付きスコアが必要です(制限なしでも可能ですが、以下からの変更が必要になります)。
このアプローチはお勧めしません。セミ頻度の更新があるラダーのトップレベルノードの分散カウンターを実行する必要があるため、読み取り時間の利点が失われる可能性が高いためです。
プレーヤーのリーダーボードを表示する頻度に応じて、アプローチを組み合わせてこれをさらに最適化できます。
より短い時間枠で「逆索引」と「定期的な更新」を組み合わせると、すべてのプレイヤーにO(1)ランキングアクセスを与えることができます。
すべてのプレイヤーでリーダーボードが「定期的な更新」の期間中に4回以上表示されている限り、お金を節約し、リーダーボードが高速になります。
基本的に各期間、たとえば5〜15分で、scores
からすべてのドキュメントを降順で読み取ります。これを使用して、_players_count
_の実行合計を保持します。新しいフィールド_scores_ranking
_を使用して、各スコアを_players_above
_という新しいコレクションに書き換えます。この新しいフィールドには、現在のスコア_player_count
_を除く現在の合計が含まれます。
プレーヤーのランクを取得するには、プレーヤーのスコアのドキュメントを_score_ranking
_->ランクから_players_above
_ + 1で読むだけです。
オンラインゲームに実装しようとしており、ユースケースで使用できる可能性のあるここで言及されていないソリューションの1つは、率直に言ってユーザーが知らないため、ユーザーが表示可能なリーダーボードにいない場合、ユーザーのランクを推定することです(または気にしますか?)22,882位か22,838位かどうか。
20位が250ポイントのスコアを持ち、合計32,000人のプレイヤーがいる場合、各ポイントは平均127位の価値がありますが、何らかのカーブを使用して、彼らが正確にジャンプしないポイントを上に移動することができます毎回配置-ランクのジャンプのほとんどはゼロポイントに近いはずです。
それを推定として識別するかどうかはあなた次第であり、本物のように見えるように数に塩を追加することができます:
// Real rank: 22,838
// Display to user:
player rank: ~22.8k // rounded
player rank: 22,882nd // rounded with random salt of 44
後者を行います。
Danが言及していないソリューションは、Google Cloud Functionsと組み合わせたセキュリティルールの使用です。
ハイスコアのマップを作成します。例:
次に:
これは私にとって最も簡単なオプションに見えます。それもリアルタイムです。