DDSA Solutions
Case Study6 min read·

Design Yelp (Nearby Places / Local Search)

How to design Yelp-style nearby search for interviews: geospatial indexing, geohashes, ranking, reviews, and scaling location-based queries.

Yelp (or “find coffee near me”) is the classic geospatial interview. The hard part is not storing businesses — it is answering “points within radius R of (lat, lng), filtered by category, ranked by distance and rating” in tens of milliseconds. You will reuse geo ideas from Uber but optimize for read-heavy discovery instead of real-time driver matching.

Start with the framework: radius vs map viewport, whether live inventory (open now) matters, and review write volume vs search QPS.

Functional requirements

  • Search businesses near a location with optional category and keyword.
  • Return ranked list: distance, rating, relevance.
  • Business detail page: hours, photos, reviews.
  • Users write reviews and upload photos.
  • Optional: autocomplete for business names (typeahead).

Non-functional

  • Search p99 under ~100–150 ms.
  • Freshness: new business visible within minutes; reviews near-real-time on detail page.
  • Scale to tens of millions of businesses and high mobile QPS in dense cities.

Dense cities are the stress test

Manhattan at lunch has more businesses per km² than rural Kansas. Index design must not scan the continent to find bagels on 5th Avenue.

Capacity

50M businesses, 5K search QPS peak, average result set 20. Reviews: 1M/day. Photos via S3/CDN like Instagram. Search is the bottleneck; writes are smaller but must invalidate or update geo indexes.

Geospatial indexing options

ApproachIdeaTrade-off
GeohashEncode lat/lng as base32 string; nearby = shared prefixSimple; edge cells need neighbor hashes
QuadtreeRecursive subdivision of mapGood adaptive density; more complex
Google S2 / HilbertCell IDs on sphereProduction-grade; name it, do not implement
PostGIS / ES geoDB or Elasticsearch geo queriesPractical default in interviews

Interview favorite: geohash. Choose precision so cell size ≈ search radius (e.g. ~1.2 km for precision 6). Query the cell containing the user plus 8 neighbors. Fetch candidates, filter exact Haversine distance client-side or in app, then rank. For map viewport, compute covering geohashes for the bounding box.

Architecture

  1. Business service — metadata in PostgreSQL (business_id, name, category, lat, lng, hours).
  2. Geo index — Redis sets keyed by geohash → business_ids, or Elasticsearch geo_point index.
  3. Search / ranker — merge candidates, apply filters, score.
  4. Review service — append-only reviews; aggregate rating async.
  5. Media — S3 + CDN for photos.
  6. Cache — hot geohash result pages in Redis (caching).

Why not SQL alone?

`SELECT * FROM businesses WHERE lat BETWEEN … AND lng BETWEEN …` with a btree on lat fails at scale (lng filter still scans). Spatial indexes (GiST/SP-GiST, ES geo) or geohash buckets are required. See SQL vs NoSQL: OLTP for source of truth, specialized index for geo read path.

Ranking

Score = f(distance, rating, review_count, text relevance, business response rate). Keep the formula simple: weighted sum. Personalization is a stretch. Never rank only by distance — a 4.9 café 200 m away beats a 2.0 café 50 m away for most products.

  • Keyword: inverted index on name/category (same ideas as search engine).
  • Category filter: term filter in ES or secondary index category → ids intersected with geo set.
  • “Open now”: filter on hours; cache carefully around midnight boundaries in local TZ.

Write path for reviews

  1. POST review → review DB; enqueue rating recompute.
  2. Worker updates business.avg_rating and review_count.
  3. Invalidate cache keys for that business and surrounding geohash pages.
  4. Moderate abuse async (rate limits on review create).

Scaling tricks

  • Shard geo index by geohash prefix or region (sharding).
  • Precompute top businesses per popular geohash for anonymous homepage.
  • CDN for static business photos; API for search JSON.
  • Load-balance search tier (load balancing).

Edge cases

  • International date line / poles — use a library; mention you would not hand-roll spherical math.
  • Geohash boundaries — always include neighbors or results vanish at cell edges.
  • Moving user — mobile refetches on significant location change; debounce.
  • Duplicate listings — dedupe by place_id / address fingerprint.

Worked example

  1. User at (12.97, 77.59) searches “coffee” radius 1 km.
  2. Geohash precision 6 → cell tdr1u2 + 8 neighbors.
  3. Union business_ids from Redis sets; intersect with category=coffee.
  4. Compute distances; keep ≤ 1 km; sort by score; return top 20.
  5. Detail page loads reviews from review service; photos from CDN.

Autocomplete and discovery

As the user types “piz”, hit a prefix index scoped to the current city or viewport — reuse typeahead with a geo filter. Popular queries (“best tacos near me”) can be cached as entire result lists keyed by coarse geohash + query hash.

LayerStoreRole
Source of truthPostgreSQLBusiness rows, hours, owner
Geo + text searchElasticsearchgeo_point + filters + BM25
Hot cacheRedisTop results per geohash
MediaS3 + CDNPhotos

Fraudulent reviews and fake businesses need async moderation queues — not on the search critical path. Shadow-ban in the index by flipping an `is_visible` flag and reindexing.

Map view vs list view

Map pinch-zoom changes the bounding box constantly. Debounce search requests (300 ms) and request only the delta of newly visible geohashes when possible. List view can page with `search_after` in Elasticsearch. Return lightweight pins (id, lat, lng, rating) for the map; hydrate cards on tap to cut payload size on mobile networks.

Interview narrative

Lead with geohash + neighbors, then ranking, then review write path. Compare to Uber: Uber needs live locations updating every few seconds; Yelp businesses are mostly static coordinates. That comparison proves you pick indexes for the workload, not by buzzword.

More in this series