Search code examples
javascriptfirebasegoogle-cloud-firestoreangularfire

Firestore - How to get multiple random (non duplicated, specific number of) documents from a collection with certainty?


Using Dan McGrath's solution from https://stackoverflow.com/a/46801925/3073280 (to generate and query random indexes), I can certainly and definitely get one random document whenever the collection has one or more documents. Put another way, getting one random document is easy.

But, I have difficulty in implementing Dan McGrath's solution to get multiple random (non-duplicated) documents from a collection with certainty. I am referring to Dan McGrath’s rinse and repeat solution.

So, with reference to my pseudo code below, how to get exactly 3 documents* (non duplicated) at random when the collection has 5 documents?

import { Component, OnInit } from '@angular/core';

import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { throwError, interval, of }  from 'rxjs';
import { retry, switchMap, mergeMap, take } from 'rxjs/operators';


@Component({
  selector: 'app-firestore',
  templateUrl: './firestore.component.html',
  styleUrls: ['./firestore.component.css']
})
export class FirestoreComponent implements OnInit {

  constructor(private afs: AngularFirestore) {

  } 

  // This is the sample data structure in Firestore with only 5 documents in the collection
  // -Collection name is testRandom
  // --Document Auto ID
  // --- { random: 1 }
  // --Document Auto ID
  // --- { random: 2 }
  // --Document Auto ID
  // --- { random: 4, remark: 'intentionally skip 3' }
  // --Document Auto ID
  // --- { random: 5 }
  // --Document Auto ID
  // --- { random: 7, remark: 'intentionally skip 6' }

  getRandomIntInclusive(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1) + min); //The maximum is inclusive and the minimum is inclusive
  }

  /*
  * This will return one document at random but this will not guarantee return if the random number is 3 or 6
  * */
  selectSingleRandomDocument_byQueryingRandomIndexes() {
    return this.afs.collection<Item>('testRandom',
        ref => ref.where('random', '==', this.getRandomIntInclusive(1,7)).limit(1)
    )
      .valueChanges()
      .pipe(
        mergeMap(doc => {
          if (doc.length > 0) {
            return of(doc);
          } else {
            return throwError('no random document queried because the random number is either 3 or 6, hence throw error so it will retry')
          }
        }),
      )
  }

  /*
  * This will query for 3 documents but it will not guarantee return 3 documents if error thrown
  * */
  rinseAndRepeat() {
    interval(2000)
      .pipe(
        take(3),
        switchMap(val => this.selectSingleRandomDocument_byQueryingRandomIndexes()
            .pipe(retry(5)) // return 5 times if get throwError from singleRandomDocument
        )
      )
      .subscribe();
  }

}

In brief, how to get exactly 3 documents* (non duplicated) at random when the collection has 5 documents?

*Note: In production, it would query for 80 documents at random out of thousands of documents from a collection; hence, please do not suggest reading the whole collection and shuffle the documents randomly then read the top 80 documents.


Solution

  • It seems like it would be as simple as checking the random ints to ensure they are unique before querying. There are probably more elegant ways to achieve this in react, but this seems like it would work.

    getSetOfRandomInts(num) {
       const set = new Set();
       while(set.size < num) { // retry if we get dups
         set.add(this.getRandomIntInclusive(1,7));
       }
       return set;
    }
    
    selectSingleDocumentByRandomIndex(randomNumber) {
        return this.afs.collection<Item>('testRandom',
            ref => ref.where('random', '==', randomNumber).limit(1)
        )
          .valueChanges()
          .pipe(
            mergeMap(doc => {
              if (doc.length > 0) {
                return of(doc);
              } else {
                return throwError('no random document queried because the random number is either 3 or 6, hence throw error so it will retry')
              }
            }),
          )
      }
    
    rinseAndRepeat() {
        interval(2000)
          .pipe(
            () => this.getSetOfRandomInts(3),
            switchMap(randomInt => this.selectSingleDocumentByRandomIndex(randomInt)
                .pipe(retry(5))
            )
          )
          .subscribe();
      }