Firebase-js-sdk: Could we have more than one array-contains filter?

Created on 27 Aug 2018  路  4Comments  路  Source: firebase/firebase-js-sdk


  • Operating System version: macOS
  • Browser version: Google Chrome Version 68.0.3440.106
  • Firebase SDK version: 5.4.1
  • Firebase Product: firestore

Steps to reproduce:

Error: Invalid query. Queries only support a single array-contains filter.

Relevant Code:

  .firestore()
  .collection('friends')
  .where('userIds', 'array-contains', user.uid)
  .where('userIds', 'array-contains', otherUserId)
  .get()

The above code produces the above error.

Would it be possible to have more than one array-contains filter?

firestore

Most helpful comment

TL;DR: while that query works, it will perform poorly at scale. We don't want to allow that with array-contains.

The "map trick" query you're performing there is using the default single-field indexes and is performing a _merge join_ to produce the results. In this case we're scanning the index on userIds.0 concurrently with a scan on userIds.1 and return documents that are in both.

Merge joins are problematic because they have the potential to scan many index rows without producing many result rows. The general promise we try to make with the Firestore service is that the work required to produce the result set scales with the result set, not the number of documents in the collection, i.e. that queries cost O(result set). Merge joins are a case where you can break your app with poor query performance.

A query like this one also uses a merge join by default:

db.collection("Projects")
    .whereField("name", isEqualTo: "Foo")
    .whereField("department", isEqualTo: "Bar")

For this query you can restore query performance to O(result set) by creating a composite index on (name, department). The same can't be done with a map trick query.

Another limitation of the map trick you're doing in your query is that it can't be combined with inequality filters or order-bys because you can't create a composite index. We specifically set out to build array-contains to allow people to create composite indexes for cases where you'd use the map trick today.

However, if we allow multiple array indexes in a composite index then we need the Cartesian product of index rows and that's the combinatorial explosion we're trying to avoid so we've disallowed it. We've taken the additional step of disallowing merge joins between array indexes to avoid needing to create that kind of index to keep queries running at O(result set).

All 4 comments

We don't support this currently because we're worried about the combinatorial explosion of index rows required to support this kind of query. We're not planning on adding support for this any time soon.

Usually you can work around this by fixing some of the parameters in other fields. For example, here you might take the current user.id and putting that into an owner field (though I'm not sure this is entirely what your example is trying to do).

Thanks @wilhuff. After creating the issue I guessed there could be some problems with too many similar queries. You are correct, there are ways around it.

@wilhuff Just out of curiosity. When you can do something like this to solve the problem why does it not have a combinatorial explosion issue?

Projects:
  P1:
   userIds: {
     "0": true,
     "1": true,
     "2": true
   }

Then you can query:

db.collection("Projects")
    .whereField("userIds.0", isEqualTo: true)
    .whereField("userIds.1", isEqualTo: true)

TL;DR: while that query works, it will perform poorly at scale. We don't want to allow that with array-contains.

The "map trick" query you're performing there is using the default single-field indexes and is performing a _merge join_ to produce the results. In this case we're scanning the index on userIds.0 concurrently with a scan on userIds.1 and return documents that are in both.

Merge joins are problematic because they have the potential to scan many index rows without producing many result rows. The general promise we try to make with the Firestore service is that the work required to produce the result set scales with the result set, not the number of documents in the collection, i.e. that queries cost O(result set). Merge joins are a case where you can break your app with poor query performance.

A query like this one also uses a merge join by default:

db.collection("Projects")
    .whereField("name", isEqualTo: "Foo")
    .whereField("department", isEqualTo: "Bar")

For this query you can restore query performance to O(result set) by creating a composite index on (name, department). The same can't be done with a map trick query.

Another limitation of the map trick you're doing in your query is that it can't be combined with inequality filters or order-bys because you can't create a composite index. We specifically set out to build array-contains to allow people to create composite indexes for cases where you'd use the map trick today.

However, if we allow multiple array indexes in a composite index then we need the Cartesian product of index rows and that's the combinatorial explosion we're trying to avoid so we've disallowed it. We've taken the additional step of disallowing merge joins between array indexes to avoid needing to create that kind of index to keep queries running at O(result set).

Was this page helpful?
0 / 5 - 0 ratings