【问题标题】:Getting the index of an object in an ordered list in Firebase在 Firebase 的有序列表中获取对象的索引
【发布时间】:2014-04-24 03:02:53
【问题描述】:

我正在使用 Firebase 构建排行榜。玩家在排行榜中的位置使用 Firebase 的优先级系统进行跟踪。

在我的程序执行的某个时刻,我需要知道给定用户在排行榜中的位置。我可能有成千上万的用户,因此遍历所有用户以找到具有相同 ID 的对象(从而给我索引)并不是一个真正的选择。

有没有更高效的方法来确定 Firebase 中有序列表中对象的索引?

编辑:我正在尝试找出以下内容:

/
---- leaderboard
--------user4 {...}
--------user1 {...}
--------user3 {...} <- what is the index of user3, given a snapshot of user3?
--------...

【问题讨论】:

  • 你有数据结构的例子吗?你看过leaderboard example 及其数据架构吗?
  • 刚刚添加了我的数据结构示例。假设我只有一个 UID(“user3”)和/或“user3”节点的快照。我如何知道它在“排行榜”(用作有序列表)中的位置/索引?是的,我确实看过排行榜的例子。不幸的是,它没有在排行榜中列出子节点的索引。

标签: javascript firebase


【解决方案1】:

如果您正在处理数十或数百个元素并且不介意占用带宽,请参阅Katos 答案。

如果您要处理数以千计的记录,则需要遵循pperrin 的回答中原则上概述的方法。以下答案详细说明了这一点。

第 1 步:设置 Flashlight 以使用 ElasticSearch 索引您的排行榜

Flashlight 是一个方便的节点脚本,可以将 elasticsearch 与 Firebase 数据同步。

Read about how to set it up here.

第 2 步:修改 Flashlight 以允许您将查询选项传递给 ElasticSearch

在撰写本文时,Flashlight 让您无法告诉 ElasticSearch 您只对匹配的文档的数量感兴趣,而不是文档本身。

I've submitted this pull request 使用简单的一行修复程序来添加此功能。如果在您阅读此答案时尚未关闭,只需手动更改您的手电筒副本/叉子即可。

第 3 步:执行查询!

这是我通过 Firebase 发送的查询:

{
    index: 'firebase',
    type: 'allTime',
    query: {
        "filtered": {
            "query": {
                "match_all": {}
            },
            "filter": {
                "range": {
                    "points": {
                        "gte": minPoints
                    }
                }
            }
        }
    },
    options: {
        "search_type": "count"
    }
};

points 替换为您的用户的字段跟踪点名称,将minPoints 替换为您感兴趣的排名的用户的点数。

响应将类似于:

{
    max_score: 0,
    total: 2
}

total 是拥有相同或更多点数的用户数量——也就是用户的排名!

【讨论】:

  • 伟大的工作丹尼!很有创意。我也合并了 PR。
【解决方案2】:

由于 Firebase 存储对象而不是数组,因此元素在列表中没有“索引”——JavaScript 和扩展的 JSON 对象本质上是无序的。正如Ordered Docs 中的解释和leaderboard example 中所展示的那样,您可以通过使用优先级来完成排序。

一组操作:

var ref = new Firebase('URL/leaderboard');
ref.child('user1').setPriority( newPosition /*score?*/ );

一个读操作:

var ref = new Firebase('URL/leaderboard');
ref.child('user1').once('value', function(snap) {
   console.log('user1 is at position', snap.getPriority());
});

【讨论】:

  • 就像 firebase 上的排行榜示例一样,我使用分数来设置优先级,而不是位置。例如,user1 的优先级可能为 500,而 user3 的优先级可能为 1000。当然,user1user3 都不会分别占据第 500 位和第 1000 位。您如何建议我重组我的数据,以便在这种情况下我可以获取节点的位置?
  • 没有位置。它是一个对象(即键是无序的)。如果您仔细考虑这一点,您会发现“位置”实际上是一个任意数字,随着记录的添加、删除和重新定位,它会不断变化。每次发生变化时,您都需要重新计算(或将项目放入数组并读取索引;像 Angular 这样的工具可以在这里提供帮助)
  • 谢谢加藤。我一直都知道leaderboard 实际上是一个对象,我只是想问您是否可以想出一种方法来构造 FB 中的数据,从而使获取对象位置变得微不足道。无论哪种方式,我都很好奇 Angular 会有什么帮助?我正在使用骨干网。除了每次我需要知道特定对象的等级时迭代超过 1000 个元素之外,我想不出更好的办法。
  • 我想每次我想知道排名时都可以进行二分搜索。至少我可以把它归结为对数时间。
  • 快速问题:有什么方法可以获取 firebase 引用中子元素的计数(无需获取快照)?如果能做类似 ref.endAt(currentRank-1).count() 之类的事情,那就太棒了。
【解决方案3】:

为了获得你想要的信息,在某些时候,一个进程将不得不枚举节点来计算它们。所以问题是计算发生的地点/时间。

在客户端中使用 .count() 意味着每次需要时都会完成,它会非常准确,但处理/流量繁重。

如果您保留一个单独的计数索引,则需要定期刷新或不断更新(每次插入都会导致剩余条目的混排)。

