How To: Geo-queries in firestore v9

How To: Geo-queries in firestore v9

A simple and straightforward introduction to geolocation queries in firebase.

Aim: To query our firestore collection of documents that have Geopoint data and return results based on proximity to users' location.

Motivation:

Consider the following scenario, we have an application that needs to display restaurants nearby a user's location. We only want to retrieve restaurants that are in proximity to the user. This proximity is defined by "us", the developer.

If we were using a Postgres database, using PostGIS extension we can easily do many different types of geo-queries. In firebase, when leveraging firestore database we need to use a few "tricks". This article will explore a specific implementation of this "nearby" or "near to us" feature we find in many modern location-driven applications that we use today.

Setup:

Let's assume we are developing a nextjs web application, using firebase for our 'Backend-as-a-service" platform.

We will need the following dependencies in our package.json for doing geo-queries:

"ngeohash": "^0.6.3",
"geolib": "^3.3.3",

Here we are using ngeohash to create unique hash strings based on the geographic latitude and longitude coordinates of the restaurants, also geolib will help us convert distances into different units (imperial, metric etc)

Implementation:

First, let's take a look at the UI to get a clear picture. Here the "red" circle is to highlight the marker of the user and the "green" circle is to highlight the restaurant marker. In other words, the red marker is the user's current location.

In our firestore collection, we have n-items but we are returning only those restaurants which are geographically within a 10km radius around the user's location i.e the red-maker.

Now, to implement this functionality we need the following:

  • Track and update the user's geo coordinates, and make them available to the whole next app.

  • Create a bounding box around the user's location, this is the location constraint for the results we are getting from our firestore collection of "restaurants" in this case.

Let's take look at Tracking and updating user's live location

This code snippet is a custom hook useGeoLocation we have created it using the Location API of the browser. This API is widely available and has good accessibility across all browsers and versions. It returns pos:{latitude, longitude} if the user has granted us relevant permission in the browser, otherwise it will return error and we are also returning the loading state.

Then in the _app.tsx file within our nextjs project we are using this data like so, we then can access this longitude and latitude in all pages and components through the coords prop.

Now, let's take a look at how to store data in firestore, so that we can later query it based on location data i.e geo-query. Below is a snippet of a document in the "restaurants" collection. Take note of the two highlighted fields restaurantGeoCoords and restaurantGeohash, when saving any document which we later want to perform geo-query we must have these two fields, the names can be different but the types are Geopint and String.

To see how to create Geopoint in firebase, you can take a look at this for reference. Now to create a Geohash we are using the ngeohash library. To create a Geohash we can do this,

This encoding is handled by the library and you can read more about it here.

After this, let's now look at how to retrieve data based on our user's current location. For this, let's take a look at how to create a bounding box around the user's location like so,

This code snippet getGeoHashRange is helping us create a bounding box, it accepts longitude, latitude and r as arguments. The latitude and longitude are the user's and the r is a radius around the user's location. We are returning the following object {upper, lower} which is 2 hash values, one is the upper limit and the other is the lower limit. Here, the hashes represent a range of longitude and latitude values that are acceptable for the user's location.

Now, we need to use this and query firestore to return relevant data based on this geolocation data. We can do this like so,

In the above code snippet, we are getting the range object to create our bounding box. Then within our fetchRestaurants we are building our query, here you can see how we are using the where clause and using the range along with restaurantGeohash of the documents in the firestore collection to filter or constraint our results. Finally, it is worth mentioning that there are edge cases and some unwanted documents may pass the geo-query constraints due to the way bounding boxes work in this approach, for this reason, we have another higher-order helper of filter on our restaurant data that you can see has the relevant comment before it. This is necessary to filter out and avoid unwanted results due to any edge cases.

Conclusion:

This is one approach to doing geo-queries in your application with collections that have geopoint data and geohash set as a field when using firestore database. This is not the only approach and many optimizations can be made, enjoy exploring this topic further and hope you can build this feature out with ease using this article as a starting point.