How can I query my Firestore database based on age range and location.
For example, let's say I want to get all users that are between the age of 18 and 30, and are within 5 miles me.
Here's a simplified version of my current database structure.
users
uid_0
age: 21
uid_1
age: 24
To filter by age parameters, I know I can do this:
// Swift
db.collection("users")
.whereField("age", isGreaterThanOrEqualTo: 18)
.whereField("age", isLessThanOrEqualTo: 30)
For location, I've read geofire can be used, which would add an additional node such as:
_geofire
uid_0:
g: "asdaseeefef"
l:
0: 52.2101515118818
1: -0.3215188181881
uid_1:
g: "oposooksok"
l:
0: 50.1234567898788
1: -0.8789999595988
But I'm unsure how to add a location query on top of my original gender age query (tbh I'm uncertain how to make a location query even by itself). But in regards to combining the two, my main concern is that the Firestore docs specify that you can only apply a range filter on one field, and I already am applying one for the age
field.
Is it possible to filter by location while also filtering by an age range?
I also looked into Firestore's geo queries which looked somewhat promising, but was rather confusing to follow, so I'm unsure if this would have the same limitations.
CodePudding user response:
Firestore (and many other NoSQL databases) can only perform relational conditions, such as isGreaterThanOrEqualTo
/isLessThanOrEqualTo
/startAt
/endAt
, on a single field, due to how their indexes work behind the scenes.
To allow you to perform geoqueries on such a database, libraries such as GeoFire use so-called GeoHash values, which magically merge the two latitude and longitude values into a single value (the g
in your data structure) that you can perform a range condition on. It's quite magical really. I did a talk on the topic a few years ago, which I highly recommend checking out: Geo-querying Firebase and Firestore.
Now if you'd also like to filter on another property such as age, you'd have to figure out a way to express the value of age into a single type with the longitude and latitude in a way that all three values can be filtered in one go. So you'd have to come up with a GeoHashAndAge type, which (while definitely interesting) seems a bit beyond what most of us are willing to go through.
That unfortunately leaves you with only two options: you can either pre-filter the data or post-filter it.
Pre-filtering means that you add one or more fields to each document that allow you to perform the necessary age filter without needing a relational condition. For example, if a use-case in your app is that you want people over 18, add a field isOver18
to each document with a boolean value, and you can filter on that with an equality check, which can be combined with the range filter on the geohash. This may not be possible for all use-cases, but when it is possible it allows you to leave the filtering to the database.
Post-filtering is simplest: you just perform the age filtering in your application code after retrieving the documents from Firestore. This always works, but of course means that you're reading more documents that are needed.
CodePudding user response:
In order to use geoqueries you have to store the geohash instead of the latitude and longitude to be able to query it.
// Compute the GeoHash for a lat/lng point
let latitude = 51.5074
let longitude = 0.12780
let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
let hash = GFUtils.geoHash(forLocation: location)
//Then store 'hash' in the user document
And then if you store it in the same user document where you have the age field the final query should look something like this.
// Find users within 5km of me.
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278) //Your position
let radiusInM: Double = 5 * 1000 //You may need to convert it to meters if you have it in miles
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
let queryBounds = GFUtils.queryBounds(forLocation: center,
withRadius: radiusInM)
let queries = queryBounds.map { bound -> Query in
return db.collection("users")
.whereField("age", isGreaterThanOrEqualTo: 18)
.whereField("age", isLessThanOrEqualTo: 30)
.order(by: "g")
.start(at: [bound.startValue])
.end(at: [bound.endValue])
}