Firestore:如何在集合中获取随机文档
Posted
技术标签:
【中文标题】Firestore:如何在集合中获取随机文档【英文标题】:Firestore: How to get random documents in a collection 【发布时间】:2018-03-29 16:00:11 【问题描述】:对于我的应用来说,能够从 firebase 的集合中随机选择多个文档至关重要。
由于 Firebase 没有内置的本地函数(据我所知)来实现执行此操作的查询,因此我的第一个想法是使用查询游标来选择随机开始和结束索引,前提是我有编号集合中的文档。
这种方法会起作用,但只能以有限的方式起作用,因为每次都会按顺序提供每个文档及其相邻文档;但是,如果我能够通过其父集合中的索引选择一个文档,我可以实现随机文档查询,但问题是我找不到任何描述如何执行此操作或即使您可以执行此操作的文档。
这是我想做的事情,请考虑以下 Firestore 架构:
root/
posts/
docA
docB
docC
docD
然后在我的客户端(我在 Swift 环境中)我想编写一个可以执行此操作的查询:
db.collection("posts")[0, 1, 3] // would return: docA, docB, docD
无论如何我可以做一些事情吗?或者,有没有其他方法可以以类似的方式选择随机文档?
请帮忙。
【问题讨论】:
获取随机文档的一种简单方法是将所有帖子键放入一个数组(docA
、docB
、docC
、docD
)然后打乱数组并获取前三个条目,因此随机播放可能会返回 docB
、docD
、docA
之类的内容。
好吧,这是个好主意!但是您将如何获得发布密钥?感谢您的回复。
希望这个链接在逻辑上会有所帮助:***.com/a/58023128/1318946
【参考方案1】:
使用随机生成的索引和简单查询,您可以从 Cloud Firestore 中的集合或集合组中随机选择文档。
这个答案分为 4 个部分,每个部分都有不同的选项:
-
如何生成随机索引
如何查询随机索引
选择多个随机文档
为持续的随机性重新播种
如何生成随机索引
这个答案的基础是创建一个索引字段,当按升序或降序排序时,会导致所有文档被随机排序。有不同的方法来创建它,所以让我们看看 2,从最容易获得的开始。
自动识别版本
如果您使用的是我们的客户端库中提供的随机生成的自动 ID,您可以使用同一系统来随机选择一个文档。在这种情况下,随机排序的索引是文档id。
稍后在我们的查询部分中,您生成的随机值是一个新的自动 ID(ios、android、Web),您查询的字段是 __name__
字段和“低值” ' 后面提到的是一个空字符串。这是迄今为止生成随机索引的最简单方法,并且无论语言和平台如何都可以使用。
默认情况下,文档名称 (__name__
) 仅索引升序,您也无法重命名现有文档,除非删除和重新创建。如果您需要其中任何一个,您仍然可以使用此方法并将自动 ID 存储为名为 random
的实际字段,而不是为此重载文档名称。
随机整数版本
在编写文档时,首先在有界范围内生成一个随机整数,并将其设置为一个名为random
的字段。根据您期望的文档数量,您可以使用不同的有界范围来节省空间或降低冲突风险(这会降低此技术的有效性)。
您应该考虑需要哪些语言,因为会有不同的考虑。虽然 Swift 很简单,但 javascript 可能有一个陷阱:
32 位整数:非常适合小型 (~10K unlikely to have a collision) 数据集 64 位整数:大型数据集(注意:JavaScript 本身不支持,yet)这将创建一个随机排序的文档索引。稍后在我们的查询部分中,您生成的随机值将是这些值中的另一个,而后面提到的“低值”将是-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;
)。
【讨论】:
谢谢 Dan 我非常感谢您的回复,但指的是不可知论版本(这听起来对我来说更好),如果我想获得多个随机文档,我将不得不将此查询称为多个次?或者增加查询的限制(这将返回随机集群,但这些集群中的文档将始终处于相同的序列中)? 正确,这两个选项都是可行的。前者(多次调用)会更慢,但如果经常这样做,会导致更少的重复序列。后者(更大的限制)会很快,但会增加再次看到相同序列的机会。请注意后者,随着更多文档的添加,顺序可能会发生变化。您还可以在更新文档时重做随机数以更改更多的序列。 非常酷的解决方案丹!事实上......这在实时数据库上也应该是可能的,不是吗? 添加到Solutions 页面会很棒 这么多工作而不是简单地添加orderByRandom()
api:\【参考方案2】:
发布此内容以帮助将来遇到此问题的任何人。
如果您使用 Auto ID,您可以生成一个新的 Auto ID 并查询最接近的 Auto ID,如 Dan McGrath's Answer 中所述。
我最近创建了一个随机报价 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)
如果没有找到文档,则再次调用它,操作相反。
我希望这会有所帮助!
【讨论】:
文档 ID 极不可能遇到这个问题,但如果有人复制它并使用更小的 ID 空间,我建议将第一个 where 子句从 '>' 更改为 ' >='。这可以防止在它们只有 1 个文档的边缘情况下发生故障,并且key
的选择方式恰好是 1 个文档的 id。
感谢您在此处发布的出色答案。我有一个问题,'admin.firestore.FieldPath.documentId()' 到底指的是什么?
我正在使用 Flutter,这不会随机获取文档。它很有可能返回相同的文档。最终它将获得随机文档,但 90% 的时间是相同的文档
@MobileMon 的原因是因为解决方案缺少 orderBy,因此 limit(1) 没有按预期获得“最接近”随机值。我下面的解决方案可以。我也取 10 并在本地随机化。【参考方案3】:
刚刚在 Angular 7 + RxJS 中完成了这项工作,所以在这里与需要示例的人分享。
我使用了@Dan McGrath 的答案,并选择了以下选项:随机整数版本 + 冲洗并重复多个数字。我还使用了这篇文章中解释的东西:RxJS, where is the If-Else Operator? 在流级别上进行 if/else 语句(如果你们中的任何人需要这方面的入门知识)。
还请注意,我使用 angularfire2 来轻松将 Firebase 集成到 Angular 中。
代码如下:
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;
【讨论】:
对于未来的读者:为了清楚起见,我更新了我的答案并将“文档 ID 不可知版本”部分重命名为“随机整数版本” 更新了我的答案以匹配您的更改。 非常简洁的解决方案。很好,但是您在代码中的哪个位置对多个数字进行冲洗和重复? @choopage-JekBao 据我了解,Rinse & Repeat 意味着获取一个新的随机数,然后在每次调用 buttonClicked() 方法时发出请求。说得通? :P【参考方案4】:经过和朋友的激烈争论,我们终于找到了解决办法
如果您不需要将文档的 id 设置为 RandomID,只需将文档命名为集合大小的大小即可。
例如,集合的第一个文档名为“0”。 第二个文档名称应为“1”。
然后,我们只需读取集合的大小,例如N,就可以得到[0~N)范围内的随机数A。
然后,我们可以查询名为 A 的文档。
这种方式可以为集合中的每个文档提供相同的随机概率。
【讨论】:
你在哪里保存集合的大小?或者您每次创建新文档时都在计算它? @ShadeToD 统计大尺寸文档已经有很多分布式计数器等解决方案。顺便说一句..如何标记其他?看来@+id 还不够【参考方案5】:毫无疑问,以上接受的答案是超级有用的,但有一种情况,例如如果我们有一些文档的集合(大约 100-1000 个)并且我们想要一些 20-30 个随机文档,前提是该文档不能重复。 (案例在随机问题应用等...)。
上述解决方案的问题: 对于集合中的少量文档(比如 50 个),重复的概率很高。为了避免这种情况,如果我像这样存储 Fetched Docs Id 和 Add-in Query:
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue).where("__name__", isNotEqualTo:"PreviousId")
.order(by: "random")
.limit(to: 1)
这里的 PreviousId 是所有已获取元素的 Id 已经意味着 n 个先前 Id 的循环。 但是在这种情况下,网络调用会很高。
我的解决方案: 维护一个特殊文档并仅保留此集合的 ID 记录,并首次获取此文档,然后执行所有随机性工作并检查以前未在 App 站点上获取的内容。因此,在这种情况下,网络调用将仅与所需的文档数量相同 (n+1)。
我的解决方案的缺点: 必须维护一个文档,以便在添加和删除时写入。但是如果读取频率很高,那么在大多数情况下都会发生写入,这很好。
【讨论】:
【参考方案6】:我有一种方法可以在 Firebase Firestore 中随机获取一个列表文档,这非常简单。当我在 Firestore 上上传数据时,我创建了一个字段名称“位置”,其随机值从 1 到 1 百万。当我从 Fire 商店获取数据时,我将按字段“位置”设置 Order 并为其更新值,大量用户加载数据和数据总是更新,它将是随机值。
【讨论】:
不错的解决方案,但我会不必要地添加更多 Firestore Ops @HimanshuRawat 你是对的,如果你的应用拥有庞大的用户群,那么它会产生巨大的影响【参考方案7】:对于那些使用 Angular + Firestore,基于@Dan McGrath 技术的人,这里是代码 sn-p。
下面的代码 sn -p 返回 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';
【讨论】:
【参考方案8】:与 rtdb 不同,firestore id 不是按时间顺序排列的。因此,如果您使用 firestore 客户端自动生成的 id,那么使用 Dan McGrath 描述的 Auto-Id 版本很容易实现。
new Promise<Timeline | undefined>(async (resolve, reject) =>
try
let randomTimeline: Timeline | undefined;
let maxCounter = 5;
do
const randomId = this.afs.createId(); // AngularFirestore
const direction = getRandomIntInclusive(1, 10) <= 5;
// The firestore id is saved with your model as an "id" property.
let list = await this.list(ref => ref
.where('id', direction ? '>=' : '<=', randomId)
.orderBy('id', direction ? 'asc' : 'desc')
.limit(10)
).pipe(take(1)).toPromise();
// app specific filtering
list = list.filter(x => notThisId !== x.id && x.mediaCounter > 5);
if (list.length)
randomTimeline = list[getRandomIntInclusive(0, list.length - 1)];
while (!randomTimeline && maxCounter-- >= 0);
resolve(randomTimeline);
catch (err)
reject(err);
)
【讨论】:
【参考方案9】:好的,即使您正在为 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())
【讨论】:
【参考方案10】:其他解决方案更好,但我似乎很难理解,所以我想出了另一种方法
使用递增数字作为 ID,如 1,2,3,4,5,6,7,8,9,注意删除文档,否则我们 有一个我不见了
获取集合中的文档总数,像这样,我不知道有比这更好的解决方案
let totalDoc = db.collection("stat").get().then(snap=>snap.size)
现在我们有了这些,创建一个空数组来存储随机数字列表,假设我们想要 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
最后我们从 fire store 中获取数据,并通过 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 获得更好的东西或内置查询
【讨论】:
查询数据库中的每个文档并不是最好的主意(您必须为读取的每个文档付费)" let totalDoc = db.collection("stat").get().then (snap=>snap.size)" 这可以通过存储一个文档计数器来解决,每次添加文档时计数器都会增加,每次删除文档时计数器都会减小。 那将是一个更好的解决方案,但是如果删除的文档不是数据库中的最后一个文档怎么办【参考方案11】:根据@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);
);
);
【讨论】:
【参考方案12】:NodeJs 的工作代码:
你可以复制粘贴它,它会运行。
我使用 nodeJs 创建了一个云函数,它从集合中选择一个随机文档并将其复制到另一个集合。
这是集合中的文档。
工作代码:
//Step-0: all the documents in main list are named as number from 1, 2, 3, etc
//Step-1: Finding out the total number of documents in the collection with all the videoIds.
//Step-2: Generating a random number between 1 and total no of documents.
//Step-3: Picking a random document based on the generated number
//step-4: getting its fields data and pasting to ytVideoTimeline
//Time based function which triggers after every 2 hours.
exports.videoTimeLine2hourScheduled = functions.pubsub.schedule('0 */2 * * *').onRun(async(context) =>
//Step-1: Finding out the total number of documents in the collection with all the videoIds.
var totalNoOfVideosInMainList;
await admin
.firestore()
.collection("ytVideoMainList")
.get()
.then((docs) => totalNoOfVideosInMainList = docs.size;
);
//Step-2: Generating a random number between 1 and total no of documents.
var randomVideoDocId = Math.floor(Math.random() * totalNoOfVideosInMainList) + 1;
//Step-3: Picking a random document based on the generated number
var youtubeVideoData;
await admin
.firestore()
.collection("ytVideoMainList")
.doc(`$randomVideoDocId`)
.get()
.then((doc)=>
if (!doc.exists)
console.log('No such User document!');
console.log('Document data:', doc.data());
else
//step-4: addsing all it's data to youtubeVideoData
console.log('Document data:', doc.data());
youtubeVideoData = doc.data();
return true;
)
.catch((err) =>
console.log('Error getting document', err);
return false;
);
//step-4: getting its fields data and pasting to ytVideoTimeline
await admin.firestore()
.collection('ytVideoTimeline')
.doc(`$randomVideoDocId`)
.set(youtubeVideoData);
return null;
);
(顺便说一句,我正在为 firebase 控制台和互联网上的所有其他网站使用深色主题的“Dark Reader”chrome 扩展。)
【讨论】:
【参考方案13】:如果您使用的是 autoID,这也可能对您有用...
let collectionRef = admin.firestore().collection('your-collection');
const documentSnapshotArray = await collectionRef.get();
const records = documentSnapshotArray.docs;
const index = documentSnapshotArray.size;
let result = '';
console.log(`TOTAL SIZE=====$index`);
var randomDocId = Math.floor(Math.random() * index);
const docRef = records[randomDocId].ref;
result = records[randomDocId].data();
console.log('----------- Random Result --------------------');
console.log(result);
console.log('----------- Random Result --------------------');
【讨论】:
【参考方案14】:您可以使用listDocuments()
属性仅获取查询文档ID 列表。然后使用以下方式生成随机id,获取DocumentSnapshot和get()
属性。
var restaurantQueryReference = admin.firestore().collection("Restaurant"); //have +500 docs
var restaurantQueryList = await restaurantQueryReference.listDocuments(); //get all docs id;
for (var i = restaurantQueryList.length - 1; i > 0; i--)
var j = Math.floor(Math.random() * (i + 1));
var temp = restaurantQueryList[i];
restaurantQueryList[i] = restaurantQueryList[j];
restaurantQueryList[j] = temp;
var restaurantId = restaurantQueryList[Math.floor(Math.random()*restaurantQueryList.length)].id; //this is random documentId
【讨论】:
以上是关于Firestore:如何在集合中获取随机文档的主要内容,如果未能解决你的问题,请参考以下文章
如何在flutter中获取firestore文档的documentid?
Cloud Firestore:如何在我的集合查询中获取文档引用并将其映射为 JSON 值?