Skip to main content
โšก Calmops

Fuzzy (Regex) Search in MongoDB with Go โ€” Practical Guide

Implement safe, performant regex-based fuzzy search using the official mongo-go-driver

Fuzzy search using regular expressions is a common requirement in apps that let users search for names, titles, slugs, or other short text fields. Regex-based queries are flexible, but if used incorrectly they can be slow, insecure, or produce surprising results.

This guide shows practical, production-ready examples for performing regex (fuzzy) queries in MongoDB with Go using the official mongo-go-driver. Examples progress from basic queries to safer and higher-performance patterns. Each example includes imports, error handling, and notes about performance or safety.

Contents

  • Introduction
  • Connection setup (official driver)
  • Basic regex query
  • Case-insensitive search
  • Prefix, suffix, and contains patterns
  • Using primitive.Regex vs $regex in bson.M
  • Escaping user input and preventing regex injection
  • Pagination, projection, and sorting with regex
  • Error handling specifics for regex queries
  • Performance tips and indexing strategies
  • When to use text indexes or Atlas Search instead
  • Conclusion & best practices

Target audience: Go developers building search features with MongoDB.


Connection setup (mongo-go-driver)

This helper shows a typical connection setup and a getCollection helper used by the examples below.

package main

import (
 "context"
 "fmt"
 "log"
 "time"

 "go.mongodb.org/mongo-driver/mongo"
 "go.mongodb.org/mongo-driver/mongo/options"
)

// connect returns a connected *mongo.Client and a cancel func for the context.
func connect(uri string) (*mongo.Client, func(), error) {
 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 clientOpts := options.Client().ApplyURI(uri)
 client, err := mongo.Connect(ctx, clientOpts)
 if err != nil {
  cancel()
  return nil, nil, err
 }
 // Verify connection
 if err := client.Ping(ctx, nil); err != nil {
  _ = client.Disconnect(ctx)
  cancel()
  return nil, nil, err
 }
 // return the client and a cleanup function
 return client, func() {
  _ = client.Disconnect(context.Background())
  cancel()
 }, nil
}

func getCollection(client *mongo.Client) *mongo.Collection {
 // `appdb` and `users` are realistic example names
 return client.Database("appdb").Collection("users")
}

Run connect at application startup; reuse the *mongo.Client throughout the app.


1) Basic regex query

Simple example: find users whose username field contains abc (case-sensitive by default).

import (
 "context"
 "fmt"
 "log"

 "go.mongodb.org/mongo-driver/bson"
 "go.mongodb.org/mongo-driver/mongo"
)

func basicContainsExample(coll *mongo.Collection, queryWord string) {
 // pattern: contains 'abc' anywhere
 pattern := ".*" + queryWord + ".*"
 filter := bson.M{"username": bson.M{"$regex": pattern}}

 ctx := context.Background()
 cur, err := coll.Find(ctx, filter)
 if err != nil {
  log.Printf("Find error: %v", err)
  return
 }
 defer cur.Close(ctx)

 var results []bson.M
 if err := cur.All(ctx, &results); err != nil {
  log.Printf("Cursor.All error: %v", err)
  return
 }
 fmt.Printf("found %d results\n", len(results))
}

Notes:

  • The pattern above includes .* around queryWord โ€” this is a contains search.
  • This example is simple, but building patterns directly from user input is dangerous (see escaping below).

Use the i option to make the match case-insensitive. In BSON you can pass options via $options or use primitive.Regex.

import (
 "context"
 "fmt"
 "log"
 "regexp"

 "go.mongodb.org/mongo-driver/bson"
 "go.mongodb.org/mongo-driver/bson/primitive"
)

// Using $regex + $options
func caseInsensitiveWithOptions(coll *mongo.Collection, q string) {
 safe := regexp.QuoteMeta(q)
 filter := bson.M{"username": bson.M{"$regex": safe, "$options": "i"}}

 ctx := context.Background()
 cur, err := coll.Find(ctx, filter)
 if err != nil {
  log.Printf("Find error: %v", err)
  return
 }
 defer cur.Close(ctx)
 var out []bson.M
 _ = cur.All(ctx, &out)
 fmt.Println(out)
}

