Elasticsearch Cheat Sheet
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
matchquery 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 →
matchquery with fuzziness - Mid-string search → ngram tokeniser on a text field
- Combined autocomplete + search → completion for suggestions, then full
matchquery 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"] }
