Firestore:如何在集合中获取随机文档

问题描述 投票:20回答:5

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

由于Firebase(我知道)没有内置本机功能来实现这样做的查询,我首先想到的是使用查询游标来选择随机的开始和结束索引,前提是我有多少个文档集合。

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

这是我希望能够做到的,请考虑以下firestore架构:

root/
  posts/
     docA
     docB
     docC
     docD

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

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

无论如何我能做到这一点吗?或者,有不同的方式我可以以类似的方式选择随机文档吗?

请帮忙。

swift database firebase data-modeling google-cloud-firestore
5个回答
53
投票

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

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

  1. 如何生成随机索引
  2. 如何查询随机索引
  3. 选择多个随机文档
  4. 重新定位持续随机性

How to generate the random indexes

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

Auto-Id version

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

稍后在我们的查询部分中,您生成的随机值是一个新的auto-id(iOSAndroidWeb),您查询的字段是__name__字段,后面提到的'low value'是一个空字符串。到目前为止,这是生成随机索引的最简单方法,无论语言和平台如何都可以。

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

Random Integer version

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

您应该考虑您需要哪种语言,因为会有不同的考虑因素。虽然Swift很容易,但JavaScript显然可以有一个问题:

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

How to query the random indexes

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

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

Wrap-around

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

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)

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

Bi-directional

环绕方法易于实现,并允许您在仅启用升序索引的情况下优化存储。一个缺点是价值被不公平地屏蔽的可能性。例如,如果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)

Selecting multiple random documents

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

Rinse & Repeat

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

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

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

Keep it coming

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

与该解决方案的权衡是重复的顺序。虽然文档是随机排序的,但如果您最终重叠范围,您将看到之前看到的相同模式。有一些方法可以缓解下一节重新播种中讨论的这种问题。

这种方法比“冲洗和重复”更快,因为您将在最佳情况下请求所有文档单个呼叫或最差情况2呼叫。

Reseeding for ongoing randomness

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

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

Multi-Random

您可以随时为每个文档创建多个随机索引,然后每次随机选择其中一个索引,而不是担心重新播种。例如,将字段random设为包含子字段1到3的地图:

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

现在你将随机查询random.1,random.2,random.3,创建更大的随机性扩展。这基本上交换了增加的存储空间,以节省必须重新设置的增加的计算(文档写入)。

Reseed on writes

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

Reseed on reads

如果生成的随机值不是均匀分布的(它们是随机的,那么这是预期的),那么可能会在适当的时间内选择相同的文档。在读取后用随机新值更新随机选择的文档很容易抵消这种情况。

由于写入更昂贵且可以热点,因此您可以选择仅在读取时更新一部分时间(例如,if random(0,100) === 0) update;)。


7
投票

发布此信息以帮助将来遇到此问题的任何人。

如果您使用自动ID,则可以生成新的自动ID并查询Dan McGrath's Answer中提到的最近的自动ID。

我最近创建了一个随机引用api,需要从firestore集合中获取随机引用。 这就是我解决这个问题的方法:

var db = admin.firestore();
var quotes = db.collection("quotes");

var key = quotes.doc().id;

quotes.where(admin.firestore.FieldPath.documentId(), '>=', key).limit(1).get()
.then(snapshot => {
    if(snapshot.size > 0) {
        snapshot.forEach(doc => {
            console.log(doc.id, '=>', doc.data());
        });
    }
    else {
        var quote = quotes.where(admin.firestore.FieldPath.documentId(), '<', key).limit(1).get()
        .then(snapshot => {
            snapshot.forEach(doc => {
                console.log(doc.id, '=>', doc.data());
            });
        })
        .catch(err => {
            console.log('Error getting documents', err);
        });
    }
})
.catch(err => {
    console.log('Error getting documents', err);
});

查询的关键是:

.where(admin.firestore.FieldPath.documentId(), '>', key)

如果没有找到文件,则再次调用该操作。

我希望这有帮助! 如果有兴趣,你可以在my API找到GitHub的这个特定部分


2
投票

刚刚在Angular 7 + RxJS中做了这个工作,所以在这里与想要一个例子的人分享。

我使用了@Dan McGrath的答案,我选择了这些选项:Random Integer version + Rinse&Repeat for multiple numbers。我还使用了本文中解释的内容:RxJS, where is the If-Else Operator?在流级别上创建if / else语句(就像你们中的任何人需要一个引子)。

另请注意,我使用angularfire2在Angular中轻松实现Firebase集成。

这是代码:

import { Component, OnInit } from '@angular/core';
import { Observable, merge, pipe } from 'rxjs';
import { map, switchMap, filter, take } from 'rxjs/operators';
import { AngularFirestore, QuerySnapshot } from '@angular/fire/firestore';

@Component({
  selector: 'pp-random',
  templateUrl: './random.component.html',
  styleUrls: ['./random.component.scss']
})
export class RandomComponent implements OnInit {

  constructor(
    public afs: AngularFirestore,
  ) { }

  ngOnInit() {
  }

  public buttonClicked(): void {
    this.getRandom().pipe(take(1)).subscribe();
  }

  public getRandom(): Observable<any[]> {
    const randomNumber = this.getRandomNumber();
    const request$ = this.afs.collection('your-collection', ref => ref.where('random', '>=', randomNumber).orderBy('random').limit(1)).get();
    const retryRequest$ = this.afs.collection('your-collection', ref => ref.where('random', '<=', randomNumber).orderBy('random', 'desc').limit(1)).get();

    const docMap = pipe(
      map((docs: QuerySnapshot<any>) => {
        return docs.docs.map(e => {
          return {
            id: e.id,
            ...e.data()
          } as any;
        });
      })
    );

    const random$ = request$.pipe(docMap).pipe(filter(x => x !== undefined && x[0] !== undefined));

    const retry$ = request$.pipe(docMap).pipe(
      filter(x => x === undefined || x[0] === undefined),
      switchMap(() => retryRequest$),
      docMap
    );

    return merge(random$, retry$);
  }

  public getRandomNumber(): number {
    const min = Math.ceil(Number.MIN_VALUE);
    const max = Math.ceil(Number.MAX_VALUE);
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }
}


0
投票

我有一种方法可以在Firebase Firestore中随机获取列表​​文档,这非常简单。当我在Firestore上传数据时,我创建一个字段名称“position”,其随机值为1到1 milions。当我从Fire商店获取数据时,我将按字段“位置”设置并更新它的值,很多用户加载数据和数据总是更新,它将是随机值。


0
投票

对于那些使用Angular + Firestore的人,以@Dan McGrath技术为基础,这里是代码片段。

下面的代码片段返回1个文档。

  getDocumentRandomlyParent(): Observable<any> {
    return this.getDocumentRandomlyChild()
      .pipe(
        expand((document: any) => document === null ? this.getDocumentRandomlyChild() : EMPTY),
      );
  }

  getDocumentRandomlyChild(): Observable<any> {
      const random = this.afs.createId();
      return this.afs
        .collection('my_collection', ref =>
          ref
            .where('random_identifier', '>', random)
            .limit(1))
        .valueChanges()
        .pipe(
          map((documentArray: any[]) => {
            if (documentArray && documentArray.length) {
              return documentArray[0];
            } else {
              return null;
            }
          }),
        );
  }

1).expand()是一个rxjs递归操作,以确保我们肯定从随机选择中获取文档。

2)为了使递归按预期工作,我们需要有2个独立的函数。

3)我们使用EMPTY来终止.expand()运算符。

import { Observable, EMPTY } from 'rxjs';
© www.soinside.com 2019 - 2024. All rights reserved.