1

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) 
Hareesh
  • 6,770
  • 4
  • 33
  • 60

1 Answers1

1

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>
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
  • Thanks for your answer, i tried it and with some errors. i updated what i tired with my question. please help – Hareesh Jan 20 '18 at 07:20
  • Ah yes, you have an array of objects, so the scan must be made a little more sophisticated. Do you have an id property on the object, such as `questionId`? – Richard Matsen Jan 20 '18 at 07:38
  • not right now, but i can do it easily with `.snapshotChanges`. lets assume i have a field `questionId`, which will be some thing like `lpybtKeZpr7lPDZMQtcO` – Hareesh Jan 20 '18 at 07:42
  • That would work, but this is just a small local change - really shouldn't influence the way you access firestore. The array items have an index anyway, I'll use that and you can dial it back if you feel it's more appropriate. – Richard Matsen Jan 20 '18 at 08:01
  • I'm a bit worried about the concat error, which may be due to a missing import. – Richard Matsen Jan 20 '18 at 08:33
  • Your code works, but when i put i in to my code it giving null. if i comment `.skipWhile(array => array.length < sampleSize)` i am getting single random value each time. any idea? – Hareesh Jan 20 '18 at 09:16
  • I'll have a look and come back to it tomorrow (will test against firestore). BTW, you can debug a little by putting `do(x => console.log(x))` at different steps. It won't change the outcome. – Richard Matsen Jan 20 '18 at 09:27