Working with Arrays, Lists, and Sets

Many apps need to store array-like data structures in documents. For example a user document might contain a list of the user's top five friends, or a blog post document might contain a set of associated categories.

Although Cloud Firestore can store arrays, it does not support querying array members or updating single array elements. However, you can still model this kind of data by leveraging the other capabilities of Cloud Firestore.

Before continuing, make sure you have read about the Cloud Firestore data model.

Solution: A map of values

One way to model array-like data in Cloud Firestore is to represent it as a map from keys to boolean values. For example, suppose that in your blogging app each post is tagged with a number of categories. If you use an array, a blog-post document looks like the following example:

Web

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

Swift

struct PostArray {

    let title: String
    let categories: [String]

    init(title: String, categories: [String]) {
        self.title = title
        self.categories = categories
    }

}

let myArrayPost = PostArray(title: "My great post",
                            categories: ["technology", "opinion", "cats"])

Android

public class ArrayPost {
    String title;
    String[] categories;

    public ArrayPost(String title, String[] categories) {
        this.title = title;
        this.categories = categories;
    }
}

ArrayPost myArrayPost = new ArrayPost("My great post", new String[]{
        "technology", "opinion", "cats"
});

What if you want to query for all posts that are part of the "cats" category? With the data structure above, there is no way to perform this query.

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

Web

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

Swift

struct PostDict {

    let title: String
    let categories: [String: Bool]

    init(title: String, categories: [String: Bool]) {
        self.title = title
        self.categories = categories
    }

}

let post = PostDict(title: "My great post", categories: [
    "technology": true,
    "opinion": true,
    "cats": true
])

Android

public class MapPost {
    String title;
    Map<String,Boolean> categories;

    public MapPost(String title, Map<String,Boolean> categories) {
        this.title = title;
        this.categories = categories;
    }
}

Map<String, Boolean> categories = new HashMap<>();
categories.put("technology", true);
categories.put("opinion", true);
categories.put("cats", true);
MapPost myMapPost = new MapPost("My great post", categories);

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

Web

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

Swift

db.collection("posts")
    .whereField("categories.cats", isEqualTo: true)
    .getDocuments() { (querySnapshot, err) in

        // ...

}

Android

db.collection("posts")
        .whereEqualTo("categories.cats", true)
        .get()
        .addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
            // ...
        });

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

What if you want to do a more complex query, such as all blog posts in a single category ordered by date posted? You might try a query like this:

Invalid query

Web

 db.collection('posts')
    .where('categories.cats', '==', true)
    .orderBy('timestamp');)

Swift

db.collection("posts")
    .whereField("categories.cats", isEqualTo: true)
    .order(by: "timestamp")

Android

db.collection("posts")
        .whereEqualTo("categories.cats", true)
        .orderBy("timestamp");

Queries on multiple fields with range filters require a composite index. Unfortunately, this is not possible with the approach shown above. Indexes must be defined on specific field paths. You would have to create an index on each possible field path (such as categories.cats or categories.opinion) which is impossible to do in advance if your categories are user generated.

You can work around this limitation by encoding all of the information for the query into the map values:

Web

// The value of each entry in 'categories' is a unix timestamp
{
  title: "My great post",
  categories: {
    technology: 1502144665,
    opinion: 1502144665,
    cats: 1502144665
  }
}

Swift

struct PostDictAdvanced {

    let title: String
    let categories: [String: UInt64]

    init(title: String, categories: [String: UInt64]) {
        self.title = title
        self.categories = categories
    }

}

let dictPost = PostDictAdvanced(title: "My great post", categories: [
    "technology": 1502144665,
    "opinion": 1502144665,
    "cats": 1502144665
])

Android

public class MapPostAdvanced {
    String title;
    Map<String,Long> categories;

    public MapPostAdvanced(String title, Map<String,Long> categories) {
        this.title = title;
        this.categories = categories;
    }
}

Map<String, Long> categories = new HashMap<>();
categories.put("technology", 1502144665L);
categories.put("opinion", 1502144665L);
categories.put("cats", 1502144665L);
MapPostAdvanced myMapPostAdvanced = new MapPostAdvanced("My great post", categories);

Now you can express the desired query as a set of conditions on a single field, avoiding the need for a compound index:

Valid query

Web

 db.collection('posts')
    .where('categories.cats', '>', 0)
    .orderBy('categories.cats');)

Swift

db.collection("posts")
    .whereField("categories.cats", isGreaterThan: 0)
    .order(by: "categories.cats")

Android

db.collection("posts")
        .whereGreaterThan("categories.cats", 0)
        .orderBy("categories.cats");

Compared to using a map of booleans, this technique makes it more difficult to update data, because the map values must be kept in sync with one another.

Limitations

The solution shown above is a great way to simulate array-like structures in Cloud Firestore, but you should be aware of the following limitations:

  • Indexing limits - A single document can have only 20,000 properties in order to use Cloud Firestore built-in indexes. If your array-like data structure grows to tens of thousands of members, you may run into this limit.
  • Document size - Cloud Firestore is optimized for small documents and enforces a 1MB size limit on documents. If your array can expand arbitrarily, it is better to use a subcollection, which has better scaling performance.

Send feedback about...

Need help? Visit our support page.