system-design47 min read

How to Design a Search Engine — A Senior+ Guide | Ayodhyya

How to Design a Search Engine

Building a Google-Scale System — Crawl, Index, Rank, Serve, and Monetize Trillions of Web Pages

Senior+ System Design Guide 10,000+ Words 20 Deep-Dive Sections C# · Mermaid · Real-World Case Studies

1. Introduction & Why Search Engines Are Hard

A search engine is arguably the most complex large-scale distributed system ever built by humanity. Google processes approximately 8.5 billion searches per day, indexing over 400 billion web pages, and returning results in under 200 milliseconds. Behind that deceptively simple search box lies a machinery of web crawlers spanning millions of machines, inverted indexes consuming petabytes of storage, ranking algorithms that blend graph theory with deep neural networks, and an ad auction system that generates over $200 billion in annual revenue.

Building a search engine is not merely a data retrieval problem. It is a multi-disciplinary challenge that spans information retrieval, distributed systems, machine learning, natural language processing, network engineering, and real-time advertising. The latency requirements are brutal: every millisecond of additional response time costs revenue and user trust. The consistency requirements are nuanced: stale results are acceptable for some queries but catastrophic for breaking news. The scale requirements are staggering: the index must be updated continuously while serving trillions of queries without downtime.

In this deep-dive, we will dissect every major subsystem of a modern search engine — from the web crawler that discovers and fetches pages, through the indexing pipeline that tokenizes and stores content, to the ranking engine that determines which results appear first. We will examine how search engines handle spell correction, autocomplete, image search, video search, voice search, advertisements, caching, and fault tolerance. Throughout, we will use C# code examples, Mermaid architecture diagrams, and real-world numbers from Google, Bing, Baidu, and other production systems.

Why this matters for senior engineers: Search engine design questions are among the most common system design interview topics at FAANG companies. Even if you are not building a search engine, the underlying patterns — inverted indexes, distributed ranking, ad auctions, and query pipelines — appear in recommendation systems, e-commerce platforms, log analysis systems, and any domain that requires fast full-text retrieval over massive datasets.

Historical Context

The modern search engine evolved from academic information retrieval systems of the 1960s and 1970s. The SMART system at Cornell, developed by Gerard Salton, introduced the vector space model and TF-IDF weighting. In the 1990s, AltaVista and Lycos brought web search to the masses, but it was Google's PageRank algorithm — treating the web as a graph and ranking pages by link authority — that revolutionized search quality. Today, search engines combine classical information retrieval with deep learning models like BERT, MUM, and large language models that understand semantic meaning, not just keyword matching.

Key insight: The fundamental challenge has shifted from "finding relevant documents" to "understanding user intent." A search for "apple" could mean the fruit, the company, or the record label. Modern search engines must resolve this ambiguity using context, user history, and real-time signals — all within 200 milliseconds.

2. Functional & Non-Functional Requirements

Functional Requirements

  1. Web Crawling: Continuously discover and fetch web pages from the public internet, respecting robots.txt, politeness policies, and crawl rate limits.
  2. Indexing: Process crawled pages into an inverted index that maps terms to document IDs, positions, and metadata. Support full-text, phrase, and boolean queries.
  3. Query Processing: Parse user queries, correct spelling, generate autocomplete suggestions, and retrieve matching documents from the index.
  4. Ranking: Score and排序 results using a combination of relevance signals (TF-IDF, BM25), authority signals (PageRank), freshness, and machine learning models.
  5. SERP Generation: Render search results pages with titles, URLs, snippets, knowledge panels, image carousels, video results, and "People Also Ask" sections.
  6. Multi-Modal Search: Support image search (reverse image lookup), video search, and voice search with speech-to-text transcription.
  7. Ad Serving: Run a real-time ad auction for sponsored results, integrating with advertiser bid management and quality scoring systems.
  8. Spell Correction & Autocomplete: Detect misspelled queries and suggest corrections. Provide type-ahead autocomplete suggestions as users type.
  9. Personalization: Tailor results based on user location, search history, language preferences, and device type.
  10. Freshness: Index breaking news and trending topics within minutes of publication.

Non-Functional Requirements

PropertyTargetRationale
Availability99.99% (52 min/year downtime)Search is mission-critical; downtime directly impacts revenue and user trust
Latency (p50)< 100msUsers expect near-instant results
Latency (p99)< 300ms99th percentile must still feel responsive
Throughput100,000+ QPS per datacenterGoogle handles ~100K queries/second globally
Index Freshness< 1 min for news; < 24h for general webBreaking news must appear quickly
Index Size400B+ pagesCovers the vast public web
ConsistencyEventual consistency acceptableStale results are tolerable for most queries
Durability99.999999999% (11 nines)Index regeneration is extremely expensive
Interview tip: Always clarify whether the search engine is for the public web (like Google) or an enterprise/document search system (like Elasticsearch). The scale and crawl requirements differ dramatically. Public web search requires petabyte-scale indexes and autonomous crawlers, while enterprise search may operate on a bounded corpus with known document schemas.

3. Capacity Estimation & Back-of-Envelope Math

Before designing any system, we must quantify the scale. Let us estimate the storage, bandwidth, and compute requirements for a Google-scale search engine.

Index Size Estimation

Assume 400 billion web pages with an average page size of 500 KB (HTML + text). The raw crawl data alone is:

400B × 500KB = 200 PB of raw HTML. However, the inverted index is far more compact. After compression, a typical inverted index stores approximately 1-3 bytes per token. With an average of 1,000 unique terms per page across 400 billion pages, the compressed inverted index is roughly:

400B × 1,000 terms × 2 bytes = 800 TB for the term-to-document mappings. Add document metadata, URLs, PageRank scores, and link graph data, and the total index storage is approximately 2-5 PB.

QPS Estimation

With 8.5 billion queries per day:

8.5B / 86,400 seconds = ~98,400 QPS

Peak traffic (morning hours in US/Europe overlap) can be 2-3x average, so we must design for ~250,000 QPS peak.

Bandwidth Estimation

Each query response averages 50 KB (JSON payload with results, snippets, metadata). At peak QPS:

250,000 QPS × 50 KB = 12.5 GB/s = 100 Gbps egress per datacenter. Globally, across 30+ datacenters, total egress exceeds 3 Tbps.

Crawl Bandwidth

To keep the index fresh, we must continuously crawl the web. At 400 billion pages with an average refresh cycle of 30 days:

400B / 30 days = ~13.3B pages/day = ~154,000 pages/second

At 500 KB per page, the crawl download rate is approximately 154,000 × 500KB = 77 GB/s. This requires thousands of crawler nodes distributed across multiple datacenters.

MetricEstimated Value
Total web pages indexed400 billion
Average page size500 KB
Raw crawl data200 PB
Compressed inverted index2-5 PB
Queries per second (avg)~100,000
Queries per second (peak)~250,000
Crawl pages per second~154,000
Egress per datacenter~100 Gbps
Storage servers needed~50,000-100,000
Crawler nodes needed~10,000-50,000
Back-of-envelope sanity check: Google is known to operate over 2.5 million servers. Our estimate of 50,000-100,000 for search indexing alone is consistent, since Google also runs YouTube, Gmail, Maps, Cloud, and dozens of other services on its infrastructure.

4. Data Model & Storage Schema

Core Entities

A search engine's data model revolves around four primary entities: Documents (web pages), the Inverted Index (term-to-document mappings), the Link Graph (hyperlinks between pages), and Query Logs (user search history for ranking improvements).

C#
public class WebPage
{
    public string Url { get; set; }                    // Canonical URL
    public string DocumentId { get; set; }             // Unique numeric/docid
    public byte[] RawHtml { get; set; }                // Raw HTML content
    public string ExtractedText { get; set; }          // Cleaned body text
    public string Title { get; set; }                  // Page title
    public string Language { get; set; }               // Detected language (ISO 639)
    public DateTime CrawledAt { get; set; }            // Crawl timestamp
    public DateTime LastModified { get; set; }         // Last-Modified header
    public int ContentLength { get; set; }             // Bytes
    public Dictionary<string, string> Headers { get; set; } // HTTP headers
    public List<Outlink> Outlinks { get; set; }       // Outgoing hyperlinks
    public double PageRankScore { get; set; }          // Computed PageRank
    public PageClassification Classification { get; set; } // Spam quality etc.
}

public class Outlink
{
    public string TargetUrl { get; set; }
    public string AnchorText { get; set; }             // Link anchor text
    public bool IsNofollow { get; set; }               // rel="nofollow"
}

public class IndexEntry
{
    public string Term { get; set; }                   // Tokenized term
    public List<Posting> Postings { get; set; }        // List of matching docs
}

public class Posting
{
    public string DocumentId { get; set; }             // Reference to WebPage
    public int TermFrequency { get; set; }             // TF in document
    public List<int> Positions { get; set; }           // Token positions
    public byte FieldMask { get; set; }                // Title=1, Body=2, URL=4
    public float StaticRank { get; set; }              // Precomputed PageRank
    public DateTime LastIndexed { get; set; }          // Freshness signal
}

public class LinkEdge
{
    public string SourceDocId { get; set; }            // Linking page
    public string TargetDocId { get; set; }            // Linked page
    public string AnchorText { get; set; }             // Anchor text
    public float Weight { get; set; }                  // Link importance
}

public class QueryLog
{
    public string QueryId { get; set; }
    public string QueryText { get; set; }
    public string UserId { get; set; }                 // Anonymized
    public DateTime Timestamp { get; set; }
    public string UserLocation { get; set; }
    public string DeviceType { get; set; }
    public List<string> ClickedResults { get; set; }   // Click-through data
    public int DwellTimeMs { get; set; }               // Time on clicked result
}

