Skip to content
TobussSystems in Practice

Elasticsearch Cheat Sheet

4 min read

A practical reference covering Elasticsearch’s core mental model (index, document, mapping, inverted index, sharding) and how to build fast type-ahead autocomplete with the Completion Suggester.


How Indexes Work

Elasticsearch is a distributed search and analytics engine built on Apache Lucene. It trades strict consistency and transactions for extremely fast full-text search.

Key mental model shifts from SQL:

  • Index ≈ database (not a SQL index). It is a logical container for documents split into shards.
  • Document ≈ row — a JSON object, the smallest unit of storage. Documents are immutable; updates are delete + reinsert.
  • Mapping ≈ schema — defines how each field is indexed and stored.
  • text field: analysed (tokenised, lowercased, stemmed) — for full-text search.
  • keyword field: stored as-is — for exact match and filtering.

The inverted index is the core data structure. Traditional DBs map document → words. Elasticsearch maps word → list of documents. A search for “capital gains” looks up both terms in the inverted index, intersects the document lists, and scores relevance. This is why ES is fast and why SQL LIKE '%text%' is slow.

Indexing pipeline: analyse text → build inverted index entries → write to in-memory buffer → flush to disk (Lucene segment) → searchable. Typical delay ~1 second — hence “near real-time.”

Shards: each index is split into N primary shards (Lucene indexes). Shards live on different nodes and are queried in parallel (fan-out → fan-in). Replicas provide failover and extra read throughput. If a node dies, a replica is promoted to primary.

Inverted Index (heart of Elasticsearch):

  Documents:
    Doc 1: "capital gains tax"
    Doc 2: "capital asset"

  Inverted index built:
    capital  →  [Doc 1, Doc 2]
    gains    →  [Doc 1]
    tax      →  [Doc 1]
    asset    →  [Doc 2]

  Query "capital gains":
    lookup capital → [Doc 1, Doc 2]
    lookup gains   → [Doc 1]
    intersect      → [Doc 1]  ← returned, scored by relevance

  Index → shards across nodes:
  ┌─────────────────────────────────────┐
  │ Index: legal_docs  (5 shards)       │
  │  [Shard 0] [Shard 1] [Shard 2]     │  Node 1
  │  [Shard 3] [Shard 4]               │  Node 2
  │  [Replica 0..4]                    │  Node 3
  └─────────────────────────────────────┘
// Create index with explicit mapping
PUT /legal_docs
{
  "settings": { "number_of_shards": 5, "number_of_replicas": 1 },
  "mappings": {
    "properties": {
      "title":          { "type": "text" },      // analysed, full-text search
      "court":          { "type": "keyword" },   // exact match / aggregation
      "effective_date": { "type": "date" },
      "amount":         { "type": "double" }
    }
  }
}

// Index a document
POST /legal_docs/_doc
{
  "doc_id":   "ITAT_2021_123",
  "title":    "Capital Gains Exemption under Section 54F",
  "court":    "ITAT",
  "content":  "The assessee claimed exemption under section 54F..."
}

// Full-text search
POST /legal_docs/_search
{
  "query": {
    "match": { "title": "capital gains" }
  }
}

// Exact filter (keyword) + full-text (text) combined
POST /legal_docs/_search
{
  "query": {
    "bool": {
      "must":   [{ "match":  { "title": "capital gains" } }],
      "filter": [{ "term":   { "court": "ITAT" }          }]
    }
  }
}

Autocomplete with the Completion Suggester

For type-ahead / autocomplete (e.g. user types “capit” → suggestions: “capital gains”, “capital asset”, “capital receipt”), the Completion Suggester is the correct choice over a match query.

Why Completion Suggester:

  • Purpose-built for prefix-based type-ahead
  • Backed by an in-memory FST (finite state transducer) — sub-millisecond latency
  • Supports weighted ranking (important terms appear first)
  • Not suitable for mid-string or fuzzy matching — use match query with ngrams for those cases

Design pattern: index terms (concepts, section names, legal vocabulary) separately from full documents. This keeps the autocomplete index small and fast.

When to use alternatives:

  • Fuzzy matching → match query with fuzziness
  • Mid-string search → ngram tokeniser on a text field
  • Combined autocomplete + search → completion for suggestions, then full match query on selection
User types: "capit"
      │
      ▼
POST /legal_autocomplete/_search  (Completion Suggester)
      │
      ▼  FST prefix lookup (in-memory, ~1ms)
      │
      ▼
suggestions ranked by weight:
  1. "capital gains"    (weight: 10)
  2. "capital asset"    (weight: 8)
  3. "capital receipt"  (weight: 6)
      │
      ▼
GET /autocomplete?query=capit
→ { "suggestions": ["capital gains", "capital asset", "capital receipt"] }
// 1. Create autocomplete index with completion field
PUT /legal_autocomplete
{
  "mappings": {
    "properties": {
      "term": {
        "type": "completion",
        "analyzer": "simple",
        "preserve_separators": true,
        "max_input_length": 50
      },
      "category": { "type": "keyword" }
    }
  }
}

// 2. Index terms with weights
POST /legal_autocomplete/_doc
{ "term": { "input": ["capital gains", "capital gain tax"], "weight": 10 }, "category": "TAX_CONCEPT" }

POST /legal_autocomplete/_doc
{ "term": { "input": ["capital asset"], "weight": 8 }, "category": "TAX_CONCEPT" }

// 3. Query — user typed "capit"
POST /legal_autocomplete/_search
{
  "suggest": {
    "tax-suggest": {
      "prefix": "capit",
      "completion": { "field": "term", "size": 5 }
    }
  }
}

// 4. Java service (Elasticsearch Java API Client 8.x)
public class AutocompleteService {

    private final ElasticsearchClient client;

    public List<String> suggestTerms(String prefix) throws IOException {
        SearchResponse<Void> response = client.search(s -> s
            .index("legal_autocomplete")
            .suggest(su -> su
                .suggesters("tax-suggest", sug -> sug
                    .prefix(prefix)
                    .completion(c -> c.field("term").size(5))
                )
            ),
            Void.class
        );
        return response.suggest()
            .get("tax-suggest").get(0).options().stream()
            .map(CompletionSuggestOption::text)
            .collect(Collectors.toList());
    }
}

// 5. REST endpoint
// GET /autocomplete?query=capit
// → { "suggestions": ["capital gains", "capital asset", "capital receipt"] }