Hareesh
Hareesh

Reputation: 6900

How to get random data with angularfire2 valuechanges method

I have questions$ observable which returns more than 10 question objects from Firestore.

this.questions$ = this.moduleCollection$.doc(module.id)
  .collection('questions').valueChanges();

Now i want to limit the result with 10 questions randomly.

I can limit the query like this

this.questions$ = this.moduleCollection$.doc(module.id)
  .collection('questions',ref => ref.limit(10)).valueChanges();

But i don't know how to get it randomly, is there any Rxjs operators do so?

What i have tried (Extending @Richard Matsen's answer)

const sampleSize = 2
const randomIndex = (array) => Math.floor(Math.random() * array.length)
const addIndexes = (array) => array.map((item, index) => {
    item['id'] = index  
    return item
  })
const removeIndexes = (array) => array.map(item => {
   delete item.id 
   return item
 })

this.questions$ = 
this.moduleCollection$.doc(module.id).collection('questions').valueChanges()
    .map(addIndexes)
    .map(r => r[randomIndex(r)])
    .repeat()
    .scan((a, c) => a.map(a => a.id).indexOf(c.id) === -1 ? a.concat(c) : a, [])
    .skipWhile(array => array.length < sampleSize)
    .take(1)
    .map(array => array.sort((a, b) => a.id - b.id))
    .map(removeIndexes) 

Upvotes: 1

Views: 421

Answers (1)

Richard Matsen
Richard Matsen

Reputation: 23503

This sample demos a pure rxjs way to take a random sample from an array, with no duplicates.

const source = Rx.Observable.of([{val: 'a'}, {val: 'b'}, {val: 'c'}, {val: 'd'}, {val: 'e'}])

const sampleSize = 2
const randomIndex = (array) => Math.floor(Math.random() * array.length)
const addIndexes = (array) => array.map((item, index) => {
    item['id'] = index  
    return item
  })
const removeIndexes = (array) => array.map(item => {
   delete item.id 
   return item
 })

const randomSample = (source, sampleSize) =>
  source
    .map(addIndexes)
    .map(r => r[randomIndex(r)])
    .repeat()
    .scan((a, c) => a.map(a => a.id).indexOf(c.id) === -1 ? a.concat(c) : a, [])
    .skipWhile(array => array.length < sampleSize)
    .take(1)
    .map(array => array.sort((a, b) => a.id - b.id))
    .map(removeIndexes) 

randomSample(source, sampleSize).subscribe(console.log)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>


With firestore .valueChanges()

The firestore .valueChanges() method is designed to push new arrays each time the data changes on the cloud, so this observable source will never complete.
This means that repeat() never fires. To make it work, we need to wrap the sampling code.

const source = Rx.Observable.of([{val: 'a'}, {val: 'b'}, {val: 'c'}, {val: 'd'}, {val: 'e'}])

const sampleSize = 2
const randomIndex = (array) => Math.floor(Math.random() * array.length)
const addIndexes = (array) => array.map((item, index) => {
    item['id'] = index  
    return item
  })
const removeIndexes = (array) => array.map(item => {
   delete item.id 
   return item
 })

const randomSample = (source, sampleSize) =>
  source
    .mergeMap(array => 
      Rx.Observable.of(array)
        .map(addIndexes)
        .map(r => r[randomIndex(r)])
        .repeat()
        .scan((a, c) => a.map(a => a.id).indexOf(c.id) === -1 ? a.concat(c) : a, [])
        .skipWhile(array => array.length < sampleSize)
        .take(1)
        .map(array => array.sort((a, b) => a.id - b.id))
        .map(removeIndexes) 
     )

randomSample(source, sampleSize).subscribe(console.log)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>


Using non-Rx sampling

One thing that stands out in the above code is that we are converting an observable of an array to an array, then back to an inner observable of array (all to get a 'complete' event).

So, it is more practical to work on the array directly.
(Has been tested with firestore .valueChanges() as well)

const source = Rx.Observable.of([{val: 'a'}, {val: 'b'}, {val: 'c'}, {val: 'd'}, {val: 'e'}])

const sampleSize = 2
const getRandomUniqueSample = (arr, sampleSize) => {
  let result = [],
      taken = {}; 
  while (result.length < Math.min(sampleSize, arr.length)) {
    let x = Math.floor(Math.random() * arr.length);
    if (!(x in taken)) {
      result.push(arr[x])
      taken[x] = null;
    }
  }
  return result;
}

const randomSample = (source, sampleSize) =>
  source
    .mergeMap(array => 
      Rx.Observable.of(getRandomUniqueSample(array, sampleSize))
    )

randomSample(source, sampleSize).subscribe(console.log)
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>

Upvotes: 1

Related Questions