Distributed search
Find the documents that best match a free-text query, ranked by relevance, fast,
across more data than one machine holds. Getting it wrong means either slow
LIKE '%term%' scans that melt the primary database, or a search box that
returns the wrong results and erodes user trust — both are silent until traffic
or corpus size exposes them.
When to reach for this
Users type words and expect ranked, relevant matches — not exact-key lookups.
The corpus is text-heavy (documents, products, logs, messages), queries are
ad-hoc (any term, any combination), and results need ranking, highlighting,
facets, or typeahead. Reach for it when a WHERE col LIKE or full-table scan is
already the read bottleneck, or when you need fuzzy/partial matching a B-tree
index cannot serve.
When NOT to
The access pattern is fetch-by-known-key or a fixed filter — a primary database
index serves that far more cheaply and consistently; keep it in data-storage.
The corpus is tiny (thousands of rows): an in-process filter or the database's
built-in full-text index is enough — a separate search cluster is pure
operational overhead (YAGNI). Search is a derived, eventually-consistent copy
of your data; never make it the system of record.
Clarify first
- Corpus size and growth — document count, average doc size, total index
bytes? (→
back-of-the-envelope) This decides shard count.
- Query QPS and shape — read-heavy? term queries, phrase, fuzzy, facets,
autocomplete? Latency target (p99)?
- Indexing freshness — must a new/edited doc be searchable in seconds
(near-real-time) or is minutes/hours of lag fine?
- Relevance bar — is exact term-match enough, or do users expect "best"
results (ranking, synonyms, typo tolerance)?
- Write rate — how many docs/sec change? This sizes the indexing pipeline.
The options
The pipeline (almost always present): a source emits document changes →
an indexing pipeline transforms/analyzes them → the inverted index stores
term→document postings → the query path matches and ranks. For a crawl-based
system (web search), prepend crawl → parse → dedupe; that crawler is its own
subsystem feeding the same pipeline.
Index build mode
- Batch / bulk reindex: rebuild the whole index periodically. Use when the
corpus changes slowly or freshness in hours is acceptable.
- Near-real-time (incremental): apply changes continuously so docs are
searchable in seconds. Use when users expect to find what they just wrote.
Ranking model
- Boolean / filter only: match, no scoring. Use for exact filtering (tags,
facets) where order doesn't matter.
- TF-IDF / BM25 (lexical): score by term frequency and rarity. The default
full-text relevance model; cheap and explainable.
- Hybrid (lexical + signals): blend BM25 with popularity, recency, or
business boosts. Use when "best" means more than word overlap.
Autocomplete
- Prefix trie / FST in memory: sub-millisecond typeahead from a prefix. Use
for suggestion-as-you-type.
- Edge-n-gram index: prefix matching inside the main index. Use when
suggestions must also respect filters/relevance, at higher cost.
Distribution: split the index into shards (each a self-contained
inverted index over a doc subset) for capacity, and replicas per shard for
read throughput and fault tolerance. Sharding theory lives in data-storage.
Trade-offs
| Option |
What it solves |
What it worsens |
Change it when |
| Batch reindex |
Simple, atomic swap, no live-write complexity |
Stale until next build; full rebuild is costly |
Users need fresh results → near-real-time |
| Near-real-time |
Seconds-fresh; no full rebuild |
Segment churn, merge load, refresh cost on writes |
Write rate or merge cost overwhelms nodes → batch/larger refresh interval |
| Boolean/filter |
Cheapest; deterministic |
No notion of "best" result |
Users judge result quality → add BM25 |
| BM25 |
Good relevance, explainable, cheap |
Ignores popularity/recency/intent |
Word-overlap isn't enough → hybrid signals |
| Hybrid signals |
Matches business/user intent |
Complex, harder to debug, needs tuning data |
Tuning cost exceeds value → fall back to BM25 |
| Prefix trie/FST |
Fastest typeahead |
Separate structure to build/refresh; ignores filters |
Suggestions need filters/relevance → edge-n-gram |
| More shards |
Parallelism, fits big corpus |
Per-query fan-out + merge overhead; tiny shards waste resources |
Fan-out latency dominates → fewer, larger shards |
| More replicas |
Read QPS + HA |
More RAM/disk; replication lag on writes |
Write amplification hurts → fewer replicas |
Behavior under stress
Search amplifies trouble through fan-out and derived-data lag.
- Query fan-out tail latency: every query hits all shards; the slowest shard
sets the response time. One hot or GC-paused shard drags every query.
Mitigate: size shards evenly, add replicas, cap result depth, use timeouts +
partial results.
- Indexing vs query contention: a write/merge surge (bulk import, reindex)
steals CPU and I/O from queries, spiking latency. Mitigate: throttle bulk
indexing, schedule big merges off-peak, isolate index vs query node roles.
- Hot shard / skew: an uneven shard key concentrates docs or popular terms on
one node. Mitigate: hash-route documents; reroute or split the hot shard.
- Deep pagination:
from=100000 forces every shard to sort huge windows.
Mitigate: cursor/search_after, cap page depth.
- Pipeline backlog: if the source produces changes faster than the indexer
consumes, freshness lag grows unbounded. Mitigate: backpressure and a durable
buffer (→
messaging-streaming); monitor lag, not just throughput.
- Cold cache after restart: filesystem/page cache is empty, so latency spikes
until it warms. Mitigate: warm critical queries; ramp traffic.
Monitor: per-shard p99 query latency, indexing lag (source→searchable),
segment/merge count, heap/GC, shard balance, and rejected/queued requests.
How to apply
- Clarify the inputs — pin corpus size, query QPS and shape, freshness
target, and the relevance bar (see Clarify first). If a DB index or built-in
full-text serves the access pattern, stop — you don't need a search cluster.
- Pick from the trade-off table — choose a build mode (freshness), a ranking
model (relevance bar), and whether autocomplete needs its own structure.
- Set the key knobs — shard count (from index bytes ÷ target shard size),
replica count (from read QPS + HA), the analyzer/tokenizer (language, stemming,
synonyms), and the refresh interval (freshness vs write cost).
- Stress-test the choice — walk Behavior under stress (fan-out tail,
indexing contention, hot shard, deep pagination, pipeline backlog) and confirm
a mitigation for each one the traffic profile can trigger.
- Size it with numbers — confirm shards fit the corpus and per-shard size
stays in a healthy range, replicas cover peak QPS, and indexing throughput
keeps lag inside the freshness budget (→ Numbers that matter).
- Pick a provider — default to the generic recipe; open a provider file only
if the user named a cloud (see Choosing a provider).
Dos and don'ts
Do
- Treat the index as a derived, rebuildable copy — keep the system of record in
data-storage and be able to fully reindex from it.
- Size shards before launch (corpus ÷ target shard size); aim for even, not tiny,
shards to bound fan-out cost.
- Feed the indexing pipeline through a durable buffer so a write surge can't
outrun the indexer or lose changes.
- Start ranking with BM25; add popularity/recency signals only when word-overlap
demonstrably misses intent.
- Monitor indexing lag separately from query latency — freshness fails silently.
Don't
- Don't use a search engine as your primary store or for transactional writes.
- Don't reach for a separate cluster when a DB full-text index or in-process
filter covers a small corpus (YAGNI).
- Don't allow deep
from/offset pagination; use cursors instead.
- Don't over-shard — many tiny shards add fan-out and merge overhead without
benefit.
- Don't run heavy bulk reindex unthrottled against a cluster serving live queries.
Numbers that matter
The quantities that drive the design: total index bytes (corpus × per-doc
overhead, then ÷ target shard size to get shard count), read QPS × fan-out (each
query touches every shard) for replica sizing, and indexing throughput vs change
rate to keep freshness lag inside budget. A single shard performs well within a
bounded size range; past it, split. Use back-of-the-envelope for the latency,
QPS, and storage figures — don't restate them here.
Interface sketch
The contracts are the document, the query, and the postings. A document is a
typed record with analyzed fields, e.g. { "id": "p123", "title": "...", "body": "...", "tags": ["a","b"], "price": 9.99 }. A query names fields,
match type, filters, sort, and pagination cursor, e.g. q="wireless earbuds", fields=[title^2, body], filter={tag:audio}, sort=_score, search_after=<cursor>.
The inverted index maps each term → posting list of (doc_id, term_freq, positions); ranking reads these to compute BM25. Decide the analyzer
(tokenizer, lowercasing, stemming, synonyms) up front — it is part of the
contract and changing it requires a reindex.
Choosing a provider
Default to the generic recipe above (Elasticsearch/OpenSearch, Lucene, Solr,
self-hosted). If the user names a cloud, read
references/providers/<provider>.md for the managed-service mapping,
quotas/limits, and provider-specific trade-offs. If no file exists for that
provider, the generic recipe is the answer.
Diagram
To visualize the source → indexing pipeline → inverted index (sharded +
replicated) → query/rank path, or the crawl→parse→dedupe front end, use the
in-plugin architecture-diagram skill — show the indexing path and the query
fan-out as distinct flows, with replicas behind each shard.
Related building blocks
data-storage — alternative to a DB LIKE/full-table scan for text search;
search owns the inverted index, while sharding/partitioning and replication
theory live there — name and link, don't re-teach.
messaging-streaming — depends on this for the index-update pipeline: a
durable buffer carries document changes to the indexer and absorbs write
surges (delivery, ordering, backpressure are owned there).
caching — pairs with this to cache hot/repeated queries and reduce fan-out
load (stampede, hot-key, eviction handling owned there).
back-of-the-envelope — feeds into this skill: corpus bytes, QPS, and
freshness numbers size the shards, replicas, and pipeline.
system-design — owned-concept lives in the orchestrator: the reasoning
loop, the trade-off method, and the ten failure modes.
References
references/deep-dive.md — inverted-index internals (postings, segments,
merges), the crawl/index/search pipeline, BM25 scoring, near-real-time refresh,
autocomplete (trie/FST/n-gram), shard routing and replica reads. Read when
designing the search layer in detail.
references/providers/{generic,aws,azure,gcp}.md — service mappings,
limits, and pitfalls per environment.
1---2name: distributed-search3description: This skill should be used when the user designs a "search system", needs "full-text search", asks about an "inverted index", "Elasticsearch / OpenSearch", "relevance ranking" (TF-IDF/BM25), "search autocomplete / typeahead", an "indexing pipeline", or "faceted search". It gives the crawl/index/search architecture, index sharding and replication, ranking, and near-real-time indexing. Use it whenever users must query text by relevance rather than fetch rows by key, even if they don't say "search engine".4---56# Distributed search78Find the documents that best match a free-text query, ranked by relevance, fast,9across more data than one machine holds. Getting it wrong means either slow10`LIKE '%term%'` scans that melt the primary database, or a search box that11returns the wrong results and erodes user trust — both are silent until traffic12or corpus size exposes them.1314## When to reach for this15Users type words and expect ranked, relevant matches — not exact-key lookups.16The corpus is text-heavy (documents, products, logs, messages), queries are17ad-hoc (any term, any combination), and results need ranking, highlighting,18facets, or typeahead. Reach for it when a `WHERE col LIKE` or full-table scan is19already the read bottleneck, or when you need fuzzy/partial matching a B-tree20index cannot serve.2122## When NOT to23The access pattern is fetch-by-known-key or a fixed filter — a primary database24index serves that far more cheaply and consistently; keep it in `data-storage`.25The corpus is tiny (thousands of rows): an in-process filter or the database's26built-in full-text index is enough — a separate search cluster is pure27operational overhead (YAGNI). Search is a *derived, eventually-consistent* copy28of your data; never make it the system of record.2930## Clarify first31- **Corpus size and growth** — document count, average doc size, total index32 bytes? (→ `back-of-the-envelope`) This decides shard count.33- **Query QPS and shape** — read-heavy? term queries, phrase, fuzzy, facets,34 autocomplete? Latency target (p99)?35- **Indexing freshness** — must a new/edited doc be searchable in seconds36 (near-real-time) or is minutes/hours of lag fine?37- **Relevance bar** — is exact term-match enough, or do users expect "best"38 results (ranking, synonyms, typo tolerance)?39- **Write rate** — how many docs/sec change? This sizes the indexing pipeline.4041## The options4243**The pipeline** (almost always present): a source emits document changes →44an **indexing pipeline** transforms/analyzes them → the **inverted index** stores45term→document postings → the **query path** matches and ranks. For a crawl-based46system (web search), prepend crawl → parse → dedupe; that crawler is its own47subsystem feeding the same pipeline.4849**Index build mode**50- **Batch / bulk reindex:** rebuild the whole index periodically. Use when the51 corpus changes slowly or freshness in hours is acceptable.52- **Near-real-time (incremental):** apply changes continuously so docs are53 searchable in seconds. Use when users expect to find what they just wrote.5455**Ranking model**56- **Boolean / filter only:** match, no scoring. Use for exact filtering (tags,57 facets) where order doesn't matter.58- **TF-IDF / BM25 (lexical):** score by term frequency and rarity. The default59 full-text relevance model; cheap and explainable.60- **Hybrid (lexical + signals):** blend BM25 with popularity, recency, or61 business boosts. Use when "best" means more than word overlap.6263**Autocomplete**64- **Prefix trie / FST in memory:** sub-millisecond typeahead from a prefix. Use65 for suggestion-as-you-type.66- **Edge-n-gram index:** prefix matching inside the main index. Use when67 suggestions must also respect filters/relevance, at higher cost.6869**Distribution**: split the index into **shards** (each a self-contained70inverted index over a doc subset) for capacity, and **replicas** per shard for71read throughput and fault tolerance. Sharding theory lives in `data-storage`.7273## Trade-offs7475| Option | What it solves | What it worsens | Change it when |76|---|---|---|---|77| Batch reindex | Simple, atomic swap, no live-write complexity | Stale until next build; full rebuild is costly | Users need fresh results → near-real-time |78| Near-real-time | Seconds-fresh; no full rebuild | Segment churn, merge load, refresh cost on writes | Write rate or merge cost overwhelms nodes → batch/larger refresh interval |79| Boolean/filter | Cheapest; deterministic | No notion of "best" result | Users judge result quality → add BM25 |80| BM25 | Good relevance, explainable, cheap | Ignores popularity/recency/intent | Word-overlap isn't enough → hybrid signals |81| Hybrid signals | Matches business/user intent | Complex, harder to debug, needs tuning data | Tuning cost exceeds value → fall back to BM25 |82| Prefix trie/FST | Fastest typeahead | Separate structure to build/refresh; ignores filters | Suggestions need filters/relevance → edge-n-gram |83| More shards | Parallelism, fits big corpus | Per-query fan-out + merge overhead; tiny shards waste resources | Fan-out latency dominates → fewer, larger shards |84| More replicas | Read QPS + HA | More RAM/disk; replication lag on writes | Write amplification hurts → fewer replicas |8586## Behavior under stress87Search amplifies trouble through **fan-out** and **derived-data lag**.8889- **Query fan-out tail latency:** every query hits all shards; the slowest shard90 sets the response time. One hot or GC-paused shard drags every query.91 *Mitigate:* size shards evenly, add replicas, cap result depth, use timeouts +92 partial results.93- **Indexing vs query contention:** a write/merge surge (bulk import, reindex)94 steals CPU and I/O from queries, spiking latency. *Mitigate:* throttle bulk95 indexing, schedule big merges off-peak, isolate index vs query node roles.96- **Hot shard / skew:** an uneven shard key concentrates docs or popular terms on97 one node. *Mitigate:* hash-route documents; reroute or split the hot shard.98- **Deep pagination:** `from=100000` forces every shard to sort huge windows.99 *Mitigate:* cursor/`search_after`, cap page depth.100- **Pipeline backlog:** if the source produces changes faster than the indexer101 consumes, freshness lag grows unbounded. *Mitigate:* backpressure and a durable102 buffer (→ `messaging-streaming`); monitor lag, not just throughput.103- **Cold cache after restart:** filesystem/page cache is empty, so latency spikes104 until it warms. *Mitigate:* warm critical queries; ramp traffic.105106**Monitor:** per-shard p99 query latency, indexing lag (source→searchable),107segment/merge count, heap/GC, shard balance, and rejected/queued requests.108109## How to apply1101. **Clarify the inputs** — pin corpus size, query QPS and shape, freshness111 target, and the relevance bar (see *Clarify first*). If a DB index or built-in112 full-text serves the access pattern, stop — you don't need a search cluster.1132. **Pick from the trade-off table** — choose a build mode (freshness), a ranking114 model (relevance bar), and whether autocomplete needs its own structure.1153. **Set the key knobs** — shard count (from index bytes ÷ target shard size),116 replica count (from read QPS + HA), the analyzer/tokenizer (language, stemming,117 synonyms), and the refresh interval (freshness vs write cost).1184. **Stress-test the choice** — walk *Behavior under stress* (fan-out tail,119 indexing contention, hot shard, deep pagination, pipeline backlog) and confirm120 a mitigation for each one the traffic profile can trigger.1215. **Size it with numbers** — confirm shards fit the corpus and per-shard size122 stays in a healthy range, replicas cover peak QPS, and indexing throughput123 keeps lag inside the freshness budget (→ *Numbers that matter*).1246. **Pick a provider** — default to the generic recipe; open a provider file only125 if the user named a cloud (see *Choosing a provider*).126127## Dos and don'ts128**Do**129- Treat the index as a derived, rebuildable copy — keep the system of record in130 `data-storage` and be able to fully reindex from it.131- Size shards before launch (corpus ÷ target shard size); aim for even, not tiny,132 shards to bound fan-out cost.133- Feed the indexing pipeline through a durable buffer so a write surge can't134 outrun the indexer or lose changes.135- Start ranking with BM25; add popularity/recency signals only when word-overlap136 demonstrably misses intent.137- Monitor indexing lag separately from query latency — freshness fails silently.138139**Don't**140- Don't use a search engine as your primary store or for transactional writes.141- Don't reach for a separate cluster when a DB full-text index or in-process142 filter covers a small corpus (YAGNI).143- Don't allow deep `from`/`offset` pagination; use cursors instead.144- Don't over-shard — many tiny shards add fan-out and merge overhead without145 benefit.146- Don't run heavy bulk reindex unthrottled against a cluster serving live queries.147148## Numbers that matter149The quantities that drive the design: total index bytes (corpus × per-doc150overhead, then ÷ target shard size to get shard count), read QPS × fan-out (each151query touches every shard) for replica sizing, and indexing throughput vs change152rate to keep freshness lag inside budget. A single shard performs well within a153bounded size range; past it, split. Use `back-of-the-envelope` for the latency,154QPS, and storage figures — don't restate them here.155156## Interface sketch157The contracts are the document, the query, and the postings. A **document** is a158typed record with analyzed fields, e.g. `{ "id": "p123", "title": "...",159"body": "...", "tags": ["a","b"], "price": 9.99 }`. A **query** names fields,160match type, filters, sort, and pagination cursor, e.g. `q="wireless earbuds",161fields=[title^2, body], filter={tag:audio}, sort=_score, search_after=<cursor>`.162The **inverted index** maps each term → posting list of `(doc_id, term_freq,163positions)`; ranking reads these to compute BM25. Decide the analyzer164(tokenizer, lowercasing, stemming, synonyms) up front — it is part of the165contract and changing it requires a reindex.166167## Choosing a provider168Default to the generic recipe above (Elasticsearch/OpenSearch, Lucene, Solr,169self-hosted). If the user names a cloud, read170`references/providers/<provider>.md` for the managed-service mapping,171quotas/limits, and provider-specific trade-offs. If no file exists for that172provider, the generic recipe is the answer.173174## Diagram175To visualize the source → indexing pipeline → inverted index (sharded +176replicated) → query/rank path, or the crawl→parse→dedupe front end, use the177in-plugin `architecture-diagram` skill — show the indexing path and the query178fan-out as distinct flows, with replicas behind each shard.179180## Related building blocks181- `data-storage` — *alternative to* a DB `LIKE`/full-table scan for text search;182 search owns the inverted index, while sharding/partitioning and replication183 theory live there — name and link, don't re-teach.184- `messaging-streaming` — *depends on* this for the index-update pipeline: a185 durable buffer carries document changes to the indexer and absorbs write186 surges (delivery, ordering, backpressure are owned there).187- `caching` — *pairs with* this to cache hot/repeated queries and reduce fan-out188 load (stampede, hot-key, eviction handling owned there).189- `back-of-the-envelope` — *feeds into* this skill: corpus bytes, QPS, and190 freshness numbers size the shards, replicas, and pipeline.191- `system-design` — *owned-concept lives in* the orchestrator: the reasoning192 loop, the trade-off method, and the ten failure modes.193194## References195- **`references/deep-dive.md`** — inverted-index internals (postings, segments,196 merges), the crawl/index/search pipeline, BM25 scoring, near-real-time refresh,197 autocomplete (trie/FST/n-gram), shard routing and replica reads. Read when198 designing the search layer in detail.199- **`references/providers/{generic,aws,azure,gcp}.md`** — service mappings,200 limits, and pitfalls per environment.