Storage Choices

DataStorage SystemRationale
Inverted IndexCustom LSM-tree on SSD/NVMeNeed maximum read throughput; custom format outperforms general-purpose KV stores
Link GraphDistributed graph store (Bigtable-style)PageRank computation requires efficient adjacency list traversal
Document Store (raw HTML)Object storage (GCS/S3)Cold storage for re-crawl; not on hot path
Document MetadataDistributed KV store (Bigtable)Frequent point lookups during snippet generation
Query LogsAppend-only log → data lakeWrite-heavy, read rarely for ML training
PageRank ScoresIn-memory distributed cacheMust be fast for ranking; recomputed weekly
Autocomplete TrieIn-memory replicated trieSub-millisecond lookups required
Schema evolution: The inverted index schema must be versioned. When adding new field types (e.g., structured data from Schema.org) or changing compression codecs, old index segments can continue serving queries while new segments are built with the updated schema. This is analogous to online schema migration in databases.

5. High-Level Architecture Overview

A search engine's architecture can be decomposed into two major pipelines: the offline indexing pipeline (crawl → parse → index → rank) and the online serving pipeline (query → parse → retrieve → rank → render). These pipelines are decoupled through the index, which acts as the shared state between them.

graph TB subgraph "Offline: Indexing Pipeline" A[Web Crawler] -->|Raw HTML| B[DNS Resolver] B -->|IP Address| C[Fetcher Pool] C -->|HTTP Response| D[HTML Parser & Extractor] D -->|Clean Text + Metadata| E[Tokenizer & Stemmer] E -->|Token Stream| F[Inverted Index Builder] F -->|Index Segments| G[Index Merge Service] G -->|Final Shards| H[Distributed Index Storage] F -->|Link Data| I[Link Graph Builder] I -->|Adjacency Lists| J[PageRank Computer] J -->|Scores| K[Rank Score Store] end subgraph "Online: Query Serving Pipeline" L[Load Balancer] -->|Query| M[Query Parser] M -->|Parsed Query| N[Spell Correction] N -->|Corrected| O[Autocomplete Service] O -->|Suggestions| P[Index Router] P -->|Scatter-Gather| H H -->|Matching Docs| Q[Initial Ranker - BM25] Q -->|Top-K Candidates| R[ML Ranker - LambdaMART] R -->|Ranked Results| S[Snippet Generator] S -->|SERP Data| T[Ad Auction Service] T -->|Ads + Organic| U[Response Assembler] U -->|JSON| V[CDN Edge] V -->|HTML| W[User Browser] end J -.->|PageRank Scores| R K -.->|Static Rank| Q style A fill:#f78166,color:#000 style H fill:#58a6ff,color:#000 style R fill:#7ee787,color:#000 style T fill:#d29922,color:#000

Key Design Principles

  • Separation of indexing and serving: The indexing pipeline runs continuously in the background, while the serving pipeline handles real-time queries. This allows independent scaling and failure isolation.
  • Shard everything: The index is partitioned across thousands of machines. A single query fans out to many shards in parallel, and results are merged at the aggregator layer.
  • Multi-level ranking: A cheap first-pass ranker (BM25) reduces millions of candidates to thousands, then an expensive ML ranker (LambdaMART/BERT) produces the final ordering.
  • Graceful degradation: If the ML ranker is slow, fall back to BM25. If a shard is unavailable, serve results from remaining shards. If autocomplete fails, skip suggestions.

Dataflow Summary

The offline pipeline continuously crawls the web, builds inverted index segments, computes PageRank, and publishes new index shards. The online pipeline receives user queries, fans out to index shards, collects candidate documents, ranks them through multiple stages, generates snippets, runs the ad auction, and assembles the final response. The two pipelines are connected through the distributed index storage and the PageRank score store.

Architecture pattern: This offline/online split is a universal pattern for search-like systems. You will see the same architecture in log analysis platforms (ELK stack), recommendation engines, and real-time analytics systems. Master this pattern and you can apply it broadly.

6. Web Crawler Design

The web crawler is the system's front door — it discovers and fetches web pages from the internet. A production crawler must handle billions of URLs, respect politeness policies, detect content changes, manage a crawl frontier, and recover from failures gracefully.

Crawler Architecture

graph LR A[Seed URLs] --> B[URL Frontier / Priority Queue] B --> C[DNS Resolver Cache] C --> D[Fetcher Threads] D --> E[HTTP Response Handler] E --> F[Robots.txt Checker] F --> G[Content Filter] G --> H[Duplicate Detector - SimHash] H --> I[HTML Parser] I --> J[URL Extractor] J --> B I --> K[Index Pipeline] H -->|New/Changed| L[Change Detector] L --> B style B fill:#58a6ff,color:#000 style H fill:#f78166,color:#000 style K fill:#7ee787,color:#000

URL Frontier Management

The URL frontier is a priority queue that determines which URLs to crawl next. It must support: (1) priority-based scheduling so important pages (high PageRank, high traffic) are crawled more frequently, (2) politeness constraints per host (rate limiting, crawl-delay from robots.txt), (3) URL deduplication to avoid re-crawling identical URLs, and (4) persistence across crawler restarts.

C#
public class CrawlFrontier
{
    private readonly ConcurrentDictionary<string, CrawlTask> _urlQueue;
    private readonly PriorityQueue<CrawlTask, double> _priorityQueue;
    private readonly Dictionary<string, HostCrawlPolicy> _hostPolicies;
    private readonly BloomFilter<string> _urlBloomFilter;
    private readonly SimHashDuplicateDetector _duplicateDetector;

    public async Task<CrawlTask?> GetNextUrlAsync()
    {
        while (_priorityQueue.TryDequeue(out var task, out var priority))
        {
            var host = new Uri(task.Url).Host;
            var policy = _hostPolicies.GetOrAdd(host, CreateDefaultPolicy);

            if (!policy.CanCrawlNow())
            {
                _priorityQueue.Enqueue(task, priority);
                await Task.Delay(policy.TimeUntilNextCrawl());
                continue;
            }

            if (_urlBloomFilter.MightContain(task.Url))
            {
                var existing = await GetCrawlStateAsync(task.Url);
                if (existing?.ContentHash == task.ExpectedHash)
                    continue; // Skip unchanged URL
            }

            policy.RecordCrawl();
            return task;
        }
        return null;
    }

    public void SubmitUrls(IEnumerable<string> discoveredUrls, string sourceUrl)
    {
        foreach (var url in discoveredUrls)
        {
            var normalized = NormalizeUrl(url);
            if (!IsAllowedByRobots(normalized)) continue;
            if (!IsInScope(normalized)) continue;

            var priority = ComputePriority(normalized, sourceUrl);
            var task = new CrawlTask
            {
                Url = normalized,
                DiscoveredFrom = sourceUrl,
                Priority = priority,
                DiscoveredAt = DateTime.UtcNow
            };
            _priorityQueue.Enqueue(task, priority);
            _urlBloomFilter.Add(normalized);
        }
    }

    private double ComputePriority(string url, string sourceUrl)
    {
        double score = 0;
        score += GetPageRankEstimate(url) * 0.4;
        score += GetDomainAuthority(url) * 0.3;
        score += GetFreshnessBoost(url) * 0.2;
        score += GetUrlDepthScore(url) * 0.1;
        return score;
    }
}

Politeness and Robots.txt

Every well-behaved crawler must respect the robots.txt file, which specifies which paths a crawler is allowed to access and the required crawl delay between requests to the same host. Violating these policies can result in IP bans and legal action. The crawler maintains a per-host robots.txt cache with TTL-based expiration, and enforces rate limits using token bucket algorithms.

Duplicate Detection with SimHash

Near-duplicate detection is critical to avoid wasting crawl resources and index storage. SimHash produces a 64-bit fingerprint of a document's content. Two documents with Hamming distance ≤ 3 between their SimHash values are considered near-duplicates. This approach is used by Google to detect mirror sites and lightly modified content.

C#
public class SimHashDuplicateDetector
{
    private const int HASH_BITS = 64;
    private const int NEAR_DUPLICATE_THRESHOLD = 3;

    public ulong ComputeSimHash(string text)
    {
        var tokens = Tokenize(text);
        var vector = new double[HASH_BITS];

        foreach (var token in tokens)
        {
            var hash = MurmurHash3(token);
            for (int i = 0; i < HASH_BITS; i++)
            {
                if ((hash & (1UL << i)) != 0)
                    vector[i] += 1.0;
                else
                    vector[i] -= 1.0;
            }
        }

        ulong simhash = 0;
        for (int i = 0; i < HASH_BITS; i++)
        {
            if (vector[i] > 0)
                simhash |= (1UL << i);
        }
        return simhash;
    }

    public bool IsNearDuplicate(ulong hash1, ulong hash2)
    {
        ulong xor = hash1 ^ hash2;
        int hammingDistance = BitOperations.PopCount(xor);
        return hammingDistance <= NEAR_DUPLICATE_THRESHOLD;
    }
}
Failure mode: A crawler that does not enforce rate limits can inadvertently launch a denial-of-service attack against target websites. Production crawlers must implement per-host rate limiting, honor robots.txt directives, and detect when a target site returns 503 (Service Unavailable) or 429 (Too Many Requests) responses. The crawler should back off exponentially when encountering these signals.

7. Inverted Index Construction

