搜索

首页  >  问答  >  正文

获取集合中随机文档的Firestore方法

对于我的应用程序来说,能够从 Firebase 的集合中随机选择多个文档至关重要。

由于 Firebase 中没有内置的本机函数(据我所知)来实现执行此操作的查询,因此我的第一个想法是使用查询游标来选择随机的开始和结束索引,前提是我有数字集合中的文档数量。

这种方法可以工作,但只能以有限的方式工作,因为每个文档每次都会与其相邻文档按顺序提供;但是,如果我能够通过其父集合中的索引选择文档,我可以实现随机文档查询,但问题是我找不到任何文档来描述如何执行此操作,或者即使可以执行此操作。

这是我想要做的事情,请考虑以下 firestore 架构:

root/
  posts/
     docA
     docB
     docC
     docD

然后在我的客户端(我在 Swift 环境中)我想编写一个可以执行此操作的查询:

db.collection("posts")[0, 1, 3] // would return: docA, docB, docD

我可以做一些类似的事情吗?或者,是否有其他方法可以以类似的方式选择随机文档?

请帮忙。

P粉277305212P粉277305212403 天前801

全部回复(2)我来回复

  • P粉668113768
  • P粉985686557

    P粉9856865572023-10-20 09:50:51

    使用随机生成的索引和简单查询,您可以从 Cloud Firestore 中的集合或集合组中随机选择文档。

    这个答案分为 4 个部分,每个部分都有不同的选项:

    1. 如何生成随机索引
    2. 如何查询随机索引
    3. 选择多个随机文档
    4. 重新播种以获得持续的随机性

    如何生成随机索引

    这个答案的基础是创建一个索引字段,当按升序或降序排序时,会导致所有文档随机排序。有多种不同的方法来创建它,所以让我们看一下 2,从最容易获得的方法开始。

    自动识别版本

    如果您使用我们的客户端库中提供的随机生成的自动 ID,您可以使用同一系统随机选择文档。在这种情况下,随机排序的索引文档ID。

    稍后在我们的查询部分中,您生成的随机值是一个新的自动 ID (iOSAndroidWeb),您查询的字段是 __name__ 字段,并且 '后面提到的“low value”就是一个空字符串。这是迄今为止生成随机索引的最简单方法,并且无论语言和平台如何,都可以工作。

    默认情况下,文档名称 (__name__) 仅按升序索引,并且除了删除和重新创建之外,您也无法重命名现有文档。如果您需要其中任何一个,您仍然可以使用此方法,只需将自动 ID 存储为名为 random 的实际字段,而不是为此目的重载文档名称。

    随机整数版本

    当你编写文档时,首先生成一个有界范围内的随机整数,并将其设置为一个名为random的字段。根据您期望的文档数量,您可以使用不同的有界范围来节省空间或降低冲突风险(这会降低此技术的有效性)。

    您应该考虑您需要哪种语言,因为会有不同的考虑因素。虽然 Swift 很简单,但 JavaScript 却有一个值得注意的问题:

    • 32 位整数:非常适合小型 (~10K 不太可能发生冲突)数据集
    • 64 位整数:大型数据集(注意:JavaScript 本身不支持,

    这将创建一个索引,其中的文档随机排序。稍后在我们的查询部分中,您生成的随机值将是这些值中的另一个,而后面提到的“低值”将是 -1。

    如何查询随机索引

    现在您已经有了一个随机索引,您将需要查询它。下面我们看一些选择 1 个随机文档的简单变体,以及选择多个 1 个文档的选项。

    对于所有这些选项,您需要生成一个新的随机值,其形式与您在编写文档时创建的索引值相同,由下面的变量 random 表示。我们将使用该值在索引上查找随机点。

    环绕式

    现在您有了随机值,您可以查询单个文档:

    let postsRef = db.collection("posts")
    queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
                       .order(by: "random")
                       .limit(to: 1)

    检查是否已返回文档。如果没有,请再次查询,但使用随机索引的“低值”。例如,如果您执行随机整数,则 lowValue0

    let postsRef = db.collection("posts")
    queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
                       .order(by: "random")
                       .limit(to: 1)

    只要您有一个文档,就保证您至少返回 1 个文档。

    双向

    环绕方法实现起来很简单,并且允许您在仅启用升序索引的情况下优化存储。缺点之一是价值观可能受到不公平的保护。例如,如果 10K 中的前 3 个文档(A、B、C)具有随机索引值 A:409496、B:436496、C:818992,则 A 和 C 被选择的机会小于 1/10K,而B 因 A 的接近而被有效屏蔽,并且只有大约 1/160K 的机会。

    您可以在 >=<=<= 之间随机选择,而不是单向查询并在未找到值时回绕,这会降低概率不公平屏蔽的值减半,代价是索引存储加倍。

    如果一个方向没有返回结果,则切换到另一个方向:

    queryRef = postsRef.whereField("random", isLessThanOrEqualTo: random)
                       .order(by: "random", descending: true)
                       .limit(to: 1)
    
    queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
                       .order(by: "random")
                       .limit(to: 1)

    选择多个随机文档

    通常,您需要一次选择多个随机文档。根据您想要的权衡,有两种不同的方法可以调整上述技术。

    冲洗并重复

    这个方法很简单。只需重复该过程,包括每次选择一个新的随机整数。

    此方法将为您提供随机的文档序列,而不必担心重复看到相同的模式。

    代价是它会比下一个方法慢,因为它需要为每个文档单独往返服务。

    继续保持

    在此方法中,只需增加所需文档的限制数量即可。这有点复杂,因为您可能会在调用中返回 0..limit 文档。然后,您需要以相同的方式获取丢失的文档,但限制减少到仅存在差异。如果您知道文档总数比您要求的数量多,则可以通过忽略在第二次调用(但不是第一次)时永远无法取回足够文档的边缘情况来进行优化。

    此解决方案的权衡是重复序列。虽然文档是随机排序的,但如果最终出现重叠范围,您将看到与之前看到的相同模式。有一些方法可以减轻这种担忧,我们将在下一节有关重新播种的内容中讨论。

    此方法比“冲洗并重复”更快,因为您将在最好情况下一次调用或最坏情况下两次调用中请求所有文档。

    重新播种以获得持续的随机性

    虽然如果文档集是静态的,则此方法会随机为您提供文档,但返回每个文档的概率也将是静态的。这是一个问题,因为根据它们获得的初始随机值,某些值可能具有不公平的低或高概率。在许多用例中,这很好,但在某些用例中,您可能希望增加长期随机性,以便有更均匀的机会返回任何 1 个文档。

    请注意,插入的文档最终会交织在中间,逐渐改变概率,删除文档也是如此。如果给定文档数量,插入/删除率太小,有一些策略可以解决这个问题。

    多随机

    您不必担心重新播种,您始终可以为每个文档创建多个随机索引,然后每次随机选择其中一个索引。例如,让字段 random 成为包含子字段 1 到 3 的映射:

    {'random': {'1': 32456, '2':3904515723, '3': 766958445}}

    现在您将随机查询 random.1、random.2、random.3,从而创建更大的随机性分布。这本质上是用增加的存储空间来节省因重新播种而增加的计算(文档写入)。

    写入时重新设定种子

    每次更新文档时,都会重新生成random 字段的随机值。这将在随机索引中移动文档。

    读取时重新播种

    如果生成的随机值不是均匀分布的(它们是随机的,因此这是预期的),则可能会在不适当的时间内选择同一文档。通过在读取随机选择的文档后使用新的随机值更新该文档,可以轻松解决此问题。

    由于写入成本更高并且可能成为热点,因此您可以选择仅在读取时间的子集时进行更新(例如,if random(0,100) === 0) update;)。< /p>

    回复
    0
  • 取消回复