Firestore:如何获取集合中的随机文档
Firestore: How to get random documents in a collection
我的应用程序能够从 firebase 的集合中随机 select 多个文档是至关重要的。
由于 Firebase 没有内置的原生函数(据我所知)来实现执行此操作的查询,我的第一个想法是使用查询游标 select 随机开始和结束索引前提是我有集合中的文档数量。
这种方法可行,但效果有限,因为每次都会按相邻文档的顺序提供每个文档;但是,如果我能够通过其父集合中的索引 select 文档,我可以实现随机文档查询,但问题是我找不到任何描述如何执行此操作的文档,即使你可以做到这一点。
这是我希望能够执行的操作,请考虑以下 firestore 架构:
root/
posts/
docA
docB
docC
docD
然后在我的客户端(我在 Swift 环境中)我想编写一个可以执行此操作的查询:
db.collection("posts")[0, 1, 3] // would return: docA, docB, docD
无论如何我可以做一些类似的事情吗?或者,有没有其他方法可以 select 以类似方式随机生成文档?
请帮忙。
使用随机生成的索引和简单查询,您可以从 Cloud Firestore 中的集合或集合组中随机 select 文档。
这个答案分为 4 个部分,每个部分都有不同的选项:
- 如何生成随机索引
- 如何查询随机索引
- 选择多个随机文档
- 为持续的随机性重新播种
如何生成随机索引
这个答案的基础是创建一个索引字段,当按升序或降序排序时,会导致所有文档随机排序。有不同的方法来创建它,所以让我们看看 2,从最容易获得的开始。
自动识别版本
如果您使用我们的客户端库中提供的随机生成的自动 ID,您可以使用同一系统随机 select 文档。在这种情况下,随机排序的索引 是 文档 ID。
稍后在我们的查询部分,您生成的随机值是一个新的auto-id (iOS, Android, Web),您查询的字段是__name__
字段,而'low value'后面提到的是一个空字符串。这是迄今为止最简单的生成随机索引的方法,并且无论语言和平台如何。
默认情况下,文档名称 (__name__
) 仅按升序索引,除了删除和重新创建之外,您也无法重命名现有文档。如果您需要其中任何一个,您仍然可以使用此方法并将自动 ID 存储为名为 random
的实际字段,而不是为此目的重载文档名称。
随机整数版本
编写文档时,首先生成一个有界范围内的随机整数,并将其设置为名为random
的字段。根据您期望的文档数量,您可以使用不同的边界范围来保存 space 或降低冲突风险(这会降低此技术的有效性)。
您应该考虑您需要哪些语言,因为会有不同的考虑因素。虽然 Swift 很简单,但 JavaScript 值得注意的是有一个陷阱:
- 32 位整数:非常适合小型 (~10K unlikely to have a collision) 数据集
- 64 位整数:大型数据集(注意:JavaScript 本身不支持,yet)
这将创建一个索引,您的文档会随机排序。后面我们的查询部分,你生成的随机值会是另外一个这些值,后面提到的'low value'就是-1.
如何查询随机索引
现在您有了一个随机索引,您将要查询它。下面我们看看 select 1 个随机文档的一些简单变体,以及 select 多于 1 个的选项。
对于所有这些选项,您需要生成一个新的随机值,其形式与您在编写文档时创建的索引值的形式相同,由下面的变量 random
表示。我们将使用此值在索引上找到一个随机点。
环绕
现在您有了一个随机值,您可以查询单个文档:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
.order(by: "random")
.limit(to: 1)
检查这是否return编辑了文档。如果没有,请再次查询,但使用 'low value' 作为您的随机索引。例如,如果您执行随机整数,则 lowValue
是 0
:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
.order(by: "random")
.limit(to: 1)
只要您有一个文档,就可以保证您至少 return 有一个文档。
双向
环绕方法易于实施,并且允许您仅启用升序索引来优化存储。一个缺点是价值观可能会受到不公平的保护。例如,如果 10K 中的前 3 个文档 (A,B,C) 的随机索引值为 A:409496、B:436496、C:818992,则 A 和 C 的索引值刚好小于 1/10K被 selected 的机会,而 B 被 A 的接近有效地屏蔽并且只有大约 1/160K 的机会。
与其单向查询并在找不到值时回绕,不如在 >=
和 <=
之间随机 select,这样可以减少不公平的概率以双倍索引存储为代价将值屏蔽一半。
如果一个方向return没有结果,切换到另一个方向:
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)
选择多个随机文档
通常,您会希望一次 select 超过 1 个随机文档。有 2 种不同的方法可以根据您想要的权衡来调整上述技术。
冲洗并重复
这个方法很简单。只需重复该过程,包括每次 select 一个新的随机整数。
此方法将为您提供随机序列的文档,而无需担心重复看到相同的模式。
权衡是它会比下一个方法慢,因为它需要为每个文档单独往返服务。
继续加油
在这种方法中,只需将限制中的数量增加到所需的文档即可。它有点复杂,因为您可能会在通话中 return 0..limit
文档。然后您需要以相同的方式获取丢失的文件,但限制减少到只有差异。如果您知道总共有比您要求的数量更多的文档,您可以通过忽略在第二次调用(而不是第一次)时永远不会取回足够文档的边缘情况来进行优化。
此解决方案的权衡是重复序列。虽然文档是随机排序的,但如果您最终出现重叠范围,您会看到与之前看到的相同的模式。有一些方法可以减轻这种担忧,下一节将讨论重新播种。
这种方法比 'Rinse & Repeat' 更快,因为您将在最好的情况下一次调用或最坏的情况下调用 2 次来请求所有文档。
为持续的随机性重新播种
虽然如果文档集是静态的,此方法会随机为您提供文档,但每个文档被 returned 的概率也将是静态的。这是一个问题,因为根据它们获得的初始随机值,某些值可能具有不公平的低或高概率。在许多用例中,这很好,但在某些情况下,您可能希望增加长期随机性以获得更均匀的机会 returning 任何 1 个文档。
请注意,插入的文档最终会交织在一起,逐渐改变概率,删除文档也是如此。如果考虑到文档数量,insert/delete 比率太小,有一些策略可以解决这个问题。
多随机
不必担心重新播种,您始终可以为每个文档创建多个随机索引,然后每次随机 select 其中一个索引。例如,让字段 random
成为具有子字段 1 到 3 的映射:
{'random': {'1': 32456, '2':3904515723, '3': 766958445}}
现在您将随机查询 random.1、random.2、random.3,从而产生更大范围的随机性。这实质上是用增加的存储空间来节省必须重新播种的增加的计算(文档写入)。
写入重新播种
每次更新文档时,重新生成 random
字段的随机值。这将在随机索引中移动文档。
重新播种阅读
如果生成的随机值不是均匀分布的(它们是随机的,所以这是预期的),那么可能会在不适当的时间内选择同一个文档。这很容易通过在读取后用新的随机值更新随机 selected 文档来解决。
由于写入更昂贵并且会产生热点,您可以选择仅在读取部分时间时更新(例如,if random(0,100) === 0) update;
)。
我有一种方法可以在 Firebase Firestore 中随机获取列表文档,非常简单。当我在 Firestore 上上传数据时,我创建了一个字段名称 "position",其随机值从 1 到 1 百万。当我从 Fire 商店获取数据时,我将按字段 "Position" 设置排序并为其更新值,很多用户加载数据和数据总是更新,它将是随机值。
对于那些使用 Angular + Firestore 的人,基于@Dan McGrath 技术,这里是代码片段。
下面的代码片段 returns 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';
张贴此内容以帮助将来遇到此问题的任何人。
如果您使用的是自动 ID,则可以生成一个新的自动 ID 并查询最接近的自动 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)
如果没有找到文档,则以相反的操作再次调用它。
希望对您有所帮助!
刚刚在 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;
}
}
与 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);
}
})
好的,我会 post 回答这个问题,即使你这样做是为了 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())
}
}
}
}
其他解决方案更好,但我似乎很难理解,所以我想出了另一种方法
使用递增的数字作为ID,如1,2,3,4,5,6,7,8,9,注意删除文件否则我们
有一个丢失的 I'd
获取集合中的文档总数,类似这样,我不知道有比这更好的解决方案
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 的 built-in 查询
基于@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);
}
});
}
});
}
经过与朋友的激烈争论,终于找到了解决办法
如果您不需要将文档的 ID 设置为 RandomID,只需将文档命名为集合大小的大小即可。
例如,集合的第一个文档名为“0”。
第二个文档名称应为“1”。
然后,我们只要读取集合的大小,比如N,就可以得到[0~N]范围内的随机数A。
然后,我们可以查询名为A的文档。
这种方式可以为集合中的每个文档提供相同的随机概率。
毫无疑问,以上接受的答案非常有用,但有一种情况,如果我们收集了一些文档(大约 100-1000 个),我们想要一些 20-30 个随机文档,前提是文档不能重复。 (案例在随机问题应用等...)。
上述解决方案存在问题:
对于集合中的少量文档(比如 50),重复的可能性很高。为避免这种情况,如果我像这样存储 Fetched Docs Id 和加载项查询:
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue).where("__name__", isNotEqualTo:"PreviousId")
.order(by: "random")
.limit(to: 1)
这里 PreviousId 是所有被获取的元素的 Id Already 意味着 n 个先前 Id 的循环。
但是在这种情况下,网络调用会很高。
我的解决方案:
维护一个特殊文档并仅记录此集合的 ID,并第一次获取此文档,然后执行所有随机性操作并检查以前未在 App 站点上获取的文档。因此在这种情况下,网络调用将仅与所需的文档数相同 (n+1)。
我的解决方案的缺点:
得维护一个文件所以写上增删改查。但这很好,如果读取非常频繁,那么在大多数情况下会发生写入。
如果您使用的是 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 --------------------');
您可以使用 listDocuments()
属性 仅获取查询 文档 ID 列表。然后使用以下方式生成随机id并得到 DocumentSnapshot with 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
我的应用程序能够从 firebase 的集合中随机 select 多个文档是至关重要的。
由于 Firebase 没有内置的原生函数(据我所知)来实现执行此操作的查询,我的第一个想法是使用查询游标 select 随机开始和结束索引前提是我有集合中的文档数量。
这种方法可行,但效果有限,因为每次都会按相邻文档的顺序提供每个文档;但是,如果我能够通过其父集合中的索引 select 文档,我可以实现随机文档查询,但问题是我找不到任何描述如何执行此操作的文档,即使你可以做到这一点。
这是我希望能够执行的操作,请考虑以下 firestore 架构:
root/
posts/
docA
docB
docC
docD
然后在我的客户端(我在 Swift 环境中)我想编写一个可以执行此操作的查询:
db.collection("posts")[0, 1, 3] // would return: docA, docB, docD
无论如何我可以做一些类似的事情吗?或者,有没有其他方法可以 select 以类似方式随机生成文档?
请帮忙。
使用随机生成的索引和简单查询,您可以从 Cloud Firestore 中的集合或集合组中随机 select 文档。
这个答案分为 4 个部分,每个部分都有不同的选项:
- 如何生成随机索引
- 如何查询随机索引
- 选择多个随机文档
- 为持续的随机性重新播种
如何生成随机索引
这个答案的基础是创建一个索引字段,当按升序或降序排序时,会导致所有文档随机排序。有不同的方法来创建它,所以让我们看看 2,从最容易获得的开始。
自动识别版本
如果您使用我们的客户端库中提供的随机生成的自动 ID,您可以使用同一系统随机 select 文档。在这种情况下,随机排序的索引 是 文档 ID。
稍后在我们的查询部分,您生成的随机值是一个新的auto-id (iOS, Android, Web),您查询的字段是__name__
字段,而'low value'后面提到的是一个空字符串。这是迄今为止最简单的生成随机索引的方法,并且无论语言和平台如何。
默认情况下,文档名称 (__name__
) 仅按升序索引,除了删除和重新创建之外,您也无法重命名现有文档。如果您需要其中任何一个,您仍然可以使用此方法并将自动 ID 存储为名为 random
的实际字段,而不是为此目的重载文档名称。
随机整数版本
编写文档时,首先生成一个有界范围内的随机整数,并将其设置为名为random
的字段。根据您期望的文档数量,您可以使用不同的边界范围来保存 space 或降低冲突风险(这会降低此技术的有效性)。
您应该考虑您需要哪些语言,因为会有不同的考虑因素。虽然 Swift 很简单,但 JavaScript 值得注意的是有一个陷阱:
- 32 位整数:非常适合小型 (~10K unlikely to have a collision) 数据集
- 64 位整数:大型数据集(注意:JavaScript 本身不支持,yet)
这将创建一个索引,您的文档会随机排序。后面我们的查询部分,你生成的随机值会是另外一个这些值,后面提到的'low value'就是-1.
如何查询随机索引
现在您有了一个随机索引,您将要查询它。下面我们看看 select 1 个随机文档的一些简单变体,以及 select 多于 1 个的选项。
对于所有这些选项,您需要生成一个新的随机值,其形式与您在编写文档时创建的索引值的形式相同,由下面的变量 random
表示。我们将使用此值在索引上找到一个随机点。
环绕
现在您有了一个随机值,您可以查询单个文档:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
.order(by: "random")
.limit(to: 1)
检查这是否return编辑了文档。如果没有,请再次查询,但使用 'low value' 作为您的随机索引。例如,如果您执行随机整数,则 lowValue
是 0
:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
.order(by: "random")
.limit(to: 1)
只要您有一个文档,就可以保证您至少 return 有一个文档。
双向
环绕方法易于实施,并且允许您仅启用升序索引来优化存储。一个缺点是价值观可能会受到不公平的保护。例如,如果 10K 中的前 3 个文档 (A,B,C) 的随机索引值为 A:409496、B:436496、C:818992,则 A 和 C 的索引值刚好小于 1/10K被 selected 的机会,而 B 被 A 的接近有效地屏蔽并且只有大约 1/160K 的机会。
与其单向查询并在找不到值时回绕,不如在 >=
和 <=
之间随机 select,这样可以减少不公平的概率以双倍索引存储为代价将值屏蔽一半。
如果一个方向return没有结果,切换到另一个方向:
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)
选择多个随机文档
通常,您会希望一次 select 超过 1 个随机文档。有 2 种不同的方法可以根据您想要的权衡来调整上述技术。
冲洗并重复
这个方法很简单。只需重复该过程,包括每次 select 一个新的随机整数。
此方法将为您提供随机序列的文档,而无需担心重复看到相同的模式。
权衡是它会比下一个方法慢,因为它需要为每个文档单独往返服务。
继续加油
在这种方法中,只需将限制中的数量增加到所需的文档即可。它有点复杂,因为您可能会在通话中 return 0..limit
文档。然后您需要以相同的方式获取丢失的文件,但限制减少到只有差异。如果您知道总共有比您要求的数量更多的文档,您可以通过忽略在第二次调用(而不是第一次)时永远不会取回足够文档的边缘情况来进行优化。
此解决方案的权衡是重复序列。虽然文档是随机排序的,但如果您最终出现重叠范围,您会看到与之前看到的相同的模式。有一些方法可以减轻这种担忧,下一节将讨论重新播种。
这种方法比 'Rinse & Repeat' 更快,因为您将在最好的情况下一次调用或最坏的情况下调用 2 次来请求所有文档。
为持续的随机性重新播种
虽然如果文档集是静态的,此方法会随机为您提供文档,但每个文档被 returned 的概率也将是静态的。这是一个问题,因为根据它们获得的初始随机值,某些值可能具有不公平的低或高概率。在许多用例中,这很好,但在某些情况下,您可能希望增加长期随机性以获得更均匀的机会 returning 任何 1 个文档。
请注意,插入的文档最终会交织在一起,逐渐改变概率,删除文档也是如此。如果考虑到文档数量,insert/delete 比率太小,有一些策略可以解决这个问题。
多随机
不必担心重新播种,您始终可以为每个文档创建多个随机索引,然后每次随机 select 其中一个索引。例如,让字段 random
成为具有子字段 1 到 3 的映射:
{'random': {'1': 32456, '2':3904515723, '3': 766958445}}
现在您将随机查询 random.1、random.2、random.3,从而产生更大范围的随机性。这实质上是用增加的存储空间来节省必须重新播种的增加的计算(文档写入)。
写入重新播种
每次更新文档时,重新生成 random
字段的随机值。这将在随机索引中移动文档。
重新播种阅读
如果生成的随机值不是均匀分布的(它们是随机的,所以这是预期的),那么可能会在不适当的时间内选择同一个文档。这很容易通过在读取后用新的随机值更新随机 selected 文档来解决。
由于写入更昂贵并且会产生热点,您可以选择仅在读取部分时间时更新(例如,if random(0,100) === 0) update;
)。
我有一种方法可以在 Firebase Firestore 中随机获取列表文档,非常简单。当我在 Firestore 上上传数据时,我创建了一个字段名称 "position",其随机值从 1 到 1 百万。当我从 Fire 商店获取数据时,我将按字段 "Position" 设置排序并为其更新值,很多用户加载数据和数据总是更新,它将是随机值。
对于那些使用 Angular + Firestore 的人,基于@Dan McGrath 技术,这里是代码片段。
下面的代码片段 returns 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';
张贴此内容以帮助将来遇到此问题的任何人。
如果您使用的是自动 ID,则可以生成一个新的自动 ID 并查询最接近的自动 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)
如果没有找到文档,则以相反的操作再次调用它。
希望对您有所帮助!
刚刚在 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;
}
}
与 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);
}
})
好的,我会 post 回答这个问题,即使你这样做是为了 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())
}
}
}
}
其他解决方案更好,但我似乎很难理解,所以我想出了另一种方法
使用递增的数字作为ID,如1,2,3,4,5,6,7,8,9,注意删除文件否则我们 有一个丢失的 I'd
获取集合中的文档总数,类似这样,我不知道有比这更好的解决方案
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 的 built-in 查询
基于@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);
}
});
}
});
}
经过与朋友的激烈争论,终于找到了解决办法
如果您不需要将文档的 ID 设置为 RandomID,只需将文档命名为集合大小的大小即可。
例如,集合的第一个文档名为“0”。 第二个文档名称应为“1”。
然后,我们只要读取集合的大小,比如N,就可以得到[0~N]范围内的随机数A。
然后,我们可以查询名为A的文档。
这种方式可以为集合中的每个文档提供相同的随机概率。
毫无疑问,以上接受的答案非常有用,但有一种情况,如果我们收集了一些文档(大约 100-1000 个),我们想要一些 20-30 个随机文档,前提是文档不能重复。 (案例在随机问题应用等...)。
上述解决方案存在问题: 对于集合中的少量文档(比如 50),重复的可能性很高。为避免这种情况,如果我像这样存储 Fetched Docs Id 和加载项查询:
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue).where("__name__", isNotEqualTo:"PreviousId")
.order(by: "random")
.limit(to: 1)
这里 PreviousId 是所有被获取的元素的 Id Already 意味着 n 个先前 Id 的循环。 但是在这种情况下,网络调用会很高。
我的解决方案: 维护一个特殊文档并仅记录此集合的 ID,并第一次获取此文档,然后执行所有随机性操作并检查以前未在 App 站点上获取的文档。因此在这种情况下,网络调用将仅与所需的文档数相同 (n+1)。
我的解决方案的缺点: 得维护一个文件所以写上增删改查。但这很好,如果读取非常频繁,那么在大多数情况下会发生写入。
如果您使用的是 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 --------------------');
您可以使用 listDocuments()
属性 仅获取查询 文档 ID 列表。然后使用以下方式生成随机id并得到 DocumentSnapshot with 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