Designing a Distributed Log Search Engine

A system design interview guide to building a log search engine that ingests exabytes, indexes them for near-real-time search, and answers a query across a fleet in seconds — the machinery behind systems like Elasticsearch, OpenSearch, and Loki.

Logs are the highest-volume, most detailed telemetry a system produces, and searching them is deceptively hard: you want to type a free-text query and instantly find the handful of relevant lines out of trillions, spanning data from seconds ago to years ago. That requires two ideas working together — an inverted index that maps terms to the documents containing them, so a search never scans everything, and a distributed, tiered architecture that shards the index across many machines and moves aging data down a hot–warm–cold ladder onto cheap object storage. This guide builds that engine: the index model borrowed from Lucene, sharding and time-partitioning, the tiered storage that makes exabytes affordable, and the scatter-gather query path that turns a query string into a plan executed across shards.

Contents

  1. Requirements & Scope
  2. Back-of-the-Envelope Estimates
  3. The Index Model & Segments
  4. Sharding & Time-Partitioned Indices
  5. Ingest & Tiered Storage
  6. Query Execution & Scatter-Gather
  7. Columnar Analytics, Caching & Compaction
  8. Bottlenecks & Tradeoffs
  9. Summary

1. Requirements & Scope

The engine ingests a firehose of log lines and serves search over them. Pin down the requirements.

Interview tip: Two ideas carry this whole design — the inverted index (so search is a lookup, not a scan) and time-partitioning + tiering (so most queries touch only recent shards and old data lives on cheap storage). If you anchor on those two, the rest of the design follows naturally.

2. Back-of-the-Envelope Estimates

Assume 5,000,000 log lines/second at 500 bytes each, retained 13 months.

QuantityEstimateVolume math
Ingest~2.5 GB/s raw5M lines/s × 500 B = 2.5×109 B/s.
Daily volume~216 TB/day raw2.5 GB/s × 86,400 s.
Retained (compressed)~20–30 PB216 TB/day × ~395 days × ~1 replica ÷ ~4× compression → tens of PB.
Index overhead+10–100% of rawFull inverted index can equal or exceed raw size — hence index only high-value fields.
Hot tier~1–2 days on SSDMost queries hit recent data; keep only days hot, age the rest to warm/cold object storage.

The math drives the architecture directly: index overhead can rival the raw data, so you cannot fully index everything; and retaining PBs is only affordable on object storage, so the design must keep cold data searchable from S3-class storage rather than local disk.

3. The Index Model & Segments

The core data structure is the inverted index, the same one Lucene provides beneath Elasticsearch and OpenSearch. Instead of storing documents and scanning them, it stores, for every term, the list of documents that contain it — a postings list. A search for level:ERROR becomes an O(1) dictionary lookup returning the exact matching documents, and combining terms is a fast intersection or union of postings lists.

Log lines inverted into a term-to-postings index, then organized into shards of immutable segments within a time-partitioned index
Each log line is analyzed into terms; the inverted index maps every term (like status:500 or svc:api) to the postings list of documents containing it. An index is time-partitioned (one per day), split into shards for scale, and each shard is a set of immutable segments.

Three properties of the Lucene model matter for the design:

# inverted index: term -> postings list
"level:ERROR"  -> [log0, log2, log7, ...]
"svc:api"      -> [log0, log1, log5, ...]
"status:500"   -> [log0, log2, ...]

# query "ERROR from api" = intersect the postings lists
match = postings["level:ERROR"] & postings["svc:api"]   # fast set intersection

4. Sharding & Time-Partitioned Indices

One machine cannot hold exabytes, so the index is partitioned two ways.

Time-partitioned indices

Logs are overwhelmingly queried by recent time, so the primary partitioning is by time: create a new index per day (or hour), e.g. logs-2026-07-06. This is the highest-leverage decision in the design because it makes two things trivial. Queries with a time range touch only the relevant indices and skip the rest entirely (partition pruning). And retention/tiering is a whole-index operation — to expire or move old data you drop or migrate an entire old index, with no per-document deletes.

Shards within an index

Each time index is split into shards — each shard being an independent Lucene index — so ingest and search scale horizontally. A document routes to a shard by hashing a routing key (default the doc id, or a tenant id to co-locate a tenant’s data). Each shard has replicas on other nodes for availability and read throughput. Shard count is a real tradeoff: too few and you cannot parallelize or grow; too many and per-shard overhead and query fan-out costs dominate (the “oversharding” problem).

PartitioningByBuys
Time-partitioned indexEvent time (per day/hour)Prune by time; tier/expire whole indices cheaply.
ShardHash of routing keyHorizontal ingest & search scale; parallelism.
ReplicaCopy of a shardAvailability + read throughput.

