Search & Autocomplete — Architecture Deep Dive
How search actually works, layer by layer: text analysis, the inverted index, query execution, typo tolerance, filters and facets, autocomplete candidate generation and ranking, indexing correctness, refresh/reindexing, the ranking pipeline, pagination, caching, sharding and replication.
Text Analysis: Documents and Queries
Before any term can be looked up in an index, both the documents being indexed and the queries searching for them must go through the same analysis pipeline — otherwise a perfectly reasonable query will silently fail to match a perfectly reasonable document.
The core idea: analyze(document text) and analyze(query text) need to produce compatible terms. If indexing lowercases and stems terms but the query path doesn't (or uses a different stemmer), matches that should happen won't. This asymmetry is a common, hard-to-notice bug class in real search systems.
Steps in a typical analysis pipeline
Tokenization
Splitting raw text into individual terms — usually on whitespace and punctuation, though this gets more nuanced for languages without clear word boundaries (e.g. Chinese, Japanese).
Normalization / lowercasing
Reducing case variation ("iPhone" and "iphone" should typically match) and sometimes accent/diacritic folding ("café" vs "cafe").
Stemming vs lemmatization
Stemming crudely truncates words toward a common root ("running" → "run", "runs" → "run") using rules, sometimes producing non-words. Lemmatization uses vocabulary/grammar knowledge to find the dictionary base form, at higher computational cost. Neither is universally applied — some fields (brand names, SKUs, exact-match identifiers) should not be stemmed at all.
Stopwords
Very common words ("the", "a", "is") are sometimes removed to shrink index size and reduce noise in scoring. This is domain-dependent — stopword removal can break phrase queries or meaningful short queries (e.g. a band literally named "The The").
Language-specific analyzers
Different languages need different tokenization, stemming and stopword rules. A single multilingual corpus commonly needs a per-field or per-document language-aware analyzer, not one global pipeline.
Synonyms
Expanding or rewriting terms so that "sneaker" also matches documents containing "trainer", or "tv" matches "television". Synonym rules are typically applied at index time, query time, or both, depending on the desired trade-off between index size and query flexibility.
Field-specific analysis
Not every field should be analyzed the same way. A product title field is usually analyzed (tokenized, lowercased, maybe stemmed) for relevance matching, while a sku or status field is usually kept as an exact keyword field for precise filtering and sorting, not textual relevance.
Inverted Index & Posting Lists
The inverted index is the data structure that makes full-text search fast: instead of scanning documents to find terms, it maps each term directly to the documents containing it.
Conceptually: term → posting list. For the term "iphone", the posting list might be [doc_1, doc_42, doc_891, ...]. A posting entry is usually richer than a bare document ID — it can also carry term frequency (how many times the term appears in that document), positions (where in the document, needed for phrase search), and field information (which field the term appeared in).
Working with posting lists
Sorted postings & intersection/union
Posting lists are typically kept sorted by document ID, which allows efficient merge-style intersection (AND) and union (OR) across multiple terms' posting lists — similar to merging two sorted arrays.
Phrase search needs positions
Matching a phrase like "wireless iphone case" isn't just an AND of three terms — it requires that the position of "iphone" is one more than "wireless", and "case" one more than "iphone", in the same document and field. This is only possible if position information was stored at index time.
Compression
Posting lists for common terms can be very long. Techniques such as delta encoding (storing the gap between consecutive document IDs rather than the IDs themselves, since gaps are usually small numbers) and variable-length integer encoding substantially reduce storage and I/O cost.
Skip structures & top-k without full scans
For very long posting lists, skip pointers let a query jump ahead in a sorted posting list instead of stepping through every entry — useful when intersecting a long list against a much shorter one. Search engines also use techniques (score-sorted postings, dynamic pruning) to stop early once they're confident the current top-k can't be beaten, rather than scoring every candidate.
Query Processing
A search query goes through its own pipeline before it ever touches a posting list — one that must stay compatible with how documents were analyzed at index time.
The serving flow: raw query → normalize/analyze → parse → retrieve postings → apply filters → score candidates → merge/rank → return. Each stage narrows or reshapes the candidate set.
Query semantics
AND / OR semantics
A multi-term query can require all terms to match (AND — intersect posting lists) or any term to match (OR — union posting lists), with most real systems using OR-with-scoring-preference for match quality (documents matching more terms score higher) rather than strict boolean AND.
Phrase queries
Require adjacent-position matches across terms in the same field, as covered above — more expensive than a simple term match because position lists must be checked, not just presence.
Exact fields vs analyzed text
A query against an exact keyword-style field (e.g. status: "active") is a direct term lookup with no analysis; a query against an analyzed text field goes through the same tokenizer/stemmer/synonym pipeline as indexing did, so the query terms end up compatible with the indexed terms.
Prefix queries
Matching all terms starting with a given prefix ("iph*") — the mechanism autocomplete relies on, covered in more depth later in this page.
Fuzzy / typo matching
Matching terms within some edit distance of the query term, covered in its own section next — it materially expands the set of terms that must be considered.
Filters vs scoring clauses
A filter clause (e.g. category = "electronics", price <= 500) constrains which documents are eligible without necessarily contributing to the relevance score, and can typically be cached/reused more efficiently than a scored clause. A scoring clause (a keyword match) determines both eligibility and how strongly a document should rank. Conflating the two is a common design mistake — see Filters & Facets below.
Typo Tolerance and Fuzzy Search
Users misspell things constantly. Whether and how a search system tolerates this is a deliberate, bounded design decision — not a free feature to switch on everywhere.
Approaches
Edit-distance / fuzzy term matching
Match query terms against index terms within some edit distance (insertions, deletions, substitutions) — e.g. "iphoen" matching "iphone" at edit distance 2. This expands a single query term into a set of index terms to look up.
N-grams
Break terms into overlapping character sequences (e.g. trigrams: "iph", "pho", "hon", "one") and index those instead of/alongside whole terms, so partial character overlap can still produce a match even with a typo.
Spelling correction / query rewriting
Suggest or silently substitute a corrected query ("Did you mean: iphone?") using a dictionary, edit distance, or a model trained on query logs — distinct from directly fuzzy-matching every term.
Known popular-query corrections
For high-traffic queries, precompute common misspelling → correct-query mappings from historical query logs rather than computing fuzzy matches live for every request.
The core trade-off: recall vs latency
Every fuzzy-matching approach trades recall (finding more possibly-relevant matches) against latency and cost (more terms to look up, more posting lists to merge, more candidates to score). Left unbounded, fuzzy matching on short queries can explode: a 2-character prefix with edit distance 2 can match a very large fraction of the vocabulary.
Minimum prefix/query length
Many systems only enable fuzzy matching once the query reaches a minimum length (e.g. 3-4 characters), since short queries have too many plausible fuzzy matches to be useful or cheap.
Maximum edit distance
Bounding edit distance to 1-2 keeps the expanded term set manageable; higher distances rarely improve relevance meaningfully while sharply increasing cost.
Filters and Facets
Complete Design promises filters and facets as a functional requirement — this is where they're actually explained.
A keyword query determines which documents are textually relevant and how they should be ranked. A filter additionally constrains which documents are eligible at all — category = "electronics", price BETWEEN 100 AND 500, in_stock = true — typically without contributing to the relevance score itself. Combining the two: keyword relevance decides ranking among eligible documents; filters decide the eligible set.
Facets
A facet is an aggregation: "how many matching documents fall into each brand / price bucket / rating tier?" — the counts shown next to filter checkboxes in most e-commerce search UIs. Computing this requires scanning (or maintaining precomputed structures over) the matching document set and grouping by field value, which is a materially different operation from ranking the top-k documents.
Field structures for filtering
Exact-match and range filtering is typically served by structures optimized for that purpose (sorted/columnar value structures, exact-match term dictionaries for keyword fields) rather than the same scored full-text structures used for relevance.
Facet cost at scale
Facet aggregation over a high-cardinality field (thousands of distinct brand values, for example) or over a very large matching set can be considerably more expensive than just retrieving top-k results — it's proportional to the matching-set size and field cardinality, not a fixed cost.
Cache implications
Every distinct combination of filters is effectively a distinct query for caching purposes. A product search page with 8 filterable facets can produce an enormous number of realistic filter combinations — this dramatically increases cache-key cardinality and lowers effective hit rate versus caching keyword-only queries. See Search Cache Design below.
Autocomplete Candidate Generation
There is no single canonical way to generate autocomplete candidates — several genuinely different mechanics exist, each with different trade-offs.
Approaches compared
Search-engine prefix approach
Reuse the existing full-text search infrastructure's native prefix-query capability. Simpler operationally (one system to run), but performance depends heavily on the specific engine, query type and concurrent load — it may or may not reliably hit an aggressive latency budget under heavy full-text query traffic sharing the same cluster.
Edge n-gram / prefix-term approach
At index time, generate and index every prefix of a term ("iphone" → "i", "ip", "iph", "ipho", "iphon", "iphone") as separate indexed terms. Query-time prefix matching becomes a fast, direct term lookup — at the cost of a meaningfully larger index (write amplification proportional to average term length) and index-time cost.
Trie / FST-like prefix structure
A dedicated tree/automaton structure purpose-built for prefix traversal. Can be very compact and efficient for prefix lookup and, if top-k completions are precomputed per node, can turn suggestion retrieval into a single lookup rather than a full subtree walk. Operational complexity, memory footprint and update cost vary considerably by implementation — it is not automatically cheaper or simpler than the alternatives above.
Precomputed / query-log suggestion index
Precompute a ranked suggestion list per prefix from historical query logs ("what did users who typed this prefix actually search for?"). Excellent for popular, frequently-repeated queries, but needs explicit handling for freshness (new/trending queries), decay of old trends, and spam/inappropriate-suggestion filtering — a raw query log is not automatically a safe suggestion source.
This module's Complete Design retains a dedicated prefix structure as its chosen illustrative architecture, on the reasoning that a strict, predictable low-latency budget under concurrent full-text query load justified isolating autocomplete from the main search path. That is a defensible choice under those assumptions — not evidence that a dedicated trie is the universally correct answer.
What suggestions actually represent
Autocomplete candidates are not always dictionary-term completions. Depending on the product, suggestions can represent searchable entities/products directly ("iPhone 15 Pro" as a suggestion, not just "iphone" as a term), previously popular full queries, categories, or a deliberately mixed list combining several of these types with its own internal ranking.
Autocomplete Ranking
Finding candidates that match a prefix is only step one. Deciding which handful of matches to actually show, and in what order, is a separate ranking problem.
Signals
Popularity / frequency
How often a term or query has historically been searched or selected — the most common baseline signal for autocomplete ranking.
Recency / trending
Recently surging query volume can matter more than long-run historical popularity, especially around news, events or product launches.
Lexical prefix quality
How well the candidate actually matches the typed prefix (exact prefix match vs a looser fuzzy match) — a factor when suggestions blend multiple generation sources.
Locale / market
The same prefix can warrant different suggestions in different languages, regions or storefronts.
Category / context
Suggestions can be weighted by the section of the product a user is browsing, or by category-specific popularity.
Personalization (optional)
A user's own search/purchase history can re-rank or supplement generic popularity-based suggestions, at the cost of added complexity and reduced suggestion cacheability.
Precomputed vs dynamic ranking
Precomputing a ranked top-k suggestion list per prefix (updated periodically by the indexing/aggregation pipeline) makes query-time serving very cheap — a direct lookup, no live scoring. Dynamic ranking (scoring candidates at query time) allows fresher, more context-sensitive results at higher query-time cost. Many production systems use a hybrid: precomputed popularity as a base signal, with light dynamic adjustment for locale, category or personalization.
Autocomplete Request Control
Autocomplete fires a request on every keystroke, which is a very different traffic shape from a deliberate full-search submission — it needs its own request-control layer on both client and server.
Client-side controls
Debounce
Wait a short interval (commonly on the order of 100-200ms, tuned per product) after the last keystroke before firing a request, so a fast typist doesn't generate one request per character.
Cancellation of obsolete requests
When a newer keystroke fires a new request, cancel or ignore the in-flight request for a now-stale prefix, so its (delayed) response doesn't overwrite a newer, more relevant one.
Minimum prefix length
Skip firing a request at all until the query reaches a minimum length, since very short prefixes (1-2 characters) tend to be the least useful and most expensive to serve well.
Server-side controls
Rate limiting
Autocomplete's per-user request rate is inherently higher than full search's — rate limiting needs to be calibrated for that shape, not reuse full-search limits directly.
Caching hot prefixes
Very short and very common prefixes ("a", "ip", "sh") are requested extremely often across many users and are excellent, high-reuse cache candidates.
Ignoring stale responses via sequence/version
Attach a monotonically increasing request sequence number (or timestamp) to each autocomplete request; the client only applies a response if it's the response to the most recent request it issued, discarding responses to superseded requests even if they arrive out of order over the network.
Indexing Pipeline Correctness
Keeping the search index synchronized with the source database is not just "stream changes and apply them" — it needs explicit handling of ordering, duplication and partial failure to stay correct.
What every indexing pipeline needs
Event key / document ID
Every change event needs a stable identifier for which document it applies to, so updates and deletes for the same document can be correctly correlated.
Source version / update sequence
A monotonically increasing version or sequence number from the source (an updated_at timestamp, a row version, an LSN) that lets the indexer detect and reject stale updates.
Idempotent upsert
Applying the same update event twice must produce the same index state as applying it once — necessary because most delivery pipelines are at-least-once, not exactly-once, so duplicate events are expected, not exceptional.
Delete / tombstone handling
A delete event must actually remove (or mark removed) the document from the index — including correctly handling the case where a delete event arrives before, or racing with, an update event for the same document.
Duplicate events
The pipeline (broker redelivery, connector retries, at-least-once semantics) can and will redeliver the same event. Idempotent upserts, keyed on document ID and applied only if the version increases, absorb this safely.
Out-of-order events
Network/broker/partition behavior can deliver events out of sequence. Without version checking, a delayed older update can silently overwrite a newer one.
Retries / DLQ / poison documents
A document that consistently fails to index (malformed data, a schema mismatch) should retry with backoff and eventually land in a dead-letter queue rather than blocking the pipeline indefinitely or being silently dropped.
Indexing lag
The gap between a source change being committed and it becoming visible in the search index — driven by pipeline throughput, batching, and downstream refresh behavior (see next section).
Refresh, Segments and Visibility
A document reaching a search node is not the same thing as that document becoming visible to search queries — there's a distinct near-real-time visibility mechanism in between.
At a conceptual level (deliberately not tied to any one vendor's exact internals): incoming documents are first buffered and indexed into an in-memory or write-optimized structure. A refresh operation periodically makes newly indexed documents visible as a new searchable segment. In the background, a separate merge process compacts many small segments into fewer, larger ones, since query performance generally degrades as the number of segments a query must check grows.
The freshness/cost trade-off
A more aggressive (more frequent) refresh interval makes new documents searchable sooner, at the cost of more frequent segment creation and more background merge work competing for the same CPU/I/O resources serving live queries.
Why this matters
This is a second, independent source of indexing lag beyond pipeline lag (covered above): even after a document reaches the search node, it may sit in a buffered, not-yet-refreshed state briefly before becoming queryable. Total observed freshness is pipeline lag plus refresh lag.
Full Reindexing and Zero-Downtime Index Migration
Sometimes incremental updates aren't enough and the entire index needs to be rebuilt from scratch — this is a major, frequently-overlooked production concern.
When a full reindex is required
Analyzer / schema changes
Changing tokenization, stemming, synonym rules or field mappings affects how existing documents were analyzed — old documents need to be re-analyzed under the new rules to match consistently.
Ranking / index-field changes
Adding a new field used for ranking or filtering that existing documents don't yet have populated.
Corruption recovery
A damaged index (bad deployment, storage fault, bug) may need to be rebuilt from the authoritative source rather than repaired in place.
Major transformation changes
A significant change to how source data is transformed into indexed documents.
The safe migration pattern
A defensible, ordered approach: (1) create a new versioned index; (2) bulk-load a snapshot of the source data into it; (3) catch up on changes that occurred during the rebuild, using the same pipeline mechanics as normal incremental indexing; (4) validate document counts and spot-check search quality against the new index; (5) atomically switch an alias/routing layer so queries now hit the new index; (6) keep the old index briefly, available for rollback; (7) delete the old index once the new one is confirmed healthy.
Ranking Pipeline
Retrieval finds documents that could match. Ranking decides which of those matches are actually the best answer to the query — a materially different problem with its own staged pipeline.
Stages
1. Lexical / candidate retrieval
Pull a bounded candidate set from the inverted index using term matches and filters — this stage cares about eligibility and a fast first-pass score, not final precision.
2. Inexpensive first-stage score
Score the candidate set with a comparatively cheap lexical relevance function. BM25 is the standard baseline here.
3. Optional re-ranking of a bounded set
A smaller top-N slice of the first-stage results (not the full candidate set) can be re-scored with more expensive signals or models — this bounds the expensive stage's cost independent of how large the full candidate/match set was.
4. Business rules / eligibility
Final adjustments — promoted placements, eligibility exclusions (out-of-stock, region-restricted), diversity constraints — applied after relevance ranking, where appropriate for the product.
BM25, conceptually
BM25 is often reduced to "term frequency times inverse document frequency," which undersells it. Conceptually: a term contributes more to a document's score the more it appears in that document (term frequency), but with diminishing returns — the 10th occurrence of a word matters much less than the 2nd. A term also contributes more if it's rare across the whole corpus (inverse document frequency) — a match on a distinctive word is more informative than a match on a common one. BM25 also normalizes for document length, so a short document isn't penalized just for having fewer chances to contain a term, and a long document isn't rewarded just for sheer bulk.
Pagination and Stable Search Results
Paging through search results looks like a simple UI feature. In a distributed, sharded search system, deep pagination is a genuine architectural problem.
Why deep OFFSET pagination is expensive
A naive OFFSET 10000 LIMIT 10 style request in a scatter-gather search system requires every shard involved in the query to compute and return its top 10010 locally-ranked results (not just the 10 the user actually wants), so the coordinator can merge and pick the correct globally-ranked page. Coordinator work and per-shard work both grow with page depth, not just with page size — this is one of the least obvious costs in distributed search.
There's also a correctness problem independent of cost: if the underlying document set or ranking changes between the request for page 1 and the request for page 2 (a document is added, removed, or re-scored), the OFFSET-based "page 2" can silently show duplicate or skipped results relative to what the user actually saw on page 1.
Cursor / search-after pagination
Instead of an offset, the client passes forward the stable sort values of the last result seen (e.g. [score: 8.42, doc_id: 991823]) and the next page is retrieved as "give me results after this sort position," which each shard can answer without recomputing everything from the start. A deterministic tiebreaker (commonly document ID) is required alongside the primary sort value, since ties on the primary sort field alone would otherwise make the cursor ambiguous.
Consistency caveat
Search-after pagination is efficient, but it is not automatically immune to the underlying document set or ranking changing between page requests — if ranking/index contents shift mid-pagination, results can still move relative to what was on a previous page. Where stronger consistency is genuinely required (rare for typical product search, more relevant for some compliance/audit use cases), point-in-time or snapshot semantics can pin the view a pagination session sees, at additional resource cost.
Search Cache Design
A search result cache keyed only on the raw query string is a correctness bug waiting to happen — the actual cache key needs to capture everything that can change the response.
Cache-key dimensions
Normalized query
The query text after the same normalization applied at index/analysis time — so "iPhone " and "iphone" hit the same cache entry rather than being treated as different queries.
Filters
Every applied filter (category, price range, in-stock) is part of what makes a response valid for that cache entry — two requests with the same query text but different filters are different queries.
Sort
An explicit sort order (price ascending, newest first) changes the response and must be part of the key.
Locale / market / tenant
Multi-region or multi-tenant systems can return entirely different result sets for the same query text depending on locale, storefront or tenant.
Page / cursor
Each page (or cursor position) is effectively a distinct response.
Ranking / model version
If the ranking model or index version changes, previously cached responses may reflect stale ranking logic — the model/index version should be part of the key, or the cache should be invalidated on ranking changes.
Personalization segment
Where personalization exists but is bucketed into a manageable number of segments (rather than being fully per-user), the segment can be included in the key without collapsing to a per-user cache with near-zero reuse.
Other cache-design mechanics
TTL
Cache lifetime is a workload-dependent tuning decision, not a universal constant — see Trade-offs → Cache TTL vs Freshness.
Invalidation difficulty
Proactively invalidating a cached search result when the underlying documents change is hard in general, since many different queries could be affected by one document update — most systems rely on TTL expiry rather than precise invalidation.
Request coalescing
Collapse many concurrent identical requests (same full cache key) into a single upstream query, fanning the one result out to all waiters — this is the primary defense against thundering-herd spikes on cache expiry, independent of TTL length.
Stale-while-revalidate (optional)
Serve a slightly-expired cached entry immediately while asynchronously refreshing it in the background, trading a small amount of staleness for consistently low latency, even right at TTL expiry.
Negative / empty-result caching
Caching "no results found" responses can protect the cluster from repeated expensive misses, but must be done carefully — a genuinely fixed typo or a newly-added document could turn a correct empty result into a stale incorrect one if the negative-cache TTL is too long.
Sharding, Routing and Scatter-Gather
A single node cannot hold an arbitrarily large corpus in memory or serve arbitrarily high query volume. Sharding splits the index across nodes — but how you shard changes how a query has to be executed.
Sharding strategies
Hash / document sharding
Partition documents across shards by a hash of the document ID. Produces well-balanced document distribution across shards, but since any document could be on any shard, a broad keyword query generally must be sent to every shard to find all matches — it doesn't inherently reduce fan-out.
Routing by tenant / category / domain key
Route documents (and, correspondingly, queries that specify that key) to a specific shard or shard subset based on a meaningful key — a query that specifies "tenant X" or "category: electronics" only needs to hit the shard(s) holding that tenant/category, meaningfully reducing fan-out for queries that can supply the routing key. The trade-off: this risks uneven shard sizes if some tenants/categories are much larger than others (a hot-shard risk, covered in Failure & Scale Scenarios), and complicates any query that genuinely needs to search across categories/tenants.
Scatter-gather execution
For queries that must reach multiple shards, the standard execution pattern is: the coordinator sends the query to the relevant shards (all shards, for a hash-sharded broad query; a subset, for a routable query); each shard computes its own local top-k independently; the coordinator merges all shards' local top-k lists and computes the final global top-k.
Oversampling
Because a document that's only the 5th-best match on its own shard could still be the true global 3rd-best overall, each shard typically needs to return somewhat more than the final desired top-k ("oversample") for the coordinator to reliably compute the correct global top-k after merging — returning only the exact final page size per shard risks silently missing legitimately better-ranked results that happened to land on a less-represented shard.
Tail latency and coordination cost
The coordinator's response time is bounded by the slowest shard it queried, not the average — so more shards queried generally means higher exposure to tail latency from any single slow or degraded shard, plus more merge work at the coordinator.
Replication and Partial Results
Replication exists for availability and read throughput — but what actually happens when a shard is degraded or unreachable is a product decision, not an automatic guarantee.
What replicas provide
Availability / read capacity
Each shard typically has one or more replicas holding a copy of that shard's data, allowing reads to be served from any replica and providing a fallback if the primary is unavailable.
Primary/replica behavior is implementation-specific
Exactly how writes propagate to replicas, how promotion happens on primary failure, and what consistency a replica read offers relative to the primary all vary by system and configuration — this module deliberately does not bind these mechanics to one vendor's exact behavior.
A query can succeed from an alternate replica
If a specific replica or the primary for a shard is unavailable, the coordinator can often route that shard's portion of the query to a different available replica instead, without failing the whole query.
When a shard is genuinely unavailable
If no replica for a shard can answer at all, the product has an explicit choice to make: fail the whole query (return an error, ensuring no incomplete/misleading result is shown) or return partial results (show what the reachable shards found, potentially missing some genuinely-matching documents that live on the unavailable shard).
Partial results must be observable
If a system chooses to return partial results rather than failing outright, that partial-ness must be explicitly surfaced (a response flag, a UI indicator, a logged/monitored event) rather than silently presented as a complete result set — a user (or a downstream system) treating a partial result as complete can draw wrong conclusions, especially for any product where completeness matters (compliance search, financial reporting search).
- Search quality starts with analysis: documents and queries must pass through the same normalization, or the inverted index cannot match them.
- Autocomplete is candidate generation plus ranking over a prefix structure (trie/FST or edge n-grams); request control (debounce, cancellation, caching) matters as much as the index.
- Freshness is a pipeline property: refresh creates searchable segments, merges compete with live queries, and CDC or event backlogs are recovered only while the log retains them.
- Schema or analyzer changes go to a new versioned index, then an atomic alias switch — never a destructive in-place rebuild.
- Deep offset pagination makes every shard compute up to the offset; use search-after with a deterministic tiebreaker for stable, constant-cost paging.
- Hash sharding balances data but fans broad queries out to every shard; routed sharding cuts fan-out at the risk of hot shards. Response time follows the slowest shard queried.
- Replication improves availability but not absolutely: decide explicitly whether a shard with no available copy fails the query or returns clearly marked partial results.