4

I like to count the number of documents in a subcollection with firestore cloud functions.

My database looks like that: groups/{groupId}/members/{memberId}

I like to count the number of members (memberId) for each group. That means every group can have a different amount of members, and they can increase or decrease flexible.

Would be happy about your ideas :-).

Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
Alexandra
  • 79
  • 7
  • You may have a look at the following SO Question/Answer. It does not exactly answer your question but is very close to it. https://stackoverflow.com/questions/54653836/how-to-use-query-in-firebase-firestore/54659412#54659412 – Renaud Tarnec Feb 21 '19 at 16:14
  • @RenaudTarnec Thanks but does not help as I need an unlimited and exact number of documents. Maybe you have another idea? thanks – Alexandra Feb 21 '19 at 16:29
  • Just keep another node with a count in it. When you add a node, increment the counter, when you delete a node, decrement it. It's a tiny amount of data and having that 'counter' node won't really impact anything. – Jay Feb 22 '19 at 17:35

2 Answers2

5

Took me a while to get this working, so thought I'd share it for others to use:

'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

exports.countDocumentsChange = functions.firestore.document('library/{categoryId}/documents/{documentId}').onWrite((change, context) => {

    const categoryId = context.params.categoryId;
    const categoryRef = db.collection('library').doc(categoryId)
    let FieldValue = require('firebase-admin').firestore.FieldValue;

    if (!change.before.exists) {

        // new document created : add one to count
        categoryRef.update({numberOfDocs: FieldValue.increment(1)});
        console.log("%s numberOfDocs incremented by 1", categoryId);

    } else if (change.before.exists && change.after.exists) {

        // updating existing document : Do nothing

    } else if (!change.after.exists) {

        // deleting document : subtract one from count
        categoryRef.update({numberOfDocs: FieldValue.increment(-1)});
        console.log("%s numberOfDocs decremented by 1", categoryId);

    }

    return 0;
});
Rob Phillips
  • 295
  • 3
  • 9
2

I think about two possible approaches.

1. Directly count the document of the collection

You would use the size property of the QuerySnapshot like

admin.firestore().collection('groups/{groupId}/members/{memberId}')
    .get()
    .then(querySnapshot => {
        console.log(querySnapshot.size);
        //....
        return null;
    });

The main problem here is the cost, if the sub-collection contains a lot of documents: by executing this query you will be charged for one read for each doc of the sub-collection.

2. Another approach is to maintain some counters for each sub-collection

You would write two Cloud Functions, based on Distributed Counters, as presented in this Firebase documentation item: https://firebase.google.com/docs/firestore/solutions/counters. We use 3 shards in the following example.

Firstly a Cloud Function would increase the counter when a new doc is added to the subCollec sub-collection:

//....
const num_shards = 3;
//....

exports.incrementSubCollecCounter = functions
  .firestore.document('groups/{groupId}/members/{memberId}')
  .onCreate((snap, context) => {

    const groupId = context.params.groupId;

    const shard_id = Math.floor(Math.random() * num_shards).toString();
    const shard_ref = admin
      .firestore()
      .collection('shards' + groupId)
      .doc(shard_id);

    if (!snap.data().counterIncremented) {
      return admin.firestore().runTransaction(t => {
        return t
          .get(shard_ref)
          .then(doc => {
            if (!doc.exists) {
              throw new Error(
                'Shard doc #' +
                  shard_id +
                  ' does not exist.'
              );
            } else {
              const new_count = doc.data().count + 1;
              return t.update(shard_ref, { count: new_count });
            }
          })
          .then(() => {
            return t.update(snap.ref, {
              counterIncremented: true    //This is important to have the Function idempotent, see https://cloud.google.com/functions/docs/bestpractices/tips#write_idempotent_functions
            });
          });
      });
    } else {
      console.log('counterIncremented NOT NULL');
      return null;
    }
  });

Then a second Cloud Function would decrease the counter when a doc is deleted from the subCollec sub-collection:

exports.decrementSubCollecCounter = functions
  .firestore.document('groups/{groupId}/members/{memberId}')
  .onDelete((snap, context) => {

    const groupId = context.params.groupId;

    const shard_id = Math.floor(Math.random() * num_shards).toString();
    const shard_ref = admin
      .firestore()
      .collection('shards' + groupId)
      .doc(shard_id);

    return admin.firestore().runTransaction(t => {
      return t.get(shard_ref).then(doc => {
        if (!doc.exists) {
          throw new Error(
            'Shard doc #' +
              shard_id +
              ' does not exist.'
          );
        } else {
          const new_count = doc.data().count - 1;
          return t.update(shard_ref, { count: new_count });
        }
      });
    });
  });

Here, compared to solution 1, since we have 3 shards, when you want to know the number of docs in the subCollec sub-collection, you need to read only 3 documents.

Have a look at the documentation for details about how to initialize the distributed counters. You have to initialize once for each groupId collection (i.e. admin.firestore().collection('shards' + groupId))

Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • thanks, tried it but receive error message: " ReferenceError: groupId is not defined " for line ".collection('shards' + groupId)". Any idea? – Alexandra Feb 21 '19 at 21:26
  • You should verify that groupId has the correct value after the line const groupId = context.params.groupId; by doing console.log(groupId) and looking at the output in the Cloud Function log. – Renaud Tarnec Feb 22 '19 at 05:21
  • Always getting the following error - not sure why. Error: Shard doc #0 does not exist. at t.get.then.doc (/user_code/index.js:119:21) at process._tickDomainCallback (internal/process/next_tick.js:135:7) – Alexandra Feb 22 '19 at 18:37
  • Yes, as I said at the bottom of my answer: « Have a look at the documentation for details about how to initialize the distributed counters. You have to initialize once for each groupId collection (i.e. admin.firestore().collection('shards' + groupId) ». You have to study and fully understand this doc https://firebase.google.com/docs/firestore/solutions/counters – Renaud Tarnec Feb 22 '19 at 19:05
  • 1
    It is actually a bad idea to use shards in this case. It costs more to read the documents and won't be more than 1 write a second. See my answer here: https://stackoverflow.com/questions/46554091/cloud-firestore-collection-count/61050621#61050621 – Jonathan Apr 05 '20 at 23:17