简体   繁体   English

排行榜排名与Firebase

[英]Leaderboard ranking with Firebase

I have project that I need to display a leaderboard of the top 20, and if the user not in the leaderboard they will appear in the 21st place with their current ranking.我有一个项目,我需要显示前 20 名的排行榜,如果用户不在排行榜中,他们将以当前排名出现在第 21 位。

Is there efficient way to this?有有效的方法吗?

I am using Cloud Firestore as a database.我正在使用 Cloud Firestore 作为数据库。 I believe it was mistake to choose it instead of MongoDB but I am in the middle of the project so I must do it with Cloud Firestore.我认为选择它而不是 MongoDB 是错误的,但我正处于项目的中间,所以我必须使用 Cloud Firestore 来完成。

The app will be use by 30K users.该应用程序将由 30K 用户使用。 Is there any way to do it without getting all the 30k users?有没有什么办法可以在不吸引所有 30k 用户的情况下做到这一点?

 this.authProvider.afs.collection('profiles', ref => ref.where('status', '==', 1)
        .where('point', '>', 0)
        .orderBy('point', 'desc').limit(20))

This is code I did to get the top 20 but what will be the best practice for getting current logged in user rank if they are not in the top 20?这是我为获得前 20 名所做的代码,但是如果他们不在前 20 名中,获得当前登录用户排名的最佳实践是什么?

Finding an arbitrary player's rank in leaderboard, in a manner that scales is a common hard problem with databases.以可扩展的方式在排行榜中查找任意玩家的排名是数据库常见的难题。

There are a few factors that will drive the solution you'll need to pick, such as:有几个因素将推动您需要选择的解决方案,例如:

  • Total Number players总人数
  • Rate that individual players add scores评价单个玩家的得分
  • Rate that new scores are added (concurrent players * above)添加新分数的比率(并发玩家 * 以上)
  • Score range: Bounded or Unbounded分数范围:有界或无界
  • Score distribution (uniform, or are their 'hot scores')分数分布(统一,或者是他们的“热门分数”)

Simplistic approach简单化的方法

The typical simplistic approach is to count all players with a higher score, eg SELECT count(id) FROM players WHERE score > {playerScore} .典型的简单方法是计算所有得分较高的玩家,例如SELECT count(id) FROM players WHERE score > {playerScore}

This method works at low scale, but as your player base grows, it quickly becomes both slow and resource expensive (both in MongoDB and Cloud Firestore).这种方法适用于小规模,但随着您的玩家群的增长,它很快变得既缓慢又耗费资源(在 MongoDB 和 Cloud Firestore 中)。

Cloud Firestore doesn't natively support count as it's a non-scalable operation. Cloud Firestore 本身不支持count因为它是不可扩展的操作。 You'll need to implement it on the client-side by simply counting the returned documents.您需要在客户端通过简单地计算返回的文档来实现它。 Alternatively, you could use Cloud Functions for Firebase to do the aggregation on the server-side to avoid the extra bandwidth of returning documents.或者,您可以使用 Cloud Functions for Firebase 在服务器端进行聚合,以避免返回文档的额外带宽。

Periodic Update定期更新

Rather than giving them a live ranking, change it to only updating every so often, such as every hour.与其给他们实时排名,不如将其更改为仅每隔一段时间(例如每小时)更新一次。 For example, if you look at Stack Overflow's rankings, they are only updated daily.例如,如果您查看 Stack Overflow 的排名,它们只会每天更新。

For this approach, you could schedule a function , orschedule App Engine if it takes longer than 540 seconds to run.对于这种方法,您可以安排一个函数,或者如果运行时间超过 540 秒,则安排 App Engine The function would write out the player list as in a ladder collection with a new rank field populated with the players rank.该函数将在ladder集合中写出玩家列表,并在新的rank字段中填充玩家等级。 When a player views the ladder now, you can easily get the top X + the players own rank in O(X) time.当玩家现在查看天梯时,您可以在 O(X) 时间内轻松获得顶部 X + 玩家自己的排名。

Better yet, you could further optimize and explicitly write out the top X as a single document as well, so to retrieve the ladder you only need to read 2 documents, top-X & player, saving on money and making it faster.更好的是,您可以进一步优化并将顶部 X 显式写出作为单个文档,因此要检索梯子,您只需要阅读 2 个文档,top-X 和播放器,既节省资金又加快速度。

