Yelp System Design Interview: Nearby Business Search at Scale
Yelp hosts hundreds of millions of reviews across tens of millions of businesses, and a single search like 'sushi near me' has to filter by distance, category, hours, and rating, then rank the survivors, in well under a second. The traffic is overwhelmingly reads. Someone typing a query and panning a map generates far more load than the trickle of new reviews and business edits flowing in. The hard part is not storing the data, it is answering 'what is good and open within 2 km of this exact lat/long' fast, for many concurrent users, over a constantly moving map viewport.
Yelp is a local discovery product, so the interview is really about geospatial search over a mostly static, read-heavy dataset. You have businesses with a location, categories, hours, price, and a stream of user reviews that roll up into a rating. A query carries a point or a map bounding box plus filters, and you must return the most relevant businesses ranked by a blend of distance, rating, and text match. The core design problem is the spatial index: a plain latitude and longitude B-tree cannot answer 'within N km' efficiently, so you reduce 2D proximity to a 1D or hierarchical key using geohash, a quadtree, or Google S2 cells, and let a search engine like Elasticsearch or Lucene do the filtering and scoring. Ranking is a two-phase retrieve-then-rank pipeline: the search engine recalls a candidate pool cheaply, then a learning-to-rank model reorders it. Everything sits behind read replicas and aggressive caching because the same popular queries and neighborhoods repeat constantly. Writes (new reviews, rating recomputation, business edits) go through a separate path and are indexed near real time.
Asked at: Asked at Yelp, Google, Uber, DoorDash, Amazon, and most companies with a maps or local-search surface. It also appears as generic variants like 'design Nearby' or 'design a proximity service' at Meta and Lyft.
Why this question is asked
Interviewers like this problem because proximity search forces a candidate to confront a data structure question that a relational index does not solve cleanly. A B-tree on latitude and a B-tree on longitude each answer a 1D range, but intersecting two ranges to get a circle is wasteful, so the candidate has to reach for a real spatial index and defend the choice between geohash, quadtree, and S2. It also tests read-heavy system thinking: sharding by geography, caching hot regions, using read replicas, and keeping a search index in sync with the source of truth. Finally it has a satisfying ranking layer, since 'nearby' alone is a bad experience and the candidate has to reason about combining distance with rating, review count, and relevance without letting one signal dominate.
Requirements
Always clarify these in the first 5 minutes of the interview. Do not start drawing boxes until both lists are agreed.
Functional requirements
- Search businesses near a point or within a map bounding box, filtered by distance radius.
- Filter results by category, price tier, open-now, rating threshold, and free-text keyword.
- Rank results by a blend of distance, rating, review count, and text relevance.
- Return business detail: name, address, coordinates, hours, categories, photos, aggregate rating.
- Let users write, edit, and delete reviews with a star rating and text body.
- Recompute a business's aggregate rating and review count when reviews change.
- Support autocomplete and typeahead on business names and categories.
- Paginate and re-query as the user pans or zooms the map to a new viewport.
- Let business owners create and edit listings, which must appear in search after indexing.
Non-functional requirements
- Search latency under roughly 200 ms at P99 for a typical filtered query.
- Very high read-to-write ratio; the system is optimized for query throughput.
- Horizontal scalability by geography so dense metros do not overload a single shard.
- Freshness: a new review or business edit should appear in search within seconds to minutes.
- High availability for the read path; a stale result is better than an error.
- Accurate distance filtering that accounts for spherical geometry, not flat-plane math.
- Cost efficiency, since search infrastructure is the dominant spend for a discovery product.
Back-of-envelope scale estimates
Show your math. Pulling numbers from thin air signals you have not thought about the load.
Businesses indexed
~10s of millions (estimate)
Yelp covers many countries and metros. Public figures cite tens of millions of businesses. Treat the exact count as an estimate; the point is the index is large but static enough to precompute.
Reviews stored
Hundreds of millions (order of magnitude)
Yelp has publicly referenced review counts in the hundreds of millions. Derived implication: reviews outnumber businesses roughly 10-to-1, so rating rollups matter more than raw review lookups.
Search QPS
~5,000-20,000 QPS peak (estimate)
Derived: assume ~50M monthly active users, ~10 searches each per month = 500M searches/month = ~190 avg QPS. Apply a 25-50x peak-to-average factor for evening and weekend spikes to land in the thousands to low tens of thousands. Estimate only.
Read-to-write ratio
~1000:1 or higher (estimate)
Derived: searches and detail views vastly outnumber new reviews. If ~500M searches/month meet a few hundred thousand new reviews/day, reads dominate writes by three or more orders of magnitude, which justifies read replicas and heavy caching.
Storage for core data
~single-digit TB (estimate)
Derived: tens of millions of businesses at a few KB of structured fields is tens of GB; hundreds of millions of reviews at ~1 KB of text is a few hundred GB to low TB. Photos dominate raw bytes and live in object storage plus a CDN, not the search index.
Spatial cells per query
~4-10 cells (typical)
A covering algorithm for a 2 km radius over S2 or a quadtree typically resolves to a handful of mixed-level cells, so the engine scans a small candidate set instead of the whole index. This is the efficiency argument for hierarchical spatial indexing.
High-level architecture
A search request carries a location (a point plus radius, or a map bounding box) and a set of filters. It hits an API gateway, then a search coordinator service. The coordinator translates the geographic query into a spatial key range: it computes the geohash prefix, or the covering set of quadtree or S2 cells, for the requested area, and picks the geo-shard that owns that region. It fans the query out to the micro-shards inside that shard, where a search engine (Elasticsearch, or Yelp's Lucene-based Nrtsearch) filters candidate businesses by the spatial cells and the other predicates, and returns a cheap first-pass score. This is the recall phase: get a good candidate pool quickly. The coordinator merges results from the micro-shards, then a ranking layer rescores the pool with a learning-to-rank model that blends distance, rating, review count, text relevance, and business features into a final order. Popular queries and hot regions are served from a cache in front of all this. The business and review data lives in a relational store as the source of truth, replicated to read replicas for detail pages; a change-data pipeline streams edits and new reviews into the search index so the index stays fresh within seconds to minutes without the read path ever touching the primary database.
In a real interview, sketch this on the whiteboard before diving into any single box.
Core components
Walk through each service. The interviewer wants to hear what each one owns, not just the names.
Search coordinator
The query router. It converts a geographic request into spatial cells, selects the correct geo-shard, fans the query to the micro-shards within it, and merges their partial results. It is stateless and horizontally scalable, so it absorbs read spikes by adding instances.
Spatial index / search engine
Elasticsearch or a Lucene-based engine like Yelp's Nrtsearch holds the searchable business documents keyed by spatial cell plus category, hours, and text fields. It executes the geo filter and the first-pass scoring. Segments are immutable, which makes replication and near-real-time indexing efficient.
Ranking service
Takes the recalled candidate pool and applies a machine-learned model to reorder it. Features include the per-component scores from the search engine, distance, aggregate rating, review count, and business attributes. Separating ranking from recall lets the model evolve without touching the index.
Business and review store
The relational source of truth for business listings and user reviews. It handles writes with transactional integrity and serves detail pages through read replicas. It is not on the search hot path; the search engine is the query surface.
Indexing pipeline
A change-data-capture stream that carries business edits and new or deleted reviews from the primary store into the search engine and into rating rollups. It keeps the index fresh and decouples write latency from index latency.
Caching layer
A distributed cache (Redis or Memcached) plus a CDN. It stores rendered results for hot queries and hot map tiles, business detail payloads, and precomputed rating aggregates. Because the same popular neighborhoods and searches repeat, the cache absorbs a large share of read traffic.
Media and object storage
Photos and other heavy assets sit in object storage fronted by a CDN, referenced by URL from the business record. Keeping media out of the search index and the relational store keeps those systems small and fast.
Data model
Pick the right store per table. Justify each choice with the access pattern, not by reflex.
businessesbusiness_idnamelatitudelongitudegeo_cellcategoriesSource of truth for a listing. geo_cell stores the precomputed geohash or S2 cell id so the search index can shard and filter by location. Also holds address, price tier, and hours.
business_hoursbusiness_idday_of_weekopen_timeclose_timeNormalized hours so the search engine can answer open-now. Often denormalized into the search document as a bitmask or interval set to avoid a join at query time.
reviewsreview_idbusiness_iduser_idstarsbodycreated_atAppend-heavy relative to businesses. Indexed by business_id for detail pages. Rating rollups are computed from this table rather than read row-by-row on every search.
business_rating_rollupbusiness_idavg_starsreview_countupdated_atPrecomputed aggregate updated by the indexing pipeline when reviews change. Denormalized into the search document so ranking reads rating and count without touching the reviews table.
search_documentbusiness_idgeo_cellcategoriestext_blobopen_intervalsrating_signalsThe flattened, denormalized record inside Elasticsearch or Nrtsearch. Combines fields from several tables into one document so a query never joins. This is what gets sharded by geography.
Deep dives
These are the conversations the interviewer is steering you toward. Practice each one until you can talk through it without notes.
Why latitude/longitude B-trees fail and what a spatial index buys you
The naive design stores latitude and longitude as two columns with a B-tree on each. To find businesses within 2 km you compute a bounding box and issue 'latitude between A and B and longitude between C and D'. The problem is that each B-tree answers only one dimension, and the database intersects two wide 1D ranges. A query near a coast or a dense metro can scan a huge strip of the index and then discard most rows because they fail the other dimension or fall outside the true circle. Spatial indexes fix this by mapping 2D location to a single ordered key that preserves locality, so nearby points sit near each other in the index. Geohash interleaves the bits of latitude and longitude into one string; a shared prefix means physical proximity. Quadtrees recursively split space into four quadrants and store the path. Google S2 projects the sphere onto a cube and lays a Hilbert space-filling curve across it, producing 64-bit cell ids where numeric closeness implies spatial closeness. All three turn 'points near here' into 'keys in this small set of ranges', which is exactly what a B-tree or an inverted index is good at.
Geohash versus quadtree versus S2: the real trade-offs
Geohash is the simplest. It is a string prefix, trivial to store and to shard, and any database can range-scan it. Its weaknesses are boundary effects and uneven cell sizes: two points can be physically close but sit in different geohash cells with no shared prefix, so you must query the cell plus its eight neighbors to avoid missing results near an edge, and cells distort as you move away from the equator. Quadtrees adapt to density, subdividing only where there are many businesses, which is great for a Manhattan-versus-desert imbalance, but the tree needs rebalancing on heavy insert churn and is more naturally an in-memory structure than a persisted, shardable key. S2 gives the best spherical accuracy and a clean covering algorithm: for any circle or polygon you can compute the minimal set of cells, typically a handful at mixed levels, that covers the area, which keeps the candidate scan tight and handles boundaries without the eight-neighbor hack. The cost is complexity and a heavier library. For a Yelp-style read-heavy product with a fairly static set of businesses, a hierarchical scheme like S2 or a fixed geohash precision indexed in a search engine is the pragmatic choice; quadtrees shine more for transient, fast-moving points like live drivers.
Elasticsearch geo queries and how the filter actually runs
In practice you do not hand-roll the cell math on the read path; you let the search engine do it. Elasticsearch stores a geo_point field and supports geo_distance to filter within a radius of a center point and geo_bounding_box to filter to a rectangle, which maps directly to a map viewport. Under the hood modern Elasticsearch and Lucene index geo points with a BKD tree, a disk-friendly structure that handles multi-dimensional range queries efficiently, so the distance and box filters are fast without you managing geohash prefixes yourself. Geohash still shows up for aggregation: the geohash_grid aggregation buckets points into a grid at a chosen precision (1 to 12, where precision 12 cells cover under a square meter), which is how you build heatmaps and cluster pins on a zoomed-out map. The important interview point is that the geo predicate is a filter, not a scorer. You filter to the candidate set cheaply, and you let relevance scoring and the ranking model decide order, because filters are cacheable and cheap while scoring every business on the planet would not scale.
Two-phase retrieve-then-rank and learning-to-rank
Ranking is split into recall and precision. In the recall phase the search engine runs the geo filter plus category, hours, and text predicates and returns a candidate pool with a cheap first-pass score built from per-component subquery scores: one for name match, one for category, one for text relevance, one for distance. The engine is deliberately used as a recall engine, tuned to not miss good candidates rather than to get the order perfect. The precision phase then rescores that pool with a learning-to-rank model. Yelp described training a model that treats the component scores and business features as inputs and learns the weighting instead of hand-tuning a linear blend, which let them capture non-linear interactions and adapt as the data shifts; they reported moving an F1 metric from 91 to 95 percent on a labeled gold dataset. The architectural reason to separate the phases is that recall must run over a large index and stay cheap, while ranking runs over a few hundred candidates and can afford an expensive model. You can also swap ranking models without reindexing.
Geo-sharding and micro-sharding for a dense-metro workload
Business density is wildly uneven. A single downtown block can hold more restaurants than a rural county. If you shard purely by a hash of business_id you scatter a metro's businesses across every shard, so every 'near me' query fans out to the whole cluster. The better approach is to shard by geography first, so San Francisco and Chicago live in separate indexes and a query touches only the shard that owns the requested area. Within a geo-shard you still need to spread load, so you micro-shard, for example by business_id modulo n, to split a hot metro across several nodes and let queries run in parallel. The coordinator routes to the right geo-shard, fans out to its micro-shards, and merges. The tension is that geo-sharding by fixed regions can create hot shards during local peak hours and awkward cross-boundary queries at region edges, so you size regions by expected load rather than by area and accept some fan-out for queries that straddle a boundary.
Keeping the index fresh without slowing writes: near-real-time indexing
The relational store is the source of truth, but search runs against a separate index, so the two must be kept in sync without coupling write latency to index latency. A change-data-capture pipeline streams business edits, new reviews, and rating rollup updates out of the primary and into the search engine. The engine ingests these into new immutable segments that become searchable within seconds to minutes, which is the near-real-time property. Lucene-based engines lean on this hard: because segments are immutable, a replica can pull a finished segment from a primary instead of re-indexing the same document itself. Yelp built Nrtsearch on exactly this idea, using near-real-time segment replication where replicas download segments over gRPC and restore from S3 backups on startup. That model is why a new review can raise a business's rating and reorder search results shortly after it is written, while the user who wrote the review got a fast transactional acknowledgment from the primary database, not a wait on the index.
Read-heavy scaling: replicas, caching, and precomputed aggregates
Because reads dominate writes by orders of magnitude, most of the design is about not recomputing the same answer. Business detail pages read from database read replicas so the primary is reserved for writes. Search results and map tiles for popular queries and neighborhoods are cached, keyed by the normalized query plus the rounded viewport, so 'coffee near Union Square' does not re-run the full pipeline for every user. Aggregate ratings and review counts are precomputed into a rollup rather than summed from the reviews table on each search, which turns an expensive aggregation into a single field read that is denormalized into the search document. Search engine replicas serve query fan-out and are scaled independently of the indexing primaries; Yelp's move to segment replication was partly to make adding read replicas cheap, since a replica just pulls prebuilt segments instead of re-indexing. The general rule is to push work to write time and to the cache, so the common read path is close to a lookup.
Trade-offs to discuss
Every senior interviewer expects you to surface at least 3 of these. Pick the decisions, state the alternatives, and justify your choice.
Geohash versus S2 for the spatial key
Geohash is dead simple, string-prefixed, and works in any database, but it suffers boundary misses that force querying neighbor cells and it distorts with latitude. S2 gives accurate spherical cells and a tight covering algorithm at the cost of a heavier library. Pick geohash for simplicity and portability, S2 when accuracy and clean coverings matter.
Quadtree versus fixed-grid indexing
Quadtrees adapt to density and keep dense metros balanced, but they need rebalancing on churn and are more naturally in-memory. A fixed grid indexed in a search engine is simpler to persist and shard. For a mostly static business set, the fixed grid wins; for fast-moving transient points, the quadtree wins.
Filter-then-rank versus scoring everything
Scoring every candidate with the full relevance model is accurate but does not scale to a planet of businesses. Using the geo predicate as a cheap cacheable filter, then ranking only a few hundred survivors, keeps latency low with almost no relevance loss. The two-phase pipeline is the standard answer.
Search engine as recall engine versus doing ranking inside it
You can push scoring into Elasticsearch, but the relevance logic becomes hard to iterate on and couples ranking to the index. Extracting ranking into a separate model service lets the model change without reindexing and captures non-linear feature interactions, at the cost of an extra hop and a candidate pool transfer.
Geo-sharding by region versus hashing by business_id
Hashing spreads load evenly but scatters every metro across all shards, so each query fans out to the whole cluster. Geo-sharding keeps a query on one region's shard but risks hot shards during local peaks and messy cross-boundary queries. Real systems geo-shard for locality and micro-shard inside a region to spread load.
Near-real-time index versus synchronous index-on-write
Indexing synchronously with the write gives instant search visibility but makes user writes wait on the index and couples the two systems' availability. A change-data-capture pipeline with near-real-time segments keeps writes fast and search fresh within seconds, accepting a small visibility lag as the trade.
How Yelp actually does it
Yelp's real search stack has moved through three generations. It started on custom Lucene, migrated its core business search to Elasticsearch with a coordinator that geo-shards by region and micro-shards by business_id modulo n, and then built Nrtsearch, a Lucene-based engine using near-real-time segment replication where replicas pull immutable segments rather than re-indexing, which Yelp reported cut latency 30 to 50 percent at P50 through P99 and infrastructure cost by up to 40 percent by running on spot instances. Ranking is a documented two-phase design: Elasticsearch as a recall engine producing per-component scores, then a learning-to-rank model that Yelp reported lifted an F1 metric from 91 to 95 percent. On the storage side, Elasticsearch and Lucene index geo points with BKD trees and expose geo_distance and geo_bounding_box filters plus geohash_grid aggregations for map clustering. The spatial-index trade-offs (geohash boundary effects, quadtree density adaptation, S2 Hilbert-curve cells and coverings) are well documented in Google's S2 library and Uber's H3 writeups, which are the standard references for this class of problem.
Sources
- Yelp Engineering: Moving Yelp's Core Business Search to Elasticsearch
- Yelp Engineering: Nrtsearch, Yelp's Fast, Scalable and Cost Effective Search Engine
- Yelp Engineering: Learning to Rank for Business Matching
- Elasticsearch Reference: Geo queries (geo_distance, geo_bounding_box)
- Elasticsearch Reference: Geohash grid aggregation
Lessons to study before this interview
If any of these topics are fuzzy, the interviewer will catch it. Each lesson is 15 to 60 minutes with diagrams, code, and a quiz.
Related system design interview questions
Practice these next. They lean on the same core building blocks as Yelp.