False Alarm: Serious Bug in MongoDB Go Driver’s Geospatial distance calculation

Final Edit: False Alarm

It’s not MongoDB, It’s not the MongoDB Go driver, it’s something I did.
I was especially careful to double and triple check Long and Lat and still something went wrong.

Since reporting bugs in MongoDB is not longer possible for the average human, because you now have to be a MongoDB employee with an @mongodb.com email address and 2FA to even link your mongodb account with MongoDB’s JIRA, I’ll have to write this blog post.

I did try to contact customer service online but I’m not waiting around to help YOU on my time. Seriously WTF? If you don’t want bug reports don’t put up a “reporting bugs” page and make it virtually impossible to do. Just say “we don’t want bug reports from common plebs”.

Correction: You have a very short window where you can select if you’re an employee or customer/user when trying to sign in to MongoDB’s Jira. Since I was gathering information in other tabs I have missed that window and it redirected to the login for employees by default.

Correction2: It’s not MongoDB, it’s MongoDB’s Go driver.
But in my defense, if the sign in mechanism of MongoDB’s Jira wasn’t so bad this post wouldn’t be here to begin with but an issue on their Jira.

On to the bug. It started when I hit inconsistencies in the distance results vs the results of 2 other geospatial libraries. The 2 libraries are:

“github.com/kellydunn/golang-geo”
“github.com/golang/geo/s2”

MongoDB uses the s2 lib (c++ version) as well. And from their GeoJSON documentation they require the ‘Point’ type to be in Longitude, Latitude order.

At first I didn’t know how to use the s2 lib so I asked the question on their repository on github:
https://github.com/golang/geo/issues/85

And everything is explained there, but I’ll give you a summary.

In essence, as I already wrote, MongoDB requires the GeoJSON Point to be in Long, Lat order, but internally, since the very beginning MongoDB calculates the distance between 2 points in Lat, Long order. It uses Latitude for Longitude and Longitude for Latitude in their Point to Point distance calculation.

And since I can’t report the bug I have 2 options:
– do some manual indexing and querying
– not use MongoDB

I’m now stuck at a project I’ve been working on for the last 3 months that was close to being feature complete and now I have to switch technologies.
How come no one in 3 major version has noticed this before?
Has no one used the GeoJSON capabilities of MongoDB?
Or were they also not able to report this bug? Or maybe it was reported but ignored.

So TL;DR:

Move alone nothing to see here.

MongoDB Go Driver‘s Point to Point distance calculation is wrong because they confuse Longitude and Latitude internally – and there is no way to report bugs for people who are not mongodb employees or affiliates.

When doing the same $geoNear aggregation query in MongoDB Compass the results are correct.
But when using the official Go driver the results are incorrect.
However this also answers why no one stumbled upon this issue in 3 major versions. And I thought I had discovered America 😀

import (
“context”
“fmt”
“log”
“time”

“github.com/golang/geo/s2”
geo “github.com/kellydunn/golang-geo”
“go.mongodb.org/mongo-driver/bson”
“go.mongodb.org/mongo-driver/bson/primitive”
“go.mongodb.org/mongo-driver/mongo”
mopts “go.mongodb.org/mongo-driver/mongo/options”
“go.mongodb.org/mongo-driver/mongo/readpref”
“go.mongodb.org/mongo-driver/x/bsonx”
)

const earthRadiusKm = 6371.01 // See https://github.com/golang/geo/blob/master/s2/s2_test.go#L46-L51

var (
Database = “testgeo”
DefaultTimeout = time.Second * 10
ProfileCollection = “profile”
)

type (
Profile struct {
ID primitive.ObjectID bson:"_id,omitempty" json:"id"
Location Location bson:"location" json:"location"
}
ProfileResult struct {
ID primitive.ObjectID bson:"_id,omitempty" json:"id"
Location Location bson:"location" json:"location"
Distance float64 bson:"distance" json:"distance"
}
Location struct {
Type string json:"type" bson:"type"
Coordinates []float64 json:"coordinates" bson:"coordinates"
}
)