5. Ingest & Tiered Storage

Affordability at exabyte scale comes from moving data down a cost/latency ladder as it ages, since query frequency drops sharply with age.

Log sources to ingest/route to indexers, feeding a hot tier that ages to warm and then cold object storage
Log sources ship to an ingest layer that routes by time and tenant to indexers, which build inverted-index segments in the hot tier (local SSD, fully indexed, near-real-time). As indices age they roll to a warm tier (cheaper nodes) and then to a cold tier on object storage in columnar (Parquet) form — still searchable, far cheaper.

Data moves down the ladder automatically by index age via a lifecycle policy, and is eventually deleted when it exceeds retention. Because everything is a whole-index operation, tiering is cheap and does not touch individual documents.

6. Query Execution & Scatter-Gather

A query is expressed in a DSL and executed in three phases: parse the query string into an AST, plan it (resolve which indices and shards it touches, prune by time and tenant, choose whether to use the inverted index, doc values, or points), and execute it across shards with scatter-gather.

Query parsed and planned by a coordinator, scattered to shards for local search, then gathered and merged
A coordinator parses and plans the query, pruning to only the shards that can hold matching data. It scatters the query to those shards, each of which searches locally and returns partial results; the coordinator gathers, merges (top-K, dedupe), and returns — caching where it can.

The execution model is scatter-gather: a coordinator fans the query to every relevant shard, each shard runs the query against its local segments and returns a partial result (top-K hits, or partial aggregation), and the coordinator merges the partials into the final answer. Two optimizations are essential at scale. Pruning — using the time range to skip whole indices and shards — is what keeps a “last 15 minutes” query from touching a year of data. And caching — of filter results, of frequently hit shards, of aggregation results — absorbs repeated dashboard queries. A two-phase approach (query phase returns just doc ids + scores; fetch phase retrieves the full documents only for the final winners) avoids shipping large documents from shards that will not make the top-K.

function search(dsl, time_range):
  ast     = parse(dsl)                          # -> filters, full-text, aggs
  shards  = plan(ast, time_range)               # prune by time + tenant
  partials = scatter(shards, ast)               # each shard searches locally
  return gather(partials, ast.agg)              # merge top-K / aggregations

7. Columnar Analytics, Caching & Compaction

Three supporting mechanisms round out the engine.

Columnar for analytics. Full-text search wants an inverted index, but analytical queries (“count errors per service per hour over a month”) want to scan a few columns over vast rows — exactly what a columnar format like Parquet excels at. So cold data is stored columnar on object storage: cheap, compressible, and fast to scan for aggregations. Modern systems increasingly split the two — an inverted index for search on hot data, columnar files for analytics on cold data — rather than paying full indexing cost forever.

Caching. Query patterns are heavily repeated (dashboards refresh, alerts re-run), so cache aggressively: OS page cache for hot segments, a query/filter cache for reused predicates, and a results cache for identical dashboard queries. Caching is often the difference between a sub-second and a multi-second dashboard.

Compaction / merging. Because segments are immutable and small ones accumulate, background merging rewrites many small segments into fewer large ones. This bounds the number of segments a query must visit (search cost grows with segment count), and is the only time tombstoned/deleted documents are physically reclaimed. On warm data, an explicit force-merge into a single segment maximizes read efficiency since no more writes are coming.

8. Bottlenecks & Tradeoffs

9. Summary

A distributed log search engine combines the inverted index (search as lookup, not scan) with time-partitioned, sharded, tiered storage (recent data hot and fully indexed, old data cheap on object storage), executed with scatter-gather across shards.

ConcernMechanism
How do we search without scanning?Inverted index: term → postings list; combine terms by set intersection/union.
How are writes made cheap & safe?Immutable segments, append-only, with a translog for durability; near-real-time via refresh.
How do we scale & prune?Time-partitioned indices (prune by time) split into hashed shards with replicas.
How do we afford exabytes?Hot/warm/cold tiers; cold data searchable from object storage (searchable snapshots).
How do we run a query?Parse → plan (prune) → scatter to shards → gather & merge; cache repeated queries.
How do we serve analytics cheaply?Columnar (Parquet) for cold data; scan a few columns over many rows.
How do we keep queries fast over time?Background compaction merges small segments and reclaims deleted space.
The recurring theme: make search a lookup, not a scan, and let time and temperature organize the data. Time-partitioning prunes almost every query down to a slice of the corpus; tiering pushes the vast, rarely-touched majority onto cheap storage; and immutable segments make writes append-only and reads lock-free. The inverted index does the finding; the distributed, tiered architecture makes it affordable at exabyte scale.