Flutter: Reading and merging firestore data by date - flutter

I am looking for a way to merge the data I'm reading from Cloud Firestore by date. My aim is to read all the documents from Cloud Firestore, then place all documents of a certain date under ONE ExpansionTile. i.e. One ExpansionTile per date, and each ExpansionTile may contain many individual documents depending on how much was uploaded that day.
I'm having no issues reading the data from Cloud Firestore, but I am struggling to figure out how to organise and merge the documents by date once I have the information at hand. Keeping in mind that there will be multiple different dates and all with different amounts of documents per date.
As seen in the picture below, I currently have three documents with the same date, and therefore three different ExpansionTiles when creating them in the app. I am looking to convert these three into one ExpansionTile
I am currently reading the data from Cloud Firestore by date using the flutter_bloc package:
//Retrieves firebase data of all orders done in the past
Stream<OrderHistoryState> _loadOrderHistory() async* {
try {
_orderSubscription?.cancel();
var orderHistory = Firestore.instance
.collection('orders')
.orderBy('date', descending: true);
_orderSubscription = orderHistory.snapshots().listen(
(event) {
if (event.documents.length > 0) {
add(OrderHistoryUpdate(
documents: event.documents,
));
} else {
print("No Order to Load");
add(NoOrderHistory());
}
},
);
} catch (e) {
print("Error with Tracking Status, $e");
add(ErrorOrderHistory());
}
}

For those of you who may be curious, this was my solution to the problem. It may be a bit long winded, so any suggestions to shorten/improve it are still greatly appreciated:
//Make sure that there are documents within firebase
if (state.orderHistoryDocuments.length != 0) {
//Adding the first document manually so that
//the forloop section has something to compare dates
//This listForEachDate is for storing a collection
//of all documents of one date
listForEachDate = [state.orderHistoryDocuments[0].data];
//listOfDateLists is a list of all listForEachDate
//This gives us a list of lists with the documents
//separated by date i.e.
//[[date1, date1], [date2], [date3, date3, date3], etc]
listOfDateLists = [];
//i = 1 because index 0 already added above
for (int i = 1;
i < state.orderHistoryDocuments.length;
i++) {
//If the current index's date matches that of the previous
//index's date, then add it to the listForEachDate
if (state.orderHistoryDocuments[i]
.data["dateCreated"] ==
state.orderHistoryDocuments[i - 1]
.data["dateCreated"]) {
listForEachDate
.add(state.orderHistoryDocuments[i].data);
//If [index]date does not match [index - 1]date
//Then add the current listForEachDate to the
//listOfDateLists i.e. add sublist to list of lists
} else {
listOfDateLists.add(listForEachDate);
//Clear the listForEachDate so that we can create
//a new clean list of new dates
listForEachDate = [];
//Add the new date to the listForEachDate
//so that the process can start again
listForEachDate
.add(state.orderHistoryDocuments[i].data);
}
}
//Once the document has been iterated through,
//Add to the big list.
listOfDateLists.add(listForEachDate);
}

Related

add Filter Search in Streambuilder - flutter

I want to get documents from Firestore with the conditions. There are 5 conditions where 4 of which are optional. If I use where with streamBuilder, I have to create many indexes.
So, I'm going to try this method, I want to get your opinion about this.
Create a list for the store DocumentSnapshot
List<DocumentSnapshot> documents = [];
Store the snapshot to List
documents = streamSnapshot.data.docs;
Filter data like this:
if (searchText.length > 0) {
documents = documents.where((element) {
return element
.get('Title')
.toString()
.toLowerCase()
.contains(searchText.toLowerCase());
}).toList();
}

Flutter query random snapshot from firebase [duplicate]