func main() {
lat1 := 9.1829321
lng1 := 48.7758459
lat2 := 45.749428
lng2 := 15.39967
// lat1 := 9.653194
// lng1 := 48.709797
// lat2 := 9.182313
// lng2 := 48.783187
p1 := geo.NewPoint(lat1, lng1)
// p1 := geo2.NewPoint(lng1, lat1)
p2 := geo.NewPoint(lat2, lng2)
// p2 := geo2.NewPoint(lng2, lat2)

dist := p1.GreatCircleDistance(p2)
fmt.Printf(“great circle distance: %f\n”, dist)

pg1 := s2.PointFromLatLng(s2.LatLngFromDegrees(lat1, lng1))
// pg1 := s2.PointFromLatLng(s2.LatLngFromDegrees(lng1, lat1))
pg2 := s2.PointFromLatLng(s2.LatLngFromDegrees(lat2, lng2))
// pg2 := s2.PointFromLatLng(s2.LatLngFromDegrees(lng2, lat2))
sdist := pg1.Distance(pg2) * earthRadiusKm
// odist := pg2.Distance(pg1) * earthRadiusKm
fmt.Printf(“s2 distance: %f\n”, sdist)
// fmt.Printf(“s2 r-distance: %f\n”, odist)
// fmt.Printf(“mongo result: %f\n”, 5165738.082454552)

mp1 := new(Profile)
mp1.Location = NewPoint(lng1, lat1)

mp2 := new(Profile)
mp2.Location = NewPoint(lng2, lat2)

client, e := mongoInit()
if e != nil {
log.Fatal(e.Error())
}
defer func() {
if e = client.Disconnect(context.Background()); e != nil {
panic(e)
}
}()

res1, e := CreateProfile(client, mp1)
if e != nil {
log.Fatal(e.Error())
}

res2, e := CreateProfile(client, mp2)
if e != nil {
log.Fatal(e.Error())
}

mp1.ID = res1.InsertedID.(primitive.ObjectID)
mp2.ID = res2.InsertedID.(primitive.ObjectID)

var radius int32
radius = int32(6371) * 1000
geoStage := bson.D{{
“$geoNear”, bson.D{
{“near”, mp1.Location},
{“distanceField”, “distance”},
{“minDistance”, 1},
{“maxDistance”, radius},
{“spherical”, true},
},
},
}
result, e := AggregateProfile(client, mongo.Pipeline{geoStage})
if e != nil {
return
}
fmt.Printf(“mongodb distance: %f\n”, result[0].Distance)

}

func mongoInit() (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
client, e := mongo.Connect(ctx, mopts.Client().ApplyURI(“mongodb://localhost:27017”))
if e != nil {
return nil, e
}

ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
e = client.Ping(ctx, readpref.Primary())
if e != nil {
return nil, e
}

// drop database
_ = client.Database(Database).Drop(context.Background())

// create geoJSON 2dsphere index
ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()

db := client.Database(Database)
indexOpts := mopts.CreateIndexes().SetMaxTime(DefaultTimeout)

// Index to location 2dsphere type.
pointIndexModel := mongo.IndexModel{
Options: mopts.Index(),
Keys: bsonx.MDoc{
“location”: bsonx.String(“2dsphere”),
},
}

profileIndexes := db.Collection(“profile”).Indexes()

// _, e = profileIndexes.CreateOne(ctx, pointIndexModel, indexOpts)
_, e = profileIndexes.CreateOne(ctx, pointIndexModel, indexOpts)
if e != nil {
return nil, e
}

return client, nil
}

// NewPoint returns a GeoJSON Point with longitude and latitude.
func NewPoint(long, lat float64) Location {
return Location{
“Point”,
[]float64{long, lat},
}
}

func CreateProfile(client *mongo.Client, m *Profile) (*mongo.InsertOneResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
c := client.Database(Database).Collection(ProfileCollection)
return c.InsertOne(ctx, m)
}

func AggregateProfile(client *mongo.Client, pipeline interface{}) ([]*ProfileResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
c := client.Database(Database).Collection(ProfileCollection)
cursor, e := c.Aggregate(ctx, pipeline)
if e != nil {
return nil, e
}
ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout)
defer cancel()
var m []*ProfileResult
if e := cursor.All(ctx, &m); e != nil {
return nil, e
}
return m, nil
}

 

One Reply to “False Alarm: Serious Bug in MongoDB Go Driver’s Geospatial distance calculation”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.