Designing a Distributed Log Search Engine

A system design interview guide to building a log search engine that retains tens of petabytes, 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.

Student edition · start with the guarantee

Build a searchable retained log with explicit visibility, durability, query completeness, and authorization boundaries.

Correctness contract: Accepted logs have defined durable copies; refresh visibility is separate from commit. Search responses disclose missing shards and enforce tenant/time filters.

Basic HTTP, databases, and programming are assumed. Specialized terms are explained below. Named products motivate the problem; this is an educational design, not a claim about their current internals. Capacity numbers are illustrative decimal payload estimates, not benchmarks. Add indexes, protocol overhead, replicas, headroom, and failure capacity before provisioning.

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 petabytes 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. API & Data Model
  5. Sharding & Time-Partitioned Indices
  6. Ingest & Tiered Storage
  7. Query Execution & Scatter-Gather
  8. Columnar Analytics, Caching & Compaction
  9. Bottlenecks & Tradeoffs
  10. Failure Modes
  11. 45-Minute Interview Walkthrough
  12. Common Mistakes
  13. Summary
  14. Check Yourself
  15. Key Terms
  16. Further Reading
  17. Concepts to understand first
  18. Worked example
  19. Runnable Python model

Concepts to understand first

Each definition includes a concrete example and the condition under which the mechanism is useful. You can read this guide without opening another guide.

Inverted indexes and columnar storage

An inverted index maps searchable terms to document postings. A columnar file groups values by column so analytical scans can read only selected fields. They optimize different access patterns.

Example. 'ERROR' → document IDs {2,7}; 'service=api' → {1,2,7}. Intersection returns {2,7}. A monthly count may instead scan only time, service, and status columns.

Limit. Tokenization affects meaning, indexes have storage/write cost, and incomplete shard responses must be marked. Search refresh is visibility, not the same as durable commit.

Partition pruning and predicate pushdown

Pruning excludes storage partitions that cannot match a filter. Column projection reads only requested columns. Predicate pushdown lets file/row-group statistics skip blocks that cannot match; selected blocks still require row filtering.

Example. For a one-day query over a 365-day lake, date pruning can skip 364 days. Min/max time statistics may skip additional row groups only if those statistics are selective.

Limit. Benefits are not universally independent or multiplicative. Row-group sizes, data clustering, skew, metadata, small files, and storage format determine real bytes read.

Write-ahead log and fsync

A WAL records updates before data pages are considered committed. Recovery replays it after a crash. fsync (or an equivalent storage contract) requests that buffered bytes reach durable storage rather than only process/OS memory.

Example. A server acknowledges after its WAL commit and required replica writes are durably accepted. A process crash can then reconstruct the committed update.

Limit. Appending to a file alone does not guarantee power-loss durability. Device behavior, filesystem ordering, replicas, corruption, and the declared fault budget still matter.

Offsets and checkpoints

An offset is a position in one log partition. A checkpoint is a recoverable, consistent record of input positions and processing state. On restart, state must correspond exactly to the input that is considered processed.

Example. If a batch increments a total and crashes before saving its offset, replay increments twice. Commit the total, dedup state, and partition position together, or use a compatible transactional/idempotent sink.

Limit. Saving an offset after a write avoids that specific loss window but can still duplicate; framework checkpoints alone do not make arbitrary external effects transactional.

Cardinality

The number of distinct identities a store must track. A metric series is its metric name plus the full label combination. The product of label-value counts is an upper bound when not every combination occurs.

Example. 100 hosts × 20 routes × 5 status codes can produce up to 10,000 series. Adding a unique request_id can make every request create a new series.

Limit. Hashing every ID to a distinct digest does not reduce cardinality or guarantee anonymity. Use bounded categories, explicit limits, or intentional fixed-bucket aggregation.

1. Requirements & Scope

Intuition

Think of the index at the back of a textbook. You do not read every page to find “photosynthesis”. You look it up and jump to pages 42 and 97. An inverted index does that for log lines. Time partitions are like one book per day, so a query about yesterday opens one book.

Clarifying questions to ask

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 ÷ ~4× compression ≈ 21 PB. One logical copy: the hot tier adds a replica for a few days only, and object storage handles its own durability.
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.

Worked estimate, step by step

StepMathResult
Ingest5M lines/s × 500 B2.5 GB/s
Per day (raw)2.5 GB/s × 86,400 s216 TB/day
Per day (compressed)216 TB ÷ 454 TB/day
Primary shards per day54 TB ÷ 50 GB target shard≈ 1,080 shards
Hot tier (2 days, 1 replica)54 TB × 2 days × 2 copies≈ 216 TB SSD
Retained 13 months54 TB × 395 days≈ 21 PB
Read the table

Only about 1% of the data sits on SSD. The other 99% must live on cheap object storage and still be searchable.

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

An inverted index maps analyzed terms to postings (document IDs). Analysis/tokenization determines what terms mean. Doc values support sorting/aggregation; numeric/spatial point indexes support range filtering. Immutable segments let readers use snapshots while new segments are added.

Refresh makes recently indexed data searchable, often from filesystem cache. Durability comes from the translog/WAL fsync and replica acknowledgement policy or a durable commit. A Lucene commit persists index state; translog/engine flush reclaims no-longer-needed recovery records under its policy. 'Refresh=searchable, flush=durable' alone is misleading because acknowledged translog-backed writes can be durable before flush.

Replica loss, asynchronous durability mode, or corruption can still lose state outside the declared guarantee. State the configuration, not 'a crash loses nothing' universally. Updates/deletes create version/tombstone bookkeeping; merge reclaims space later.

4. API & Data Model

