Multi-Source Recommendation Retrieval and Ranking

Search Engineering intermediate Building with AI ~35 min read

Overview

Production recommendation systems rarely rely on one model. A robust service usually combines multiple retrieval sources: popular items for cold starts, latest items for freshness, item-to-item neighbors for similarity, user-to-user neighbors for collaborative behavior, and matrix-factorization candidates for personalized latent preferences. A ranker then merges, filters, and orders those candidates into the final list.

Gorse is a useful concrete source because its documentation describes this as a multi-way recommender pipeline with separate retrieval and ranking stages. It ingests users, items, and feedback; trains collaborative filtering and click-through-rate models; calculates neighbors; caches candidate sets; and serves filtered recommendations through horizontally scalable prediction nodes. The transferable lesson is the architecture: separate candidate generation from final ranking, combine heterogeneous signals, and keep fallback paths for sparse data.

This topic teaches how to design that retrieval-and-ranking pipeline, how collaborative filtering fits alongside simpler heuristics, and how to evaluate the output without confusing offline model fit with online recommendation quality.

Core Concepts

Candidate Generation

Candidate generation is the high-recall stage. It quickly produces a manageable set of items that might be relevant to a user. Each source has different strengths: popular and latest items handle sparse users, item-to-item neighbors exploit content or interaction similarity, user-to-user neighbors find people with overlapping histories, and matrix factorization predicts personalized user-item affinity in a latent vector space.

Ranking

Ranking is the precision stage. It takes candidates from multiple generators, computes features for each user-item pair, filters items the user has already consumed, applies business constraints, and sorts candidates by predicted utility. Gorse describes a ranker that merges recommendation results and latest items, then uses a trained factorization machine for final ranking in its GitHub repository recommender example.

Feedback Loops

Recommendations improve only if the system records user feedback. Positive signals may include clicks, stars, purchases, likes, or long dwell time. Negative or neutral signals may include skips, hides, short dwell time, and repeated non-click impressions. The feedback schema must distinguish event types because a purchase, a click, and a view do not carry the same training value.

Key Concept: Retrieval Maximizes Coverage; Ranking Maximizes Order

The generator stage should avoid missing plausible items. The ranker stage should decide which plausible items deserve the top slots. Mixing those responsibilities creates brittle systems that either overfit one ranking model or flood the user with weak candidates.

How It Works

Pipeline Shape

A recommender pipeline starts with durable data stores for users, items, and feedback. Offline or background workers build derived artifacts: item neighbors, user neighbors, collaborative filtering embeddings, popularity lists, freshness lists, and ranking models. Serving nodes load those artifacts, retrieve candidates from several sources, deduplicate them, filter ineligible items, score the survivors, and return the top K.

type Candidate struct {
    ItemID string
    Source string
    Score  float64
}

func Recommend(userID string, k int) []Item {
    candidates := append([]Candidate{},
        popularCandidates(userID, 200)...,
        latestCandidates(userID, 100)...,
        itemNeighborCandidates(userID, 300)...,
        userNeighborCandidates(userID, 300)...,
        collaborativeFilteringCandidates(userID, 300)...,
    )

    candidates = dedupeByItem(candidates)
    candidates = filterSeenAndUnavailable(userID, candidates)
    ranked := rankWithFeatures(userID, candidates)
    return takeTopK(ranked, k)
}

Matrix Factorization

Collaborative filtering via matrix factorization represents users and items as vectors in a shared latent space. The predicted preference for user u and item i is often modeled as a dot product between the user vector and item vector. Training adjusts those vectors so observed positive interactions score higher than unobserved or negative examples.

func predict(user, item []float64) float64 {
    var score float64
    for i := range user {
        score += user[i] * item[i]
    }
    return score
}

Matrix factorization is compact and effective for interaction-heavy systems, but it struggles with brand-new users and items. That is why it usually appears as one source in a broader recommender rather than the only source.

Practical Applications

Content and Product Discovery

Feeds, catalogs, documentation portals, and marketplaces use recommendations to surface relevant items before users know what to search for. A multi-source pipeline can blend personalized history with global popularity and freshness so new users see useful defaults while returning users receive more tailored suggestions.

Repository and Knowledge Discovery

Gorse’s GitRec example recommends GitHub repositories by importing repositories as items, users as profiles, and interactions as feedback. Similar mechanics apply to enterprise knowledge systems: recommend documents, dashboards, support articles, or examples based on user actions and item metadata.

Search Result Enhancement

Recommendation sources can augment search ranking. If a user searches a broad term, candidate generators can add related items from similar users or item-neighbor graphs, while the ranker combines lexical relevance, semantic relevance, popularity, freshness, and personalization.

Implementation Patterns

Separate Offline Artifacts from Serving

Training neighbors and factorization models can be expensive. Keep those jobs in background workers or a master node, then publish compact artifacts into a cache or blob store used by serving nodes. This avoids recomputing models on the request path and enables horizontal scaling for prediction.

type ArtifactStore interface {
    LoadItemNeighbors(version string) NeighborIndex
    LoadUserNeighbors(version string) NeighborIndex
    LoadFactorModel(version string) FactorModel
    ActiveVersion() string
}

func RefreshModels(store ArtifactStore) ServingState {
    version := store.ActiveVersion()
    return ServingState{
        Items: store.LoadItemNeighbors(version),
        Users: store.LoadUserNeighbors(version),
        Model: store.LoadFactorModel(version),
    }
}

Keep Fallbacks Explicit

Cold-start users, sparse catalogs, filters, and safety constraints can reduce a personalized candidate set below the requested count. A reliable recommender has ordered fallback sources: similar items, latest items, popular items, editorial picks, or query-based retrieval. The fallback path should be visible in telemetry so teams can detect when personalization is not actually driving the result.

Rank with Source Features

Do not discard where a candidate came from. Source identity, source score, source count, item age, popularity, user affinity, and category match are useful ranker features. A candidate retrieved by three independent generators often deserves different treatment from one retrieved only because it is globally popular.

Performance Considerations

Common Pitfalls

Further Reading