2

This is my database structure.
cities -> landmarks -> attractions(each one is a collection)

Query
List all attractions under a particular city.

Solution
Save city_id in each attraction document
Use a collection group query and filter based on city_id to get attractions of a particular city.

My Question
Instead of saving city_id in each attraction document, can I just specify the document to the collectionGroupQuery which now only searches in subcollections under this particular document.

In the above example, I specify the city document's full path and i should be able to list all attractions without filtering based on city_id.

CommandSpace
  • 1,026
  • 9
  • 10

1 Answers1

10

This should work using FieldPath.documentId().

Each document has a hidden __name__ field in its data and this is the target of FieldPath.documentId().

For a normal collection query, FieldPath.documentId() would just be the document's ID. However, for a collection group query, this value is the document's path.

We should be able to use this to find all matching document paths that start with the given city's document path like so:

const cityRef = firebase.firestore().doc('cities/cityId');

firebase.firestore().collectionGroup('attractions')
  .orderBy(firebase.firestore.FieldPath.documentId())
  .startAt(cityRef.path + "/"),
  .endAt(cityRef.path + "/\uf8ff")
  .get()
  .then((querySnapshot) => {
    console.log("Found " + querySnapshot.size + " docs");
    querySnapshot.forEach((doc) => console.log("> " + doc.ref.path))
  })
  .catch((err) => {
    console.error("Failed to execute query", err);
  })

Edit: While the above code would function if the SDK allowed it, it currently throws an error about having an odd number of segments because of the extra /.

For now, as long as all your city IDs are the same length (as they would be if using the default docRef.add()), the below code would function:

const cityRef = firebase.firestore().doc('cities/cityId');

firebase.firestore().collectionGroup('attractions')
  .orderBy(firebase.firestore.FieldPath.documentId())
  .startAt(cityRef.path),
  .endAt(cityRef.path + "\uf8ff")
  .get()
  .then((querySnapshot) => {
    console.log("Found " + querySnapshot.size + " docs");
    querySnapshot.forEach((doc) => console.log("> " + doc.ref.path))
  })
  .catch((err) => {
    console.error("Failed to execute query", err);
  })

Where the above block fails is if the document ID have different lengths. Let's say you only wanted documents under "/cities/New York", if another city called "New Yorkshire" was also in the cities collection, it would have it's results included too.

samthecodingman
  • 17,653
  • 3
  • 24
  • 45
  • 1
    Interesting approach! Do you have a source for "for a collection group query, this value is the document's path", as I don't think I've ever read that before? – Frank van Puffelen Jun 19 '21 at 20:13
  • @FrankvanPuffelen From the way I understood it, it's a matter of uniqueness. In a single collection, only one document may have a given ID but across multiple collections, many docs may share an ID, so it uses their path for uniqueness. The [source of the JS SDK](https://github.com/firebase/firebase-js-sdk/blob/2e6d95a0851304a9b7c069f55e5c8fb817a5e3cc/packages/firestore/src/lite/query.ts#L673-L716) has some explicit error handling for handling any bad inputs. [@doug](https://stackoverflow.com/users/807126) referenced this behaviour [here](https://stackoverflow.com/a/56189687/3068190). – samthecodingman Jun 19 '21 at 20:35
  • In that link Doug suggests explicitly storing the document ID inside the document itself. I don't think I've ever seen a prefix condition on the document ID, let alone on the path that it may contain (I don't think I've ever seen that statement either). I'm trying to set up a repro for this now, as (if this is indeed possible, even if complex) it would invalidate quite some existing answers. – Frank van Puffelen Jun 19 '21 at 20:38
  • @FrankvanPufflen when you say "invalidate quite some existing answers", do you mean that this is a bug or something that is unintentionally useful? Also I wasn't referring to the answer Doug gave, but his comment regarding the path being returned when used on Collection Group queries. – samthecodingman Jun 19 '21 at 20:41
  • 1
    Whenever folks have asked: can I query collections under a certain path, the answer I've given (and hence others have given after that) has been "this is currently not possible with Firestore". If your answer works, those answers would be incorrect. – Frank van Puffelen Jun 19 '21 at 20:51
  • @FrankvanPuffelen Maybe I should have been more vocal about it when I found out about this quirk months ago while working on [this gist](https://gist.github.com/samthecodingman/aea3bc9481bbab0a7fbc72069940e527). In my sandbox this approach worked fine but I haven't tried it at scale nor tested if it causes hotspotting. – samthecodingman Jun 19 '21 at 21:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/233969/discussion-between-frank-van-puffelen-and-samthecodingman). – Frank van Puffelen Jun 19 '21 at 21:01
  • 1
    Just marked as "recommended by Google Cloud". Really good answer! – Alex Mamo Jun 29 '21 at 08:33
  • Very nice discovery. @FrankvanPuffelen based on your discussion in the break out room, it sounds like this is a supported feature. Any plans to add it to Firestore's documentation? – Johnny Oshika Sep 06 '21 at 20:58