How To Limit CollectionGroup Queries to a Particular Path

Learn how to easily limit the sub collections searched by collectionGroup query using a pathPrefix.

FirebaseFirestore

In this tutorial, you will learn:

  1. The big problem with collectionGroup.
  2. How to solve the problem.

When you’re done, you’ll be using collectionGroup like a pro to target dispersed data with ease.

The Problem

collectionGroup queries are great for fast searches across dispersed but similar-use-case collections. But they have one problem: they can return too much data. For example:

typescript
db.collectionGroup("some-collection-id");

Gives you every collection and subcollection with the ID: "some-collection-id". If your database structure has two paths:

collection1/{a-doc}/some-collection-id
collection2/{a-doc}/subcollection1/{a-different-doc}/some-collection-id

And it turns out you only want data from the sub collections stored down the collection1/{a-doc}/some-collection-id path, tough luck, you’re stuck with everything.

The Solution

The solution is to use the __name__ field which is attached to every document, an orderBy, startAt, and endAt. I learned how to do this from this Stack Overflow post:

CollectionGroupQuery but limit search to subcollections under a particular document

But I’ve turned the method into a fully fledged function you can copy and paste in and use easily:

typescript
/**

export async function collectionGroupWithPathPrefixQuery(
  collectionGroup: string,
  pathPrefix: string,
  db: admin.firestore.Firestore
): Promise<admin.firestore.Query<admin.firestore.DocumentData>> {
  let startPathPrefix = pathPrefix;
  let endPathPrefix = pathPrefix + "\uf8ff"; // \uf8ff is the high Unicode character for inclusive upper bound

  const pathParts = pathPrefix.split("/");

  if (pathParts.length % 2 !== 0) {
    const collectionRef = db.collection(pathPrefix);

    const firstSnapshot = await collectionRef.orderBy("__name__").limit(1).get();

    // Requires an index to be built
    const lastSnapshot = await collectionRef.orderBy("__name__", "desc").limit(1).get();

    if (firstSnapshot.empty || lastSnapshot.empty) {
      throw new Error("Collection is empty");
    }

    startPathPrefix = firstSnapshot.docs[0].ref.path;
    endPathPrefix = lastSnapshot.docs[0].ref.path;
  }

  const query = db.collectionGroup(collectionGroup).orderBy("__name__").startAt(startPathPrefix).endAt(endPathPrefix);

  return query;
}

In short, the great thing about this method is the following:

  1. It uses orderBy to make the database query on the server side.
  2. This saves data being sent to the client, decreasing query completion time, and data transfer costs.
  3. It returns the query builder so you can add your own statements to it if you like.

Final Words

I hope you find this helpful, and it lets you use collectionGroup queries in places you’ve never thought to use them, or speeds up your code in places you’re already using them!