In Firestore, how can you do a compound query involving a key in a map without creating an index for every key?


In Firestore, how can you do a compound query involving a key in a map without creating an index for every key?

For example, consider a collection which holds blog posts, and each blog post has categories.

Post {
    title: ..
    ...
    categories: {
        cats: true
        puppies: true
    }   
}

In order to query posts in a particular category in a paginated way, we would do something like this:

let query = db.collection(`/posts`)
    .where(`categories.${categoryId}`, '==', true)
    .orderBy('createdAt')
    .startAfter(lastDate)
    .limit(5);

But it seems that this would require a composite index (categories.<categoryId> and createdAt) for every single category. Is there any way around this?

In my case, it isn't feasible to create composite indices for every category since categories are user-generated, and could easily exceed 200 (the limit for composite indices in Firestore).

This is doable by setting the value of each category to what you want to sort on. Firestore has a guide that covers this.

Post {
    title: ..
    ...
    categories: {
        cats: createdAt
        puppies: createdAt
    }   
}

let query = db.collection(`/posts`)
    .where(`categories.${categoryId}`, '>', 0)
    .orderBy(`categories.${categoryId}`)
    .startAfter(lastDate)
    .limit(5);

As far as I know Firestore should auto-generate those indexes. From the documentation page on arrays, lists, and sets:

Consider this alternative data structure, where each category is the key in a map and all values are true:

// Sample document in the 'posts' collection
{
    title: "My great post",
    categories: {
        "technology": true,
        "opinion": true,
        "cats": true
    }
}

Now it's easy to query for all blog posts within a single category:

// Find all documents in the 'posts' collection that are
// in the 'cats' category.
db.collection('posts')
    .where('categories.cats', '==', true)
    .get()
    .then(() => {
        // ...
    });
)

This technique relies on the fact that Cloud Firestore creates built-in indexes for all document fields, even fields in a nested map.

While the lefthand-side of your where condition may be variable, that doesn't change the fact that these indexes should auto-generated (as far as I can see).


Now Firestore allows the array-contains operator.
If you want to filter documents which contain specific value, try this.

First, change Map field to Array field.

Post {
    title: ..
    ...
    categories: [
        cats,
        puppies
    ]
}

Second, use array-contains and orderBy for each different fields.

let query = db.collection(`/posts`)
    .where('categories', 'array-contains', 'cats')
    .orderBy('createdAt')
    .startAfter(lastDate)
    .limit(5);

You can check the official document about array-contains operator from here.


Try restructuring your data store. Firebase documentation is very helpful here.

Query limitations

Cloud Firestore does not support the following types of queries:

  • Queries with range filters on different fields, as described in the previous section.
  • 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.
  • Queries of individual array members. You can, however, model and query array-like data using the techniques in Working with Arrays, Lists, and Sets.
  • Logical OR queries. In this case, you should create a separate query for each OR condition and merge the query results in your app.
  • 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).