Cloud Firestore Geo Queries
A voyage through the options for querying Cloud Firestore by geographic proximity or map bounds.
I teach a web development university course. During the course, students build an Airbnb like web application with Firebase. Every year there is one technical challenge that gets these new devs hung up when using Cloud Firestore.
How to query for nearby geo-coordinates in Cloud Firestore? The use case is always the same, display a map, and show all of the rooms for rent either around the center of the map or inside the entire bounds of the visible map.
This article presents the same solutions that I present to my students:
- KISS and forget: just get the entire collection and filter in memory
- 1-D query: limit the query by the longitude and then filter by latitude in memory
- Use geohashes!
- Use a 3rd party search tool that supports geo-searching, like Algolia
There is no perfect solution. Like most programming problems, the perfect solution depends on the specifics of your project, your users, and your business constraints.
Let’s take them 1 by 1…
KISS and forget (about scaling)
Being a developer for over 13 years, 8 of them using Agile methodologies, you learn to balance quality, complexity, performance, and functionality. This means weighing every feature or line of code against the value it will bring to your users and the time it will take to implement and maintain.
The KISS principle tells us the simplest solution is always the best solution. Why not just download the entire collection and filter by longitude and latitude in memory? Until you have thousands of documents, the performance costs will be negligible.
Bonus 🚀
There is an easy performance improvement too; if the user enters a city or postal code we can optimize by adding a city or postal code constraint to the Cloud Firestore queries. This optimization certainly isn’t ideal for an application with a map that can pan anywhere in the world, but it is a viable solution that should be considered because of its simplicity.
This is what I tell my students: until you have thousands of documents to query from Cloud Firestore, even a slow portable phone can work through an array of a thousand documents in memory. Never forget the simple solution. The YAGNI principle tells us to add functionality when you actually need it. I consider this solution the perfect starting point.
1-D query
We can take our KISS solution and make an easy performance enhancement. Cloud Firestore will let us use less-than and greater-than operators on a single document field in a complex query. Therefore, we can include the minimum and maximum longitude visible on the map in the Cloud Firestore query. Once the results are fetched from Cloud Firestore, we can then filter the latitude in memory.
Depending on your data, this could give you approximately a 50% performance and memory consumption improvement over the KISS solution. Boom 💥! This represents a great compromise solution before exploring more complex solutions.
Once again, if we follow KISS and YAGNI principles, this solution can be a step along the way to a more complex and scalable solution.
Geohashes
This is the most complex solution. The reward for that complexity is a robust and scalable solution without requiring any 3rd party search services.
First, what is a geohash?
It’s a fancy way to encode longitude and latitude (don’t worry there’s an excellent npm package that takes care of the heavy lifting). When treating longitude and latitude as a grid, encoding with geohashes lets us control the size of the grid rectangles with the length of the hash (the longer the hash, the smaller the rectangle). Furthermore, it is encoded in a way that the closer two rectangles are on the earth, the more likely they will have similar geohashes.
OK, so how does that help?
Using some math and approximation, we can draw big rectangles around a geographic point. Then we can query Cloud Firestore for documents that have hashes inside those rectangles. This means that in the worst case, you will need to do 9 queries to Cloud Firestore to fetch all of the documents to populate the map. But there is good news since the hashes are likely similar. If we get lucky, some of them are consecutive and we can query Cloud Firestore with a range to reduce the number of required queries. Often we can optimize down to 4 to 6 range querries.
Geohashes come with baggage 🛅: being a grid that is based on longitude and latitude, the rectangles are neither square nor regular in their geometric area. The area of rectangles gets smaller the farther they are from the equator. All of that means that we generally do a lot of approximation (coding for the worst case). The result is that in most cases you will query for a substantially larger area than will be shown to the user.
The good news is that geohashes is a proven technique and is natively supported by several databases (see MongoDB spatial indexes, ElasticSearch geo_point, and Redis GeoHash). While we need to write some code to use geohashes, the result is perfectly compatible with Cloud Firestore.
Algolia and other 3rd party search services
I call this the most “cloud-first” solution. Why roll our own solution when we can pay someone else to use their service? Algolia lets us do just that. Plus, just like Firebase, the generous monthly free usage is most likely enough to get your app off the ground and in the hands of users without any out of pocket costs 💲!
It works like this… you add a Cloud Function that will keep Algolia synchronized with the data in Cloud Firestore that you want to be able to geo search. The Cloud Function gets triggered whenever a document containing geo coordinates is written (or deleted). In turn, it ensures that Algolia gets synched with the change.
In your web application, you can directly query Algolia using either a radius around the center point of the map or a with the bounding box of the map. Algolia takes care of all of the hard work! Plus you can add enough data into the search results that you don’t need to query Cloud Firestore at all while the map is panning and zooming 🔍.
This solution is fast, scales, and is much simpler than geo-hashes.
So what are the downsides of Algolia?
The first point is subjective: you will be relying on yet another service provider for your application. Sure the free plan is more than generous enough to get you started, but it is one more provider to monitor and potentially pay for if your app starts to have success (there are worse problems to have 😉).
The second point is that to use a Cloud Function to keep Algolia synchronized with Cloud Firestore, you will need to pull out your credit card 💳 and sign up for the Firebase Blaze plan so that your Cloud Function can call APIs outside of Google Cloud. For that reason alone, I don’t encourage my students to go down this path. However, it is important to note that even after entering your credit card, you still get a generous amount of free outgoing network traffic (5GB at the time of writing). That means this solution probably won’t cost you a penny while you get your app off the ground.
Conclusion
The point that I try to make to the university students is that, as with most things in software development, (nearly) anything is possible. However, we all work with constraints (time, money, management, etc…).
While at first glance, many students are disappointed in the lack of built-in geo-searching capabilities of Cloud Firestore. However, upon deeper investigation, we find that all of the tools necessary to build a first-rate geo-based application are at our fingertips. If you want your Firebase application to mimic the functionality of AirBnB or Google Maps, you can do it.
Just don’t jump into the most complex solution without considering the simpler solutions first.