File: search-autocomplete/search_autocomplete.py

Date: 2026-06-05

Time: 14:02

Purpose

This file implements a search autocomplete system — the kind of typeahead you see in Google Search. It's a system design interview implementation that demonstrates how to serve prefix-based query suggestions ranked by frequency, with time-decay so stale queries lose relevance over time.

The file owns three responsibilities:

1. Storage & retrieval of queries via a trie with per-node top-k caches (AutocompleteTrie)

2. Batched ingestion of raw search queries (QueryCollector)

3. Service-layer concerns like blocklisting offensive terms and fuzzy matching typos (AutocompleteService)

Key Components

TrieNode

A dataclass representing a single node in the trie. Each node carries:

AutocompleteTrie

The core data structure. Constructor takes k (cache size, default 10) and decay_factor (hourly decay multiplier, default 0.99).

Critical methods:

QueryCollector

A write-buffer that accumulates raw (query, timestamp) records and flushes them as aggregated increments. On flush(), it groups by lowercased query, sums counts, takes the max timestamp, then calls trie.increment per unique query. This models the real-world pattern of batching analytics events before updating the trie.

AutocompleteService

The top-level API that composes the trie and collector. Adds:

Patterns

Top-k cache per node — This is the defining design choice. Rather than DFS-ing the subtree on every query, each node maintains a precomputed list of the best completions below it. The tradeoff: writes are more expensive (every insert/increment updates caches along the entire path from root to leaf), but reads are O(prefix_length) regardless of subtree size. This matches the real-world read-heavy workload of autocomplete.

Write-time cache maintenance, read-time decay — Frequencies are stored raw; decay is computed lazily during searchprefix only when currenttime is provided. This avoids periodic background jobs to age out all entries and keeps the stored state clean.

Normalization at the boundary — Every public method lowercases and truncates queries to 200 chars immediately on entry. Internal methods can assume normalized input.

Soft deletiondelete zeroes out the node but doesn't prune the trie path. Simpler but means deleted queries leave structural residue.

Dependencies

Imports: Only stdlib — dataclasses for TrieNode, time for timestamps. No external dependencies.

Imported by: testsearchautocomplete.py — the test suite exercises the trie, collector, and service.

Flow

A typical lifecycle:

1. AutocompleteService is created with a k, decay factor, and optional blocklist.

2. Queries arrive via recordquery(), which buffers in QueryCollector then immediately flushes (so it's effectively unbatched in this implementation — each recordquery calls flush()).

3. flush() aggregates the buffer and calls trie.increment() for each unique query.

4. increment() walks the trie, creating nodes as needed, bumps the frequency, then calls updatecachesonpath to propagate the change bottom-up through every ancestor's cache.

5. On a read, suggest("fo") calls searchprefix, walks to the node for "fo", and returns its topk_cache (optionally with decay applied).

6. The service filters blocklisted terms and returns the final list.

Invariants

Error Handling

Minimal — this is a data structure implementation, not a network service. Key behaviors:

Topics to Explore

Beliefs