This approach would really work for any number of players and any write rate since it's done out of band.这种方法确实适用于任何数量的播放器和任何写入速率,因为它是在带外完成的。 You might need to adjust the frequency though as you grow depending on your willingness to pay.随着您的成长,您可能需要根据支付意愿调整频率。 30K players each hour would be $0.072 per hour($1.73 per day) unless you did optimizations (eg, ignore all 0 score players since you know they are tied last).除非您进行了优化(例如,忽略所有 0 分玩家,因为您知道他们排在最后),否则每小时 30K 玩家将是每小时 0.072 美元(每天 1.73 美元)。

Inverted Index倒排索引

In this method, we'll create somewhat of an inverted index.在这种方法中,我们将创建某种倒排索引。 This method works if there is a bounded score range that is significantly smaller want the number of players (eg, 0-999 scores vs 30K players).如果存在明显小于想要的玩家数量的有界分数范围(例如,0-999 分数 vs 30K 玩家),则此方法有效。 It could also work for an unbounded score range where the number of unique scores was still significantly smaller than the number of players.它也适用于无限的分数范围,其中唯一分数的数量仍然明显小于玩家数量。

Using a separate collection called 'scores', you have a document for each individual score (non-existent if no-one has that score) with a field called player_count .使用一个名为“scores”的单独集合,您有一个文档,用于每个单独的分数(如果没有人拥有该分数,则不存在),其中包含一个名为player_count的字段。

When a player gets a new total score, you'll do 1-2 writes in the scores collection.当玩家获得新的总分时,您将在scores集合中写入 1-2 次。 One write is to +1 to player_count for their new score and if it isn't their first time -1 to their old score.一次写入是对player_count的新分数 +1,如果这不是他们的第一次,则对他们的旧分数 -1。 This approach works for both "Your latest score is your current score" and "Your highest score is your current score" style ladders.此方法适用于“您的最新分数是您当前的分数”和“您的最高分数是您当前的分数”样式的阶梯。

Finding out a player's exact rank is as easy as something like SELECT sum(player_count)+1 FROM scores WHERE score > {playerScore} .找出玩家的确切排名就像SELECT sum(player_count)+1 FROM scores WHERE score > {playerScore}

Since Cloud Firestore doesn't support sum() , you'd do the above but sum on the client side.由于 Cloud Firestore 不支持sum() ,您可以执行上述操作,但在客户端进行 sum 。 The +1 is because the sum is the number of players above you, so adding 1 gives you that player's rank. +1 是因为总和是在你之上的玩家数量,所以加 1 给你那个玩家的排名。

Using this approach, you'll need to read a maximum of 999 documents, averaging 500ish to get a players rank, although in practice this will be less if you delete scores that have zero players.使用这种方法,您需要阅读最多 999 个文档,平均 500ish 才能获得玩家排名,但实际上,如果您删除零个玩家的分数,这会更少。

Write rate of new scores is important to understand as you'll only be able to update an individual score once every 2 seconds* on average, which for a perfectly distributed score range from 0-999 would mean 500 new scores/second**.理解新分数的写入率很重要,因为您平均只能每 2 秒* 更新一次单个分数,对于 0-999 的完美分布分数范围,这意味着 500 个新分数/秒**。 You can increase this by using distributed counters for each score.您可以通过为每个分数使用分布式计数器来增加这一点。

* Only 1 new score per 2 seconds since each score generates 2 writes * 每 2 秒只有 1 个新分数,因为每个分数生成 2 次写入
** Assuming average game time of 2 minute, 500 new scores/second could support 60000 concurrent players without distributed counters. ** 假设平均游戏时间为 2 分钟,500 个新分数/秒可支持 60000 名并发玩家,无需分布式计数器。 If you're using a "Highest score is your current score" this will be much higher in practice.如果您使用“最高分数是您当前的分数”,这在实践中会高得多。

Sharded N-ary Tree分片 N 叉树

This is by far the hardest approach, but could allow you to have both faster and real-time ranking positions for all players.这是迄今为止最难的方法,但可以让您为所有玩家提供更快和实时的排名位置。 It can be thought of as a read-optimized version of of the Inverted Index approach above, whereas the Inverted Index approach above is a write optimized version of this.可以将其视为上述倒排索引方法的读取优化版本,而上面的倒排索引方法是该方法的写入优化版本。

You can follow this related article for 'Fast and Reliable Ranking in Datastore' on a general approach that is applicable.您可以按照此相关文章了解适用的一般方法,了解“数据存储中的快速可靠排名” For this approach, you'll want to have a bounded score (it's possible with unbounded, but will require changes from the below).对于这种方法,您需要有一个有界分数(无界是可能的,但需要从下面进行更改)。