// Using primitive.Regex
func caseInsensitiveWithPrimitive(coll *mongo.Collection, q string) {
 safe := regexp.QuoteMeta(q)
 re := primitive.Regex{Pattern: safe, Options: "i"}
 filter := bson.M{"username": re}

 ctx := context.Background()
 cur, err := coll.Find(ctx, filter)
 if err != nil {
  log.Printf("Find error: %v", err)
  return
 }
 defer cur.Close(ctx)
 var out []bson.M
 _ = cur.All(ctx, &out)
 fmt.Println(out)
}

Notes:

  • regexp.QuoteMeta is used to escape any regex meta-characters in user input (important for safety).
  • If you intentionally allow regex syntax from trusted users, skip quoting but validate patterns.

3) Prefix, suffix, and contains patterns (and index-friendliness)

  • Prefix search (can be index-friendly if an index exists on the field): ^prefix
  • Suffix search: suffix$ (not index-friendly)
  • Contains: .*term.* (generally not index-friendly)

Examples:

// Prefix search (good for using an index on `username`)
prefix := "^" + regexp.QuoteMeta(q)
filter := bson.M{"username": bson.M{"$regex": prefix, "$options": "i"}}

// Suffix search (may require collection scan)
suffix := regexp.QuoteMeta(q) + "$"
filter = bson.M{"username": bson.M{"$regex": suffix, "$options": "i"}}

// Contains (likely to be slow on large collections)
contains := ".*" + regexp.QuoteMeta(q) + ".*"
filter = bson.M{"username": bson.M{"$regex": contains, "$options": "i"}}

Index tip: create an index on username for prefix searches:

db.users.createIndex({ username: 1 })

MongoDB can use a B-tree index for regex queries that are left-anchored (i.e., patterns starting with ^literal). Suffix and contains queries cannot use a standard B-tree index and will be significantly slower on large datasets.


4) Escaping user input and preventing regex injection

Never construct regex patterns directly from raw user input. Always escape metacharacters using regexp.QuoteMeta unless accepting regex syntax is an explicit, validated feature.

Example helper:

import (
 "regexp"

 "go.mongodb.org/mongo-driver/bson"
)

func safeContainsFilter(field, userInput string) bson.M {
 safe := regexp.QuoteMeta(userInput)
 pattern := ".*" + safe + ".*"
 return bson.M{field: bson.M{"$regex": pattern, "$options": "i"}}
}

Why:

  • Prevents users from injecting complex patterns that cause catastrophic backtracking.
  • Ensures the pattern matches the literal characters the user typed.

If you must accept raw regex input from trusted users (e.g., advanced admins), validate patterns with regexp.Compile first and consider limiting pattern length or complexity.

if _, err := regexp.Compile(userRegex); err != nil {
 return fmt.Errorf("invalid regex: %w", err)
}

5) Pagination, projection, and sorting with regex queries

Combine filters with options.Find() to limit returned fields and results.

import "go.mongodb.org/mongo-driver/mongo/options"

func findUsersPage(coll *mongo.Collection, q string, page, pageSize int) ([]bson.M, error) {
 ctx := context.Background()
 filter := safeContainsFilter("username", q)

 opts := options.Find().SetLimit(int64(pageSize)).SetSkip(int64((page-1)*pageSize)).
  SetProjection(bson.M{"username": 1, "email": 1}).SetSort(bson.D{{"username", 1}})

 cur, err := coll.Find(ctx, filter, opts)
 if err != nil {
  return nil, err
 }
 defer cur.Close(ctx)
 var out []bson.M
 if err := cur.All(ctx, &out); err != nil {
  return nil, err
 }
 return out, nil
}

6) Error handling specifics for regex queries

  • Find can return errors for malformed queries or network issues โ€” always check err.
  • When compiling user-supplied regex (if accepting raw regex), regexp.Compile returns an error you should report to the caller.
  • Timeouts: wrap queries with a context that has a timeout to avoid long-running DB operations.

Example using a timeout:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cur, err := coll.Find(ctx, filter)

Handling server-side long-running work: log slow queries and consider adding guards in the application (max pattern length, disallow certain constructs, etc.).


