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 😀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
package main 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 } |