48

If I have a simple collection such as:

Fruits:
  Banana:
   title: "Banana"
   vitamins: ["potassium","B6","C"]
  Apple:
   title: "Apple"
   vitamins: ["A","B6","C"]

And if I want to search fruits that contain vitamin B6, I would do the following:

db.collection("Fruits").whereField("vitamins", arrayContains: "A").getDocuments() { (querySnapshot, err)  in
        if let err = err {
            print("Error getting documents: \(err)")
        } else {
            for document in querySnapshot!.documents {
                print("\(document.documentID) => \(document.data())")
            }
        }
    }

And that would show me all the Fruits in my collection that contain the vitamin A. However, how would I be able to search fruits that contain multiple vitamins, say vitamins B6 and C? I cannot simply search ["B6","C"] as it would then be looking for an array as opposed to independent strings.

Is this even possible in Cloud Firestore? If not is there any alternative way to do this?

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
Lucas Azzopardi
  • 1,131
  • 2
  • 11
  • 21

6 Answers6

72

It would be tempting to look for documents that match multiple conditions, by chaining the conditions in the query:

db.collection("Fruits")
    .whereField("vitamins", arrayContains: "B6")
    .whereField("vitamins", arrayContains: "C")

But the documentation on compound queries suggests that you cannot currently have multiple arrayContains conditions in a single query.

So, it is not possible with what you have today. Consider instead using a map structure instead of an array:

Fruits:
  Banana:
   title: "Banana"
   vitamins: {
     "potassium": true,
     "B6": true,
     "C": true
   }

Then you could query like this:

db.collection("Fruits")
    .whereField("vitamins.B6", isEqualTo: true)
    .whereField("vitamins.C", isEqualTo: true)
abhay tripathi
  • 3,547
  • 4
  • 20
  • 25
Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • 1
    Thank you this was helpful it seems I will still be able to do everything with this method – Lucas Azzopardi Mar 04 '19 at 17:03
  • 14
    @LucasAzzopardi the only pitfall here is that Firestore does not permit more than 200 composite indices, which means that if you're performing compound queries just on vitamins, you should be fine because there aren't hundreds of vitamins. However, if there were 100 vitamins, for instance, and you performed compound queries that required a composite index for each query, then you would max out quickly because each composite index must specify the specific vitamin. You cannot simply include `vitamins` in the composite index, it would have to be `vitamins.potassium`, `vitamins.b6`, etc. – trndjc Mar 04 '19 at 17:11
  • @bsod Are you sure that a composite index is required here? https://firebase.google.com/docs/firestore/query-data/index-overview#composite_indexes – Doug Stevenson Mar 04 '19 at 17:56
  • 1
    @DougStevenson you're right that we don't know if OP is performing a compound query but in the event that this query is compounded and a composite index is required, he would have to create an index for each specific vitamin. And that's something to consider right now before his data model scales. – trndjc Mar 04 '19 at 18:02
  • @bsod The query I showed should not require a composite index, as it is only checking for equality. So it should be a non-issue, unless I missed something. – Doug Stevenson Mar 04 '19 at 18:09
  • 1
    @DougStevenson IF the OP needs to perform this check in a compound query, then it becomes an issue. For example, if the OP wanted to query fruits with B6 with a sugar content greater than X and less than Y, then he would need a composite index for that query and if there were 50 vitamins, he would need 50 composite indices, 25% of his entire lot. – trndjc Mar 04 '19 at 18:12
  • Is there any way to make this dynamic. As in, if I am searching for different vitamins each time, would there be a way in which I can search for them without hardcoding it in? In this scenario, I would have to be searching for exactly 3 vitamins each time – Lucas Azzopardi Mar 05 '19 at 03:57
  • https://stackoverflow.com/questions/48036975/firestore-multiple-conditional-where-clauses – Doug Stevenson Mar 05 '19 at 04:11
  • Thanks so much for this! I was under the impression you could pass in an array of strings to search, but no, you can only pass in one item. – Supertecnoboff Sep 30 '19 at 20:49
  • @Alex Mamo also suggest me https://stackoverflow.com/questions/59658641/wherearraycontains-limit-to-10 – Zar E Ahmer Jan 09 '20 at 07:23
  • 3
    This is good solution. But I tried it and straight away ran into the compound index issue that @bsod mentioned. As I generate these fields dynamically there is no way I can create indexes for all of the possible fields. If there is any way around this then please let me know! I have just replaced one problem for another. – MadMac Aug 27 '20 at 04:20
  • what about android? –  Jun 03 '21 at 15:38
  • https://dev.to/jdgamble555/firestore-many-to-many-part-2-array-contains-all-46p9 – Jonathan Aug 28 '21 at 04:58
10

