How to Use Geospatial Data Type in Redis®
Introduction
Redis® is an open-source, in-memory data structure store. Its core data types include String
, List
, Hash
, Set
, Sorted Set
, HyperLogLog
, Bitmap
, and Geospatial
indexes. The Geospatial data type allows you to store location data in form of coordinates. When stored, you can use Geospatial indexes to query this data in different ways.
This article explains how to use the Geospatial data type to store and query coordinate data using a Vultr Managed Database for Caching. You will use Geospatial commands and build a sample application.
Prerequisites
To follow the instructions in this article, make sure you:
Deploy a Vultr Managed Database for Caching by following the Quickstart guide.
Deploy a Ubuntu 22.04 Vultr server to manage your connections.
Using SSH, access the server as a non-root sudo user.
Using Snap, install the Go environment on the server.
$ snap install go --classic
Install jq, a command line JSON processor.
$ sudo apt-get install jq
Install Redis® CLI.
$ sudo apt-get install redis
Geospatial Commands
In this section, explore the Redis® Geospatial commands and test them using the Redis® CLI tool as described below.
Using redis-cli
, connect to your Vultr Managed Database for Caching using a connection string.
$ redis-cli -u rediss://default:[DATABASE_PASSWORD]@[DATABASE_HOST]:[DATABASE_PORT]
Replace DATABASE_PASSWORD
, DATABASE_HOST
, and DATABASE_PORT
with the actual values from your connection string.
When connected you should access your Redis® Database console with the >
prompt.
Add Geospatial Data (GEOADD
)
The GEOADD
command is used to add longitude, latitude, and name to a specified key. Add the coordinates for London and Paris to a key named Cities
:
> GEOADD Cities -0.127758 51.507351 "London"
> GEOADD Cities 2.352222 48.856614 "Paris"
The following output is displayed for both cases:
(integer) 1
Retrieve Geospatial Data (GEOPOS
)
GEOPOS
retrieves the longitude and latitude of one or more elements from the Geospatial index represented by a specified key.
To get the Geospatial coordinates of London and Paris, run the following command.
> GEOPOS Cities "London" "Paris"
Output:
1) 1) "-0.12775629758834839"
1) "51.50735008547815141"
2) 1) "2.35221952199935913"
1) "48.85661473867625659"
Calculate Distance (GEODIST
)
Redis® can calculate the distance between two elements in a Geospatial index using the GEODIST
command. It supports various units for distance such as meters (m
), kilometers (km
), miles (mi
), and feet (ft
).
To calculate the distance between London and Paris in kilometers, run the command below.
> GEODIST Cities "London" "Paris" km
Output:
"343.6460"
Retrieve Nearby Elements (GEORADIUS
)
The GEORADIUS
command fetches elements located within a specified radius of a given point. This could be particularly useful for a location-based service where you might want to find places within a certain distance of a user's location.
Here's how to find cities within a 500 km radius of a point (longitude: 2.3522, latitude: 48.8566):
> GEORADIUS Cities 2.3522 48.8566 500 km WITHDIST WITHCOORD
Output:
1) 1) "Paris"
2) "0.0022"
3) 1) "2.35221952199935913"
2) "48.85661473867625659"
2) 1) "London"
2) "343.6467"
3) 1) "-0.12775629758834839"
2) "51.50735008547815141"
Retrieve Nearby Elements By Member (GEORADIUSBYMEMBER
)
GEORADIUSBYMEMBER
is similar to GEORADIUS
, but provides the longitude and latitude of an element, you can specify a member already stored in the Geospatial index. For example, to find cities within a 500 km radius of "Paris", run the following command.
> GEORADIUSBYMEMBER Cities "Paris" 500 km WITHDIST WITHCOORD
Output:
1) 1) "Paris"
2) "0.0000"
3) 1) "2.35221952199935913"
2) "48.85661473867625659"
2) 1) "London"
2) "343.6460"
3) 1) "-0.12775629758834839"
2) "51.50735008547815141"
Geospatial Hash (GEOHASH
)
GEOHASH
provides a Geohash string representing the position of one or more elements in the Geospatial index.
For example, to get the Geohashes for "London" and "Paris", run the command below.
> GEOHASH Cities "London" "Paris"
Output:
1) "gcpvj0dup10"
2) "u09tvw0f6t0"
Geospatial Search (GEOSEARCH
)
GEOSEARCH
is an upgraded version of GEORADIUS
and GEORADIUSBYMEMBER
with more flexible options. You can perform a radial search (like GEORADIUS
), member-based search (like GEORADIUSBYMEMBER
), or rectangular search by providing the southwest and northeast coordinates of a rectangle.
For example, to find cities within a 500 km radius of "Paris", run the command below.
> GEOSEARCH Cities FROMMEMBER "Paris" BYRADIUS 500 km
Output:
1) "Paris"
2) "London"
Geospatial Search and Store (GEOSEARCHSTORE
)
GEOSEARCHSTORE
is similar to GEOSEARCH
, but instead of returning the results, it stores them in a destination key.
To store the names of cities within a 500 km radius of Paris in a key named
NearbyCities
, run:> GEOSEARCHSTORE NearbyCities Cities FROMMEMBER "Paris" BYRADIUS 500 km STOREDIST
Output:
(integer) 2
NearbyCities
is a Sorted Set. Use the command below to query it.> ZRANGE NearbyCities 0 1 WITHSCORES
Your Output should look like the one below.
1) "Paris" 2) "0" 3) "London" 4) "343.64597544862346"
Exit the Redis® console.
> QUIT
Create a Sample Application
In this section, the sample application is an API for a service that allows users to find coffee shops close to them. It consists of the following endpoints.
- A REST endpoint to load location data for coffee shops: For simplicity, the store location loading is a one-time process that can be invoked on demand because it’s exposed as a REST endpoint. Once invoked, the shop data is read from a JSON file and saved in a Redis® hash (one for each shop).
- A REST endpoint to locate coffee shops: This endpoint provides the ability to search for stores within a given radius of a specific location (latitude and longitude). Let’s imagine that you are looking for coffee shops within five miles of your location. A web or mobile application would pass the coordinates of your current location (for example, latitude: 40, longitude: -83) and send an HTTP request to the back-end application, which looks similar to:
localhost:8080/search?lat=40&long=-83&dist=5
Initialize the Project
Create a new project directory.
$ mkdir redis-geospatial-coffee-shop
Switch to the directory.
$ cd redis-geospatial-coffee-shop
Create a new Go module.
$ go mod init redis-geospatial-coffee-shop
The above creates a new
go.mod
file in your directory that stores environment dependencies.Create a new file
main.go
.$ touch main.go
Using a text editor such as
nano
, open the file.$ nano main.go
Import Libraries
Add the following code to the main.go
file.
package main
import (
"bufio"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"github.com/redis/go-redis/v9"
)
Add the init
Function and Global Variables
Append the following code to the file.
var client *redis.Client
const fileName = "store_locations.json"
const geoName = "store-locations"
const hashNamePrefix = "store:"
func init() {
redisURL := os.Getenv("REDIS_URL")
if redisURL == "" {
log.Fatal("missing environment variable REDIS_URL")
}
opt, err := redis.ParseURL(redisURL)
if err != nil {
log.Fatal("invalid redis url", err)
}
opt.TLSConfig = &tls.Config{}
client = redis.NewClient(opt)
_, err = client.Ping(context.Background()).Result()
if err != nil {
log.Fatal("ping failed. could not connect", err)
}
fmt.Println("successfully connected to redis")
}
- The
init
function reads the Vultr Managed Database for Caching URL from theREDIS_URL
environment variable and fails if it's missing. - A new
redis.Client
instance is created, the connection to the Redis® database is verified using thePing
utility. If the connection fails, the program exits with an error message.
Add the load
HTTP Handler Function
Add the load
function to the file.
func load(w http.ResponseWriter, req *http.Request) {
fmt.Println("loading location data")
f, err := os.Open(fileName)
if err != nil {
fmt.Println("cannot file", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
var stores StoreData
err = json.NewDecoder(bufio.NewReader(f)).Decode(&stores)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
pipe := client.Pipeline()
for _, store := range stores {
err = pipe.GeoAdd(req.Context(), geoName, &redis.GeoLocation{Name: store.Name, Latitude: store.Position.Lat, Longitude: store.Position.Lng}).Err()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
parts := strings.Split(store.Name, " ")
storeid := parts[len(parts)-1]
storeHashName := hashNamePrefix + storeid
err = pipe.HSet(req.Context(), storeHashName, "name", store.Name, "address", store.Address, "phone", store.Phone).Err()
if err != nil {
fmt.Println("failed to add store data", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
_, err = pipe.Exec(req.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Println("geospatial data load complete")
}
The load
function is an HTTP handler that stores coffee location data in a Geospatial data structure and other metadata in a Redis® hash. In the above confuguration:
- The function reads the JSON file containing the coffee shop data.
- Contents of the JSON file are decoded into a
StoreData
type variable calledstores
using a new JSON decoder. The JSON decoder reads from a buffered reader that wraps the file. client.Pipeline()
creates a pipeline that allows you to send multiple commands to the Redis® database without waiting for replies, this improves the data loading performance in return.- For every data in the JSON file:
- Add the store's coordinates to the Redis® geospatial data store using
pipe.GeoAdd()
. - Split up the
store.Name
into parts by spaces, retrieve thestoreid
as the last part of the split name, and constructs thestoreHashName
as the concatenation ofhashNamePrefix
andstoreid
. - Add a new hash in Redis® using
pipe.HSet()
, withstoreHashName
as the key and the fieldsname
,address
, andphone
set to the corresponding properties ofstore
. - Execute all commands in the pipeline using
pipe.Exec()
.
- Add the store's coordinates to the Redis® geospatial data store using
Add Custom Types
Add the SearchResponse
and StoreData
structs to the file.
type SearchResponse struct {
Name string `json:"name"`
Distance float64 `json:"distance"`
Address string `json:"address"`
Phone string `json:"phone,omitempty"`
Lat float64 `json:"lat"`
Long float64 `json:"long"`
}
type StoreData []struct {
Position struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
} `json:"position"`
Name string `json:"name"`
Address string `json:"address"`
Phone string `json:"phone"`
}
- The
StoreData
struct represents the data in the JSON file. It contains the position, name, address, and phone of the store. - The
SearchResponse
struct represents the response to the search request. It contains thename
,distance
,address
,phone
,latitude
, andlongitude
of the store.
Add the search
HTTP handler function
Add the search
function to the file.
func search(w http.ResponseWriter, req *http.Request) {
_lat := req.URL.Query().Get("lat")
_long := req.URL.Query().Get("long")
_dist := req.URL.Query().Get("dist")
fmt.Println("searching for stores within", _dist, "miles of lat:", _lat, "long:", _long)
lat, _ := strconv.ParseFloat(_lat, 64)
long, _ := strconv.ParseFloat(_long, 64)
dist, _ := strconv.ParseFloat(_dist, 64)
stores := client.GeoSearchLocation(req.Context(), geoName,
&redis.GeoSearchLocationQuery{
GeoSearchQuery: redis.GeoSearchQuery{
Longitude: float64(long),
Latitude: float64(lat),
Radius: float64(dist),
RadiusUnit: "mi",
Sort: "ASC"},
WithCoord: true,
WithDist: true}).
Val()
var results []SearchResponse
for _, store := range stores {
fmt.Println(store.Name)
parts := strings.Split(store.Name, " ")
storeid := parts[len(parts)-1]
storeHashName := hashNamePrefix + storeid
storeInfo, err := client.HMGet(req.Context(), storeHashName, "address", "phone").Result()
if err != nil {
fmt.Println("failed to get store info for", storeHashName, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
results = append(results, SearchResponse{Name: store.Name, Distance: store.Dist, Address: storeInfo[0].(string), Phone: storeInfo[1].(string), Lat: store.Latitude, Long: store.Longitude})
}
err := json.NewEncoder(w).Encode(results)
if err != nil {
fmt.Println("failed to encode search response", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
The search
function is an HTTP handler that searches for coffee shops within a given distance from a given location.
- First, fetches latitude, longitude, and distance from the query parameters of the request.
client.GeoSearchLocation()
is used to search for stores within the given distance from the given location. TheGeoSearchLocation()
function returns a slice ofredis.GeoLocation
structs, each containing the name, distance, latitude, and longitude of a store.- For each store in the search results:
- Split up the
store.Name
into parts by spaces, retrieve thestoreid
as the last part of the split name, and construct thestoreHashName
as the concatenation ofhashNamePrefix
andstoreid
. - Retrieve the address and phone of the store from the Redis® hash using
client.HMGet()
. - Construct a
SearchResponse
struct using the store name, distance, address, phone, latitude, and longitude. - Add the
SearchResponse
struct to theresults
slice. - The
results
slice is encoded into JSON, write it to the response.
- Split up the
Add the main
Function
Add the main
function to file.
func main() {
http.HandleFunc("/load", load)
http.HandleFunc("/search", search)
fmt.Println("started http server")
log.Fatal(http.ListenAndServe(":8080", nil))
}
The main
function is the program's entry point. It registers the /load
, /search
endpoints, and starts an HTTP server that listens for requests on port 8080
.
Save and close the main.go
file to apply all changes.
Run the Program
Fetch the JSON file with the location data for coffee shops.
$ curl https://gist.githubusercontent.com/wf9a5m75/3d40f2d6814ef5f2bc4f75f61d86741e/raw/b05457e72fa261f7a31c0315eb0340faab454121/starbucks_us_locations.json -o store_locations.json
A
store_locations.json
file is added to your directory.Fetch the Go module dependencies for the program.
$ go get
Get your Vultr Managed Database for Caching URL and set it as below.
$ export REDIS_URL=rediss://default:[DATABASE_PASSWORD]@[DATABASE_HOST]:[DATABASE_PORT]
Replace
DATABASE_PASSWORD
,DATABASE_HOST
, andDATABASE_PORT
with your actual database values.Run the program in the background.
$ go run main.go &
Output:
successfully connected to redis started http server
Load the coffee shop's location data.
$ curl http://localhost:8080/load
When successful, your output should look like the one below in the application logs.
loading location data geospatial data load complete
Search for coffee shops within two miles of the latitude
40.01390852
and the longitude-83.01119585
.$ curl "localhost:8080/search?lat=40.01390852&long=-83.01119585&dist=2" | jq
Your JSON response should look like the one below.
[ { "name": "Starbucks - OH - Columbus [W] 06432", "distance": 0, "address": "Broadway & N. High_3416 North High Street_Columbus, Ohio 43202", "phone": "614-263-1292", "lat": 40.013907925850454, "long": -83.01119595766068 }, { "name": "Starbucks - OH - Columbus 06412", "distance": 0.399, "address": "Giant Eagle-Columbus #6512_2801 N High St_Columbus, Ohio 43202", "lat": 40.01957302764158, "long": -83.01265507936478 }, { "name": "Starbucks - OH - Columbus [WD] 06425", "distance": 1.7259, "address": "Lennox Station_1570 Olentagy Road_Columbus, Ohio 43123", "phone": "614-299-0625", "lat": 39.99160998381175, "long": -83.02587300539017 }, { "name": "Starbucks - OH - Columbus [WD] 06423", "distance": 1.9337, "address": "Fiesta Lane - Lane Ave._1315 W Lane Ave_Columbus, Ohio 43221", "phone": "614-485-1202", "lat": 40.006721991363726, "long": -83.04649919271469 } ]
As displayed in the output, the search query returns four coffee shops in increasing order of distance (in miles) from the search coordinates.
Change the search coordinates and increase the search radius to 10 miles:
$ curl "localhost:8080/search?lat=34.75911&long=-92.379005&dist=10" | jq
Your JSON response should look like the one below.
[ { "name": "Starbucks - AR - Little Rock [D] 00094", "distance": 0.0001, "address": "Rodney Parham & Treasure Hill_9401 N. Rodney Parham Rd_Little Rock, Arkansas 72227", "phone": "501-223-9177", "lat": 34.75910905295357, "long": -92.37900406122208 }, { "name": "Starbucks - AR - Little Rock 00090", "distance": 0.9364, "address": "Baptist Health - Little Rock_9601 Lile Drive_Little Rock, Arkansas 72205", "phone": "501-202-2000", "lat": 34.74577388493429, "long": -92.38191694021225 }, { "name": "Starbucks - AR - Little Rock [WD] 00095", "distance": 1.4906, "address": "Cantrell & Mississippi_7525 Cantrell Rd_Little Rock, Arkansas 72207", "phone": "501-603-0094", "lat": 34.77140498529753, "long": -92.35743373632431 }, { "name": "Starbucks - AR - Little Rock [W] 00096", "distance": 1.6182, "address": "Chenal & Financial Center_11401 Financial Centre Parkway, Suite D_Little Rock, Arkansas 72211", "phone": "501-219-2337", "lat": 34.7481108978432, "long": -92.40416318178177 }, { "name": "Starbucks - AR - Little Rock 00091", "distance": 1.9743, "address": "Kroger - Little Rock AR #608_16105 D Chenal Pkwy_Little Rock, Arkansas 72211", "lat": 34.752042250361335, "long": -92.41269260644913 }, { "name": "Starbucks - AR - Little Rock [W] 00098", "distance": 2.1933, "address": "University & W. Markham_201 N University Ave_Little Rock, Arkansas 72205", "phone": "501-664-4865", "lat": 34.75300797912305, "long": -92.34109908342361 }, { "name": "Starbucks - AR - Little Rock [W] 00097", "distance": 2.493, "address": "Kavanaugh & Pierce_5719 Kavanaugh Street_Little Rock, Arkansas 72207", "phone": "501-603-9230", "lat": 34.77022127051611, "long": -92.33723133802414 }, { "name": "Starbucks - AR - Little Rock [D] 00093", "distance": 3.3727, "address": "Hwy 10 & Sam Peck_12901 Cantrell_Little Rock, Arkansas 72227", "phone": "501-954-7596", "lat": 34.79799927970131, "long": -92.41489738225937 }, { "name": "Starbucks - AR - North LIttle Rock [D] 00099", "distance": 7.5621, "address": "JFK & McCain_4824 JFK_North LIttle Rock, Arkansas 72116", "phone": "501-812-0913", "lat": 34.799299591656045, "long": -92.25510209798813 }, { "name": "Starbucks - AR - North Little Rock 00100", "distance": 9.0706, "address": "Hwy 67 & McCain_4120 E. McCain_North Little Rock, Arkansas 72116", "phone": "501-955-3450", "lat": 34.78946740827897, "long": -92.22355931997299 }, { "name": "Starbucks - AR - Little Rock [A] 00092", "distance": 9.3397, "address": "LIT - Little Rock Simply Books_1 Airport Rd_Little Rock, Arkansas 71202", "lat": 34.727693718904725, "long": -92.21905320882797 } ]
In the above output, the search query returns multiple coffee shops in increasing order of distance (miles) from the search coordinates.
To see how the search results change, try other search coordinates and radius values to see how the search results change. To stop the application, verify the background job number, and kill the job id as below.
$ kill %1
Conclusion
You have used a Vultr Managed Database for Caching as the backend datastore for an application that allows you to store and query coffee shop data based on the location. You also covered the basics of how to use Geospatial commands such as GEOADD
, GEORADIUS
, and more. For more information, visit the following resources.