根据您的数据的分布和数量,我很想使用一个后台进程,该进程只更新(/重建)索引每(例如)十或二十个添加。并索引每(例如)10 个位置。

"Leaderboard",$UserId = priority=$score
...

"Rank",'10' = $UserId,priority=$score
"Rank",'20' = $UserId,priority=$score
...

根据分数,您在 10 内获得排名,然后在“排行榜”上使用 startat/endat/count 将其降到单位。

如果您的后台进程正在监控排行榜的更新,它可能会更智能地了解其对索引的更新,或者仅根据需要进行更新。

【讨论】:

  • 嘿佩林!伟大的信息和伟大的洞察力。我昨天也意识到了同样的事情;唯一的细微差别是我现在使用弹性搜索来进行排名索引。每次请求排名时(并且不能选择迭代),我都会通过 firebase 向弹性搜索发送查询,以计算索引中 totalPoints 大于我发送的 ID 的元素的数量。那个数字,加一,就是等级。完成实施后,我将发布后续答案。
  • 我添加了一个类似于您使用 ElasticSearch 描述的实现。
【解决方案4】:

我知道这是一个老问题,但我只是想分享我的解决方案以供将来参考。首先,Firebase 生态系统发生了很大变化,我假设当前的最佳实践(即 Firestore 和无服务器功能)。我个人在构建实际应用程序时考虑了这些解决方案,并最终选择了预定的近似等级。


实时排名(最新,但价格昂贵)

在准备用户排行榜时,我做了一些假设:

  • 排行榜根据我以后称之为“得分”的数字对用户进行排名
  • 新用户在排行榜上排名最低,因此在创建用户时,他们的排名被设置为总用户数(使用 Firebase 函数设置排名,但也会将“总用户”计数器增加 1)。
  • 分数只能增加(也可以支持一些适应降低分数)。
  • 已删除的用户会在排行榜上保留一个“幽灵”位置。

每当用户提高他们的分数时,Firebase 函数会通过查询所有被超越的用户(他们的分数 >= 用户的旧分数但 decreased 提高 1 . 用户自己的排名是increased 由前面提到的查询的大小。

现在可以在客户端读取时立即获得排名。然而,提议的函数内部的排名更新是相当多的读写。操作的确切数量很大程度上取决于您的应用程序,但对于我个人的应用程序,分数变化的频率很高并且分数的相对接近性使得这种方法效率太低。我很好奇是否有人找到了更有效(实时)的替代方案。


预定排名(最简单,但昂贵且周期性)

调度一个 Firebase 函数,通过升序对整个用户集合进行简单排序,然后写回每个用户集合的排名(在批量更新中)。此过程可以每天重复,或者根据您的应用程序更频繁/不频繁地重复。对于 N 个用户,该函数总是进行 N 次读取和 N 次写入。


预定的近似排名(最便宜,但不精确和周期性)

作为“计划排名”选项的替代方案,我建议使用近似技术:不是为每个计划更新写入每个用户的确切排名,而是将用户集合(仍然像以前一样排序)简单地分成 M 个块大小相同并且绑定这些块的分数被写入单独的“统计”集合。

因此,例如:如果我们为简单起见使用 M = 3,并且我们读取按升序排序的 60 个用户,那么我们有 3 个 20 个用户的块。对于每个(仍然排序的块),我们得到最后一个(块的最低分数)和第一个用户(块的最高分数)(即包含该块的所有分数的范围)的分数。假设得分最低的块的得分范围为 20-120,第二个块的得分为 130-180,得分最高的块的得分为 200-350。我们现在只需将这些范围写入“统计”集合(无论有多少用户,写入计数都会减少到 1!)。

在排名检索时,用户只需阅读最新的“统计”文档,并通过将范围与自己的分数进行比较来近似他们的百分排名。当然,用户得分可能高于前一次“统计”更新中的最高分或低于最低分,但我会认为他们分别属于最高得分组和最低得分组。

在我自己的应用程序中,我使用了 M = 20,因此可以以 5% 的准确度显示用户百分位排名,并使用线性插值甚至在该范围内进行估计(例如,如果用户得分为 450 并且落入 40% -45%-chunk 范围为 439-474,我们估计用户的百分等级为40 + (450 - 439) / (474 - 439) * 5 = 41.57...%)。

如果您想获得真正的幻想,您还可以通过将您的预期分数分布(例如正态分布)拟合到测量范围来估计精确的百分位排名。

注意:所有用户都需要阅读“统计”文档来估算他们的排名。但是,在大多数应用程序中,并非所有用户都实际查看统计信息(因为他们不是每天都在活动,或者只是对统计信息不感兴趣)。就个人而言,我还使用“stats”文档(名称不同)来存储用户之间共享的其他 DB 值,因此无论如何该文档已经被检索到。除此之外,读取比写入便宜 3 倍。最坏的情况是 2N 次读取和 1 次写入。

【讨论】:

    猜你喜欢
    • 2011-07-05
    • 2015-03-31
    • 1970-01-01
    • 2012-11-25
    • 2018-04-13
    • 2013-10-10
    • 1970-01-01
    • 2020-01-07
    • 1970-01-01
    相关资源
    最近更新 更多