使用随机生成的索引和简单查询,您可以从 Cloud Firestore 中的集合或集合组中随机选择文档。
这个答案分为 4 个部分,每个部分都有不同的选项:
- 如何生成随机索引
- 如何查询随机索引
- 选择多个随机文档
- 为持续的随机性重新播种
如何生成随机索引
这个答案的基础是创建一个索引字段,当按升序或降序排序时,会导致所有文档被随机排序。有不同的方法来创建它,所以让我们看看 2,从最容易获得的开始。
自动识别版本
如果您使用的是我们的客户端库中提供的随机生成的自动 ID,您可以使用同一系统来随机选择一个文档。在这种情况下,随机排序的索引是文档id。
稍后在我们的查询部分中,您生成的随机值是一个新的自动 ID(iOS、Android、Web),您查询的字段是 __name__ 字段和“低值” ' 后面提到的是一个空字符串。这是迄今为止生成随机索引的最简单方法,并且无论语言和平台如何都可以使用。
默认情况下,文档名称 (__name__) 仅索引升序,您也无法重命名现有文档,除非删除和重新创建。如果您需要其中任何一个,您仍然可以使用此方法并将自动 ID 存储为名为 random 的实际字段,而不是为此重载文档名称。
随机整数版本
在编写文档时,首先在有界范围内生成一个随机整数,并将其设置为一个名为random 的字段。根据您期望的文档数量,您可以使用不同的有界范围来节省空间或降低冲突风险(这会降低此技术的有效性)。
您应该考虑需要哪些语言,因为会有不同的考虑。虽然 Swift 很简单,但 JavaScript 可能有一个陷阱:
这将创建一个随机排序的文档索引。稍后在我们的查询部分中,您生成的随机值将是这些值中的另一个,而后面提到的“低值”将是-1。
如何查询随机索引
现在您有了一个随机索引,您需要查询它。下面我们看一些简单的变体来选择一个随机文档,以及选择多于 1 个的选项。
对于所有这些选项,您需要生成一个新的随机值,其格式与您在编写文档时创建的索引值相同,由下面的变量 random 表示。我们将使用这个值在索引上找到一个随机点。
环绕
现在你有了一个随机值,你可以查询单个文档:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
.order(by: "random")
.limit(to: 1)
检查这是否返回了一个文档。如果没有,请再次查询,但对随机索引使用“低值”。例如,如果你做了随机整数,那么lowValue 就是0:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
.order(by: "random")
.limit(to: 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)
选择多个随机文档
通常,您需要一次选择超过 1 个随机文档。有 2 种不同的方法可以根据您想要的权衡来调整上述技术。
冲洗并重复
这种方法很简单。只需重复该过程,包括每次选择一个新的随机整数。
此方法将为您提供随机的文档序列,而不必担心重复看到相同的模式。
权衡是它会比下一个方法慢,因为它需要为每个文档单独往返服务。
继续前进
在这种方法中,只需将限制中的数量增加到所需文档即可。这有点复杂,因为您可能会在调用中返回 0..limit 文档。然后,您需要以相同的方式获取丢失的文档,但限制减少到只有差异。如果您知道文档总数多于您要求的数量,您可以通过忽略在第二次调用(但不是第一次)时永远无法取回足够文档的边缘情况来优化。
此解决方案的权衡是重复顺序。虽然文档是随机排序的,但如果最终出现重叠范围,您将看到与之前看到的相同的模式。有一些方法可以缓解这种担忧,我们将在下一节关于重新播种中讨论。
这种方法比“冲洗和重复”更快,因为您将在最好的情况下一次调用或最坏的情况下两次调用请求所有文档。
为持续的随机性重新播种
虽然如果文档集是静态的,则此方法会随机为您提供文档,但返回每个文档的概率也将是静态的。这是一个问题,因为某些值可能基于它们获得的初始随机值而具有不公平的低或高概率。在许多用例中,这很好,但在某些情况下,您可能希望增加长期随机性,以便更均匀地返回任何 1 个文档。
请注意,插入的文档最终会交织在中间,逐渐改变概率,删除文档也是如此。如果考虑到文档的数量,插入/删除率太小,有一些策略可以解决这个问题。
多随机
不必担心重新播种,您可以随时为每个文档创建多个随机索引,然后每次随机选择其中一个索引。例如,将字段 random 设为具有子字段 1 到 3 的地图:
{'random': {'1': 32456, '2':3904515723, '3': 766958445}}
现在您将随机查询 random.1、random.2、random.3,从而产生更大范围的随机性。这实质上是用增加的存储空间来节省不得不重新设定的增加的计算(文档写入)。
重新设置写入
每当您更新文档时,请重新生成 random 字段的随机值。这将在随机索引中移动文档。
Reseed on reads
如果生成的随机值不是均匀分布的(它们是随机的,所以这是意料之中的),那么同一个文档可能会在不适当的时间内被选中。这可以通过在读取随机选择的文档后用新的随机值更新它来轻松抵消。
由于写入更昂贵并且可以热点,您可以选择仅在读取时间的子集时更新(例如,if random(0,100) === 0) update;)。