# ingest (batched, e.g. the Elasticsearch _bulk API)
POST /logs-2026-09-25/_bulk
{ "index": {} }
{ "@timestamp": "2026-09-25T10:00:01Z", "service": "api", "level": "ERROR",
  "status": 500, "trace_id": "4bf9...", "message": "timeout calling payments" }

# search
POST /logs-*/_search
{ "query": { "bool": {
    "filter": [ { "term": { "service": "api" } },
                { "range": { "@timestamp": { "gte": "now-15m" } } } ],
    "must":   [ { "match": { "message": "timeout" } } ] } },
  "sort": [ { "@timestamp": "desc" } ], "size": 100,
  "aggs": { "per_min": { "date_histogram": { "field": "@timestamp", "fixed_interval": "1m" } } } }

Field mapping is the data model. service, level and status are keyword fields (exact terms plus doc values). message is analyzed text (tokenized into terms). @timestamp is a date (BKD points for range filters).

5. Sharding & Time-Partitioned Indices

One machine cannot hold petabytes, 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.

6. Ingest & Tiered Storage

Affordability at petabyte 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.

7. 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

8. 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.

9. Bottlenecks & Tradeoffs

10. Failure Modes

FailureWhat happensDefense
Node crash before flushRecent segments in OS cache are lostReplay the translog; replicas already hold the data
Ingest spikeIndexers fall behind; search goes staleKafka buffer in front of indexers; back-pressure; scale hot nodes
Mapping explosionThousands of dynamic fields bloat cluster stateStrict mappings, field limits, store unknown fields unindexed
Expensive query (leading wildcard, huge agg)Shards saturate; everyone slowsQuery timeouts, circuit breakers, per-tenant quotas
Hot shard (one tenant dominates)One node overloadedRoute big tenants across more shards; split by tenant + time

Worked example: visibility is not durability

A log is acknowledged, refreshed into searchable segments, and later committed.

Worked example, in orderDurably accept the log: A translog/WAL and replica policy define survival. OS file-cache visibility alone is not a power-loss guarantee.. Refresh for search: Refresh makes new segments visible to readers. It can occur independently of the durable Lucene commit.. Search multiple shards: A missing shard yields an explicitly partial response or failed query. Do not silently return incomplete totals as exact.. Compact immutable segments: Merge rewrites segments and reclaims deleted data under retention rules. Force-merge consumes resources and is only appropriate for a measured read-only index policy.1Durably accept the log2Refresh for search3Search multiple shards4Compact immutable segments
Read top to bottom. The table below explains the state and guarantee at each step.
StepState, calculation, and reasoning
1. Durably accept the logA translog/WAL and replica policy define survival. OS file-cache visibility alone is not a power-loss guarantee.
2. Refresh for searchRefresh makes new segments visible to readers. It can occur independently of the durable Lucene commit.
3. Search multiple shardsA missing shard yields an explicitly partial response or failed query. Do not silently return incomplete totals as exact.
4. Compact immutable segmentsMerge rewrites segments and reclaims deleted data under retention rules. Force-merge consumes resources and is only appropriate for a measured read-only index policy.
Check your understanding: Does refresh prove that acknowledged logs survive power loss?

No. Durability depends on fsynced translog/commit and replica settings, not mere search visibility.

Run a small, testable model

Download concepts.py. It uses only the Python 3.10+ standard library. From the directory where you saved it, run:

python3 concepts.py search
python3 concepts.py test

The model intersects postings and demonstrates date/column pruning against a small exact dataset.

The program contains deterministic assertions and failure traces. It is a learning model, not a production service: it does not implement network consensus, distributed durability, authentication, or third-party delivery. Read its contracts and try changing an input before extending it.

Primary references for these concepts

11. 45-Minute Interview Walkthrough

MinutesWhat to do
0–5Clarify rate, size, retention, freshness and query mix.
5–10Estimate: 216 TB/day raw, ~21 PB retained, only ~1% hot.
10–20Inverted index and segments. Refresh vs flush vs merge.
20–28Time-partitioned indices, shards, replicas, shard sizing.
28–38Hot/warm/cold tiers and searchable snapshots on object storage.
38–45Scatter-gather query path, caching, failure modes.

12. Common Mistakes

Watch out

13. 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 petabytes?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 petabyte scale.

14. Check Yourself

  1. How does level:ERROR AND svc:api run without a scan?
    Answer

    Look up both terms in the term dictionary. Fetch their postings lists. Intersect the two sorted lists.

  2. What is the difference between refresh and flush in Elasticsearch?
    Answer

    Refresh writes the buffer as a new searchable segment in the OS cache, with no fsync. Flush is a Lucene commit: it fsyncs segments and trims the translog.

  3. Why partition indices by day?
    Answer

    Time-range queries skip whole days. Retention and tiering move or drop whole indices, with no per-document deletes.

  4. At 54 TB/day compressed and 50 GB shards, how many primary shards per day?
    Answer

    54 TB ÷ 50 GB ≈ 1,080 shards.

  5. Why merge segments?
    Answer

    Each query visits every segment, so fewer segments is faster. Merging is also when deleted documents are really removed.

  6. How can data on S3 stay searchable?
    Answer

    A searchable snapshot keeps small metadata on the node. It fetches and caches the needed blocks from object storage on demand.

15. Key Terms

Inverted index
An inverted index maps searchable terms to document postings. A columnar file groups values by column so analytical scans can read only selected fields. They optimize different access patterns. Example and limits above.
Postings list
Sorted doc ids for one term.
Segment
An immutable mini-index inside a shard.
Refresh
Makes buffered docs searchable (~1 s).
Flush
Lucene commit plus translog trim.
Translog
Write-ahead log for crash recovery.
Doc values
Columnar per-field data for sort and aggs.
Shard
One Lucene index; the unit of scale.
Searchable snapshot
Index data read from object storage.

16. Further Reading


⌂