When using arrays, you can easily perform an OR
query. However, Firestore does not natively have an AND
query, and you can’t combine array-contains
clauses. Here is a workaround method I created.
TL;DR#
You can search for one item in array with array-contains
, for one item OR
another item with array-contains-any
, and you can search for one item AND
another item with this workaround simulating array-contains-all
. This is best for storing unlimited items in sets of 1-5 items. You can use native sorting techniques and other filters without a problem. However, if you don’t want a workaround and you don’t need complex sorting, you should consider using the Map version.
array-contains-any#
First, we need to remember the OR
query versions.
Get all students taking EITHER class X OR class Y#
query(
collection(db, 'students'),
where('classes', 'array-contains-any', [class1ID, class2ID])
);
Get all classes EITHER student X OR student Y is taking#
query(
collection(db, 'classes'),
where('students', 'array-contains-any', [student1ID, student2ID])
);
You create your own index using every combination of items in the array, which gives you query options. You could do this on the front end or on the back end in Firebase Cloud Functions.
Reusable Functions#
You can add these to a separate file and reuse them.
Delimiter#
Use whatever delimiter you want.
const DELIMETER = '.';
Add a New Array#
function addArray(arr: string[]): string[] {
// declare new array
const allSubsets: string[][] = [[]];
// go through each item
for (const element of arr) {
// go through each existing item in array
const newSubsets = allSubsets.map(subset => [...subset, element]);
// create new set
allSubsets.push(...newSubsets);
}
// remove first empty array item
allSubsets.shift();
// return sorted array separated by delimeter
return allSubsets.map(subset => subset.sort().join(DELIMETER));
}
Get an Existing Array#
function showArray(s: string[]) {
return s.filter(item => !item.includes(DELIMETER)).sort();
}
Query an Array#
function containsArray(s: string[] | string) {
return typeof s === 'string'
? s
: s.sort().join(DELIMETER);
}
Usage#
These three functions are needed to add, update, and query a document in your Firestore collection.
Add#
The addArray
function will create the indexes automatically with the desired diameter.
await addDoc(collection(db, 'students'), {
classes: addArray([
class1,
class2,
class3
])
});
Query#
To query a document, you must filter all results containing the delimiter, as you only want the real classes to show. The showArray
function will do this.
// get student record by ID
const docSnap = await getDoc(
doc(db, "students", studentID)
);
// get student data, filter classes
const data = docSnap.data();
const studentData = {
...data,
id: docSnap.id,
classes: showArray(data.classes)
};
If you’re using real-time, you will want to do the same thing before you display the data in an observable, signal, store, or effect. See Framework Setup.
Update#
When you update your arrays, you can’t use arrayUnion
or arrayRemove
. You will have to replace the existing array with a new one. You can use the JavaScript spread operator to combine the arrays. You usually have already read the document you’re updating in your production app. Don’t get charged another read for data you already have read in your client.
// reuse the classes array in studentData we have just read
updateDoc(doc(db, 'students', studentID), {
classes: addArray([
...studentData.classes,
class4,
class5,
class6
])
});
Filtering#
The previous three functions must be used to simulate array-contains-all
. You can now use the containsArray
function inside your actual array-contains
function.
Get all students taking both Class X AND Class Y#
query(
collection(db, 'students'),
where('classes', 'array-contains', containsArray([class1, class2]))
);
How this works#
In your actual database, you’re creating an index for the unique sets of items.
classes: ["math", "biology", "science"];
You can see what this actually looks like in your Firestore document. Here, we are using .
as the delimiter, but you could use anything.
You can also see how this could become overwhelming with just a small set of data. If you have three items, your combinations will be 7 items. In mathematics, there is a combinations formula.
C(max) = C(3,1) + C(3,2) + C(3,3)
Without getting into the details with factorials, this means 3 items have 7 combinations, 4 items have 15 combinations, and 5 items have 31 combinations! 10 items would need an array of 1024 items!
When to use this#
Now you can see why array-contains-all
does not exist. At the very least, they should figure out how to make it work for a few items. This is the perfect workaround if you need to store infinite items in small sets (maybe 1 to 5 items). It will also allow you to sort better without workarounds.
In the meantime, I have added it to my Firebase Wish List.
J