The inverted index is the core data structure of any search engine. It maps every unique term in the corpus to the list of documents that contain that term, along with metadata like term frequency, positions, and field information. Building this index at web scale requires careful attention to tokenization, compression, and incremental updates.

Indexing Pipeline Stages

graph TB A[Raw HTML Document] --> B[HTML Parser & Boilerplate Removal] B --> C[Text Extraction - Readability Algorithm] C --> D[Language Detection] D --> E[Tokenization - Unicode-aware] E --> F[Stop Word Removal - Optional] F --> G[Stemming / Lemmatization] G --> H[Synonym Expansion] H --> I[Position Index Builder] I --> J[Compression - Variable-byte or PForDelta] J --> K[Index Segment File] L[Link Extractor] --> M[Anchor Text Index] M --> I style A fill:#f78166,color:#000 style K fill:#7ee787,color:#000 style J fill:#58a6ff,color:#000

Tokenization and Text Processing

Tokenization splits text into individual terms. For English, this is relatively straightforward (split on whitespace and punctuation), but for languages like Chinese, Japanese, and Korean (CJK), word segmentation requires specialized libraries. After tokenization, terms are lowercased, stemmed (reducing "running" to "run"), and optionally lemmatized (reducing "better" to "good").

C#
public class IndexBuilder
{
    private readonly Dictionary<string, List<Posting>> _invertedIndex;
    private readonly Dictionary<string, DocumentMetadata> _docStore;
    private int _currentDocId;

    public void IndexDocument(WebPage page)
    {
        var docId = Interlocked.Increment(ref _currentDocId).ToString();
        var tokens = ProcessText(page.ExtractedText);
        var titleTokens = ProcessText(page.Title);

        var termPositions = new Dictionary<string, List<int>>();
        for (int i = 0; i < tokens.Count; i++)
        {
            if (!termPositions.ContainsKey(tokens[i]))
                termPositions[tokens[i]] = new List<int>();
            termPositions[tokens[i]].Add(i);
        }

        foreach (var (term, positions) in termPositions)
        {
            var posting = new Posting
            {
                DocumentId = docId,
                TermFrequency = positions.Count,
                Positions = positions,
                FieldMask = DetermineFieldMask(term, titleTokens, page.Url),
                StaticRank = page.PageRankScore,
                LastIndexed = DateTime.UtcNow
            };

            lock (_invertedIndex)
            {
                if (!_invertedIndex.ContainsKey(term))
                    _invertedIndex[term] = new List<Posting>();
                _invertedIndex[term].Add(posting);
            }
        }

        _docStore[docId] = new DocumentMetadata
        {
            Url = page.Url,
            Title = page.Title,
            Language = page.Language,
            CrawledAt = page.CrawledAt,
            ContentLength = page.ContentLength
        };
    }

    public IndexSegment FlushSegment()
    {
        var sortedTerms = _invertedIndex.Keys.OrderBy(k => k).ToList();
        var segment = new IndexSegment
        {
            TermDictionary = sortedTerms,
            PostingLists = sortedTerms.Select(t => CompressPostings(_invertedIndex[t])).ToList(),
            DocStore = _docStore,
            CreatedAt = DateTime.UtcNow
        };
        _invertedIndex.Clear();
        _docStore.Clear();
        return segment;
    }

    private byte[] CompressPostings(List<Posting> postings)
    {
        using var stream = new MemoryStream();
        using var writer = new BinaryWriter(stream);

        writer.Write(postings.Count);
        string prevDocId = "";
        foreach (var p in postings.OrderBy(p => p.DocumentId))
        {
            var docIdDelta = DeltaEncode(prevDocId, p.DocumentId);
            WriteVariableByteInt(writer, docIdDelta);
            WriteVariableByteInt(writer, p.TermFrequency);
            WritePositions(writer, p.Positions);
            writer.Write(p.StaticRank);
            prevDocId = p.DocumentId;
        }
        return stream.ToArray();
    }
}

Index Compression

Compression is essential for reducing storage costs and improving cache hit rates. Common techniques include variable-byte encoding for document IDs and term frequencies, PForDelta or Simple-9 for position lists, and dictionary encoding for the term dictionary. Google reportedly achieves 10:1 compression ratios on their inverted index using custom codecs.

Incremental Index Updates

Rebuilding the entire index from scratch takes hours and is resource-intensive. Instead, production systems use a Log-Structured Merge (LSM) approach: small in-memory index buffers are periodically flushed to disk as immutable segments. Background merge processes combine smaller segments into larger ones. Queries fan out to all segments and merge results. This approach allows near-real-time index updates without full rebuilds.

Performance consideration: The inverted index must fit in memory for optimal query performance. For a 5 PB compressed index, you would need tens of thousands of machines with 256 GB+ RAM each. Google reportedly uses custom SSDs with NVMe interfaces and proprietary controller firmware to achieve the IOPS required for index lookups at query time.

8. Index Sharding & Distribution

No single machine can hold the complete inverted index for the entire web. The index must be sharded (partitioned) across thousands of machines. The choice of sharding strategy profoundly impacts query latency, load balancing, and fault tolerance.

Sharding Strategies

StrategyDescriptionProsCons
Document-based shardingEach shard contains a subset of documents (e.g., shard = hash(docId) % N)Simple; even load; easy to add/remove shardsEvery query must fan out to all shards
Term-based shardingEach shard holds all documents for a subset of terms (e.g., shard = hash(term) % N)Single-shard lookup for single-term queriesHot terms create hotspots; imbalanced load
Hybrid shardingTerm-based primary with document-based secondary replicasBest of both worlds; load balancing via replicasComplex to maintain consistency
Geo-based shardingShards partitioned by document's primary language or regionNatural locality for geo-specific queriesUneven shard sizes; cross-shard queries for multi-language

Production search engines typically use document-based sharding with multiple replicas per shard. Each document is hashed to a primary shard, and 2-3 replicas are placed on different racks or datacenters for fault tolerance. A query router fans out to all shards in parallel, collects the top-K results from each, and merges them using a priority queue.

C#
public class IndexShardRouter
{
    private readonly List<ShardClient> _shards;
    private readonly int _replicationFactor;
    private readonly ConsistentHashRing<string> _hashRing;

    public async Task<List<RankedDocument>> QueryAsync(SearchQuery query, int topK)
    {
        var candidateShards = _hashRing.GetReplicatedShards(count: _replicationFactor);
        var shardTasks = candidateShards.Select(shard =>
            QueryShardAsync(shard, query, topK * 2) // Over-fetch for merge
        ).ToList();

        var shardResults = await Task.WhenAll(shardTasks);

        // Merge results from all shards using a tournament tree
        var mergedResults = MergeShardResults(shardResults, topK);
        return mergedResults;
    }

    private List<RankedDocument> MergeShardResults(
        List<List<RankedDocument>> shardResults, int topK)
    {
        var resultHeap = new PriorityQueue<RankedDocument, float>();

        foreach (var results in shardResults)
        {
            foreach (var doc in results)
            {
                resultHeap.Enqueue(doc, -doc.Score); // Max-heap via negation
            }
        }

        var merged = new List<RankedDocument>();
        while (resultHeap.Count > 0 && merged.Count < topK)
        {
            merged.Add(resultHeap.Dequeue());
        }
        return merged;
    }

    private async Task<List<RankedDocument>> QueryShardAsync(
        ShardClient shard, SearchQuery query, int limit)
    {
        var deadline = DateTime.UtcNow.AddMilliseconds(50); // Per-shard timeout
        try
        {
            return await shard.SearchAsync(query, limit, deadline);
        }
        catch (TimeoutException)
        {
            return new List<RankedDocument>(); // Shard timeout: skip
        }
    }
}
Hotspot mitigation: Term-based sharding alone creates hotspots — the term "the" would overwhelm a single shard. Google reportedly uses a combination of document-based sharding with query-level caching. The most popular queries (which account for a disproportionate share of traffic) are served from cache, reducing effective load on the index shards.

9. Query Processing Pipeline

The query processing pipeline transforms a raw user query into a ranked list of search results. This is the critical path that must complete in under 200 milliseconds end-to-end. Each stage in the pipeline adds latency but improves result quality, so careful engineering is required to balance speed and relevance.

Pipeline Stages

graph LR A["User Query: 'best pythn libs for ML'"] --> B[1. Query Preprocessing] B --> C[2. Spell Correction] C -->|"best python libs for ML"| D[3. Query Understanding] D --> E[4. Query Expansion] E --> F[5. Index Lookup - Scatter] F --> G[6. Initial Ranking - BM25] G --> H[7. ML Re-ranking - LambdaMART] H --> I[8. Diversity & Freshness Boost] I --> J[9. Snippet Generation] J --> K[10. Ad Auction] K --> L[11. Response Assembly] style C fill:#f78166,color:#000 style H fill:#7ee787,color:#000 style K fill:#d29922,color:#000

Stage 1-3: Preprocessing, Spell Correction, and Query Understanding

When a user types "best pythn libs for ML", the pipeline must first normalize the query (lowercase, remove special characters), detect the misspelling ("pythn" → "python"), and parse the query structure (intent: informational; entities: "python", "ML"; modifiers: "best"). The corrected query "best python libraries for machine learning" is then expanded with synonyms and related terms.

Stage 4-6: Index Lookup and Initial Ranking

The expanded query is sent to all index shards in parallel. Each shard performs a lookup of matching documents and scores them using BM25 (Best Matching 25), a probabilistic ranking function that considers term frequency, inverse document frequency, and document length. BM25 is fast (O(1) per posting lookup) and produces a reasonable initial ranking.

