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

时间:2017-10-17 20:48:11

标签: swift database firebase data-modeling google-cloud-firestore

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

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

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

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

root/
  posts/
     docA
     docB
     docC
     docD

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

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

无论如何我可以做一些事情吗?或者,我可以采用不同的方式以类似的方式选择随机文档吗?

请帮忙。

11 个答案:

答案 0 :(得分:41)

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

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

  1. 如何生成随机索引
  2. 如何查询随机索引
  3. 选择多个随机文件
  4. 继续进行随机性转播
  5. 如何生成随机索引

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

    自动标识版

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

    稍后在我们的查询部分中,您生成的随机值是新的自动ID(iOSAndroidWeb),您查询的字段为{{1} }字段,'低值'后面提到的是一个空字符串。到目前为止,这是生成随机索引的最简单方法,无论语言和平台如何都可以使用。

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

    随机整数版本

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

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

    • 32位整数:非常适合小型(~10K unlikely to have a collision)数据集
    • 64位整数:大数据集(注意:JavaScript本身不支持,yet

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

    如何查询随机索引

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

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

    环绕式

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

    random

    检查是否已返回文档。如果没有,请再次查询,但使用“低值”'为您的随机索引。例如,如果您执行了随机整数,则let postsRef = db.collection("posts") queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random) .order(by: "random") .limit(to: 1) lowValue

    0

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

    双向

    环绕方法易于实现,允许您在仅启用升序索引的情况下优化存储。一个缺点是价值被不公平地屏蔽的可能性。例如,如果10K中的前3个文档(A,B,C)具有A:409496,B:436496,C:818992的随机索引值,那么A和C的选择机会不到1 / 10K,而B被A附近有效屏蔽,只有大约1 / 160K的几率。

    您可以在let postsRef = db.collection("posts") queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue) .order(by: "random") .limit(to: 1) >=之间随机选择,而不是在单一方向查询并在未找到值的情况下回绕,这样可以将不公平屏蔽值的概率降低一半,索引存储量增加一倍的成本。

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

    <=

    选择多个随机文件

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

    冲洗&amp;重复

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

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

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

    坚持下去

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

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

    这种方法比“冲洗”和“冲洗”更快。重复&#39;因为你要在最好的情况下请求所有文件,一个电话或最坏情况下2个电话。

    正在进行随机性的重新播种

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

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

    多随机

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

    random

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

    重新编写

    每次更新文档时,请重新生成{'random': {'1': 32456, '2':3904515723, '3': 766958445}} 字段的随机值。这将在随机索引中移动文档。

    重新开始阅读

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

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

答案 1 :(得分:5)

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

如果您使用的是自动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 :(得分:1)

只是在Angular 7 + RxJS中完成了这项工作,所以在这里与需要示例的人分享。

我使用@Dan McGrath的答案,我选择了以下选项:随机整数版本+冲洗并重复多个数字。我还使用了本文中介绍的内容: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;
  }
}

答案 3 :(得分:1)

与朋友激烈争论后,我们终于找到了解决方法

如果您不需要将文档的ID设置为RandomID,只需将文档命名为集合的大小即可。

例如,集合的第一个文档被命名为“ 0”。 第二个文档名称应为“ 1”。

然后,我们只读取集合的大小,例如N,就可以得到[0〜N)范围内的随机数A。

然后,我们可以查询名为A的文档。

这种方法可以使集合中的每个文档具有相同的随机概率。

答案 4 :(得分:0)

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

