Golang and MongoDB using the official mongo-driver

Creating a connection

In this example we connect to a server running on localhost, using a database named ‘testdb’ and collection named ‘pages’.

clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(ctx, clientOptions)
if err != nil {
  log.Fatal("Could not connect to MongoDB:", err)
}
if err = client.Ping(ctx, nil); err != nil {
  log.Fatal("Failed to ping MongoDB:", err)
}
log.Println("Connected to MongoDB!")

collection := client.Database("testdb").Collection("pages")

Alternatively, here’s how to set the clientOptions to connect to MongoDB running on a remote server (in this example at IP address 10.1.2.3) and using authentication:

clientOptions := options.Client().ApplyURI("mongodb://10.1.2.3:27017").
  SetAuth(options.Credential{
    AuthSource: "testdb", Username: "myusername", Password: "mypassword",
  })

For this to work you would have previously created the user through the Mongo shell:

use testdb
db.createUser({
  user: "myuser",
  pwd: "mypassword",
  roles: [{role: "readWrite", db: "testdb"}]
})

Creating an index

MongoDB creates an automatic index on the _id field of each record, but if you want to find any other information in a reasonable time then you’ll need to create an index for it. The following code creates a standard index on a field named ‘url’.

index = mongo.IndexModel{Keys: bson.M{"url": 1}}
if _, err := collection.Indexes().CreateOne(ctx, index); err != nil {
  log.Println("Could not create index:", err)
}

It’s fine to try creating the same index again. You don’t need to have ‘IF NOT EXISTS’ as you would with SQL. The command will simply do nothing (and throw no error) if the index is already there.

If you want the index to enforce unique values in the field, then add the ‘unique’ option when setting it up.

opt := options.Index()
opt.SetUnique(true)
index := mongo.IndexModel{Keys: bson.M{"url": 1}, Options: opt}
if _, err := collection.Indexes().CreateOne(ctx, index); err != nil {
  log.Println("Could not create index:", err)
}

Each collection can have at most one text index. This allows for powerful free-text searching. The text index can cover multiple fields. The default weight of each field is ‘1’, but it’s possible to set custom weights for fields, as shown in the following example:

opt = options.Index()
opt.SetWeights(bson.M{
  "title": 5, // Word matches in the title are weighted 5× standard.
  "description": 2,
  "body": 1, // This line is unnecessary because the default weight is 1.
})
index = mongo.IndexModel{Keys: bson.M{
  "title": "text",
  "description": "text",
  "body": "text",
}, Options: opt}
if _, err := collection.Indexes().CreateOne(ctx, index); err != nil {
  log.Println("Could not create text index:", err)
}

Inserting data

Inserting one record at a time is done with the InsertOne method of the collection.

insert := bson.M{
  "url": "http://www.example.com/",
  "title": "An example web page",
}
if res, err := collection.InsertOne(ctx, insert); err == nil {
  id = res.InsertedID.(primitive.ObjectID)
  log.Println("Inserted record:", id.Hex())
} else {
  log.Fatal("Error inserting record:", err)
}

Finding data

The following example retrieves records from a collection and iterates through them. It sets a limit of 100 results returned. It uses ‘SetProjection’ to select the fields to return (rather than all of them) and to include the index relevance score in the results. The _id field is always returned. However, for some reason this doesn’t map back to the ID field in the result structure. I can’t discover why this doesn’t work!

The results are sorted by score, so the most relevant matches are first.

type Result struct {
  ID          primitive.ObjectID
  URL         string
  Title       string
  Description string
  Score       float64
}

filter := bson.M{"$text": bson.M{"$search": "my search text"}}
findOptions := options.Find()
findOptions.SetLimit(100)
findOptions.SetProjection(bson.M{
  "url":         1,
  "title":       1,
  "description": 1,
  "score":       bson.M{"$meta": "textScore"},
})
findOptions.SetSort(bson.M{"score": bson.M{"$meta": "textScore"}})

cur, err := collection.Find(ctx, filter, findOptions)
if err != nil {
  log.Fatalln("Error during text search:", err)
}
defer cur.Close(ctx)

for cur.Next(ctx) {
  var result Result
  if err := cur.Decode(&result); err != nil {
    log.Fatalln("Error getting record:", err)
  }
  // For some reason Decode doesn't work for the _id field. Extract separately.
  result.ID = cur.Current.Lookup("_id").ObjectID()
  log.Println(result)
}
if err := cur.Err(); err != nil {
  log.Fatal(err)
}

If you’re sorting by multiple fields, then it’s better to use a BSON document rather than map, so that the sort criteria are applied in the correct order. The document is an ordered structure while the map is unordered. The following code gets set to sort by the ‘title’ field and then the ‘url’ field.

findOptions.SetSort(bson.D{
  {Key: "url", Value: 1},
  {Key: "title", Value: 1},
})

Regular expressions

A regex can also be used to find data. The following example finds all URLs in a collecction starting with a particular prefix:

filter := bson.M{"url": bson.M{"$regex": "^http://www\.example\.com/path.*"}}
cur, err := collection.Find(ctx, filter)

References