All articles

Cmd+K global search in Go: parallel queries across 6 modules, merge, rank, and keyboard navigation

Suman Akkisetty
Founder, KaryoSpace
April 2026
9 min read

Cmd+K search in KaryoSpace runs six concurrent queries against different MongoDB collections, merges the results by relevance and recency, and returns within the time budget of a single round trip. Here is the architecture behind it.

Why parallel queries

A naive implementation queries each module sequentially: emails first, then incidents, then tasks, then messages. At 20-30ms per query, six sequential queries take 120-180ms minimum before any network overhead. With parallel goroutines, all six queries run simultaneously and the total time is dominated by the slowest single query.

type SearchResult struct {
    Module  string // email|incident|task|message|knowledge|note
    ID      string
    Title   string
    Preview string
    URL     string
    Score   float64
    At      time.Time
}

func globalSearch(query, userID, orgID string) []SearchResult {
    var mu sync.Mutex
    var all []SearchResult
    var wg sync.WaitGroup

    sources := []func(){
        func() { results, _ := searchEmails(query, userID, orgID); merge(&mu, &all, results) },
        func() { results, _ := searchIncidents(query, orgID); merge(&mu, &all, results) },
        func() { results, _ := searchTasks(query, orgID); merge(&mu, &all, results) },
        func() { results, _ := searchMessages(query, orgID); merge(&mu, &all, results) },
        func() { results, _ := searchKnowledge(query, orgID); merge(&mu, &all, results) },
        func() { results, _ := searchNotes(query, userID); merge(&mu, &all, results) },
    }
    wg.Add(len(sources))
    for _, fn := range sources {
        go func(f func()) { defer wg.Done(); f() }(fn)
    }
    wg.Wait()
    return rankResults(all, query)
}

MongoDB text search

Each collection has a MongoDB text index on the fields most likely to match a user query. The email collection indexes subject, body, and sender. The incident collection indexes title, description, and update comments. The text index scores matches by term frequency, and the score is used in ranking.

// Email search example
func searchEmails(query, userID, orgID string) ([]SearchResult, error) {
    filter := bson.M{
        "$text":   bson.M{"$search": query},
        "user_id": userID,
        "folder":  bson.M{"$nin": []string{"Trash", "Spam"}},
    }
    opts := options.Find().
        SetProjection(bson.M{"score": bson.M{"$meta": "textScore"}}).
        SetSort(bson.D{{Key: "score", Value: bson.M{"$meta": "textScore"}}}).
        SetLimit(5)
    cursor, err := db.Collection("email_messages").Find(ctx, filter, opts)
    // ...map to SearchResult with module="email"
}

Ranking merged results

After all six goroutines complete, the results are merged and ranked. The ranking function combines three signals: the MongoDB text score (term relevance), recency (newer items rank higher), and module priority (a hardcoded weight per module type).

func rankResults(results []SearchResult, query string) []SearchResult {
    modulePriority := map[string]float64{
        "incident":  1.2,
        "task":      1.1,
        "email":     1.0,
        "knowledge": 0.9,
        "message":   0.8,
        "note":      0.7,
    }
    now := time.Now()
    for i := range results {
        age := now.Sub(results[i].At).Hours()
        recencyScore := 1.0 / (1.0 + age/168.0) // decay over 1 week
        priority := modulePriority[results[i].Module]
        results[i].Score = results[i].Score * recencyScore * priority
    }
    sort.Slice(results, func(i, j int) bool {
        return results[i].Score > results[j].Score
    })
    if len(results) > 20 { results = results[:20] }
    return results
}

Keyboard navigation

The Cmd+K modal is a pure HTML/JS component with no frontend framework dependency. Arrow keys move the selection. Enter navigates to the selected result. Escape closes the modal. The selection index is a module-level variable that resets on each new search.

document.addEventListener('keydown', e => {
    if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault(); openSearch();
    }
    if (!searchOpen) return;
    if (e.key === 'ArrowDown') { e.preventDefault(); moveSelection(1); }
    if (e.key === 'ArrowUp')   { e.preventDefault(); moveSelection(-1); }
    if (e.key === 'Enter')     { e.preventDefault(); navigateSelected(); }
    if (e.key === 'Escape')    { closeSearch(); }
});

Type filter tabs

Above the results list, filter pills let users narrow by module type: All, Email, Messages, Incidents, Tasks, Docs, Notes. Selecting a filter does not re-query the server. The full 20-result set is cached client-side, and the filter is applied in-browser by comparing each result's module field. This keeps the UI instant on filter changes.

function filterResults(type) {
    activeFilter = type;
    const visible = type === 'all'
        ? cachedResults
        : cachedResults.filter(r => r.module === type);
    renderResults(visible);
}

Result icons per module

Each module type has a distinct SVG icon that appears next to the result in the list. The icon is injected by the renderResult() function based on the module field. No CSS class or data attribute approach — just a switch that returns the correct SVG string. The icon is 16x16 and uses currentColor so it inherits the theme color automatically.

The edge case that bit us

The search endpoint did not have a per-user rate limit when it launched. A user who typed quickly triggered 8-10 searches per second, each spawning 6 goroutines. Under load, this saturated the MongoDB connection pool. The fix was a 150ms debounce on the client side (wait for the user to stop typing before sending) and a per-user rate limit of 20 searches per minute on the server. The debounce also improved the user experience by eliminating results that flash past too quickly to read.

Get in touch

Questions about KaryoSpace or this article?

Contact Us
Message sent!
We read every submission and will get back to you.