C#
public class BM25Scorer
{
    private readonly double _k1 = 1.2;  // Term frequency saturation
    private readonly double _b = 0.75;  // Document length normalization
    private readonly double _avgDocLength;
    private readonly int _totalDocuments;

    public double Score(QueryTerms query, PostingList postings)
    {
        double score = 0;
        foreach (var term in query.Terms)
        {
            if (!postings.TryGetTermPostings(term, out var termPostings))
                continue;

            double idf = Math.Log(
                (_totalDocuments - termPostings.DocumentFrequency + 0.5) /
                (termPostings.DocumentFrequency + 0.5) + 1.0);

            foreach (var posting in termPostings.Postings)
            {
                double tf = posting.TermFrequency;
                double docLen = posting.DocumentLength;
                double norm = 1 - _b + _b * (docLen / _avgDocLength);

                double termScore = idf * (tf * (_k1 + 1)) /
                                   (tf + _k1 * norm);

                // Boost title matches and URL matches
                if ((posting.FieldMask & 0x01) != 0) termScore *= 3.0;
                if ((posting.FieldMask & 0x04) != 0) termScore *= 1.5;

                score += termScore * query.TermWeights[term];
            }
        }
        return score;
    }
}

Stage 7: ML Re-ranking

After BM25 produces the top 1,000 candidates, an ML ranker (typically LambdaMART or a gradient-boosted decision tree) re-ranks them using hundreds of features: BM25 score, PageRank, click-through rate, freshness, URL quality, content quality, user engagement signals, and more. The ML ranker is more accurate but 10-100x slower than BM25, which is why it only runs on the reduced candidate set.

Stage 8-11: Post-processing

