Golang and MongoDB with go-mongo-driver — Part 3: Geospatial Queries

Orlando Monteverde
Glottery
Published in
6 min readSep 30, 2019
MongoDB

Requirements

Should I read this post?

The short answer is yes. OK, now seriously, this post assumes that you has used Go (Golang) and you have some knowledge of MongoDB. This is the third part of the post about go-mongo-driver, an alternative to the well-known mgo. Well, this time we’re going to try the geospatial queries. If this interests you, well, go ahead!

NOTE: If you already know how go-mongo-driver works and just want to learn about geospatial queries, you can go to the Starting section.

Prepare the workspace

For this project, we will use the go modules, if you do not know what I am talking about, you should probably look for it. But you can work on your GOPATH, ignore the “mod commands” and follow the post.

In the terminal, type the following command, or you can create a directory with a GUI tool, but it is not cool, right?.

$ mkdir mongo-golang-geo && cd $_

Inside the new directory, we execute the following command.

$ go mod init github.com/<username>/mongo-golang-crud

This creates the go.mod file, but do not worry about that now. With that, we have initialized the module and we can download the dependency we need for the project, as below.

$ go get -v go.mongodb.org/mongo-driver/mongo@v1.0.3

Now, we need to create the file structures. You are free to create these files the way you want, but I use the terminal for this. Just follow me, okay?

$ touch main.go point.go location.go mongo.go

Well, already the files location.go, main.go, mongo.go and point.go in your project folder. These should be as seen below.

.
├── go.mod
├── go.sum
├── location.go
├── main.go
├── mongo.go
└── point.go

NOTE: All files belong to the main package, remember to include “package main” in the top part of each * .go file.

Starting

For a fast advance and considering that we saw the configuration in the previous publication, we will start with the base code that is shown below.

// mongo.gopackage mainimport (
"context"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// DBName Database name.
const DBName = "geolocation"
var (
conn *mongo.Client
ctx = context.Background()
connString string = "mongodb://127.0.0.1:27017/glottery"
)
// Database collections.
var (
PointCollection = "points"
)
// createDBSession Create a new connection with the database.
func createDBSession() error {
var err error
conn, err = mongo.Connect(ctx, options.Client().
ApplyURI(connString))
if err != nil {
return err
}
err = conn.Ping(ctx, nil)
if err != nil {
return err
}
return nil
}

Now, try it the connection in main.go.

// main.gopackage mainimport "fmt"func main() {
if err := createDBSession(); err != nil {
fmt.Println(err)
return
}
fmt.Println("Connected") // Connected
}

The GeoJSON type

In MongoDB, you can store geospatial data as GeoJSON objects or as legacy coordinate pairs.

To calculate geometry over an Earth-like sphere, store your location data as GeoJSON objects.

To specify GeoJSON data, use an embedded document with:

- a field named type that specifies the GeoJSON object type and

- a field named coordinates that specifies the object’s coordinates.

- If specifying latitude and longitude coordinates, list the longitude first and then latitude:

Valid longitude values are between -180 and 180, both inclusive.

Valid latitude values are between -90 and 90, both inclusive.

MongoDB

Go is a statically typed language, so we need to create a type that satisfies the GeoJSON object for geospatial queries in MongoDB.

// location.go
package main
// Location is a GeoJSON type.
type Location struct {
Type string `json:"type" bson:"type"`
Coordinates []float64 `json:"coordinates" bson:"coordinates"`
}
// NewPoint returns a GeoJSON Point with longitude and latitude.
func NewPoint(long, lat float64) Location {
return Location{
"Point",
[]float64{long, lat},
}
}

For this example use the type of point, it is the most used. But it is similar to other types. The New function is a short way to instantiate new points with longitude and latitude.

Geospatial Indexes

Now we need to create a geospatial index to perform geospatial queries in a collection. Wait, what collection? I forget to create a collection. Let’s do this first.

// point.gopackage mainimport "go.mongodb.org/mongo-driver/bson/primitive"// Point is a simple type with a location for geospatial
// queries
.
type Point struct {
ID primitive.ObjectID `json:"id" bson:"_id"`
Title string `json:"title"`
Location Location `json:"location"`
}

Well, it’s time the index.