Firestore introduced the whereIn, arrayContains and arrayContainsAny methods in late 2019. These methods perform logical 'OR' queries, but have a limit of 10 clauses you can pass in (so max 10 vitamins you can search for).

Some solutions already mentioned restructuring your data. Another solution leveraging the restructuring approach is to not include the Vitamins into the Fruit document, but the other way around. This way you get all the documents 'Banana' is part of.

let vitaminsRef = db.collection('Vitamins').where('fruits', arrayContains: 'banana');

This solution allows you to circumvent the limit of 10 clauses. It gets all the 'Vitamin' documents that have 'Banana' in their 'Fruits' array in one read operation (think about pagination, if too many).

friartuck
  • 468
  • 5
  • 6
6

There's no way to do multiple array-contains queries in Firestore. You can, however, combine as many where conditions as practically possible. Therefore, consider this:

[Fruits]
    <Banana>
        title: "Banana"
        vitamins:
            potassium: true
            b6: true
            c: true

Firestore.firestore().collection("Fruits").whereField("vitamins.potassium", isEqualTo: true).whereField("vitamins.b6", isEqualTo: true)

However, this still doesn't allow a clean OR search. To query for fruits with vitamin-x or vitamin-y, you'd have to get more creative. Furthermore, this approach should not be used if there are a lot of possible values and they need to be combined with compound queries. That's because Firestore does not permit more than 200 composite indices and each composite index would need to specify the exact vitamin. Which means you'd have to create an individual composite index for vitamins.b6, vitamins.potassium, etc. and you only have 200 total.

trndjc
  • 11,654
  • 3
  • 38
  • 51
  • Is there any alternative method or database that does allow for an 'or' search as opposed to just an 'and' search? – Lucas Azzopardi Mar 06 '19 at 16:18
  • Awesome thank you this is helpful. Firestore is obviously in beta so there is no doubt that many improvements will be made – Lucas Azzopardi Mar 06 '19 at 16:47
  • You also can't sort by a different field without creating an index first (which you can't do if you don't know the fruits before hand) – Jonathan Aug 10 '21 at 22:08
3

There is no way in Firestore to query a Firestore database with more than one arrayContains. If you will use more than one, an error like this might occur:

Invalid Query. Queries only support having a single array-contains filter.

If you need to filter on more than one vitamins, you'll need to change the logic of structuring your database by creating a property for each individual vitamin that you have and then chain .whereField() function calls. I know it sounds a little weird but this is how Cloud Firestore works.

Your schema should like this:

vitamins: { 
    "potassium": true,
    "B6": true,
    "C": true
}

To find all the fruits with potassium, B6 and C vitamins, you should use a query that looks like this:

let fruitsRef = db.collection("Fruits")
    .whereField("vitamins.potassium", isEqualTo: true)
    .whereField("vitamins.B6", isEqualTo: true)
    .whereField("vitamins.C", isEqualTo: true)
Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • Is there any way to make this dynamic. As in, if I am searching for different vitamins each time, would there be a way in which I can search for them without hardcoding it in? In this scenario, I would have to be searching for exactly 3 vitamins each time. – Lucas Azzopardi Mar 05 '19 at 00:37
  • Please see Doug's recommendation. – Alex Mamo Mar 05 '19 at 08:57
3

It has been commented upon, but the accepted answer isn't feasible if your query requires you to create a composite index, because it'll likely lead to your count of composite indexes quickly reaching the limit of 200. It's hacky, but the best solution I can think of is to store - in some kind of guaranteed order, e.g. alphabetically - an array of every combination of vitamin for a given fruit, each combination being concatenated as a string. For example, as bananas contain B6, C, and potassium, an array of vitamin combinations for bananas would be:

  • B6
  • B6||C
  • B6||C||potassium
  • B6||potassium
  • C
  • C||potassium
  • potassium

Then, if your user was searching for every fruit that contains BOTH B6 AND potassium, your query would be:

db.collection("Fruits").where("vitamins", "array-contains", "B6||potassium")
nb1987
  • 1,400
  • 1
  • 11
  • 12
1

I think I found a way that works well and avoids creating all the indexes for map values

If you create all array combinations possible from an array of values

[a, b] => [[a],[b],[a,b]]

You can sort and join these with a comma or some other delimiter

[[a],[b],[a,b]].map => ['a','b','a,b']

And then store the array of strings (must be sorted) and do array-contains using a sorted, delimited search query

where('possible_value_array', 'array-contains', 'a,b')

Curious if I'm missing something here

Steven Kaspar
  • 1,147
  • 10
  • 14
  • I like this answer, as you're essentially creating your own index. However, you have to know all values and combinations before hand, or have a FB function generate them on each addition. – Jonathan Aug 10 '21 at 22:09