Firestore搜索数组包含多个值

49

如果我有一个简单的集合,比如:

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

如果我想搜索含有维生素B6的水果,我会执行以下操作:

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())")
            }
        }
    }

那将展示我收藏的所有含有维生素A的水果。然而,如果我想搜索含有多种维生素的水果,比如维生素B6和C,该怎么办?我不能简单地搜索["B6","C"],因为它会寻找一个数组而不是独立的字符串。

在Cloud Firestore中是否可能实现呢?如果不行,是否有任何替代方法?

6个回答

72

将查询条件链接起来,寻找符合多个条件的文档似乎很诱人:

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

但是复合查询文档表明,您目前无法在单个查询中使用多个arrayContains条件。

因此,现有的方案无法实现。考虑改用映射结构而不是数组:

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

然后你可以像这样查询:
db.collection("Fruits")
    .whereField("vitamins.B6", isEqualTo: true)
    .whereField("vitamins.C", isEqualTo: true)

1
谢谢,这很有帮助。看起来我仍然可以用这种方法完成所有事情。 - Lucas Azzopardi
14
@LucasAzzopardi 这里唯一的缺陷是Firestore不允许超过200个复合索引,这意味着如果你只对维生素执行复合查询,那么应该没问题,因为维生素并不会有很多种。然而,如果有100种维生素,并且你执行需要每个查询都需要一个复合索引的复合查询,那么你很快就会达到上限,因为每个复合索引都必须指定特定的维生素。你不能简单地在复合索引中包含“vitamins”,而必须是“vitamins.potassium”、“vitamins.b6”等。 - trndjc
1
@DougStevenson 您说得对,我们不知道 OP 是否执行了复合查询,但是如果此查询被合并并且需要复合索引,则他必须为每种特定的维生素创建一个索引。这是在数据模型扩展之前现在要考虑的事情。 - trndjc
1
如果OP需要在复合查询中执行此检查,则会出现问题。例如,如果OP想要查询具有B6的水果,并且糖含量大于X且小于Y,则他需要为该查询创建一个复合索引,如果有50种维生素,则他需要50个复合索引,占据了他全部批次的25%。 - trndjc
3
这是个不错的解决方案。但我尝试使用它时立刻遇到了@bsod提到的复合索引问题。由于我动态生成这些字段,所以没有办法为所有可能的字段创建索引。如果有任何绕过此问题的方法,请告诉我!我只是把一个问题换成了另一个问题。 - MadMac
显示剩余8条评论

10

Firestore在2019年底推出whereIn、arrayContains和arrayContainsAny方法。这些方法执行逻辑“OR”查询,但您可以传递的子句数量有10个限制(所以最多只能搜索10种维生素)。

一些解决方案已经提到了重组数据的方法。利用重组方法的另一个解决方案是不将维生素包含在水果文档中,而是反过来。这样,您就可以得到所有'Banana'所属的文档。

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

这个解决方案可以让您突破10个从句的限制。它通过一个读取操作获取了所有在其“水果”数组中具有“香蕉”的“维生素”文档(如果太多,请考虑分页)。


6
在Firestore中,无法进行多个数组包含查询。但是,您可以尽可能多地组合许多“where”条件。因此,请考虑以下内容:
[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)

然而,这仍然不能实现干净的OR搜索。要查询含有维生素x或维生素y的水果,您需要更有创意。此外,如果存在许多可能的值并且需要结合复合查询使用,则不应使用此方法。因为Firestore不允许超过200个复合索引,而每个复合索引都需要指定确切的维生素。这意味着您必须为维生素.b6维生素.钾等创建单独的复合索引,而您总共只有200个。

有没有其他的方法或数据库可以允许进行“或”搜索,而不仅仅是“与”搜索? - Lucas Azzopardi
太棒了,谢谢你,这很有帮助。Firestore显然还处于测试阶段,因此毫无疑问会进行许多改进。 - Lucas Azzopardi
如果你不事先知道水果,就无法创建索引,也就无法按照其他字段进行排序。 - Jonathan

3

在Firestore中,无法使用多个arrayContains查询Firestore数据库。如果您使用了多个,则可能会出现以下错误:

无效查询。查询仅支持具有单个array-contains筛选器。

如果您需要过滤多个维生素,则需要更改数据库结构的逻辑,通过创建每个单独维生素的属性,并连锁使用.whereField()函数调用。我知道这听起来有点奇怪,但这就是Cloud Firestore的工作方式。

您的模式应该如下所示:

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

为了查找所有含有钾、B6和C维生素的水果,您应该使用类似以下查询的方式:
let fruitsRef = db.collection("Fruits")
    .whereField("vitamins.potassium", isEqualTo: true)
    .whereField("vitamins.B6", isEqualTo: true)
    .whereField("vitamins.C", isEqualTo: true)

有没有办法使它变得动态化?也就是说,如果我每次搜索不同的维生素,是否有一种方法可以在不硬编码的情况下进行搜索?在这种情况下,每次我都必须搜索确切的3种维生素。 - Lucas Azzopardi
请参考道格的建议。 - Alex Mamo

3

这个问题已经有人提过评论了,但是被接受的答案不适用于需要创建复合索引的查询,因为它很可能会导致您的复合索引计数快速达到200的限制。虽然有些取巧,但我能想到的最好的解决方案是按某种保证顺序 - 比如按字母顺序 - 存储给定水果每种维生素的所有组合的数组,每个组合都作为字符串连接起来。例如,由于香蕉含有B6、C和钾,香蕉的维生素组合数组将是:

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

然后,如果您的用户正在搜索每种同时含有B6和钾的水果,您的查询将是:

db.collection("Fruits").where("vitamins", "array-contains", "B6||potassium")

1
我认为我找到了一个有效的方法,可以避免为映射值创建所有索引。
如果您想从值数组中创建所有可能的数组组合
[a, b] => [[a],[b],[a,b]]

你可以使用逗号或其他分隔符对它们进行排序和连接。
[[a],[b],[a,b]].map => ['a','b','a,b']

然后存储字符串数组(必须排序),并使用已排序的分隔搜索查询执行array-contains

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

好奇我是否漏掉了什么


我喜欢这个答案,因为你实际上是在创建自己的索引。但是,你必须事先知道所有的值和组合,或者让一个FB函数在每次添加时生成它们。 - Jonathan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接