// mongo.go
package main
import (
"context"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/x/bsonx"
)
[...]func createIndex() error {
ctx, cancel := context.
WithTimeout(context.Background(), 10*time.Second)
defer cancel()
db := conn.Database(DBName)
indexOpts := options.CreateIndexes().
SetMaxTime(time.Second * 10)
// Index to location 2dsphere type.
pointIndexModel := mongo.IndexModel{
Options: options.Index().SetBackground(true),
Keys: bsonx.MDoc{"location": bsonx.String("2dsphere")},
}
pointIndexes := db.Collection(PointCollection).Indexes()
_, err := pointIndexes.CreateOne(
ctx,
pointIndexModel,
indexOpts,
)
if err != nil {
return err
}
return nil
}

I don’t stop to explain how the MongoDB Index works, but the previous publication addressed the topic in more detail. The only novelty is the index itself.

A 2dsphere index supports queries that calculate geometries on an earth-like sphere. 2dsphere index supports all MongoDB geospatial queries: queries for inclusion, intersection and proximity. For more information on geospatial queries, see Geospatial Queries.

MongoDB

We basically do this to make geospatial queries with the geospatial operator. How many times have I said geospatial?

Adding points

There is no mystery here, inserting a document is the same as we saw in previous publications. But we writing a function called AddPoint.

// main.go
package main
import (
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func main() {
if err := createDBSession(); err != nil {
fmt.Println(err)
return
}
fmt.Println("Connected")
}
// AddPoint adds a new point to the collection.
func AddPoint(point Point) error {
coll := conn.Database(DBName).Collection(PointCollection)
point.ID = primitive.NewObjectID()
insertResult, err := coll.InsertOne(ctx, point)
if err != nil {
fmt.Printf("Could not insert new Point. Id: %s\n", point.ID)
return err
}
fmt.Printf("Inserted new Point. ID: %s\n", insertResult.InsertedID) return nil
}

Well, now go insert a point.

// main.go
package main
[...]func main() {
if err := createDBSession(); err != nil {
fmt.Println(err)
return
}
if err := createIndex(); err != nil {
fmt.Println(err)
return
}
fmt.Println("Connected")
p := Point{
Name: "Central Park",
Location: NewPoint(-73.97, 40.77),
}
err := AddPoint(p)
if err != nil {
fmt.Println(err)
return
}
}
[...]

Getting points

Finally, the most interesting part, Queries!

// main.go
package main
[...]// GetPointsByDistance gets all the points that are within the
// maximum distance provided in meters.
func GetPointsByDistance(location Location, distance int) ([]Point, error) {
coll := conn.Database(DBName).Collection(PointCollection)
var results []Point
filter := bson.D{
{"location",
bson.D{
{"$near", bson.D{
{"$geometry", location},
{"$maxDistance", distance},
}},
}},
}
cur, err := coll.Find(ctx, filter)
if err != nil {
return []Point{}, err
}
for cur.Next(ctx) {
var p Point
err := cur.Decode(&p)
if err != nil {
fmt.Println("Could not decode Point")
return []Point{}, err
}
results = append(results, p)
}
return results, nil
}

In general, it is not very different from the way of making queries that we have seen before, but we can see some newcomers.

$near: Returns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphere and 2d indexes support $near.

$geometry: Specifies a geometry in GeoJSON format to geospatial query operators.

$maxDistance: Specifies a maximum distance to limit the results of $near and $nearSphere queries. The 2dsphere and 2d indexes support $maxDistance.

But there is still a wide variety of operators, for more information I recommend the official documentation.

Ok, let’s do the search.

// main.go
package main
[...]func main() { [...] points, err := GetPointsByDistance(NewPoint(-73.97, 40.77), 50)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(points)
}

Well, this is the basic information about the MongoDB geospatial queries, but we have not touched the surface yet. If you are interested in learning more about the MongoDB queries. I recommend that you review the documentation.

Soon we will explore more about go-mongo-driver and Go in general. Thanks for reading, see you next time.

NOTE: The full code of this publication is available on GitHub.

Acknowledges

Thanks to Lebski for sharing this github gist about go-mongo-driver, it was an inspiration for this post

--

--

Orlando Monteverde
Glottery

Web developer, Gopher, Blogger, Open Source enthusiast and professional coffee drinker ☕️.