Tinder has 50 million users. When you open the app, you see a deck of profiles. Not 50 million profiles — maybe 100. Candidate generation is the step that narrows from the full user population to the small set of candidates worth scoring and presenting. It runs before the expensive ranking step, and its job is to be fast and to recall the right candidates, not to rank them perfectly.

Why Two Stages#

Ranking all 50 million users for each viewer is too expensive. A detailed compatibility score involving preference matching, behavioral signals, and ML features takes ~10ms per candidate. At 50 million candidates: 500,000 seconds. Not viable.

Candidate generation uses cheaper signals to narrow to 500-5,000 candidates. Those candidates go through the full ranker. The split: retrieval must be fast and high-recall; ranking can be slow and high-precision.

Hard Filters First#

Apply non-negotiable filters: age range, maximum distance, gender preference. These are indexed in the user database. A geospatial query (geohashing or a bounding box on coordinates) finds users within the specified distance. Age range is a simple index scan.

This typically reduces 50 million to 50,000-500,000 users within hard filter criteria.

graph TD A[User A opens app] --> B[Hard filters: age range, max distance, gender] B --> C[50M users: 200K pass hard filters] C --> D[Soft signals: activity score, photo quality, preference overlap] D --> E[200K narrowed to 2K candidates] E --> F[Full ranker: ML compatibility score per candidate] F --> G[Top 100 served as deck] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Soft Signals for Narrowing#

After hard filters, apply cheap soft signals to further narrow the candidate set:

Activity score: recently active users first. A profile inactive for 90 days is deprioritized. Stored as a Redis sorted set by last-active timestamp.

Photo quality signal: profiles with at least one verified photo score higher. A precomputed flag per profile.

Preference overlap: if user A has swiped right 70% on profiles with brown hair and user B has brown hair, that’s a soft positive signal. Precomputed as a preference vector per user.

These signals can be combined into a lightweight candidate score computed in under 1ms, used to rank the 200,000 filtered users and take the top 2,000.

Embedding-Based Retrieval#

For better recall, precompute a preference embedding per user based on their swipe history. Candidate generation becomes an approximate nearest-neighbor (ANN) search: find users whose profile embeddings are closest to user A’s preference embedding. This captures non-obvious compatibility signals that hard filters miss.

At Oracle#

Oracle CX had a “recommended contacts” feature: given a sales rep, suggest accounts they should reach out to based on similarity to their won deals. Two-stage retrieval: hard filter by territory and deal size range, then ANN search over deal characteristic embeddings to find similar accounts. The ANN step ran in under 5ms for 100,000 accounts. Full scoring ran on the top 200 results only.

What I’m Learning#

Candidate generation is about recall under a latency budget. You’d rather include false positives (candidates the ranker will reject) than miss true positives (compatible candidates never shown). The hard filter / soft signal / ANN layering is the standard approach for this class of problem.

Have you designed two-stage retrieval systems, and how did you decide where to draw the line between retrieval and ranking?