It is crucial for my application to be able to select multiple documents at random from a collection in firebase.
Since there is no native function built in to Firebase (that I know of) to achieve a query that does just this, my first thought was to use query cursors to select a random start and end index provided that I have the number of documents in the collection.
This approach would work but only in a limited fashion since every document would be served up in sequence with its neighboring documents every time; however, if I was able to select a document by its index in its parent collection I could achieve a random document query but the problem is I can't find any documentation that describes how you can do this or even if you can do this.
Here's what I'd like to be able to do, consider the following firestore schema:
root/
posts/
docA
docB
docC
docD
Then in my client (I'm in a Swift environment) I'd like to write a query that can do this:
db.collection("posts")[0, 1, 3] // would return: docA, docB, docD
Is there anyway I can do something along the lines of this? Or, is there a different way I can select random documents in a similar fashion?
Please help.
Using randomly generated indexes and simple queries, you can randomly select documents from a collection or collection group in Cloud Firestore.
This answer is broken into 4 sections with different options in each section:
How to generate the random indexes
How to query the random indexes
Selecting multiple random documents
Reseeding for ongoing randomness
How to generate the random indexes
The basis of this answer is creating an indexed field that when ordered ascending or descending, results in all the document being randomly ordered. There are different ways to create this, so let's look at 2, starting with the most readily available.
Auto-Id version
If you are using the randomly generated automatic ids provided in our client libraries, you can use this same system to randomly select a document. In this case, the randomly ordered index is the document id.
Later in our query section, the random value you generate is a new auto-id (iOS, Android, Web) and the field you query is the __name__ field, and the 'low value' mentioned later is an empty string. This is by far the easiest method to generate the random index and works regardless of the language and platform.
By default, the document name (__name__) is only indexed ascending, and you also cannot rename an existing document short of deleting and recreating. If you need either of these, you can still use this method and just store an auto-id as an actual field called random rather than overloading the document name for this purpose.
Random Integer version
When you write a document, first generate a random integer in a bounded range and set it as a field called random. Depending on the number of documents you expect, you can use a different bounded range to save space or reduce the risk of collisions (which reduce the effectiveness of this technique).
You should consider which languages you need as there will be different considerations. While Swift is easy, JavaScript notably can have a gotcha:
32-bit integer: Great for small (~10K unlikely to have a collision) datasets
64-bit integer: Large datasets (note: JavaScript doesn't natively support, yet)
This will create an index with your documents randomly sorted. Later in our query section, the random value you generate will be another one of these values, and the 'low value' mentioned later will be -1.
How to query the random indexes
Now that you have a random index, you'll want to query it. Below we look at some simple variants to select a 1 random document, as well as options to select more than 1.
For all these options, you'll want to generate a new random value in the same form as the indexed values you created when writing the document, denoted by the variable random below. We'll use this value to find a random spot on the index.
Wrap-around
Now that you have a random value, you can query for a single document:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
.order(by: "random")
.limit(to: 1)
Check that this has returned a document. If it doesn't, query again but use the 'low value' for your random index. For example, if you did Random Integers then lowValue is 0:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
.order(by: "random")
.limit(to: 1)
As long as you have a single document, you'll be guaranteed to return at least 1 document.
Bi-directional
The wrap-around method is simple to implement and allows you to optimize storage with only an ascending index enabled. One downside is the possibility of values being unfairly shielded. E.g if the first 3 documents (A,B,C) out of 10K have random index values of A:409496, B:436496, C:818992, then A and C have just less than 1/10K chance of being selected, whereas B is effectively shielded by the proximity of A and only roughly a 1/160K chance.
Rather than querying in a single direction and wrapping around if a value is not found, you can instead randomly select between >= and <=, which reduces the probability of unfairly shielded values by half, at the cost of double the index storage.
If one direction returns no results, switch to the other direction:
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
Often, you'll want to select more than 1 random document at a time. There are 2 different ways to adjust the above techniques depending on what trade offs you want.
Rinse & Repeat
This method is straight forward. Simply repeat the process, including selecting a new random integer each time.
This method will give you random sequences of documents without worrying about seeing the same patterns repeatedly.
The trade-off is it will be slower than the next method since it requires a separate round trip to the service for each document.
Keep it coming
In this approach, simply increase the number in the limit to the desired documents. It's a little more complex as you might return 0..limit documents in the call. You'll then need to get the missing documents in the same manner, but with the limit reduced to only the difference. If you know there are more documents in total than the number you are asking for, you can optimize by ignoring the edge case of never getting back enough documents on the second call (but not the first).
The trade-off with this solution is in repeated sequences. While the documents are randomly ordered, if you ever end up overlapping ranges you'll see the same pattern you saw before. There are ways to mitigate this concern discussed in the next section on reseeding.
This approach is faster than 'Rinse & Repeat' as you'll be requesting all the documents in the best case a single call or worst case 2 calls.
Reseeding for ongoing randomness
While this method gives you documents randomly if the document set is static the probability of each document being returned will be static as well. This is a problem as some values might have unfairly low or high probabilities based on the initial random values they got. In many use cases, this is fine but in some, you may want to increase the long term randomness to have a more uniform chance of returning any 1 document.
Note that inserted documents will end up weaved in-between, gradually changing the probabilities, as will deleting documents. If the insert/delete rate is too small given the number of documents, there are a few strategies addressing this.
Multi-Random
Rather than worrying out reseeding, you can always create multiple random indexes per document, then randomly select one of those indexes each time. For example, have the field random be a map with subfields 1 to 3:
{'random': {'1': 32456, '2':3904515723, '3': 766958445}}
Now you'll be querying against random.1, random.2, random.3 randomly, creating a greater spread of randomness. This essentially trades increased storage to save increased compute (document writes) of having to reseed.
Reseed on writes
Any time you update a document, re-generate the random value(s) of the random field. This will move the document around in the random index.
Reseed on reads
If the random values generated are not uniformly distributed (they're random, so this is expected), then the same document might be picked a dispropriate amount of the time. This is easily counteracted by updating the randomly selected document with new random values after it is read.
Since writes are more expensive and can hotspot, you can elect to only update on read a subset of the time (e.g, if random(0,100) === 0) update;).
Posting this to help anyone that has this problem in the future.
If you are using Auto IDs you can generate a new Auto ID and query for the closest Auto ID as mentioned in Dan McGrath's Answer.
I recently created a random quote api and needed to get random quotes from a firestore collection.
This is how I solved that problem:
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);
});
The key to the query is this:
.where(admin.firestore.FieldPath.documentId(), '>', key)
And calling it again with the operation reversed if no documents are found.
I hope this helps!
Just made this work in Angular 7 + RxJS, so sharing here with people who want an example.
I used #Dan McGrath 's answer, and I chose these options: Random Integer version + Rinse & Repeat for multiple numbers. I also used the stuff explained in this article: RxJS, where is the If-Else Operator? to make if/else statements on stream level (just if any of you need a primer on that).
Also note I used angularfire2 for easy Firebase integration in Angular.
Here is the code:
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;
}
}
The other solutions are better but seems hard for me to understand, so I came up with another method
Use incremental number as ID like 1,2,3,4,5,6,7,8,9, watch out for delete documents else we
have an I'd that is missing
Get total number of documents in the collection, something like this, I don't know of a better solution than this
let totalDoc = db.collection("stat").get().then(snap=>snap.size)
Now that we have these, create an empty array to store random list of number, let's say we want 20 random documents.
let randomID = [ ]
while(randomID.length < 20) {
const randNo = Math.floor(Math.random() * totalDoc) + 1;
if(randomID.indexOf(randNo) === -1) randomID.push(randNo);
}
now we have our 20 random documents id
finally we fetch our data from fire store, and save to randomDocs array by mapping through the randomID array
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);
});
})
I'm new to firebase, but I think with this answers we can get something better or a built-in query from firebase soon
After intense argument with my friend, we finally found some solution
If you don't need to set document's id to be RandomID, just name documents as size of collection's size.
For example, first document of collection is named '0'.
second document name should be '1'.
Then, we just read the size of collection, for example N, and we can get random number A in range of [0~N).
And then, we can query the document named A.
This way can give same probability of randomness to every documents in collection.
undoubtedly Above accepted Answer is SuperUseful but There is one case like If we had a collection of some Documents(about 100-1000) and we want some 20-30 random Documents Provided that Document must not be repeated. (case In Random Problems App etc...).
Problem with the Above Solution:
For a small number of documents in the Collection(say 50) Probability of repetition is high. To avoid it If I store Fetched Docs Id and Add-in Query like this:
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue).where("__name__", isNotEqualTo:"PreviousId")
.order(by: "random")
.limit(to: 1)
here PreviousId is Id of all Elements that were fetched Already means A loop of n previous Ids.
But in this case, network Call would be high.
My Solution:
Maintain one Special Document and Keep a Record of Ids of this Collection only, and fetched this document First Time and Then Do all Randomness Stuff and check for previously not fetched on App site. So in this case network call would be only the same as the number of documents requires (n+1).
Disadvantage of My solution:
Have to maintain A document so Write on Addition and Deletion. But it is good If reads are very often then Writes which occurs in most cases.
You can use listDocuments() property for get only Query list of documents id. Then generate random id using the following way and get DocumentSnapshot with get() property.
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
Unlike rtdb, firestore ids are not ordered chronologically. So using Auto-Id version described by Dan McGrath is easily implemented if you use the auto-generated id by the firestore client.
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);
}
})
I have one way to get random a list document in Firebase Firestore, it really easy. When i upload data on Firestore i creat a field name "position" with random value from 1 to 1 milions. When i get data from Fire store i will set Order by field "Position" and update value for it, a lot of user load data and data always update and it's will be random value.
For those using Angular + Firestore, building on #Dan McGrath techniques, here is the code snippet.
Below code snippet returns 1 document.
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() is a rxjs operation for recursion to ensure we definitely get a document from the random selection.
2) For recursion to work as expected we need to have 2 separate functions.
3) We use EMPTY to terminate .expand() operator.
import { Observable, EMPTY } from 'rxjs';
Ok I will post answer to this question even thou I am doing this for Android. Whenever i create a new document i initiate random number and set it to random field, so my document looks like
"field1" : "value1"
"field2" : "value2"
...
"random" : 13442 //this is the random number i generated upon creating document
When I query for random document I generate random number in same range that I used when 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())
}
}
}
}
Based on #ajzbc answer I wrote this for Unity3D and its working for me.
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);
}
});
}
});
}
If you are using autoID this may also work for you...
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 --------------------');
Easy (2022). You need something like:
export const getAtRandom = async (me) => {
const collection = admin.firestore().collection('...').where(...);
const { count } = (await collection.count().get()).data();
const numberAtRandom = Math.floor(Math.random() * count);
const snap = await accountCollection.limit(1).offset(numberAtRandom).get()
if (accountSnap.empty) return null;
const doc = { id: snap.docs[0].id, ...snap.docs[0].data(), ref: snap.docs[0].ref };
return doc;
}
The next code (Flutter) will return one or up to ten random documents from a Firebase collection.
None of the documents will be repeated
Max 10 documents can be retrieved
If you pass a greater numberOfDocuments than existing documents in the collection, the loop will never end.
Future<Iterable<QueryDocumentSnapshot>> getRandomDocuments(int numberOfDocuments) async {
// Queried documents
final docs = <QueryDocumentSnapshot>[];
// Queried documents id's. We will use later to avoid querying same documents
final currentIds = <String>[];
do {
// Generate random id explained by #Dan McGrath's answer (autoId)
final randomId = FirebaseFirestore.instance.collection('random').doc().id;
var query = FirebaseFirestore.instance
.collection('myCollection') // Change this for you collection name
.where(FieldPath.documentId, isGreaterThanOrEqualTo: randomId)
.limit(1);
if (currentIds.isNotEmpty) {
// If previously we fetched a document we avoid fetching the same
query = query.where(FieldPath.documentId, whereNotIn: currentIds);
}
final querySnap = await query.get();
for (var element in querySnap.docs) {
currentIds.add(element.id);
docs.add(element);
}
} while (docs.length < numberOfDocuments); // <- Run until we have all documents we want
return docs;
}