After ML re-ranking, the pipeline applies diversity filters (ensure results don't all come from the same domain), freshness boosts (for time-sensitive queries), snippet generation (extract the most relevant passage from each document), and ad auction results. The final response is assembled as JSON and returned to the user.

Latency budget breakdown: Typical p50 latency allocation: Query parsing (2ms) + Spell correction (3ms) + Index lookup (30ms) + BM25 scoring (15ms) + ML re-ranking (40ms) + Snippet generation (10ms) + Network overhead (20ms) = ~120ms total. This leaves headroom for the 200ms target.

10. Ranking Algorithm — PageRank + Machine Learning

Ranking is the heart of a search engine and the primary differentiator between competitors. Google's dominance was built on PageRank, which treated the web as a graph and ranked pages by the number and quality of incoming links. Modern ranking has evolved far beyond PageRank, incorporating hundreds of signals through machine learning models.

PageRank Algorithm

PageRank models a "random surfer" who follows links on the web at random. The probability that the surfer lands on any given page is its PageRank score. Pages with many incoming links from high-authority pages receive higher scores. The algorithm converges through iterative computation on the link graph.

C#
public class PageRankComputer
{
    private const double DAMPING_FACTOR = 0.85;
    private const double CONVERGENCE_THRESHOLD = 1e-6;
    private const int MAX_ITERATIONS = 100;

    public Dictionary<string, double> ComputePageRank(
        Dictionary<string, List<string>> adjacencyList, int totalNodes)
    {
        var scores = new Dictionary<string, double>();
        var newScores = new Dictionary<string, double>();

        // Initialize uniform distribution
        double initialScore = 1.0 / totalNodes;
        foreach (var node in GetAllNodes(adjacencyList))
            scores[node] = initialScore;

        // Compute incoming links for efficient lookup
        var incomingLinks = BuildInvertedIndex(adjacencyList);

        for (int iter = 0; iter < MAX_ITERATIONS; iter++)
        {
            double danglingSum = 0;

            foreach (var node in GetAllNodes(adjacencyList))
            {
                double rank = (1 - DAMPING_FACTOR) / totalNodes;

                // Sum contributions from incoming links
                if (incomingLinks.ContainsKey(node))
                {
                    foreach (var source in incomingLinks[node])
                    {
                        int outDegree = adjacencyList[source].Count;
                        rank += DAMPING_FACTOR * scores[source] / outDegree;
                    }
                }

                // Handle dangling nodes (pages with no outgoing links)
                if (!adjacencyList.ContainsKey(node) ||
                    adjacencyList[node].Count == 0)
                {
                    danglingSum += scores[node];
                }

                newScores[node] = rank;
            }

            // Distribute dangling node rank equally
            double danglingShare = DAMPING_FACTOR * danglingSum / totalNodes;
            foreach (var node in newScores.Keys.ToList())
                newScores[node] += danglingShare;

            // Check convergence
            double diff = scores.Keys.Sum(k =>
                Math.Abs(scores[k] - newScores[k]));

            (scores, newScores) = (newScores, scores);

            if (diff < CONVERGENCE_THRESHOLD)
            {
                Console.WriteLine($"PageRank converged after {iter + 1} iterations");
                break;
            }
        }

        return scores;
    }
}

Modern ML Ranking: LambdaMART

Today, the primary ranking model is typically LambdaMART, a learning-to-rank algorithm based on gradient-boosted decision trees. It optimizes NDCG (Normalized Discounted Cumulative Gain), directly learning to place relevant results at the top of the ranking. LambdaMART uses hundreds of features:

Feature CategoryExample FeaturesCount
Query-Document Text MatchBM25 title, BM25 body, TF-IDF cosine, phrase match score~30
Link AuthorityPageRank, domain authority, referring domains count~15
User EngagementClick-through rate, dwell time, bounce rate, long-click rate~25
Content QualityReadability score, ad-to-content ratio, page load speed~20
FreshnessPage age, last modified date, content freshness score~10
User ContextUser location, language, device, search history~15
Query FeaturesQuery length, query type, query frequency~10
C#
public class MLRanker
{
    private readonly LambdaMARTModel _model;
    private readonly FeatureExtractor _featureExtractor;
    private readonly int _maxCandidates = 1000;

    public async Task<List<RankedDocument>> RerankAsync(
        SearchQuery query, List<CandidateDocument> candidates)
    {
        // Extract features for all candidates in parallel
        var featureTasks = candidates.Select(doc =>
            Task.Run(() => _featureExtractor.ExtractFeatures(query, doc))
        ).ToArray();

        var featureVectors = await Task.WhenAll(featureTasks);

        // Score each candidate with the ML model
        var scoredCandidates = candidates
            .Zip(featureVectors, (doc, features) =>
                new { Document = doc, Score = _model.Predict(features) })
            .OrderByDescending(x => x.Score)
            .Take(100) // Return top-100 after ML re-ranking
            .Select(x => new RankedDocument
            {
                DocumentId = x.Document.DocumentId,
                Url = x.Document.Url,
                Title = x.Document.Title,
                Score = x.Score,
                Snippet = x.Document.BestSnippet
            })
            .ToList();

        return scoredCandidates;
    }
}

public class FeatureExtractor
{
    public float[] ExtractFeatures(SearchQuery query, CandidateDocument doc)
    {
        var features = new float[130];

        // Query-Document text match features (indices 0-29)
        features[0] = ComputeBM25Score(query, doc.Title);
        features[1] = ComputeBM25Score(query, doc.Body);
        features[2] = ComputeTFIDFCosine(query, doc.Body);
        features[3] = ComputeExactPhraseMatch(query, doc.Body);
        features[4] = ComputeQueryUrlOverlap(query, doc.Url);

        // Link authority features (indices 30-44)
        features[30] = doc.PageRankScore;
        features[31] = doc.DomainAuthority;
        features[32] = Math.Log(doc.ReferringDomains + 1);
        features[33] = doc.InternalLinkCount;
        features[34] = doc.ExternalLinkCount;

        // User engagement features (indices 45-69)
        features[45] = doc.ClickThroughRate;
        features[46] = doc.AverageDwellTimeSeconds;
        features[47] = doc.BounceRate;
        features[48] = doc.LongClickRate;
        features[49] = doc.ReformulationRate;

        // Content quality features (indices 50-69)
        features[50] = doc.ReadabilityScore;
        features[51] = doc.AdToContentRatio;
        features[52] = doc.PageLoadTimeMs;
        features[53] = doc.MobileFriendliness;
        features[54] = doc.HasSchemaOrgMarkup ? 1f : 0f;

        // Freshness features (indices 70-79)
        features[70] = ComputeFreshnessScore(doc.LastModified);
        features[71] = doc.IsNews ? 1f : 0f;
        features[72] = doc.PublishDateScore;

        // User context features (indices 80-94)
        features[80] = ComputeGeoRelevance(query.Location, doc.Location);
        features[81] = query.Language == doc.Language ? 1f : 0f;
        features[82] = ComputePersonalizationScore(query, doc);

        // Query features (indices 95-109)
        features[95] = query.Terms.Count;
        features[96] = query.IsNavigational ? 1f : 0f;
        features[97] = query.IsTransactional ? 1f : 0f;
        features[98] = query.QueryFrequency;

        return features;
    }
}
BERT and MUM: Google introduced BERT (Bidirectional Encoder Representations from Transformers) in 2019 to better understand query semantics. BERT helps with queries where prepositions and context matter — e.g., "parking on a hill with no curb" vs. "parking on a hill with a curb." The MUM (Multitask Unified Model) introduced in 2021 is 1000x more powerful than BERT and can process information across 75 languages and multiple modalities. These transformer models are used as feature extractors in the ranking pipeline, not as standalone rankers, due to their latency cost.

11. Spell Correction & Autocomplete

Spell correction and autocomplete are critical UX features that improve query success rates. Google estimates that spell correction affects approximately 10% of all searches. Autocomplete must respond within 50 milliseconds as users type, requiring in-memory data structures and highly optimized lookup.

Spell Correction Approaches

Modern spell correction uses a combination of: (1) edit distance (Levenshtein) to find candidate corrections, (2) language model scoring (n-gram frequency) to rank candidates, and (3) query log mining to discover common misspellings and their corrections. The correction model must balance precision (not corrupting correct queries) with recall (catching all misspellings).

C#
public class SpellCorrector
{
    private readonly Trie _dictionary;
    private readonly NGramLanguageModel _languageModel;
    private readonly Dictionary<string, string> _knownCorrections;
    private readonly BloomFilter<string> _knownQueries;

    public string CorrectQuery(string query)
    {
        var words = query.Split(' ');
        var corrected = new string[words.Length];

        for (int i = 0; i < words.Length; i++)
        {
            if (_knownCorrections.TryGetValue(words[i], out var known))
            {
                corrected[i] = known;
                continue;
            }

            if (_knownQueries.MightContain(words[i]))
            {
                corrected[i] = words[i]; // Known query, no correction
                continue;
            }

            var candidates = GetEditDistanceCandidates(words[i], maxDistance: 2);
            var context = words.Where((_, idx) => idx != i).ToArray();

            corrected[i] = candidates
                .Select(c => new
                {
                    Word = c,
                    EditScore = EditDistancePenalty(words[i], c),
                    LanguageScore = _languageModel.WordProbability(c),
                    ContextScore = _languageModel.ContextProbability(c, context),
                    FrequencyScore = GetQueryFrequency(c)
                })
                .OrderByDescending(x =>
                    0.3 * x.EditScore +
                    0.3 * x.LanguageScore +
                    0.2 * x.ContextScore +
                    0.2 * x.FrequencyScore)
                .FirstOrDefault()?.Word ?? words[i];
        }

        return string.Join(" ", corrected);
    }

    private List<string> GetEditDistanceCandidates(string word, int maxDistance)
    {
        var candidates = new List<string>();
        var queue = new Queue<(string Word, int Distance)>();
        queue.Enqueue((word, 0));
        var visited = new HashSet<string> { word };

        while (queue.Count > 0)
        {
            var (current, dist) = queue.Dequeue();
            if (dist > maxDistance) break;

            if (_dictionary.Contains(current) && current != word)
                candidates.Add(current);

            foreach (var neighbor in GetEditNeighbors(current))
            {
                if (visited.Add(neighbor))
                    queue.Enqueue((neighbor, dist + 1));
            }
        }

        return candidates;
    }
}

Autocomplete Trie Design

Autocomplete suggestions must be served from an in-memory trie (prefix tree) to achieve sub-millisecond latency. The trie stores not just prefixes but also metadata: suggestion frequency, category, and a freshness timestamp. As users type, the system traverses the trie to find the top-K most frequent completions for the given prefix.

C#
public class AutocompleteService
{
    private readonly ConcurrentTrie<List<Suggestion>> _prefixTrie;
    private readonly int _maxSuggestions = 10;

    public async Task<List<Suggestion>> GetSuggestionsAsync(
        string prefix, string userId)
    {
        prefix = prefix.ToLowerInvariant().Trim();

        var candidates = _prefixTrie.StartsWith(prefix)?
            .SelectMany(kvp => kvp.Value)
            .OrderByDescending(s => s.Frequency)
            .ThenByDescending(s => s.Freshness)
            .Take(_maxSuggestions * 2)
            .ToList() ?? new List<Suggestion>();

        // Apply personalization
        if (!string.IsNullOrEmpty(userId))
        {
            var userProfile = await GetUserProfileAsync(userId);
            candidates = candidates
                .OrderByDescending(s =>
                    s.Frequency * 0.6 +
                    ComputePersonalizationScore(s, userProfile) * 0.4)
                .Take(_maxSuggestions)
                .ToList();
        }

        return candidates;
    }

    public void RebuildFromQueryLogs(IEnumerable<QueryLog> logs)
    {
        var trie = new ConcurrentTrie<List<Suggestion>>();

        var suggestions = logs
            .GroupBy(l => l.QueryText.ToLowerInvariant())
            .Select(g => new Suggestion
            {
                Text = g.Key,
                Frequency = g.Count(),
                Freshness = g.Max(l => l.Timestamp),
                Category = ClassifyQuery(g.Key)
            })
            .ToList();

        foreach (var suggestion in suggestions)
        {
            for (int len = 1; len <= suggestion.Text.Length; len++)
            {
                var prefix = suggestion.Text.Substring(0, len);
                trie.AddOrUpdate(prefix,
                    new List<Suggestion> { suggestion },
                    (key, existing) =>
                    {
                        existing.Add(suggestion);
                        return existing;
                    });
            }
        }

        Interlocked.Exchange(ref _prefixTrie, trie);
    }
}
Sensitivity filter: Autocomplete suggestions must be filtered for offensive, harmful, or inappropriate content. Google has faced legal and regulatory pressure to prevent autocomplete from suggesting defamatory or harmful completions. The suggestion service must include a content moderation layer that filters against a curated blocklist and applies ML-based toxicity detection before returning suggestions to users.

12. Snippet Generation & SERP Rendering

The snippet is the preview text shown below each search result title and URL. A well-crafted snippet dramatically improves click-through rates. Google dynamically generates snippets by extracting the most relevant passage from the page, highlighting query terms, and sometimes including structured data (ratings, prices, dates).

Snippet Generation Pipeline

The snippet generation system must: (1) identify the most relevant passage in the document for the query, (2) truncate it to fit the display limit (~160 characters), (3) highlight query terms using bold markers, and (4) optionally extract structured snippets (featured snippets, knowledge panels). This runs after ranking is complete and must add less than 10ms of latency.

C#
public class SnippetGenerator
{
    private readonly PassageRanker _passageRanker;
    private readonly int _maxSnippetLength = 160;

    public Snippet GenerateSnippet(SearchQuery query, WebPage page)
    {
        // Split document into overlapping passages
        var passages = SplitIntoPassages(page.ExtractedText, windowSize: 50, overlap: 10);

        // Rank passages by query relevance
        var rankedPassages = _passageRanker.RankPassages(query, passages);

        var bestPassage = rankedPassages.FirstOrDefault();
        if (bestPassage == null)
            return new Snippet { Text = Truncate(page.ExtractedText, _maxSnippetLength) };

        // Truncate and highlight
        var snippetText = TruncateAtSentenceBoundary(bestPassage.Text, _maxSnippetLength);
        var highlighted = HighlightQueryTerms(snippetText, query.Terms);

        // Check for structured snippet opportunities
        var structuredData = ExtractStructuredData(page);
        var featuredSnippet = TryExtractFeaturedSnippet(query, passages);

        return new Snippet
        {
            Text = highlighted,
            IsFeatured = featuredSnippet != null,
            FeaturedContent = featuredSnippet,
            StructuredData = structuredData
        };
    }

    private string HighlightQueryTerms(string text, List<string> queryTerms)
    {
        var words = text.Split(' ');
        var highlighted = new StringBuilder();

        foreach (var word in words)
        {
            var clean = word.ToLowerInvariant().TrimEnd('.', ',', '!', '?');
            if (queryTerms.Any(qt =>
                clean.Contains(qt, StringComparison.OrdinalIgnoreCase)))
            {
                highlighted.Append($"<b>{word}</b> ");
            }
            else
            {
                highlighted.Append($"{word} ");
            }
        }

        return highlighted.ToString().Trim();
    }

    private List<Passage> SplitIntoPassages(string text, int windowSize, int overlap)
    {
        var sentences = SplitIntoSentences(text);
        var passages = new List<Passage>();

        for (int i = 0; i < sentences.Count; i += windowSize - overlap)
        {
            var passageText = string.Join(" ",
                sentences.Skip(i).Take(windowSize));
            passages.Add(new Passage
            {
                Text = passageText,
                StartOffset = sentences[i].Offset,
                Position = i
            });
        }

        return passages;
    }
}

Featured Snippets and Knowledge Panels

Featured snippets (also called "Position Zero") answer the user's question directly on the SERP without requiring a click. They are extracted from web pages that provide concise, well-structured answers. Knowledge panels are generated from structured knowledge bases (like Google's Knowledge Graph) and display entity information (people, places, things) in a rich card format.

Snippet optimization: From a system design perspective, the snippet generator must handle edge cases like non-English text (where sentence boundaries differ), code snippets (which need special formatting), and time-sensitive queries (where freshness metadata like "3 hours ago" must be included). The system must also support different snippet lengths for different devices — shorter for mobile, longer for desktop.

13. Search Result Caching Strategy

Caching is critical for search engine performance. Popular queries are repeated millions of times per day, and serving them from cache avoids expensive index lookups and ranking computations. Google reportedly serves 20-30% of all queries from cache. The caching system must handle cache invalidation for fresh content, personalize results per user, and maintain hit rates above 50% for cost efficiency.

Multi-Level Caching Architecture

graph TB A[User Query] --> B[L1: Browser Cache - 10ms] B -->|Miss| C[L2: CDN Edge Cache - 20ms] C -->|Miss| D[L3: Datacenter Query Cache - 30ms] D -->|Miss| E[L4: Index Shard Cache - 50ms] E -->|Miss| F[Index Lookup + Ranking] F -->|Store| E F -->|Store| D F -->|Store| C style B fill:#7ee787,color:#000 style C fill:#58a6ff,color:#000 style D fill:#d29922,color:#000 style E fill:#f78166,color:#000
C#
public class SearchCacheManager
{
    private readonly IDistributedCache _l3Cache;    // Redis cluster
    private readonly IMemoryCache _l4Cache;         // Local in-process
    private readonly CacheKeyBuilder _keyBuilder;
    private readonly int _defaultTtlSeconds = 300;   // 5 minutes

