Firestore query subcollections

FirebaseGoogle Cloud-Firestore

Firebase Problem Overview


I thought I read that you can query subcollections with the new Firebase Firestore, but I don't see any examples. For example I have my Firestore setup in the following way:

  • Dances [collection]
  • danceName
  • Songs [collection]
    • songName

How would I be able to query "Find all dances where songName == 'X'"

Firebase Solutions


Solution 1 - Firebase

Update 2019-05-07

Today we released collection group queries, and these allow you to query across subcollections.

So, for example in the web SDK:

db.collectionGroup('Songs')
  .where('songName', '==', 'X')
  .get()

This would match documents in any collection where the last part of the collection path is 'Songs'.

Your original question was about finding dances where songName == 'X', and this still isn't possible directly, however, for each Song that matched you can load its parent.

Original answer

This is a feature which does not yet exist. It's called a "collection group query" and would allow you query all songs regardless of which dance contained them. This is something we intend to support but don't have a concrete timeline on when it's coming.

The alternative structure at this point is to make songs a top-level collection and make which dance the song is a part of a property of the song.

Solution 2 - Firebase

UPDATE Now Firestore supports array-contains

Having these documents

    {danceName: 'Danca name 1', songName: ['Title1','Title2']}
    {danceName: 'Danca name 2', songName: ['Title3']}

do it this way

collection("Dances")
    .where("songName", "array-contains", "Title1")
    .get()...

@Nelson.b.austin Since firestore does not have that yet, I suggest you to have a flat structure, meaning:

Dances = {
    danceName: 'Dance name 1',
    songName_Title1: true,
    songName_Title2: true,
    songName_Title3: false
}

Having it in that way, you can get it done:

var songTitle = 'Title1';
var dances = db.collection("Dances");
var query = dances.where("songName_"+songTitle, "==", true);

I hope this helps.

Solution 3 - Firebase

What if you store songs as an object instead of as a collection? Each dance as, with songs as a field: type Object (not a collection)

{
  danceName: "My Dance",
  songs: {
    "aNameOfASong": true,
    "aNameOfAnotherSong": true,
  }
}

then you could query for all dances with aNameOfASong:

db.collection('Dances')
  .where('songs.aNameOfASong', '==', true)
  .get()
  .then(function(querySnapshot) {
    querySnapshot.forEach(function(doc) {
      console.log(doc.id, " => ", doc.data());
    });
   })
   .catch(function(error) {
     console.log("Error getting documents: ", error);
    });

Solution 4 - Firebase

UPDATE 2019

Firestore have released Collection Group Queries. See Gil's answer above or the official Collection Group Query Documentation


Previous Answer

As stated by Gil Gilbert, it seems as if collection group queries is currently in the works. In the mean time it is probably better to use root level collections and just link between these collection using the document UID's.

For those who don't already know, Jeff Delaney has some incredible guides and resources for anyone working with Firebase (and Angular) on AngularFirebase.

Firestore NoSQL Relational Data Modeling - Here he breaks down the basics of NoSQL and Firestore DB structuring

Advanced Data Modeling With Firestore by Example - These are more advanced techniques to keep in the back of your mind. A great read for those wanting to take their Firestore skills to the next level

Solution 5 - Firebase

NEW UPDATE July 8, 2019:

db.collectionGroup('Songs')
  .where('songName', isEqualTo:'X')
  .get()

Solution 6 - Firebase

You can always search like this:-

    this.key$ = new BehaviorSubject(null);

    return this.key$.switchMap(key =>
      this.angFirestore
        .collection("dances").doc("danceName").collections("songs", ref =>
          ref
            .where("songName", "==", X)
        )
        .snapshotChanges()
        .map(actions => {
          if (actions.toString()) {
            return actions.map(a => {
              const data = a.payload.doc.data() as Dance;
              const id = a.payload.doc.id;
              return { id, ...data };
            });
          } else {
            return false;
          }
        })
    );

Solution 7 - Firebase

I have found a solution. Please check this.

var museums = Firestore.instance.collectionGroup('Songs').where('songName', isEqualTo: "X");
		museums.getDocuments().then((querySnapshot) {
			setState(() {
			  songCounts= querySnapshot.documents.length.toString();
			});
		});

And then you can see Data, Rules, Indexes, Usage tabs in your cloud firestore from console.firebase.google.com. Finally, you should set indexes in the indexes tab.enter image description here

Fill in collection ID and some field value here. Then Select the collection group option. Enjoy it. Thanks

Solution 8 - Firebase