Iterate through all documents and sum a field - Firebase

I have iterated through all documents of specific collection and made the sum of specific property/field to get the total amount, I able to did it but the problem is that this method is not effective in terms of performance and cost performance because firebase charges according to the usage (read/write document)
So what could be the better approach to do it?
here is my code
basically there is an sub collection called 'userEntries' and it has lots of document and each document has a field called 'amount'
double totalAmount = 0;
void getTotalAmount() async {
QuerySnapshot customerEntries = await FirebaseFirestore.instance.collection('entries').doc(widget.firebaseUser!.uid).collection('userEntries').where('customerId', isEqualTo: widget.customerMap['customerId']).get();
double total = 0;
for (var element in customerEntries.docs) {
total += element['amount'];
}
setState(() {
totalAmount = total;
});
}
What I can think of is maintain a separate sub collection where I would write/update totalAmount field whenever a user would make a new entry, is it right way to do it?
What I can think of is maintain a separate sub collection where I
would write/update totalAmount field whenever a user would make a new
entry, is it right way to do it?
Yes, this is the right way to do that. There is a specific page in the Firestore documentation which explains how to do that with several documents in order to solve the maximum limit of one write to a document per second.
The code examples in this page do not show the Dart version but it is easy to implement it based on the JS example.

How can I get the document from Firebase Firestore with particular value in field?