答案 5 :(得分: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';

答案 6 :(得分:0)

与rtdb不同,firestore ID并非按时间顺序排列。因此,如果您使用Firestore客户端自动生成的ID,则可以轻松实现使用Dan McGrath描述的Auto-Id版本。

Time: 0.00350088 s
SQL: SELECT ...
Args: (...)

答案 7 :(得分:0)

好的,即使您在Android平台上也要发布该问题的答案。每当我创建一个新文档时,我都会启动随机数并将其设置为随机字段,因此我的文档看起来像

"field1" : "value1"
"field2" : "value2"
...
"random" : 13442 //this is the random number i generated upon creating document

查询随机文档时,会生成与创建文档时所用范围相同的随机数。

private val firestore: FirebaseFirestore = FirebaseFirestore.getInstance()
private var usersReference = firestore.collection("users")

val rnds = (0..20001).random()

usersReference.whereGreaterThanOrEqualTo("random",rnds).limit(1).get().addOnSuccessListener {
  if (it.size() > 0) {
          for (doc in it) {
               Log.d("found", doc.toString())
           }
} else {
    usersReference.whereLessThan("random", rnds).limit(1).get().addOnSuccessListener {
          for (doc in it) {
                  Log.d("found", doc.toString())
           }
        }
}
}

答案 8 :(得分:0)

基于@ajzbc的答案,我为Unity3D编写了此代码,并为我工作。

FirebaseFirestore db;

    void Start()
    {
        db = FirebaseFirestore.DefaultInstance;
    }

    public void GetRandomDocument()
    {

       Query query1 = db.Collection("Sports").WhereGreaterThanOrEqualTo(FieldPath.DocumentId, db.Collection("Sports").Document().Id).Limit(1);
       Query query2 = db.Collection("Sports").WhereLessThan(FieldPath.DocumentId, db.Collection("Sports").Document().Id).Limit(1);

        query1.GetSnapshotAsync().ContinueWithOnMainThread((querySnapshotTask1) =>
        {

             if(querySnapshotTask1.Result.Count > 0)
             {
                 foreach (DocumentSnapshot documentSnapshot in querySnapshotTask1.Result.Documents)
                 {
                     Debug.Log("Random ID: "+documentSnapshot.Id);
                 }
             } else
             {
                query2.GetSnapshotAsync().ContinueWithOnMainThread((querySnapshotTask2) =>
                {

                    foreach (DocumentSnapshot documentSnapshot in querySnapshotTask2.Result.Documents)
                    {
                        Debug.Log("Random ID: " + documentSnapshot.Id);
                    }

                });
             }
        });
    }

答案 9 :(得分:0)

毫无疑问,上面接受的答案是超级有用的,但是有一种情况,例如如果我们有一些文档的集合(大约 100-1000),并且我们想要一些 20-30 个随机文档,前提是该文档不能重复。 (情况在随机问题应用程序等......)。

上述解决方案的问题: 对于集合中的少量文档(比如 50 个),重复的概率很高。为了避免它,如果我像这样存储获取的文档 ID 和加载项查询:

queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue).where("__name__", isNotEqualTo:"PreviiousId")
               .order(by: "random")
               .limit(to: 1)

这里的 Prevoius Id 是所有已获取元素的 Id 已经意味着 n 个先前 Id 的循环。 但在这种情况下,网络 Call 会很高。

我的解决方案: 维护一个特殊文档并仅保留此集合的 Id 记录,并首次获取此文档,然后执行所有随机性操作并检查之前未在 App 站点上获取的内容。因此,在这种情况下,网络调用将仅与所需的文档数相同 (n+1)。

我的解决方案的缺点: 必须维护一个文件,所以写在添加和删除上。但如果读取非常频繁,那么在大多数情况下会发生写入。

答案 10 :(得分:-1)

其他解决方案更好,但似乎让我难以理解,因此我想出了另一种方法

  1. 使用增量数字作为ID,例如1,2,3,4,5,6,7,8,9,当心删除文件,否则我们 有一个我想念的人

  2. 获取集合中的文档总数,类似这样,我不知道有比这更好的解决方案

     let totalDoc = db.collection("stat").get().then(snap=>snap.size)
    
  3. 现在有了这些,创建一个空数组来存储数字的随机列表,假设我们要20个随机文档。

     let  randomID = [ ]
    
     while(randomID.length < 20) {
         const randNo = Math.floor(Math.random() * totalDoc) + 1;
         if(randomID.indexOf(randNo) === -1) randomID.push(randNo);
     }
    

    现在我们有20个随机文档ID

  4. 最后,我们从火灾存储中获取数据,并通过randomID数组进行映射,将其保存到randomDocs数组中

     const  randomDocs =  randomID.map(id => {
         db.collection("posts").doc(id).get()
             .then(doc =>  {
                  if (doc.exists) return doc.data()
              })
             .catch(error => {
                  console.log("Error getting document:", error);
             });
       })
    

我是Firebase的新手,但我认为有了这个答案,我们很快就能得到更好的东西或从firebase中获得内置查询