是否可以按位置和年龄范围查询 Firebase Firestore



如何根据年龄范围和位置查询我的 Firestore 数据库。

例如,假设我想获取年龄在 18 到 30 岁之间且距离我 5 英里范围内的所有用户。

这是我当前数据库结构的简化版本。

users
uid_0
age: 21
uid_1
age: 24

要按年龄参数过滤,我知道我可以这样做:

// Swift
db.collection("users")
.whereField("age", isGreaterThanOrEqualTo: 18)
.whereField("age", isLessThanOrEqualTo: 30)

对于位置,我已经阅读了Firebase的Geofire可以使用,这将添加一个额外的节点,例如:

_geofire
uid_0:
g: "asdaseeefef"
l: 
0: 52.2101515118818
1: -0.3215188181881
uid_1:
g: "oposooksok"
l: 
0: 50.1234567898788
1: -0.8789999595988

但是我不确定如何在我原来的性别+年龄查询之上添加位置查询(tbh 我不确定如何单独进行位置查询)。但是关于将两者结合起来,我主要担心的是 Firestore 文档指定您只能在一个字段上应用范围过滤器,而我已经为age字段应用了一个范围过滤器。

是否可以在按位置过滤的同时按年龄范围过滤?

Firestore(以及许多其他NoSQL数据库)只能在单个字段上执行关系条件,例如isGreaterThanOrEqualTo/isLessThanOrEqualTo/startAt/endAt,因为它们的索引在幕后的工作方式。

为了允许您对此类数据库执行地理查询,GeoFire 等库使用所谓的 GeoHash 值,该值神奇地将两个纬度和经度值合并为一个值(数据结构中的g),您可以对其执行范围条件。这真的很神奇。几年前我做过一个关于这个话题的演讲,我强烈建议你看看:地理查询Firebase和Firestore。


现在,如果您想筛选另一个属性(例如 age),则必须找到一种方法,将 age 的值表示为具有经度和纬度的单个类型,以便一次性筛选所有三个值。所以你必须想出一个GeoHashAndAge类型,它(虽然绝对有趣)似乎有点超出我们大多数人愿意经历的。


不幸的是,这只剩下两个选择:您可以预先过滤数据或后过滤数据。

预筛选意味着您可以向每个文档添加一个或多个字段,以便执行必要的年龄筛选器,而无需关系条件。例如,如果您的应用程序中的用例是您希望用户超过 18 岁,请向每个文档添加一个具有布尔值的字段isOver18,您可以使用相等性检查对其进行筛选,该检查可以与地理哈希上的范围筛选器结合使用。这可能不适用于所有用例,但在可能的情况下,它允许您将过滤留给数据库。

后过滤是最简单的:您只需在从 Firestore 检索文档后在应用程序代码中执行年龄过滤即可。这总是有效的,但当然意味着您正在阅读更多需要的文档。

根据 van Puffelen @Frank 的回答,我认为最好的解决方案(至少在需要扩展的应用程序的情况下)是预过滤选项,因为我们可以将数据库存储和读取成本保持在最低水平(在下面的最佳解决方案标题下解释。

其他选择的缺点

后滤波缺点:

由于我们(理论上)可以在 100 英里半径内拥有数百万用户,因此过滤后年龄选项需要一次获取所有数据,这意味着我们无法利用 Firebase 的成本/时间节省.limit(to:)方法,事情会变得昂贵、耗时、快速。

GeoAgeHash 缺点:

弗兰克GeoAgeHash的想法很有趣,理论上也是可行的,但我也不认为这在经济上是可行的,因为 - 就像 GeoHash 需要多达 9 个查询一样——通过添加年龄的第 3 维度(前两个是纬度和经度),它会将单次搜索的查询数量提高到潜在的 9 * 82(假设您查询年龄在 18 到 100 之间的用户,导致 82 个选项)。这将很快耗尽您每天 50K 读取的 Firestore 限制。

最佳解决方案

预过滤:

编辑:事实证明,此解决方案也存在重大缺陷,因为Firestore需要为每个标签(如下)创建一个复合索引,因为我们对Geohash使用范围子句,因此接近允许的200个标签的限制。如果您也按其他项目进行查询,则每个组合都需要一个复合索引,这可能会使您失望,更不用说创建那么多索引非常耗时。

话虽如此,我认为预过滤选项是最好的。对于精确的年龄过滤,您只需要确保为每个可能的年龄都有一个标签。

例如:isOver18isOver19isOver20...标记以查询最小期限约束,然后isUnder100isUnder99,一直向下查询年龄上限约束。

实际上,更现实的是,您应该存储出生日期而不是年龄(因为需要每天检查年龄并可能更新以保持准确)。

因此,出于查询目的,您将存储出生年份标签而不是年龄标签,例如出生年份isOver1900isOver1901和向上,然后是isUnder2004isUnder2003和向下。

简而言之,每个用户文档将需要 82 个isOver标签和 82 个isUnder标签,假设您计划在 18 到 100 岁之间进行查询(这会导致 82 个不同的年龄)。

示例数据库结构

Users Collection
userDoc1
birthDate: TimeInterval // I like to use TimeIntervalSince1970
isOver1900: Bool
isOver1901: Bool
isOver1902: Bool
...etc...

isUnder2004: Bool
isUnder2003: Bool
isUnder2002: Bool
...etc...
userDoc2
...
...

示例用法

// Assuming it's 2022, and you want to query users between the ages of 24 and 30
db.collection("Users")
.whereField("isUnder1998", isEqualTo: true)  // filters >= 23
.whereField("isOver1991", isEqualTo: true)   // filters <= 31
// Note: we must filter one year extra for the `isOver` clause because technically there could be some people born at the end of 1991 that are still 30, depending on what time of the year the query is made. These edge cases can easily be filtered out client-side.

相关内容

  • 没有找到相关文章

最新更新