← back to resume.sh

$ cat case-studies/high-scale-search.md

Building a High-Scale Search Orchestrator in Go

An architecture deep-dive into high-scale-search β€” a search orchestration service designed for 10B+ records at sub-200ms p99, unifying Elasticsearch, ClickHouse, Firestore, Redis, and Kafka behind a fault-tolerant pipeline.

What this is: an independent engineering project exploring architecture patterns for high-scale search β€” the numbers below (10B+ records, 50K+ QPS, 99.95% uptime) are the system's design targets, not measured production traffic. It's built to the same rigor as a production service: circuit breakers, graceful degradation, structured observability. Source on GitHub →

Why build this

Most side projects that claim to be "high-scale" stop at a README description. The goal here was the opposite: pick one hard, narrow problem β€” serving search over a dataset too large and too varied for any single datastore to handle well β€” and implement the actual resilience machinery a real search platform needs, not just the happy path.

Architecture

architecture.svg
Client API Gateway rate limit Β· auth Β· request id Search Orchestrator query parser · tokenize / normalize / spell-correct intent classifier · fulltext / analytics / facet / autocomplete cache lookup · redis query fingerprint route & execute · ES / ClickHouse / fan-out hydrate · firestore full documents Redis cache Β· L0/stale fallback Elasticsearch primary search ClickHouse analytics Β· degraded fallback Indexing path (write): Firestore write → Kafka (docs.changes) → Stream Processor → bulk buffer → Elasticsearch + ClickHouse changelog + Redis cache invalidation

The 5-level fallback chain

Search should never return a hard error. Instead of a single retry-then-fail path, every request falls through a graduated chain of degradation:

LevelSourceWhen
0Redis cacheCache hit on query fingerprint
1Elasticsearch (primary)Normal operation, behind a circuit breaker + retry
2Redis stale cachePrimary ES fails; serve slightly stale results
3ClickHouse (degraded)Both ES and cache unavailable; basic text search
4Static popular resultsAll backends down; pre-loaded popular results

Resilience mechanisms

Cache strategy

TTLs are tuned per query type rather than applied uniformly β€” high-churn data (trending) expires fast, stable data (autocomplete) is cached longer:

Query typeTTLKey pattern
Autocomplete10 minac:{prefix_hash}
Search results2 minsr:{query_hash}
Facet counts5 minfc:{category}:{filters_hash}
Trending60 sectrend:{region}
Stale fallback1 hoursr:stale:{query_hash}

Observability

Every layer emits Prometheus metrics β€” request latency histograms by intent/source/status, cache hit/miss counters, per-backend query duration, circuit breaker state, indexing lag, and Kafka consumer lag. Queries over 200ms are logged as slow; over 500ms are marked critical and written to a ClickHouse query_performance table for trend analysis. OpenTelemetry traces carry a consistent trace_id across every backend hop, so a single slow request can be followed end-to-end instead of guessed at.

Elasticsearch index strategy

What I'd do differently

The fallback chain and circuit breaker were the most valuable parts to build for real rather than describe abstractly β€” the interesting decisions (backoff caps, half-open probe timing, what "degraded" should even mean for a given query type) only show up once you implement it. If I extended this further, the next real gap is load-testing the fallback transitions themselves β€” proving level 1→2→3 handoffs stay correct under actual concurrent failure, not just in isolation.

page view count
βŽ‡ main case-study-high-scale-search.html
UTF-8 LF