    public async Task<CachedResult?> GetCachedResultAsync(SearchQuery query, UserContext user)
    {
        // L4: Local process cache (fastest, smallest)
        var localKey = _keyBuilder.BuildKey(query, includePersonalization: false);
        if (_l4Cache.TryGetValue<CachedResult>(localKey, out var localResult))
            return localResult;

        // L3: Distributed cache (Redis cluster)
        var distributedKey = _keyBuilder.BuildKey(query, includePersonalization: true);
        var distributedResult = await _l3Cache.GetAsync<CachedResult>(distributedKey);
        if (distributedResult != null)
        {
            // Populate L4 for next time
            _l4Cache.Set(localKey, distributedResult,
                TimeSpan.FromSeconds(_defaultTtlSeconds / 2));
            return distributedResult;
        }

        return null;
    }

    public async Task StoreResultAsync(SearchQuery query, UserContext user,
        List<RankedDocument> results, TimeSpan processingTime)
    {
        var cachedResult = new CachedResult
        {
            Results = results,
            CachedAt = DateTime.UtcNow,
            ProcessingTimeMs = processingTime.TotalMilliseconds
        };

        // Compute TTL based on query freshness requirements
        var ttl = ComputeAdaptiveTtl(query);

        var distributedKey = _keyBuilder.BuildKey(query, includePersonalization: true);
        await _l3Cache.SetAsync(distributedKey, cachedResult, ttl);

        var localKey = _keyBuilder.BuildKey(query, includePersonalization: false);
        _l4Cache.Set(localKey, cachedResult, TimeSpan.FromSeconds(ttl.TotalSeconds / 2));
    }

    private TimeSpan ComputeAdaptiveTtl(SearchQuery query)
    {
        // Breaking news queries get short TTL
        if (query.IsNewsQuery)
            return TimeSpan.FromMinutes(1);

        // Navigational queries can be cached longer
        if (query.IsNavigational)
            return TimeSpan.FromHours(1);

        // Time-sensitive queries (sports scores, stock prices)
        if (query.IsTimeSensitive)
            return TimeSpan.FromMinutes(2);

        // General queries
        return TimeSpan.FromMinutes(5);
    }
}

Cache Key Design

The cache key must capture all factors that affect result ranking: the query text (lowercased, normalized), user location (city-level granularity), device type, and language preference. Personalized results (based on search history) are cached separately from generic results. The key must not include user IDs for privacy reasons — instead, demographic segments (e.g., "US-mobile-en") are used.

Cache invalidation challenge: When new content is indexed or ranking models are updated, cached results become stale. The system must invalidate or refresh affected cache entries. A common approach is version tagging: each ranking model version produces results with a version tag, and caches are tagged accordingly. When a new model version is deployed, old-version cache entries are lazily evicted on access rather than bulk-invalidated.

14. Multi-Modal Search — Images, Video, Voice

Modern search engines have expanded far beyond text. Google handles billions of image searches per day, processes video content for YouTube and Google Video, and supports voice queries through Google Assistant. Each modality requires specialized indexing, retrieval, and ranking pipelines.

Image Search Architecture

graph TB A[User Image Upload / Text Query] --> B{Query Type} B -->|Text Query| C[Text-to-Image Pipeline] B -->|Image Upload| D[Reverse Image Pipeline] B -->|Voice Query| E[Voice-to-Text Pipeline] C --> F[Text Query Parser] F --> G[Image Index Lookup - Text Metadata] G --> H[Visual Relevance Scoring] D --> I[Image Feature Extraction - CNN/ViT] I --> J[Approximate Nearest Neighbor Search] J --> K[Visual Similarity Ranking] E --> L[Speech-to-Text - Whisper/USM] L --> C H --> L2[Image Search Results] K --> L2 C --> L2 style I fill:#58a6ff,color:#000 style J fill:#f78166,color:#000 style L fill:#7ee787,color:#000

Text-to-image search: Images are indexed by their surrounding text (title, alt text, caption, nearby text on the page), OCR-extracted text, and object detection labels. The query is matched against these text features using the same inverted index approach as web search.

