Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase Limits One "array-contains-any" per query - what do to if I need multiple?

Like the question states, Firebase only allows one array-contains-any per query (https://firebase.google.com/docs/firestore/query-data/queries).

What happens when someone needs a complex query? I need about 10 .where() with 4 array-contains-any clauses on my query. Is there a work around or do I have to take the results from the first array-contains-any and then callout again and again? Seems taxing and unnecessary.

var usersMatchesCollection = config.db.collection("Users");
var currentUserMatchesAllData = usersMatchesCollection.where('preferences.lookingFor', 'array-contains-any', docDataPreferences.lookingFor)
.where('info.identifyAs', '==', docDataPreferences.prefIdentifyAs)
.where('info.languages', 'array-contains-any', docDataPreferences.prefLanguages) 
.where('age', 'array-contains-any', docDataPreferences.ageRange) 
.where('profession', 'array-contains-any', docDataPreferences.prefProfession) 
.where('education', '==', docDataPreferences.prefEducation) 
.where('kids', '==', docDataPreferences.prefKids) 
.where('married', '==', docDataPreferences.prefMarried) 
.where('drinking', '==', docDataPreferences.prefDrinking) 
.where('smokingCig', '==', docDataPreferences.prefSmokingCig) 
await currentUserMatchesAllData.get().then( function (matchesQuerySnapshot) {
  matchesQuerySnapshot.forEach(function(doc) {
      console.log('doc data: ' + JSON.stringify(doc.data().id));
  })
})
like image 898
Olivia Avatar asked Sep 14 '25 23:09

Olivia


1 Answers

What your asking for is a query Cloud Firestore cannot serve, as we cannot do it in a generic way that's scales regardless of the data. There are some tricks you could employ that reduce the queries complexity, which means reducing the number of queries you'd need to break this into.

preferences.lookingFor

For the where('preferences.lookingFor', 'array-contains-any', docDataPreferences.lookingFor) condition, assuming possible different values is limited and the total number is know, you could create a mapping of possibilities and reduce it to a single equality. For example,

Let's assume 3 possible values A, B, and C, and that you must have at least 1. This means there is only 7 different query variations, which could be encoded as a series of Boolean fields:

preferences.lookingForCheck = {
    A: <if A>,
    B: <If B>,
    C: <If C>,
    AB: <if A OR B>,
    AC: <If A OR C>,
    BC: <If B OR C>,
    ABC: <if A OR B OR C>,
} 

These are fairly simple at write-time to calculate, and mean you can easily calculate which field to query against: where('preferences.lookingForCheck.BC', '==', true)

info.languages

This one is probably to extension to want to explode as combinations, so let's keep this one.

age

I'll over simplify, and assume some simple buckets, so: 21-30, 31-44, 45-64, 65+. You could make this explicit fields like above (A, B, C, D), which would be 15 fields. If you can assume the range will be continuous (e.g if 21-30 & '45-64' is selected, then 31-30 must be selected to), you can optimize even more by instead saying there 2 lists of values from which users must select one from each and the value in the second must be equal or greater than the second: lower_age: [21, 31, 45, 65+] upper_age: [30, 44, 64, 65+]

How you only need 10 combinations with a true/false value: 21-30, 21-44, 21-64, 21-65+, 30-44, 30-64, 30-65+, 45-65, 45-65+, 65+-65+

profession

This is another hard one. Since we've already committed info.languages, we can't have this as an array-contains query. We do have some options to consider.

  1. Issue 1 query per profession type, and merge the results client side. This is more code, but straight forward.
  2. Don't query this field at all, but rather filter out any documents that don't match on the client side. Assumes number of false matches is low, otherwise you'll be read a lot of documents you end up discarding.
  3. A hybrid approach, whereby you bucket professions (e.g. [white, blue, pink] collar) and the same approach discussed for the other fields, while then filter on the client side by the specific profession.

Final Query

Following the above, you'll end up with something like:

preferences = `preferences.lookingForCheck.${prefCombo}`;
ageRange = `ageRange.${lowerRange}to${upperRange}`;

var usersMatchesCollection = config.db.collection("Users");
var currentUserMatchesAllData = usersMatchesCollection.where(preferences, '==', docDataPreferences.lookingForMatch)
.where('info.identifyAs', '==', docDataPreferences.prefIdentifyAs)
.where('info.languages', 'array-contains-any', docDataPreferences.prefLanguages) 
.where(ageRange, '==', docDataPreferences.ageRange) 
.where('profession', '==', docDataPreferences.prefProfessionCollarCheck) 
.where('education', '==', docDataPreferences.prefEducation) 
.where('kids', '==', docDataPreferences.prefKids) 
.where('married', '==', docDataPreferences.prefMarried) 
.where('drinking', '==', docDataPreferences.prefDrinking) 
.where('smokingCig', '==', docDataPreferences.prefSmokingCig) 
await currentUserMatchesAllData.get().then( function (matchesQuerySnapshot) {
  matchesQuerySnapshot.forEach(function(doc) {
      // Filter by profession here
      console.log('doc data: ' + JSON.stringify(doc.data().id));
  })
})
like image 181
Dan McGrath Avatar answered Sep 16 '25 14:09

Dan McGrath