7) Performance tips and indexing strategies

  • Prefer anchored (^prefix) searches when possible and create an index on the field.
  • Avoid leading wildcards (e.g., .*term) for large collections โ€” they force collection scans.
  • Use projections to return only necessary fields.
  • Limit results using SetLimit and implement pagination with SetSkip or an efficient range-based cursor.
  • Consider a compound index if you frequently filter and sort on multiple fields.

Example: create an index for prefix search and for sorting by username:

db.users.createIndex({ username: 1 })

If you need more advanced search capabilities (relevance, tokenization, language support), use one of these alternatives:

  • MongoDB text indexes with $text (good for language-aware, stemmed searches)
  • Atlas Search (full-text, relevancy, advanced tokenizers and analyzers)
  • External search engines (Elasticsearch, MeiliSearch) for very large datasets or complex ranking needs

Text index example (create index and query with $text):

db.articles.createIndex({ title: "text", body: "text" })
db.articles.find({ $text: { $search: "mongodb fuzzy search" } })

In Go:

filter := bson.M{"$text": bson.M{"$search": "mongodb fuzzy search"}}
cur, err := coll.Find(ctx, filter)

  • Aggregation pipelines can combine regex with other stages (e.g., $project, $addFields) to compute scores or filter in complex ways.
  • Atlas Search provides powerful, indexed full-text capabilities with fine-grained control over analyzers and scoring.

Example aggregation (find and sort by string length of field โ€” contrived example):

pipeline := mongo.Pipeline{
 { {"$match", bson.D{{"title", bson.D{{"$regex", regexp.QuoteMeta(q)}, {"$options", "i"}}}}} },
 { {"$addFields", bson.D{{"len", bson.D{{"$strLenCP", "$title"}}}}} },
 { {"$sort", bson.D{{"len", 1}}} },
}
cur, err := coll.Aggregate(ctx, pipeline)

Note: aggregation can be more costly than simple queries โ€” measure performance.


Complete example: a small runnable snippet

Below is a small, self-contained example that connects to MongoDB, inserts sample documents, and demonstrates a safe contains search. It’s ready to copy into a main.go and run (you must set MONGODB_URI or change the uri value).

package main

import (
 "context"
 "fmt"
 "log"
 "os"
 "regexp"
 "time"

 "go.mongodb.org/mongo-driver/bson"
 "go.mongodb.org/mongo-driver/mongo"
 "go.mongodb.org/mongo-driver/mongo/options"
)

func main() {
 uri := os.Getenv("MONGODB_URI")
 if uri == "" {
  uri = "mongodb://localhost:27017"
 }
 client, cleanup, err := connect(uri)
 if err != nil {
  log.Fatalf("connect: %v", err)
 }
 defer cleanup()

 coll := getCollection(client)

 // Insert sample docs (ignore errors if run multiple times)
 ctx := context.Background()
 sample := []interface{}{
  bson.M{"username": "alice", "email": "[email protected]"},
  bson.M{"username": "bob_smith", "email": "[email protected]"},
  bson.M{"username": "charlie-123", "email": "[email protected]"},
 }
 _, _ = coll.InsertMany(ctx, sample)

 // Safe contains search
 userInput := "ali" // simulate user input
 safe := regexp.QuoteMeta(userInput)
 pattern := ".*" + safe + ".*"
 filter := bson.M{"username": bson.M{"$regex": pattern, "$options": "i"}}

 // Use a timeout for the query
 qctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()

 cur, err := coll.Find(qctx, filter, options.Find().SetLimit(10))
 if err != nil {
  log.Fatalf("query error: %v", err)
 }
 defer cur.Close(qctx)

 var users []bson.M
 if err := cur.All(qctx, &users); err != nil {
  log.Fatalf("cursor all: %v", err)
 }
 fmt.Printf("matched users: %v\n", users)
}

Conclusion & best practices

  • Escape user input with regexp.QuoteMeta unless you explicitly accept regex syntax.
  • Prefer left-anchored (^prefix) searches with an index for scalable performance.
  • Use text indexes or Atlas Search for language-aware search or when ranking matters.
  • Always use context timeouts and proper error handling around MongoDB operations.
  • Monitor and log slow queries; set reasonable limits on pattern size and complexity.

Comments