System Design Library

Yelp / Nearby Places

Find businesses near a location with filters, reviews and ratings.

Open the interactive version → diagrams, practice & more

Requirements

Functional

  • Search by location + filters
  • Business profiles
  • Reviews/ratings

Non-functional

  • Fast geo search
  • Read-heavy

Scale

Millions of places; global

The approach

Geo-index places (geohash/QuadTree); search service combines spatial + attribute filters; reviews aggregated; heavy caching of popular areas; ratings precomputed.

Key components

App → geo+search index · place DB · review store · cache/CDN

Numbers that matter

Senior deep-dive

The geo-index is the foundation — without an efficient spatial structure, every 'nearby' query is a full table scan.

Geohash (or QuadTree) trades perfect circle proximity for rectangular cells — this is fine; over-fetch slightly and filter by true distance. The real complexity is blending spatial + attribute filters (cuisine, open now, rating ≥ 4) — these cannot be pushed into a geo-index alone and require a search layer (Elasticsearch) that indexes both location and attributes together.

Geohash vs QuadTree: pick based on your query shape

Geohash is simpler — it's a string prefix, so 'all businesses in this cell' is a range query. But it has boundary artifacts: two points straddling a geohash boundary can be close in distance but far in hash prefix. Fix by querying the cell and its 8 neighbors. QuadTree is more flexible (adaptive cell sizes, no boundary problem) but requires a tree traversal structure in memory or a DB, which adds operational complexity.

Blending geo + attribute filters without Cartesian explosion

The naive approach — filter by geo, then filter by attributes in application code — breaks when geo returns 100k results and you need to sort by rating + distance. Elasticsearch's bool query lets you combine a geo_distance filter with attribute term/range filters in a single query, scored and paginated server-side. The key insight: index all attributes on the same document as the geo_point so the engine can evaluate them in one pass over the same inverted index.

Rating aggregates: never compute at query time

Computing average rating from raw reviews at query time requires joining millions of review rows per search request — completely unscalable. Precompute avg_rating and review_count as fields on the business document, updated asynchronously on every review write (via a message queue). Accept eventual consistency (a new review may take seconds to affect the displayed rating). For the DB, maintain running sum + count columns and compute average in the application — avoids expensive re-aggregation on every update.

Open Now: the time-zone landmine

'Open now' is deceptively hard — business hours are stored in local time (the business's time zone), but the query arrives in UTC. You must convert the query time to the business's local time zone before comparing with hours. Storing hours as a weekly schedule in local time (e.g. Mon 09:00–22:00 in 'America/Los_Angeles') is correct; converting to UTC at write time is wrong because DST shifts those hours seasonally. Index the effective UTC window for the next N hours and refresh it with a background job.

Caching popular area results

80% of searches are for a handful of popular areas (downtown SF, Times Square). Cache the top-K results per (geohash_cell, cuisine, sort_order) in Redis with a short TTL (~5 min). Cache keys can be as simple as a hash of (geohash_prefix, filter_hash). Invalidate on business update by dropping all keys that include the affected geohash. This eliminates the Elasticsearch round-trip for the common case, reducing p50 from ~50ms to ~5ms.

What breaks at scale

Hot geohash cells (downtown Manhattan has 10,000+ businesses in a single level-5 cell) mean geo-range queries return massive result sets that tank Elasticsearch shard performance. Fix by using finer geohash granularity for dense areas (level 7 in cities vs level 5 in suburbs) — but this requires dynamic resolution in the query path. The second failure: review writes become a hotspot on popular business records — batch review-count increments in Redis and flush to the DB asynchronously, rather than taking a row lock on the business record for every review submission.

In production

Yelp uses Elasticsearch as the primary search backend, indexing business attributes alongside a geo_point field for spatial queries — this lets a single query filter by 'pizza + open now + within 2km' without application-side joins. Foursquare built its own geospatial index using an S2-geometry cell hierarchy, which handles spherical geometry more accurately than geohash at the cost of implementation complexity. The real engineering challenge is keeping the index fresh: business hours change, businesses close, new ones open — Yelp runs a near-realtime indexing pipeline (Kafka → Elasticsearch bulk indexer) to propagate changes from the DB within <60 seconds. Review aggregates (star rating, review count) are maintained with counter denormalization and periodic reconciliation jobs, not computed on the fly.

Common mistakes

Related System Design Library

Part of System Design Library on SystemLore — system design interview prep with 148 deep topics, interactive diagrams, and a practice game. Practice this one →