> Query limitations > > Cloud Firestore does not support the following types of queries: > > > > 1. Queries with range filters on different fields. > > 2. Single queries across multiple collections or subcollections. Each query runs against a single collection of documents. For more > information about how your data structure affects your queries, see > Choose a Data Structure. > 3. Logical OR queries. In this case, you should create a separate query for each OR condition and merge the query results in your app. > 4. Queries with a != clause. In this case, you should split the query into a greater-than query and a less-than query. For example, although > the query clause where("age", "!=", "30") is not supported, you can > get the same result set by combining two queries, one with the clause > where("age", "<", "30") and one with the clause where("age", ">", 30).

Solution 9 - Firebase

var songs = []    
db.collection('Dances')
      .where('songs.aNameOfASong', '==', true)
      .get()
      .then(function(querySnapshot) {
        var songLength = querySnapshot.size
        var i=0;
        querySnapshot.forEach(function(doc) {
           songs.push(doc.data())
           i ++;
           if(songLength===i){
                console.log(songs
           }
          console.log(doc.id, " => ", doc.data());
        });
       })
       .catch(function(error) {
         console.log("Error getting documents: ", error);
        });

Solution 10 - Firebase

I'm working with Observables here and the AngularFire wrapper but here's how I managed to do that.

It's kind of crazy, I'm still learning about observables and I possibly overdid it. But it was a nice exercise.

Some explanation (not an RxJS expert):

  • songId$ is an observable that will emit ids
  • dance$ is an observable that reads that id and then gets only the first value.
  • it then queries the collectionGroup of all songs to find all instances of it.
  • Based on the instances it traverses to the parent Dances and get their ids.
  • Now that we have all the Dance ids we need to query them to get their data. But I wanted it to perform well so instead of querying one by one I batch them in buckets of 10 (the maximum angular will take for an in query.
  • We end up with N buckets and need to do N queries on firestore to get their values.
  • once we do the queries on firestore we still need to actually parse the data from that.
  • and finally we can merge all the query results to get a single array with all the Dances in it.
type Song = {id: string, name: string};
type Dance = {id: string, name: string, songs: Song[]};

const songId$: Observable<Song> = new Observable();
const dance$ = songId$.pipe(
  take(1), // Only take 1 song name
  switchMap( v =>
    // Query across collectionGroup to get all instances.
    this.db.collectionGroup('songs', ref =>
      ref.where('id', '==', v.id)).get()
  ),
  switchMap( v => {
    // map the Song to the parent Dance, return the Dance ids
    const obs: string[] = [];
    v.docs.forEach(docRef => {
      // We invoke parent twice to go from doc->collection->doc
      obs.push(docRef.ref.parent.parent.id);
    });
    // Because we return an array here this one emit becomes N
    return obs;
  }),
  // Firebase IN support up to 10 values so we partition the data to query the Dances
  bufferCount(10),
  mergeMap( v => { // query every partition in parallel
    return this.db.collection('dances', ref => {
      return ref.where( firebase.firestore.FieldPath.documentId(), 'in', v);
    }).get();
  }),
  switchMap( v => {
    // Almost there now just need to extract the data from the QuerySnapshots
    const obs: Dance[] = [];
    v.docs.forEach(docRef => {
      obs.push({
        ...docRef.data(),
        id: docRef.id
      } as Dance);
    });
    return of(obs);
  }),
  // And finally we reduce the docs fetched into a single array.
  reduce((acc, value) => acc.concat(value), []),
);
const parentDances = await dance$.toPromise();

I copy pasted my code and changed the variable names to yours, not sure if there are any errors, but it worked fine for me. Let me know if you find any errors or can suggest a better way to test it with maybe some mock firestore.

Solution 11 - Firebase

It could be better to use a flat data structure.
The docs specify the pros and cons of different data structures on this page.

Specifically about the limitations of structures with sub-collections:

> You can't easily delete subcollections, or perform compound queries across subcollections.

Contrasted with the purported advantages of a flat data structure:

> Root-level collections offer the most flexibility and scalability, along with powerful querying within each collection.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNelson.b.austinView Question on Stackoverflow
Solution 1 - FirebaseGil GilbertView Answer on Stackoverflow
Solution 2 - FirebasenorgematosView Answer on Stackoverflow
Solution 3 - FirebasedmartinsView Answer on Stackoverflow
Solution 4 - FirebaseMatthew MullinView Answer on Stackoverflow
Solution 5 - FirebaseNhật TrầnView Answer on Stackoverflow
Solution 6 - FirebaseAnkurView Answer on Stackoverflow
Solution 7 - FirebaseHappy SonView Answer on Stackoverflow
Solution 8 - FirebaseggDeGreatView Answer on Stackoverflow
Solution 9 - FirebaseAlok PrustyView Answer on Stackoverflow
Solution 10 - FirebaseEduardoView Answer on Stackoverflow
Solution 11 - FirebaseMattCochraneView Answer on Stackoverflow