10

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.

D V Ramana
  • 1,136
  • 11
  • 13

1 Answers1

18

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.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • 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
  • 2
    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
  • The chatroom may only be readable to those who participated in it. As there's nothing sensitive in there, I've copied the thread across to [pastebin here](https://pastebin.com/zxhZJhYR). – samthecodingman Aug 01 '22 at 04:53
  • As said in the chat, this is a valid use of the way the `documentID` field is built for collection group queries, and there are no plans to break it. In fact, I'm relying on it in the code for a talk later this month, and wished I could upvote this answer again. – Frank van Puffelen Aug 01 '22 at 14:53
  • 1
    @FrankvanPuffelen If it gets recorded I'd love a link to the talk sent to me on Twitter or the Firebase Slack. If it's relevant here, I can also edit it into the answer – samthecodingman Aug 02 '22 at 00:45
  • I just implemented it with angular firebase and works great but I´m new to firebase and was not able to grasp the mechanics that we take advantage of here. Can someone explain in a bit more detail how this works? – Max Tuzenko Apr 06 '23 at 21:35
  • 2
    @MaxTuzenko It's a flow on from how you would do ["start with X"](https://youtu.be/sKFLI5FOOHs?t=301) queries in the Realtime Database. I use the same "start with X" approach to "find all documents that have a path that starts with X". To cut off any results that don't start with X, but are sorted after it, I add an `endAt` filter to the query to make sure it returns only paths that start with X and omit the other results. `\uf8ff` is a character with a very high Unicode codepoint, which grabs basically anything useful with a lower code point - similar to `1 <= x <= 1.9999999`. – samthecodingman Apr 13 '23 at 23:39