I wouldn't recommend this approach as you'll need to do distributed counters for the top level nodes for any ladder with semi-frequent updates, which would likely negate the read-time benefits.我不会推荐这种方法,因为您需要为具有半频繁更新的任何梯子的顶级节点执行分布式计数器,这可能会抵消读取时间的好处。

三叉树示例

Final thoughts最后的想法

Depending on how often you display the leaderboard for players, you could combine approaches to optimize this a lot more.根据您为玩家显示排行榜的频率,您可以结合多种方法对其进行更多优化。

Combining 'Inverted Index' with 'Periodic Update' at a shorter time frame can give you O(1) ranking access for all players.在更短的时间范围内将“倒排索引”与“定期更新”相结合,可以为所有玩家提供 O(1) 的排名访问权限。

As long as over all players the leaderboard is viewed > 4 times over the duration of the 'Periodic Update' you'll save money and have a faster leaderboard.只要在“定期更新”期间所有玩家的排行榜被查看 > 4 次,您就可以节省资金并拥有更快的排行榜。

Essentially each period, say 5-15 minutes you read all documents from scores in descending order.基本上每个时期,比如说 5 到 15 分钟,你按降序阅读scores中的所有文件。 Using this, keep a running total of players_count .使用这个,保持players_count的运行总数。 Re-write each score into a new collection called scores_ranking with a new field players_above .将每个分数重新写入一个名为scores_ranking的新集合中, scores_ranking使用一个新字段players_above This new field contains the running total excluding the current scores player_count .这个新字段包含不包括当前分数player_count的运行总数。

To get a player's rank, all you need to do now is read the document of the player's score from score_ranking -> Their rank is players_above + 1.要获得玩家的排名,您现在需要做的就是从score_ranking -> 他们的排名为players_above + 1 读取玩家分数的文档。

One solution not mentioned here which I'm about to implement in my online game and may be usable in your use case, is to estimate the user's rank if they're not in any visible leaderboard because frankly the user isn't going to know (or care?) whether they're ranked 22,882nd or 22,838th.我将在我的在线游戏中实施并且可能在您的用例中使用的一种此处未提及的解决方案是估计用户的排名,如果他们不在任何可见的排行榜中,因为坦率地说,用户不会知道(或关心?)他们是否排名第 22,882 或 22,838。

If 20th place has a score of 250 points and there are 32,000 players total, then each point below 250 is worth on average 127 places, though you may want to use some sort of curve so as they move up a point toward bottom of the visible leaderboard they don't jump exactly 127 places each time - most of the jumps in rank should be closer to zero points.如果第 20 名的得分为 250 分并且总共有 32,000 名玩家,那么每个低于 250 的点平均价值 127 个位置,尽管您可能想要使用某种曲线,以便他们向上移动一个点到可见的底部排行榜他们每次都不会准确地跳 127 个位置 - 排名中的大多数跳跃应该更接近于零点。

It's up to you whether you want to identify this estimated ranking as an estimation or not, and you could add some a random salt to the number so it looks authentic:是否要将此估计排名确定为估计值取决于您,您可以在数字中添加一些随机盐,使其看起来真实:

// Real rank: 22,838

// Display to user:
player rank: ~22.8k    // rounded
player rank: 22,882nd  // rounded with random salt of 44

I'll be doing the latter.我会做后者。

Alternative perspective - NoSQL and document stores make this type of task overly complex.另一种观点 - NoSQL 和文档存储使这种类型的任务过于复杂。 If you used Postgres this is pretty simple using a count function.如果您使用过 Postgres,那么使用计数函数就非常简单了。 Firebase is tempting because it's easy to get going with but use cases like this are when relational databases shine. Firebase 很诱人,因为它很容易上手,但像这样的用例是关系数据库大放异彩的时候。 Supabase is worth a look https://supabase.io/ similar to firebase so you can get going quickly with a backend but its opensource and built on Postgres so you get a relational database. Supabase 值得一看https://supabase.io/类似于 firebase,因此您可以快速使用后端,但它是开源的并基于 Postgres 构建,因此您可以获得关系数据库。

A solution that hasn't been mentioned by Dan is the use of security rules combined with Google Cloud Functions. Dan 没有提到的一个解决方案是将安全规则与 Google Cloud Functions 结合使用。

Create the highscore's map.创建高分地图。 Example:示例:

  • highScores (top20)高分(前 20 名)