I'm creating todo list in which I'll have two lists one for incomplete tasks and another for completed tasks...I'm able to retrieve the data from Firestore by pulling the whole data without any condition, but for the uncompleted list, I want to retrieve the document only with particular field values. For example, each document contains the following fields as shown in the picture:
Here is the code which I've used to get the data from firebase firestore:
static List<Task> getTasks() {
//convert firebase collection to list
List<Task> tasks = [];
FirebaseFirestore.instance.collection('Todos').snapshots().listen((data) {
data.docs.forEach((doc) {
tasks.add(Task.fromMap(doc.data()));
});
});
return tasks;
}
I want similar function like this to retrieve only documents with taskStatus == true. So, I can store all those tasks which have taskStatus of true on separate list and display them accordingly.
You can use the where function. It works like below.
For more: Cloud FireStore Filtering
FirebaseFirestore.instance
.collection('Todos')
.where('taskStatus', isEqualTo: true)
.snapshots().listen((data) {
data.docs.forEach((doc) {
other_tasks.add(Task.fromMap(doc.data()));
});
});

Firestore: How to get random documents in a collection

It is crucial for my application to be able to select multiple documents at random from a collection in firebase.
Since there is no native function built in to Firebase (that I know of) to achieve a query that does just this, my first thought was to use query cursors to select a random start and end index provided that I have the number of documents in the collection.
This approach would work but only in a limited fashion since every document would be served up in sequence with its neighboring documents every time; however, if I was able to select a document by its index in its parent collection I could achieve a random document query but the problem is I can't find any documentation that describes how you can do this or even if you can do this.
Here's what I'd like to be able to do, consider the following firestore schema:
root/
posts/
docA
docB
docC
docD
Then in my client (I'm in a Swift environment) I'd like to write a query that can do this:
db.collection("posts")[0, 1, 3] // would return: docA, docB, docD
Is there anyway I can do something along the lines of this? Or, is there a different way I can select random documents in a similar fashion?
Please help.
Using randomly generated indexes and simple queries, you can randomly select documents from a collection or collection group in Cloud Firestore.
This answer is broken into 4 sections with different options in each section:
How to generate the random indexes
How to query the random indexes
Selecting multiple random documents
Reseeding for ongoing randomness
How to generate the random indexes
The basis of this answer is creating an indexed field that when ordered ascending or descending, results in all the document being randomly ordered. There are different ways to create this, so let's look at 2, starting with the most readily available.
Auto-Id version
If you are using the randomly generated automatic ids provided in our client libraries, you can use this same system to randomly select a document. In this case, the randomly ordered index is the document id.
Later in our query section, the random value you generate is a new auto-id (iOS, Android, Web) and the field you query is the __name__ field, and the 'low value' mentioned later is an empty string. This is by far the easiest method to generate the random index and works regardless of the language and platform.
By default, the document name (__name__) is only indexed ascending, and you also cannot rename an existing document short of deleting and recreating. If you need either of these, you can still use this method and just store an auto-id as an actual field called random rather than overloading the document name for this purpose.
Random Integer version
When you write a document, first generate a random integer in a bounded range and set it as a field called random. Depending on the number of documents you expect, you can use a different bounded range to save space or reduce the risk of collisions (which reduce the effectiveness of this technique).
You should consider which languages you need as there will be different considerations. While Swift is easy, JavaScript notably can have a gotcha:
32-bit integer: Great for small (~10K unlikely to have a collision) datasets
64-bit integer: Large datasets (note: JavaScript doesn't natively support, yet)
This will create an index with your documents randomly sorted. Later in our query section, the random value you generate will be another one of these values, and the 'low value' mentioned later will be -1.
How to query the random indexes
Now that you have a random index, you'll want to query it. Below we look at some simple variants to select a 1 random document, as well as options to select more than 1.
For all these options, you'll want to generate a new random value in the same form as the indexed values you created when writing the document, denoted by the variable random below. We'll use this value to find a random spot on the index.
Wrap-around
Now that you have a random value, you can query for a single document:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: random)
.order(by: "random")
.limit(to: 1)
Check that this has returned a document. If it doesn't, query again but use the 'low value' for your random index. For example, if you did Random Integers then lowValue is 0:
let postsRef = db.collection("posts")
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue)
.order(by: "random")
.limit(to: 1)
As long as you have a single document, you'll be guaranteed to return at least 1 document.
Bi-directional
The wrap-around method is simple to implement and allows you to optimize storage with only an ascending index enabled. One downside is the possibility of values being unfairly shielded. E.g if the first 3 documents (A,B,C) out of 10K have random index values of A:409496, B:436496, C:818992, then A and C have just less than 1/10K chance of being selected, whereas B is effectively shielded by the proximity of A and only roughly a 1/160K chance.
Rather than querying in a single direction and wrapping around if a value is not found, you can instead randomly select between >= and <=, which reduces the probability of unfairly shielded values by half, at the cost of double the index storage.
If one direction returns no results, switch to the other direction:
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
Often, you'll want to select more than 1 random document at a time. There are 2 different ways to adjust the above techniques depending on what trade offs you want.
Rinse & Repeat
This method is straight forward. Simply repeat the process, including selecting a new random integer each time.
This method will give you random sequences of documents without worrying about seeing the same patterns repeatedly.
The trade-off is it will be slower than the next method since it requires a separate round trip to the service for each document.
Keep it coming
In this approach, simply increase the number in the limit to the desired documents. It's a little more complex as you might return 0..limit documents in the call. You'll then need to get the missing documents in the same manner, but with the limit reduced to only the difference. If you know there are more documents in total than the number you are asking for, you can optimize by ignoring the edge case of never getting back enough documents on the second call (but not the first).
The trade-off with this solution is in repeated sequences. While the documents are randomly ordered, if you ever end up overlapping ranges you'll see the same pattern you saw before. There are ways to mitigate this concern discussed in the next section on reseeding.
This approach is faster than 'Rinse & Repeat' as you'll be requesting all the documents in the best case a single call or worst case 2 calls.
Reseeding for ongoing randomness
While this method gives you documents randomly if the document set is static the probability of each document being returned will be static as well. This is a problem as some values might have unfairly low or high probabilities based on the initial random values they got. In many use cases, this is fine but in some, you may want to increase the long term randomness to have a more uniform chance of returning any 1 document.
Note that inserted documents will end up weaved in-between, gradually changing the probabilities, as will deleting documents. If the insert/delete rate is too small given the number of documents, there are a few strategies addressing this.
Multi-Random
Rather than worrying out reseeding, you can always create multiple random indexes per document, then randomly select one of those indexes each time. For example, have the field random be a map with subfields 1 to 3:
{'random': {'1': 32456, '2':3904515723, '3': 766958445}}
Now you'll be querying against random.1, random.2, random.3 randomly, creating a greater spread of randomness. This essentially trades increased storage to save increased compute (document writes) of having to reseed.
Reseed on writes
Any time you update a document, re-generate the random value(s) of the random field. This will move the document around in the random index.
Reseed on reads
If the random values generated are not uniformly distributed (they're random, so this is expected), then the same document might be picked a dispropriate amount of the time. This is easily counteracted by updating the randomly selected document with new random values after it is read.
Since writes are more expensive and can hotspot, you can elect to only update on read a subset of the time (e.g, if random(0,100) === 0) update;).
Posting this to help anyone that has this problem in the future.
If you are using Auto IDs you can generate a new Auto ID and query for the closest Auto ID as mentioned in Dan McGrath's Answer.
I recently created a random quote api and needed to get random quotes from a firestore collection.
This is how I solved that problem:
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);
});
The key to the query is this:
.where(admin.firestore.FieldPath.documentId(), '>', key)
And calling it again with the operation reversed if no documents are found.
I hope this helps!
Just made this work in Angular 7 + RxJS, so sharing here with people who want an example.
I used #Dan McGrath 's answer, and I chose these options: Random Integer version + Rinse & Repeat for multiple numbers. I also used the stuff explained in this article: RxJS, where is the If-Else Operator? to make if/else statements on stream level (just if any of you need a primer on that).
Also note I used angularfire2 for easy Firebase integration in Angular.
Here is the code:
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;
}
}
The other solutions are better but seems hard for me to understand, so I came up with another method
Use incremental number as ID like 1,2,3,4,5,6,7,8,9, watch out for delete documents else we
have an I'd that is missing
Get total number of documents in the collection, something like this, I don't know of a better solution than this
let totalDoc = db.collection("stat").get().then(snap=>snap.size)
Now that we have these, create an empty array to store random list of number, let's say we want 20 random documents.
let randomID = [ ]
while(randomID.length < 20) {
const randNo = Math.floor(Math.random() * totalDoc) + 1;
if(randomID.indexOf(randNo) === -1) randomID.push(randNo);
}
now we have our 20 random documents id
finally we fetch our data from fire store, and save to randomDocs array by mapping through the randomID array
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);
});
})
I'm new to firebase, but I think with this answers we can get something better or a built-in query from firebase soon
After intense argument with my friend, we finally found some solution
If you don't need to set document's id to be RandomID, just name documents as size of collection's size.
For example, first document of collection is named '0'.
second document name should be '1'.
Then, we just read the size of collection, for example N, and we can get random number A in range of [0~N).
And then, we can query the document named A.
This way can give same probability of randomness to every documents in collection.
undoubtedly Above accepted Answer is SuperUseful but There is one case like If we had a collection of some Documents(about 100-1000) and we want some 20-30 random Documents Provided that Document must not be repeated. (case In Random Problems App etc...).
Problem with the Above Solution:
For a small number of documents in the Collection(say 50) Probability of repetition is high. To avoid it If I store Fetched Docs Id and Add-in Query like this:
queryRef = postsRef.whereField("random", isGreaterThanOrEqualTo: lowValue).where("__name__", isNotEqualTo:"PreviousId")
.order(by: "random")
.limit(to: 1)
here PreviousId is Id of all Elements that were fetched Already means A loop of n previous Ids.
But in this case, network Call would be high.
My Solution:
Maintain one Special Document and Keep a Record of Ids of this Collection only, and fetched this document First Time and Then Do all Randomness Stuff and check for previously not fetched on App site. So in this case network call would be only the same as the number of documents requires (n+1).
Disadvantage of My solution:
Have to maintain A document so Write on Addition and Deletion. But it is good If reads are very often then Writes which occurs in most cases.
You can use listDocuments() property for get only Query list of documents id. Then generate random id using the following way and get DocumentSnapshot with get() property.
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
Unlike rtdb, firestore ids are not ordered chronologically. So using Auto-Id version described by Dan McGrath is easily implemented if you use the auto-generated id by the firestore client.
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);
}
})
I have one way to get random a list document in Firebase Firestore, it really easy. When i upload data on Firestore i creat a field name "position" with random value from 1 to 1 milions. When i get data from Fire store i will set Order by field "Position" and update value for it, a lot of user load data and data always update and it's will be random value.
For those using Angular + Firestore, building on #Dan McGrath techniques, here is the code snippet.
Below code snippet returns 1 document.
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() is a rxjs operation for recursion to ensure we definitely get a document from the random selection.
2) For recursion to work as expected we need to have 2 separate functions.
3) We use EMPTY to terminate .expand() operator.
import { Observable, EMPTY } from 'rxjs';
Ok I will post answer to this question even thou I am doing this for Android. Whenever i create a new document i initiate random number and set it to random field, so my document looks like
"field1" : "value1"
"field2" : "value2"
...
"random" : 13442 //this is the random number i generated upon creating document
When I query for random document I generate random number in same range that I used when 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())
}
}
}
}
Based on #ajzbc answer I wrote this for Unity3D and its working for me.
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);
}
});
}
});
}
If you are using autoID this may also work for you...
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 --------------------');
Easy (2022). You need something like:
export const getAtRandom = async (me) => {
const collection = admin.firestore().collection('...').where(...);
const { count } = (await collection.count().get()).data();
const numberAtRandom = Math.floor(Math.random() * count);
const snap = await accountCollection.limit(1).offset(numberAtRandom).get()
if (accountSnap.empty) return null;
const doc = { id: snap.docs[0].id, ...snap.docs[0].data(), ref: snap.docs[0].ref };
return doc;
}
The next code (Flutter) will return one or up to ten random documents from a Firebase collection.
None of the documents will be repeated
Max 10 documents can be retrieved
If you pass a greater numberOfDocuments than existing documents in the collection, the loop will never end.
Future<Iterable<QueryDocumentSnapshot>> getRandomDocuments(int numberOfDocuments) async {
// Queried documents
final docs = <QueryDocumentSnapshot>[];
// Queried documents id's. We will use later to avoid querying same documents
final currentIds = <String>[];
do {
// Generate random id explained by #Dan McGrath's answer (autoId)
final randomId = FirebaseFirestore.instance.collection('random').doc().id;
var query = FirebaseFirestore.instance
.collection('myCollection') // Change this for you collection name
.where(FieldPath.documentId, isGreaterThanOrEqualTo: randomId)
.limit(1);
if (currentIds.isNotEmpty) {
// If previously we fetched a document we avoid fetching the same
query = query.where(FieldPath.documentId, whereNotIn: currentIds);
}
final querySnap = await query.get();
for (var element in querySnap.docs) {
currentIds.add(element.id);
docs.add(element);
}
} while (docs.length < numberOfDocuments); // <- Run until we have all documents we want
return docs;
}