Reverse image search: When a user uploads an image, a CNN (Convolutional Neural Network) or Vision Transformer extracts a dense feature vector (embedding). This embedding is compared against a database of pre-computed image embeddings using approximate nearest neighbor (ANN) algorithms like HNSW (Hierarchical Navigable Small World) or ScaNN (Google's specializedANN library).

C#
public class ImageSearchService
{
    private readonly ImageFeatureExtractor _featureExtractor;
    private readonly VectorIndex _embeddingIndex;  // HNSW or ScaNN index
    private readonly ImageMetadataStore _metadataStore;

    public async Task<List<ImageResult>> SearchByTextAsync(string query, int topK)
    {
        // Use CLIP model to convert text query to image embedding space
        var queryEmbedding = await _featureExtractor.GetTextEmbeddingAsync(query);

        // ANN search in the embedding index
        var nearestNeighbors = await _embeddingIndex.SearchAsync(
            queryEmbedding, topK * 3);

        // Re-rank with CLIP similarity + metadata features
        var results = new List<ImageResult>();
        foreach (var neighbor in nearestNeighbors)
        {
            var metadata = await _metadataStore.GetAsync(neighbor.ImageId);
            var clipScore = await ComputeCLIPScoreAsync(query, metadata);
            var combinedScore = 0.5f * neighbor.Distance + 0.3f * clipScore +
                               0.2f * metadata.PageRank;

            results.Add(new ImageResult
            {
                ImageId = neighbor.ImageId,
                ThumbnailUrl = metadata.ThumbnailUrl,
                SourcePageUrl = metadata.SourceUrl,
                Score = combinedScore
            });
        }

        return results.OrderByDescending(r => r.Score).Take(topK).ToList();
    }

    public async Task<List<ImageResult>> SearchByImageAsync(
        byte[] uploadedImage, int topK)
    {
        // Extract visual features from uploaded image
        var imageEmbedding = await _featureExtractor.GetImageEmbeddingAsync(uploadedImage);

        // Find visually similar images
        var candidates = await _embeddingIndex.SearchAsync(imageEmbedding, topK * 5);

        // Group by source URL to ensure diversity
        var diverseResults = EnsureDiversity(candidates, maxPerDomain: 2);

        return diverseResults.Take(topK).ToList();
    }
}

Voice Search

Voice search adds a speech-to-text (STT) stage before the standard text query pipeline. Google uses Universal Speech Model (USM) for real-time transcription, supporting 100+ languages. The STT output is then fed into the normal text search pipeline. Voice queries tend to be longer and more conversational than typed queries, which affects query understanding and ranking.

Multimodal embedding spaces: Models like CLIP (Contrastive Language-Image Pre-training) create a shared embedding space for text and images. This allows direct comparison between a text query and an image, enabling "search for 'sunset on a beach'" to find relevant images without explicit text annotations. Google's MUM model extends this to text, images, and video in a unified representation.

15. Ad Auction System & Monetization

Search advertising is the primary revenue model for search engines. Google's ad auction system generates over $200 billion annually. The ad auction runs for every search query that has commercial intent, selecting the most relevant and profitable ads to display alongside organic results.

Ad Auction Flow

graph LR A[User Query] --> B[Commercial Intent Detector] B -->|High Intent| C[Ad Candidate Retrieval] B -->|Low Intent| D[Skip Ads - Show Organic Only] C --> E[Bid Retrieval - Advertiser Bids] E --> F[Quality Score Computation] F --> G[Ad Rank Calculation] G --> H[Auction Winner Selection] H --> I[Ad Position Assignment] I --> J[Final Price - VCG Auction] J --> K[Ad Serving + Organic Results] style B fill:#d29922,color:#000 style F fill:#58a6ff,color:#000 style H fill:#7ee787,color:#000

The ad auction uses a generalized second-price (GSP) or Vickrey-Clarke-Groves (VCG) mechanism. Each ad's rank is determined by:

Ad Rank = Bid × Quality Score × Expected Impact

The Quality Score includes: (1) expected click-through rate, (2) ad relevance to the query, and (3) landing page quality. This ensures that high-quality, relevant ads can win even with lower bids, creating a better user experience and higher long-term revenue.

C#
public class AdAuctionService
{
    private readonly AdCandidateRetriever _retriever;
    private readonly QualityScoreComputer _qualityComputer;
    private readonly BudgetManager _budgetManager;

    public async Task<AdAuctionResult> RunAuctionAsync(
        SearchQuery query, AuctionContext context)
    {
        // Step 1: Detect commercial intent
        var intentScore = await ComputeCommercialIntentAsync(query);
        if (intentScore < 0.3)
            return AdAuctionResult.NoAds();

        // Step 2: Retrieve matching ad candidates
        var candidates = await _retriever.RetrieveAsync(query, maxCandidates: 100);

        // Step 3: Score each candidate
        var scoredAds = new List<ScoredAd>();
        foreach (var ad in candidates)
        {
            var qualityScore = await _qualityComputer.ComputeScoreAsync(query, ad);
            var budget = await _budgetManager.GetRemainingBudgetAsync(ad.AdvertiserId);
            if (budget <= 0) continue;

            var expectedCtr = ad.HistoricalCtr * qualityScore.RelevanceMultiplier;
            var adRank = ad.BidAmount * qualityScore.OverallScore * expectedCtr;

            scoredAds.Add(new ScoredAd
            {
                Ad = ad,
                QualityScore = qualityScore,
                AdRank = adRank,
                ExpectedCtr = expectedCtr
            });
        }

        // Step 4: Run VCG auction
        var winners = scoredAds
            .OrderByDescending(a => a.AdRank)
            .Take(8) // Max 8 ad slots
            .ToList();

        // Compute VCG prices (each winner pays the minimum bid
        // needed to maintain their position)
        for (int i = 0; i < winners.Count; i++)
        {
            winners[i].VcgPrice = ComputeVcgPrice(winners, i);
        }

        return new AdAuctionResult
        {
            WinningAds = winners,
            TotalAdRevenue = winners.Sum(w => w.VcgPrice),
            AuctionId = Guid.NewGuid().ToString()
        };
    }

    private double ComputeVcgPrice(List<ScoredAd> winners, int position)
    {
        if (position == winners.Count - 1)
        {
            // Last position: pays reserve price
            return 0.01;
        }

        // The price is the minimum bid needed to beat the next
        // excluded advertiser
        var nextLoser = winners[position + 1];
        var currentWinner = winners[position];

        return (nextLoser.AdRank / currentWinner.QualityScore.OverallScore) + 0.01;
    }
}
Ad relevance is critical: Showing irrelevant ads degrades user trust and reduces long-term revenue. Google penalizes advertisers with low Quality Scores by charging higher CPCs or rejecting their ads entirely. The ad auction must balance revenue optimization with user experience — a principle called "user-first advertising."

16. Reliability, Failure Modes & Disaster Recovery

A search engine is a mission-critical system that must maintain availability even when individual components fail. Google has experienced multiple incidents where entire datacenters went offline, index corruption occurred, and network partitions split the system. The design must account for all these scenarios.

Failure Modes and Mitigations

Failure ModeImpactMitigation
Index shard failurePartial results for some queries2-3 replicas per shard across different racks; automatic failover to replica within 1 second
Datacenter outageLoss of all services in one locationActive-active multi-datacenter deployment; DNS-based traffic rerouting; global load balancing
Crawler stallIndex becomes stale over timeMultiple independent crawler fleets; health monitoring with automatic restart; crawl backlog alerting
Ranking model serving failureFallback to BM25-only rankingCircuit breaker pattern; pre-cached fallback results; graceful degradation to simpler ranking
Cache cluster failureAll queries hit index directly (latency spike)Multi-level caching; local process cache as L4 fallback; load shedding to protect index
Network partitionInability to reach some shardsQuery timeout with partial results; stale replica serving; cross-datacenter replication
Corrupted index segmentQueries return wrong results or crashChecksum verification on every segment load; redundant segment storage; rebuild from raw data
DNS resolution failureCrawler cannot fetch new pagesLocal DNS cache with 24h TTL; multiple DNS resolvers; fallback to IP directly
C#
public class SearchServiceWithFallback
{
    private readonly IndexShardRouter _primaryRouter;
    private readonly ReplicaRouter _replicaRouter;
    private readonly SearchCacheManager _cache;
    private readonly CircuitBreaker _mlRankerBreaker;
    private readonly CircuitBreaker _indexBreaker;

    public async Task<SearchResult> SearchAsync(SearchQuery query)
    {
        // Level 1: Try cache first
        var cached = await _cache.GetCachedResultAsync(query, query.UserContext);
        if (cached != null) return cached.ToSearchResult();

        // Level 2: Try primary index shards
        try
        {
            var timeout = TimeSpan.FromMilliseconds(100);
            var cts = new CancellationTokenSource(timeout);

            var results = await _indexBreaker.ExecuteAsync(async () =>
            {
                return await _primaryRouter.QueryAsync(query, topK: 20);
            }, cts.Token);

            // Level 3: Try ML re-ranking with circuit breaker
            List<RankedDocument> ranked;
            if (_mlRankerBreaker.IsClosed)
            {
                ranked = await _mlRankerBreaker.ExecuteAsync(async () =>
                {
                    return await RerankWithMLAsync(query, results);
                });
            }
            else
            {
                // Fallback: use BM25 scores directly
                ranked = results;
            }

            var searchResult = await AssembleResultAsync(query, ranked);
            await _cache.StoreResultAsync(query, query.UserContext,
                ranked, TimeSpan.Zero);

            return searchResult;
        }
        catch (Exception ex) when (IsIndexFailure(ex))
        {
            // Level 4: Try replica shards
            var replicaResults = await _replicaRouter.QueryAsync(query, topK: 10);
            return await AssembleResultAsync(query, replicaResults);
        }
    }

    private async Task<List<RankedDocument>> RerankWithMLAsync(
        SearchQuery query, List<RankedDocument> candidates)
    {
        var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
        return await _mlRanker.RerankAsync(query, candidates, cts.Token);
    }
}

Circuit Breaker Pattern

The circuit breaker monitors failure rates for each downstream service. When failures exceed a threshold (e.g., 50% of requests failing in a 30-second window), the circuit opens and all requests are immediately routed to the fallback path. After a cooldown period, the circuit enters a half-open state where a few test requests probe the service. If they succeed, the circuit closes and normal traffic resumes.

Cascading failures: The most dangerous failure mode is a cascading failure, where one component's failure causes overload in another. For example, if the ML ranker slows down, query timeouts increase, causing retries, which further overload the ranker. The solution is aggressive timeout management, circuit breakers, and load shedding. Each component must have a hard timeout and reject requests rather than queue them indefinitely.

17. Cost Estimation & Infrastructure Sizing

Running a Google-scale search engine is one of the most expensive computing operations in the world. Google spends approximately $30 billion per year on infrastructure, with a significant portion dedicated to search. Let us break down the costs for a production search engine at scale.

Infrastructure Cost Breakdown

ComponentQuantityUnit CostMonthly Cost
Index servers (NVMe SSD, 256GB RAM)50,000 nodes$3,000/month$150M
Crawler nodes20,000 nodes$1,500/month$30M
Ranking/ML serving (GPU)10,000 nodes$5,000/month$50M
Cache servers (Redis/Memcached)5,000 nodes$2,000/month$10M
Datacenter facilities30 datacenters$5M/month each$150M
Network bandwidthGlobal$30M
Storage (HDFS/GCS for raw crawl data)500 PB$0.02/GB/month$10M
Ad serving infrastructure$20M
ML training (model development)$15M
Monitoring, logging, ops tools$10M

Total estimated monthly cost: ~$475M/month (~$5.7B/year)

Cost Optimization Strategies

  • Index compression: Reducing index size by 2x directly halves storage costs. Custom compression codecs can achieve 10:1 compression ratios.
  • Cache hit rate optimization: Increasing cache hit rate from 20% to 30% reduces index queries by 12.5%, saving hundreds of millions in compute.
  • Query routing intelligence: Routing simple navigational queries (e.g., "facebook login") directly to cached results avoids expensive index lookups.
  • Spot/preemptible instances for crawling: Crawler nodes can use preemptible VMs at 60-70% discount since crawl jobs can be interrupted and restarted.
  • Model distillation: Distilling large BERT models into smaller, faster models for ranking reduces GPU serving costs by 5-10x with minimal quality loss.
Revenue vs. cost: Google's search advertising revenue is approximately $200B/year, meaning the infrastructure cost ($5-10B) represents roughly 3-5% of revenue. This high margin enables continued investment in search quality improvements. For a startup building a search engine, the key challenge is achieving this cost efficiency at a much smaller scale, where per-unit costs are higher.

18. Additional Design Considerations

Personalization and User Context

Modern search engines personalize results based on user signals: location, search history, device type, time of day, and browsing behavior. Personalization must be balanced with privacy concerns — Google has faced regulatory scrutiny over data collection practices. The system must support privacy controls (e.g., incognito mode, search history deletion) and comply with GDPR, CCPA, and other regulations.

C#
public class PersonalizationEngine
{
    private readonly UserHistoryStore _historyStore;
    private readonly UserProfileStore _profileStore;
    private readonly LocationService _locationService;

    public PersonalizationSignals ComputeSignals(SearchQuery query, UserContext user)
    {
        var signals = new PersonalizationSignals();

        // Geographic context
        signals.Location = _locationService.GetLocation(user.IpAddress);
        signals.IsLocalQuery = IsLocallyRelevant(query);

        // Temporal context
        signals.TimeOfDay = DateTime.UtcNow.Hour;
        signals.IsWeekend = DateTime.UtcNow.DayOfWeek == DayOfWeek.Saturday ||
                           DateTime.UtcNow.DayOfWeek == DayOfWeek.Sunday;

        // User history context
        if (user.ConsentGiven && user.SearchHistoryEnabled)
        {
            var history = _historyStore.GetRecentQueries(user.UserId, days: 30);
            signals.QueryTopicAffinity = ComputeTopicAffinity(history);
            signals.PreferredDomains = ComputeDomainPreference(history);
            signals.SearchFrequency = history.Count;
        }

        // Device context
        signals.DeviceType = user.DeviceType;
        signals.IsMobile = user.DeviceType == "mobile";

        return signals;
    }
}

Content Freshness and Real-Time Indexing

Breaking news must appear in search results within minutes. Google's Caffeine indexing system enabled near-real-time indexing by replacing batch-based index updates with a streaming architecture. The pipeline detects new or changed pages through sitemap monitoring, RSS feeds, social media signals, and targeted crawling of news sources. Pages from authoritative news sources are indexed with priority and freshness-boosted in ranking.

Internationalization and Multi-Language Support

A global search engine must support 100+ languages with language-specific tokenization, stemming, and ranking. Some languages (Arabic, Hebrew, Urdu) are right-to-left; CJK languages require word segmentation; and tonal languages (Vietnamese, Thai) have different tokenization rules. The index must be partitioned by language for efficiency, with cross-language retrieval for multilingual queries.

Scale perspective: Handling 100+ languages means maintaining 100+ language models, 100+ tokenizer variants, and language-specific ranking features. Google's MUM model was specifically designed to handle cross-language understanding — it can learn from content in one language and apply that knowledge to search results in another language.

19. Monitoring, Observability & Quality Metrics

A search engine's health depends on continuous monitoring of hundreds of metrics across the crawling, indexing, ranking, and serving pipelines. Without robust observability, quality degradations can go undetected for hours or days.

Key Metrics Dashboard

MetricCategoryTargetAlert Threshold
Query latency (p50)Serving< 100ms> 150ms for 5 min
Query latency (p99)Serving< 300ms> 500ms for 5 min
Error rateServing< 0.01%> 0.1% for 2 min
Index freshness (news)Indexing< 5 min> 15 min for 10 min
Crawl success rateCrawling> 99%< 95% for 30 min
Cache hit ratePerformance> 30%< 20% for 1 hour
Click-through rateQualityVaries by positionDecrease > 10% week-over-week
Abandonment rateQuality< 20%> 30% for 1 hour
Revenue per queryBusinessVariesDecrease > 15% day-over-day
C#
public class SearchQualityMonitor
{
    private readonly MetricsCollector _metrics;
    private readonly AlertingService _alerting;

    public void TrackQueryMetrics(SearchQuery query, SearchResult result,
        TimeSpan latency, bool success)
    {
        // Record latency distribution
        _metrics.Histogram("search.query.latency_ms",
            latency.TotalMilliseconds,
            tags: new { region = query.Region, device = query.DeviceType });

        // Record success/failure
        _metrics.Counter("search.query.total",
            tags: new { success = success, query_type = query.Type });

        // Record ranking quality signals
        if (result.ClickedResultIndex.HasValue)
        {
            _metrics.Histogram("search.click.position",
                result.ClickedResultIndex.Value);
            _metrics.Counter("search.click.total",
                tags: new { query_type = query.Type });
        }
        else if (result.HasRefinedQuery)
        {
            _metrics.Counter("search.reformulation.total");
        }
        else
        {
            _metrics.Counter("search.abandon.total");
        }
    }

    public void TrackIndexHealth(IndexShard shard)
    {
        _metrics.Gauge("search.index.segment_count",
            shard.SegmentCount,
            tags: new { shard_id = shard.Id });

        _metrics.Gauge("search.index.age_hours",
            shard.NewestSegmentAge.TotalHours,
            tags: new { shard_id = shard.Id });

        _metrics.Gauge("search.index.size_bytes",
            shard.TotalSizeBytes,
            tags: new { shard_id = shard.Id });
    }

    public async Task CheckAlertsAsync()
    {
        var latencyP99 = await _metrics.GetPercentileAsync(
            "search.query.latency_ms", 0.99);
        if (latencyP99 > 300)
        {
            await _alerting.SendAlertAsync(AlertSeverity.Warning,
                $"Query p99 latency is {latencyP99:F0}ms (target: <300ms)");
        }

        var errorRate = await _metrics.GetRateAsync(
            "search.query.total", filter: "success=false");
        if (errorRate > 0.001)
        {
            await _alerting.SendAlertAsync(AlertSeverity.Critical,
                $"Query error rate is {errorRate:P2} (target: <0.01%)");
        }
    }
}
A/B testing at scale: Search engines constantly experiment with ranking algorithm changes, UI modifications, and feature additions. Google runs thousands of A/B tests simultaneously. The experimentation platform must support statistically rigorous testing with proper randomization, novelty effect detection, and guardrail metrics (to prevent experiments from degrading core metrics).

20. Interview Q&A Deep Dive

This section covers the most common search engine system design interview questions, with structured answers that demonstrate senior-level thinking.

Q1: How would you design Google Search from scratch?

Framework: Start with requirements (functional and non-functional), estimate scale, define the data model, design the high-level architecture, then deep-dive into each subsystem (crawler, index, ranking, serving). Focus on the trade-offs between freshness and cost, latency and accuracy, and simplicity and completeness.

Key points to cover: The offline/online pipeline split, sharding strategy, multi-level ranking (cheap BM25 → expensive ML), caching hierarchy, and fault tolerance via circuit breakers and graceful degradation.

Q2: How do you handle a query like "best restaurants near me"?

Key insight: This is a location-sensitive query. The system must: (1) detect the user's GPS location or IP-based geolocation, (2) identify the query intent as local/transactional, (3) boost results within the user's geographic radius, (4) integrate with local business data (Google Maps, Yelp), and (5) consider real-time factors like current open/closed status. The ranking model must include geo-distance as a strong feature for local queries.

Q3: How would you design the autocomplete feature?

Architecture: An in-memory trie populated from query logs, rebuilt every 1-4 hours. As the user types each character, the client sends the current prefix to the autocomplete service, which traverses the trie and returns the top-10 completions ranked by frequency and personalization. Must handle: (1) offensive content filtering, (2) personalized suggestions, (3) trending query injection, and (4) sub-50ms latency.

Q4: How do you ensure search quality doesn't degrade after deploying a new ranking model?

Multi-layered approach: (1) Offline evaluation using human quality raters and automated metrics (NDCG, MAP), (2) A/B testing on 1-5% of traffic with statistical significance checks, (3) online metrics monitoring (CTR, abandonment rate, query reformulation rate), (4) gradual rollout with automatic rollback triggers, and (5) shadow mode testing where the new model scores queries in parallel without serving results.

Q5: How do you handle a sudden traffic spike from a breaking news event?

Strategy: Breaking news events can generate 100x normal query volume for specific terms. The system must: (1) detect trending topics through real-time query volume monitoring, (2) proactively crawl news sources, (3) pre-compute and cache results for trending queries, (4) route traffic across datacenters using anycast DNS, (5) apply load shedding for non-critical queries, and (6) scale crawler and index resources elastically using cloud auto-scaling.

Q6: How do you handle index freshness vs. crawl cost trade-offs?

Approach: Not all pages need the same crawl frequency. Use a priority-based crawl scheduling system where: (1) news sites are crawled every few minutes, (2) high-authority sites are crawled daily, (3) moderate-traffic sites are crawled weekly, and (4) low-traffic sites are crawled monthly. Machine learning models predict page change frequency based on historical patterns, and the crawl budget is allocated to maximize freshness per unit of crawl cost.

Q7: How do you detect and handle spam/SEO manipulation?

Multi-signal approach: (1) Content-based spam detection using text classifiers to identify keyword stuffing, hidden text, and cloaking, (2) Link-based spam detection using algorithms that identify link farms and paid links, (3) User behavior signals (high bounce rate, low dwell time), (4) Manual penalties from quality raters, and (5) Machine learning models trained on known spam patterns. Google's algorithm updates (Panda, Penguin, Helpful Content) are specifically designed to penalize manipulation.

Q8: How do you design the system to support 100+ languages?

Architecture: (1) Language-specific tokenizers and stemmers (e.g., MeCab for Japanese, jieba for Chinese), (2) language-specific inverted index shards (or language-tagged entries within shared shards), (3) multilingual embedding models (MUM, mBERT) for cross-language retrieval, (4) machine translation integration for query translation, and (5) language-specific quality evaluation with native-speaking raters.

Q9: What happens when an index shard becomes corrupted?

Recovery plan: (1) Detect corruption through checksum verification on segment load, (2) immediately route queries to replica shards, (3) rebuild the corrupted shard from raw crawl data and surviving index segments, (4) verify rebuilt shard checksums before bringing it back online, (5) root cause analysis to determine if the corruption was caused by hardware failure, software bug, or data center incident. The key design principle is that no single shard failure should impact user-visible availability.

Q10: How would you explain the ad auction to a non-technical stakeholder?

Analogy: "Imagine an auction where bidders don't just bid money — they also get rated on the quality and relevance of what they're offering. A restaurant ad for a query about restaurants scores higher on relevance than a car ad for the same query. The auction considers both the bid amount and the quality score, so the best combination of relevance and bid wins. This ensures users see ads they actually find useful, which keeps them clicking, which maintains ad revenue for everyone."

Interview strategy: For a 45-minute system design interview, spend 5 minutes on requirements, 5 minutes on capacity estimation, 15 minutes on high-level architecture, and 20 minutes deep-diving into 2-3 components. Show breadth first (covering all major subsystems) then depth on the most interesting trade-offs. Always mention alternative approaches and explain why you chose one over the others. Demonstrate awareness of real-world constraints: cost, team size, timeline, and operational complexity.

System Design Evaluation Rubric

CriterionJuniorSeniorStaff+
Requirements GatheringLists basic featuresIncludes non-functional requirements with specific targetsIdentifies hidden requirements like privacy, compliance, and edge cases
Scale EstimationRough numbersBack-of-envelope with storage, QPS, bandwidthConsiders cost implications and identifies bottleneck resources
ArchitectureBasic componentsMulti-tier with data flowOffline/online split, multiple ranking stages, caching hierarchy
Trade-offsNames trade-offsAnalyzes pros/cons of alternativesQuantifies trade-offs with cost and performance implications
Fault ToleranceMentions replicasDescribes failover and circuit breakersGraceful degradation with multiple fallback levels
Depth of KnowledgeSurface-level conceptsSpecific algorithms and data structuresProduction-grade implementation with real-world numbers

Search Engine System Design — Senior+ Guide