Then:然后:

  1. Give the users write/read access to highScores.授予用户对 highScores 的写/读访问权限。
  2. Give the document/map highScores the smallest score in a property.给文档/地图 highScores 一个属性中的最小分数。
  3. Let the users only write to highScores if his score > smallest score.让用户只在他的分数 > 最小分数时写入 highScores。
  4. Create a write trigger in Google Cloud Functions that will activate when a new highScore is written.在 Google Cloud Functions 中创建一个写入触发器,该触发器将在写入新的高分时激活。 In that function, delete the smallest score.在该函数中,删除最小的分数。

This looks to me the easiest option.这在我看来是最简单的选择。 It is realtime as well.它也是实时的。

You could do something with cloud storage.你可以用云存储做点什么。 So manually have a file that has all the users' scores (in order), and then you just read that file and find the position of the score in that file.因此,手动拥有一个包含所有用户分数(按顺序)的文件,然后您只需读取该文件并在该文件中找到分数的 position。

Then to write to the file, you could set up a CRON job to periodically add all documents with a flag isWrittenToFile false, add them all to the file (and mark them as true).然后写入文件,您可以设置一个 CRON 作业以定期添加所有带有标志 isWrittenToFile false 的文档,将它们全部添加到文件(并将它们标记为 true)。 That way you won't eat up your writes.这样你就不会吃掉你的写作。 And reading a file every time the user wants to view their position is probably not that intensive.每次用户想要查看他们的 position 时读取一个文件可能不是那么密集。 It could be done from a cloud function.它可以从云 function 完成。

2022 Updated and Working Answer 2022 更新和工作答案

To solve the problem of having a leaderboards with user and points, and to know your position in this leaderboards in an less problematic way, I have this solution:为了解决拥有用户和积分排行榜的问题,并以一种不太成问题的方式了解您在排行榜中的 position,我有以下解决方案:

1) You should create your Firestorage Document like this 1) 你应该像这样创建你的 Firestorage 文档

在此处输入图像描述

In my case, I have a document perMission that has for each user a field, with the userId as property and the respective leaderboard points as value .在我的例子中,我有一个文档perMission ,每个用户都有一个字段,其中 userId 作为属性,相应的排行榜点作为value

It will be easier to update the values inside my Javascript code.更新我的 Javascript 代码中的值会更容易。 For example, whenever an user completed a mission (update it's points):例如,每当用户完成任务(更新它的分数)时:

import { doc, setDoc, increment } from "firebase/firestore"; 

const docRef = doc(db, 'leaderboards', 'perMission');
setDoc(docRef, { [userId]: increment(1) }, { merge: true });

The increment value can be as you want. increment值可以是你想要的。 In my case I run this code every time the user completes a mission, increasing the value.在我的例子中,每次用户完成任务时我都会运行这段代码,从而增加价值。

2) To get the position inside the leaderboards 2) 在排行榜中获得 position

So here in your client side, to get your position, you have to order the values and then loop through them to get your position inside the leaderboards.因此,在您的客户端,要获得您的 position,您必须对这些值进行排序,然后循环遍历它们以使您的 position 进入排行榜。

Here you can also use the object to get all the users and its respective points, ordered.在这里您还可以使用object获取所有用户及其各自的积分,订购。 But here I am not doing this, I am only interested in my position.但是这里我不是这样做的,我只对我的 position 感兴趣。

The code is commented explaining each block.代码注释解释了每个块。


// Values coming from the database.
const leaderboards = {
  userId1: 1,
  userId2: 20,
  userId3: 30,
  userId4: 12,
  userId5: 40,
  userId6: 2
};

// Values coming from your user.
const myUser = "userId4";
const myPoints = leaderboards[myUser];

// Sort the values in decrescent mode.
const sortedLeaderboardsPoints = Object.values(leaderboards).sort(
  (a, b) => b - a
);

// To get your specific position
const myPosition = sortedLeaderboardsPoints.reduce(
  (previous, current, index) => {
    if (myPoints === current) {
      return index + 1 + previous;
    }

    return previous;
  },
  0
);

// Will print: [40, 30, 20, 12, 2, 1]
console.log(sortedLeaderboardsPoints);

// Will print: 4
console.log(myPosition);

You can now use your position, even if the array is super big, the logic is running in the client side.您现在可以使用您的 position,即使数组非常大,逻辑也在客户端运行。 So be careful with that.所以要小心。 You can also improve the client side code, to reduce the array, limit it, etc.您还可以改进客户端代码,减少数组,限制数组等。

But be aware that you should do the rest of the code in your client side, and not Firebase side.但请注意,您应该在客户端执行 rest 代码,而不是 Firebase 端。

This answer is mainly to show you how to store and use the database in a "good way".本回答主要是告诉大家如何“好办法”存储和使用数据库。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM