Courses › System Design › Key Technologies

Elasticsearch

Inverted indexes, mappings, search APIs, cluster architecture and when to use it.

Elasticsearch is a distributed search and analytics engine built on Apache Lucene. Whenever a design needs full-text search, filtering across many fields, geo queries or aggregations over large datasets, it's the technology interviewers expect you to reach for — and the one they'll probe on.

There are two things to get out of this page:

  • How to use it in a design — what the APIs look like, how to model data, and where it fits next to your primary database.
  • How it works under the hood — enough to answer "why is this fast?" and "what are the trade-offs?" when the interviewer digs in.

Basic Concepts

[Cluster] -> [Node] -> [Index] -> [Shard] -> (Lucene Segments)

Documents

A document is the unit of data — a JSON object, like a row in a database.

{
  "id": "b-1029",
  "title": "Designing Data-Intensive Applications",
  "author": "Martin Kleppmann",
  "price": 42.99,
  "created_at": "2026-03-14T12:00:00Z",
  "tags": ["databases", "distributed-systems"]
}

Indices

An index is a collection of documents that you search together, like a table. Each index is split into shards (for scale) and each shard can have replicas (for availability and read throughput).

Mappings and Fields

A mapping is the schema: which fields exist and how each is indexed. The most important distinction is between text fields, which are analyzed (tokenized, lowercased, stemmed) for full-text search, and keyword fields, which are stored as exact values for filtering, sorting and aggregations.

{
  "properties": {
    "title":      { "type": "text" },
    "author":     { "type": "keyword" },
    "price":      { "type": "float" },
    "created_at": { "type": "date" },
    "tags":       { "type": "keyword" },
    "location":   { "type": "geo_point" }
  }
}

[!TIP] Mapping decisions are performance decisions Only index the fields you search or filter on. Indexing everything bloats storage and slows writes. Mapping a field as text when you only filter on exact values wastes work.

Basic Use

Create an Index

// PUT /books
{
  "settings": { "number_of_shards": 3, "number_of_replicas": 1 }
}

Add and Update Documents

// POST /books/_doc
{ "title": "Designing Data-Intensive Applications", "author": "Martin Kleppmann", "price": 42.99 }

// Partial update — POST /books/_update/b-1029
{ "doc": { "price": 39.99 } }

Under the hood, documents are immutable: an "update" writes a new version and marks the old one deleted. Each document has a _version, which you can use for optimistic concurrency control — the update fails if someone changed the document since you read it.

Search

Searches use the Query DSL. A typical query combines full-text matching (which affects relevance scoring) with filters (which don't, and are cacheable):

// GET /books/_search
{
  "query": {
    "bool": {
      "must":   [ { "match": { "title": "distributed systems" } } ],
      "filter": [
        { "range": { "price": { "lte": 50 } } },
        { "term":  { "tags": "databases" } }
      ]
    }
  }
}

Geospatial Search

// GET /places/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "category": "coffee" } },
        { "geo_distance": { "distance": "2km", "location": { "lat": 47.61, "lon": -122.33 } } }
      ]
    }
  }
}

Sort

Results are sorted by relevance score (BM25) by default. You can sort by fields instead, by multiple fields, or by a script that combines relevance with business signals like popularity or recency.

Pagination and Cursors

  • from/size — simple, but every shard must produce from + size results, so deep pages get expensive. Capped at 10,000 by default.
  • search_after — pass the sort values of the last result to get the next page. Efficient at any depth; the standard choice for infinite scroll.
  • Point in time (PIT) — pairs with search_after to give a consistent snapshot while paging, even as documents change.

How it works

Cluster Architecture

A cluster is a set of nodes. Nodes take on roles:

Data Nodes and Lucene Segments

Each shard is a Lucene index, which is itself a set of immutable segments.

  • New documents are buffered in memory and periodically written out as a new segment. This is the "refresh" (default: every second) — which is why Elasticsearch is near-real-time: a new document is searchable after about a second, not instantly.
  • Deletes mark documents as deleted in a separate file; the data stays until segments merge.
  • Updates are delete + insert.
  • Merging runs in the background, combining small segments into larger ones and purging deleted documents.

Immutable segments are what make Lucene fast: they cache well, need no locks for reads, and compress well.

The Inverted Index

The core data structure for full-text search. Instead of mapping documents to the words they contain, it maps each term to the list of documents containing it:

Searching for "distributed systems" becomes a fast intersection of two sorted lists, rather than a scan of every document. Text is analyzed before indexing: tokenized, lowercased, stripped of stop words, and stemmed ("designing" → "design") so queries match variants.

Doc Values

The inverted index is great for "which documents contain this term?" but bad for "what's the price of document 7?", which sorting and aggregations need. Doc values are a columnar, on-disk structure storing each field's values by document — making sorting and aggregations efficient.

Coordinating Nodes and Query Execution

A search runs in two phases:

  • Query phase — the coordinating node sends the query to one copy of every relevant shard. Each shard returns the IDs and scores of its top results.
  • Fetch phase — the coordinator merges the results, picks the global top N, and fetches only those documents.

This is why deep pagination is expensive, and why fewer, well-sized shards usually perform better than many tiny ones.

In Your Interview

Using Elasticsearch

The standard pattern: your primary database stays the source of truth, and Elasticsearch is a derived search index kept in sync asynchronously.

Client -> [API] -> (Postgres)
(Postgres) -> {Change Data Capture / Kafka} -> [Indexer] -> (Elasticsearch)
Client -> [Search API] -> (Elasticsearch)

Things to say:

  • Sync via CDC or events. Changes flow from the database to Elasticsearch through change data capture (e.g. Debezium) or an outbox + queue. The search index lags by seconds.
  • Not a primary store. It has no transactions, and durability and consistency guarantees are weaker than a relational database. Don't use it as your only copy of important data.
  • Read-heavy, write-moderate. Very high write rates or frequent updates to the same documents are expensive (every update is a reindex).
  • Denormalize. Elasticsearch has no real joins, so index documents in the shape you search them — e.g. embed the author name in each book.

[!WARNING] When not to use it If your "search" is an exact lookup by ID or a simple filter on one or two indexed columns, your database already handles it. Adding Elasticsearch adds a sync pipeline, another cluster to operate, and eventual consistency. Use it for genuinely search-shaped problems: full-text, fuzzy matching, many-field filters, facets and geo.

Lessons from Elasticsearch

Even if you never use it, its design teaches patterns worth reusing:

  • Immutable data + background merging (LSM-style) gives fast writes and cache-friendly reads.
  • Separating the query from the fetch keeps distributed queries cheap.
  • Building derived, read-optimized views from a source of truth is a general technique — the same idea behind CQRS, materialized views and precomputed feeds.

References

  • Elasticsearch documentation — *Elasticsearch Guide* (elastic.co)
  • Apache Lucene documentation (lucene.apache.org)
  • Martin Kleppmann, *Designing Data-Intensive Applications*, chapter 3 (storage and retrieval)