β‘ Elasticsearch
Complete Topic-Wise Guide with Node.js Implementation
Deep behind-the-scenes explanations β’ Every argument explained β’ Interview-ready
PART 1 β Elasticsearch Concepts
Everything you need to understand before writing a single line of code
1. What is Elasticsearch?
Elasticsearch is a distributed, open-source, RESTful search and analytics engine built on top of Apache Lucene. It stores data as JSON documents and provides near real-time search β meaning data you index becomes searchable within ~1 second.
"Elasticsearch is a distributed search engine built on Apache Lucene. It stores JSON documents, builds an inverted index on them, and lets you search millions of records in milliseconds via a REST API. It's schema-free, horizontally scalable, and designed for full-text search, log analytics, and real-time monitoring."
What actually happens when ES starts:
1. ES launches a JVM process (it's written in Java)
2. It reads elasticsearch.yml for cluster name, node name, ports, paths
3. It binds to port 9200 (REST API for your app) and port 9300 (transport protocol for node-to-node communication)
4. It joins or forms a cluster by discovering other nodes via seed hosts
5. A master node is elected using a quorum-based algorithm
6. The master manages the cluster state β a metadata map of every index, shard, and which node holds what
7. Each data node initializes its Lucene instances (one per shard) and loads the inverted index into memory
| Property | Value |
|---|---|
| Built On | Apache Lucene (Java library for full-text indexing) |
| Protocol | RESTful HTTP + JSON |
| Written In | Java |
| License | SSPL / Elastic License 2.0 (was Apache 2.0 before v7.11) |
| Search Speed | Near real-time (~1 second after indexing) |
| Data Format | JSON Documents (schema-free) |
| Default Ports | 9200 (REST API), 9300 (Transport/Node-to-Node) |
2. Why Use Elasticsearch?
Traditional databases (MySQL, PostgreSQL) use B-tree indexes which are great for exact lookups and range queries, but terrible for full-text search. Searching "best noise cancelling headphones" across millions of product descriptions would require a full table scan or a slow LIKE query.
Elasticsearch solves this with an inverted index β the same data structure used by Google, Wikipedia search, and every search engine.
"We use Elasticsearch when we need blazing-fast full-text search, complex filtering, or real-time analytics. Unlike SQL databases that scan rows sequentially, ES pre-builds an inverted index β mapping every word to the documents containing it. This gives us O(1) term lookups instead of O(n) scans. It's commonly used alongside a primary database β Postgres as source of truth, ES for search."
| Use Case | Example | Why ES Wins |
|---|---|---|
| Full-text Search | E-commerce product search | Relevance scoring, fuzzy match, synonyms, stemming |
| Log Analytics | Server/application logs | Aggregate millions of log lines in seconds |
| Autocomplete | Search-as-you-type | Edge n-gram tokenizer returns results in <50ms |
| Geospatial | Find nearby restaurants | Built-in geo_point, geo_shape queries |
| Metrics/APM | Infrastructure monitoring | Time-series aggregations, dashboards |
| Security/SIEM | Threat detection | Real-time correlation across event streams |
3. Inverted Index β Deep Dive
This is the single most important concept in Elasticsearch. Every feature β search speed, relevance scoring, fuzzy matching β exists because of the inverted index.
What is a Forward Index?
A traditional database stores data like this (forward index):
// Forward Index β how a normal DB stores data
// To find "fast", you must scan EVERY row
Doc1 β "Elasticsearch is fast and scalable"
Doc2 β "Redis is fast for caching"
Doc3 β "MongoDB is a NoSQL database"
To find all documents containing "fast", the DB must scan every single row β this is O(n) and gets slower as data grows.
What is an Inverted Index?
Elasticsearch flips this around. At index time, it breaks every text into words (tokens) and builds a map from each word to its documents:
// Inverted Index β how Elasticsearch stores data
// To find "fast", just look up the word β instant O(1)
Term β Documents (Postings List)
βββββββββββββββββββββββββββββββββββββββββββββ
"elasticsearch" β [Doc1]
"is" β [Doc1, Doc2, Doc3]
"fast" β [Doc1, Doc2] β instant lookup!
"and" β [Doc1]
"scalable" β [Doc1]
"redis" β [Doc2]
"caching" β [Doc2]
"mongodb" β [Doc3]
"nosql" β [Doc3]
"database" β [Doc3]
What ES actually stores in the inverted index:
Each entry in the postings list is NOT just a document ID. It contains:
1. Document ID β which doc contains this term
2. Term Frequency (TF) β how many times this term appears in that doc (more = more relevant)
3. Position β at which word position the term appears (needed for phrase queries like "noise cancelling")
4. Offsets β character start/end positions (needed for highlighting the matched text)
5. Field Length β how many total words the field has (shorter fields with the term rank higher)
So the real entry looks like:
"fast" β [{doc:1, tf:1, pos:[2], offset:[21-25]}, {doc:2, tf:1, pos:[2], offset:[9-13]}]
This is how ES calculates relevance scores β using BM25 algorithm which considers TF, document length, and inverse document frequency (how rare the term is across all docs).
How Multi-Word Search Works Behind the Scenes
When you search for "fast scalable":
β Analyzer breaks query into tokens
["fast", "scalable"]
β Look up each term in inverted index
"fast" β [Doc1, Doc2] "scalable" β [Doc1]
β Merge results (OR by default)
Result: [Doc1 (score: 2.4), Doc2 (score: 1.1)]
Doc1 ranks higher because it matches BOTH terms
4. Core Concepts
| ES Term | RDBMS Equivalent | Definition (say this in interview) |
|---|---|---|
| Cluster | Database Server | A group of one or more nodes working together. Identified by a unique name. Holds ALL your data. |
| Node | Single DB Instance | A single server (JVM process) in the cluster. Each node has a unique ID and stores data on disk. |
| Index | Table | A collection of documents with similar structure. You search within an index. Like a "table" but flexible. |
| Document | Row | A single JSON object stored in an index. The smallest unit of data in ES. Has a unique _id. |
| Field | Column | A key-value pair in a document. Each field has a type (text, keyword, integer, date, etc). |
| Mapping | Schema | Defines field types and how they're indexed. Dynamic (auto-detect) or explicit (you define). |
| Shard | Partition | An index is split into shards for horizontal distribution. Each shard is a complete Lucene index with its own inverted index. |
| Replica | Read Replica | A copy of a primary shard on a different node. Provides fault tolerance + read throughput. |
The hierarchy behind the scenes:
Cluster β Nodes β Indices β Shards β Segments β Documents
The part most people miss is Segments. Each shard is made of multiple immutable segments. When you index a document:
1. It first goes to an in-memory buffer
2. Every 1 second (the refresh interval), the buffer is written to a new segment on disk
3. That segment becomes searchable (this is why ES is "near real-time" β 1 sec delay)
4. Segments are immutable β they never change. Updates create new segments; deletes just mark docs as deleted
5. Periodically, small segments are merged into larger ones (background merge process) to keep things efficient
This immutable segment design is WHY ES is so fast β no locks needed for reads, and the OS can cache segments aggressively.
"A Cluster has Nodes. Each Node holds Indices. Each Index is split into Shards for horizontal distribution. Each Shard is a full Lucene index made of immutable Segments. Replicas are copies of shards on different nodes for fault tolerance. Documents are JSON objects stored within an Index, and Fields are the key-value pairs inside each document."
5. Elasticsearch vs Traditional Database
Elasticsearch
β Full-text search (milliseconds on millions of docs)
β Built-in relevance scoring (BM25)
β Horizontal scaling (just add nodes)
β Schema-flexible (dynamic mapping)
β Real-time analytics & aggregations
β Fuzzy search, autocomplete, synonyms built-in
β No ACID transactions
β Not ideal as primary data store
β Eventual consistency (not strong)
β No joins between indices
PostgreSQL / MySQL
β ACID transactions (strong consistency)
β Complex JOINs & relationships
β Referential integrity (foreign keys)
β Mature, proven, huge ecosystem
β Full-text search is very slow at scale
β Vertical scaling primarily
β No relevance scoring
β Schema changes = migrations
β LIKE '%query%' cannot use index = full scan
Why SQL LIKE is slow vs ES:
SELECT * FROM products WHERE description LIKE '%noise cancelling%'
This forces PostgreSQL to do a sequential scan β reading every single row and checking if the string contains "noise cancelling". With 10 million rows, this takes seconds.
In ES, "noise cancelling" was already broken into tokens ["noise", "cancelling"] at index time, and each token points to matching doc IDs in the inverted index. The lookup is O(1) per term, then ES merges the posting lists. Result: milliseconds.
"Elasticsearch is NOT a replacement for your primary database. The best architecture is: PostgreSQL/MongoDB as the source of truth for writes and transactions, and Elasticsearch as a read-optimized search layer. You sync data from your DB to ES using change data capture, application-level dual writes, or tools like Logstash/Debezium."
6. How a Write (Index) Works β Behind the Scenes
When you send a document to Elasticsearch, here's every single step that happens internally:
β
Step 1: Coordinating Node receives the request
Any node can be a coordinating node. It determines which shard owns this doc.
β
Step 2: Routing β shard = hash(_id) % number_of_shards
The doc ID is hashed to determine which primary shard gets it.
This is why you CANNOT change shard count after index creation.
β
Step 3: Primary Shard receives the document
The doc is written to the Translog (write-ahead log) for crash recovery.
Then it goes into the In-Memory Buffer.
β
Step 4: Refresh (every 1 second by default)
The in-memory buffer is written to a new Segment (immutable).
The segment is now SEARCHABLE. This is the "near real-time" delay.
β
Step 5: Replicate to Replica Shards
The primary forwards the write to all replica shards in parallel.
Once all replicas confirm, the client gets a success response.
β
Step 6: Flush (every 30 min or when translog gets big)
Segments are fsync'd to disk. Translog is cleared.
Data is now durable even if power goes out.
β
Step 7: Merge (background)
Many small segments are merged into fewer large segments.
Deleted docs are permanently removed during merge.
"When you index a document, the coordinating node routes it to the correct primary shard using hash(id) % num_shards. The primary writes it to the translog for durability, then to an in-memory buffer. Every 1 second, the buffer is flushed to a new immutable Lucene segment β that's when it becomes searchable. The write is then replicated to all replica shards. Periodically, segments are merged in the background to optimize read performance."
7. How a Search Works β Behind the Scenes
Search is a two-phase process: Query phase and Fetch phase.
βββ QUERY PHASE βββ
β
Step 1: Coordinating node broadcasts query to ALL shards
If index has 5 shards, query goes to all 5 (primary or replica)
β
Step 2: Each shard searches its LOCAL inverted index
Looks up "iphone" in inverted index β gets matching doc IDs + scores
Each shard returns only doc IDs + scores (lightweight)
β
Step 3: Coordinating node MERGES results from all shards
Sorts by score, applies from/size pagination
Now knows the TOP N document IDs
βββ FETCH PHASE βββ
β
Step 4: Coordinating node fetches actual documents
Sends multi-get to only the shards that have the top N docs
Each shard returns full _source JSON for requested docs
β
Step 5: Return results to client
{ hits: { total: 42, hits: [ {_source: {...}}, ... ] } }
Why deep pagination is expensive:
If you request from: 10000, size: 10, EVERY shard must return its top 10,010 results to the coordinating node. With 5 shards, that's 50,050 results to merge β just to return 10 docs. This is why from + size is capped at 10,000 by default, and why you should use search_after for deep pagination.
"ES search is a scatter-gather pattern with two phases. In the Query phase, the coordinating node broadcasts the query to all shards, each shard searches its local inverted index and returns just doc IDs + scores. The coordinator merges and ranks these. In the Fetch phase, it retrieves the actual document bodies only for the top N results. This two-phase design minimizes network transfer."
8. Analyzers & Tokenizers
An analyzer is the text processing pipeline that runs BOTH at index time (when you store data) and at search time (when you query). It determines how text is broken into searchable terms.
The 3-Stage Pipeline
β Stage 1: Character Filters
Strip HTML, replace characters, pattern replace
"The Quick-Brown FOX jumped! twice"
β Stage 2: Tokenizer
Split text into individual tokens (words)
["The", "Quick", "Brown", "FOX", "jumped", "twice"]
β Stage 3: Token Filters
lowercase β remove stop words β stemming
["quick", "brown", "fox", "jump", "twice"]
β These tokens go into the inverted index
Why "Running" matches "run" β Stemming explained:
The stemmer token filter reduces words to their root form:
β’ "running" β "run"
β’ "jumped" β "jump"
β’ "happier" β "happi"
β’ "universities" β "univers"
Both the indexed text AND the search query go through the same analyzer. So when you index "I was running" it stores "run". When you search "runners", it becomes "run". They match!
Common mistake: Using a term query on an analyzed text field. The field stores "run" but you're searching for "Running" (not analyzed) β no match. Always use match for text fields.
Built-in Analyzers
| Analyzer | What It Does | Input β Output |
|---|---|---|
| standard (default) | Unicode tokenizer + lowercase | "Quick Brown" β ["quick", "brown"] |
| simple | Splits on non-letters + lowercase | "2-Fast cars!" β ["fast", "cars"] |
| whitespace | Splits on whitespace only | "Quick Brown" β ["Quick", "Brown"] |
| keyword | No tokenization β entire string as one token | "New York" β ["New York"] |
| english | Standard + stop words + stemming | "the runners are running" β ["runner", "run"] |
Edge N-Gram β For Autocomplete
Breaks a word into progressive prefixes:
// edge_ngram with min_gram:2, max_gram:5
// Input: "iPhone"
// Tokens: ["iP", "iPh", "iPho", "iPhon"]
// (after lowercase): ["ip", "iph", "ipho", "iphon"]
// Now searching "iph" matches because "iph" is a stored token!
"An analyzer has 3 stages: character filter (strip HTML, replace chars), tokenizer (split into tokens), and token filters (lowercase, stemming, stop words). The same analyzer runs at both index and search time to ensure terms match. For autocomplete, we use edge_ngram tokenizer at index time but standard analyzer at search time β so 'ip' typed by user matches the pre-built prefix tokens."
9. Mappings (Schema Definition)
Mapping defines how each field is stored and indexed. It's like a database schema, but more powerful because the same field can be indexed multiple ways simultaneously.
Field Types
| Type | Use For | Indexed As | Supports |
|---|---|---|---|
| text | Full-text (descriptions, titles) | Analyzed β inverted index | match, match_phrase, fuzzy |
| keyword | Exact values (IDs, status, tags) | Not analyzed β exact term | term, terms, sort, aggs |
| integer/long/float | Numbers | BKD tree (numeric index) | range, sort, aggs |
| date | Dates/timestamps | Internally as epoch millis | range, date_histogram |
| boolean | true/false | Term index | term filter |
| nested | Array of objects | Separate hidden documents | nested query (preserves object boundaries) |
| geo_point | Lat/lon coordinates | Geohash + quad tree | geo_distance, geo_bounding_box |
text vs keyword β the #1 source of confusion:
When you store "Apple iPhone":
β’ As text: Analyzer runs β stored as tokens ["apple", "iphone"] in inverted index. You can search with match and it'll find "apple" or "iphone". But you CANNOT sort or aggregate on it (tokens are scattered).
β’ As keyword: Stored as-is: "Apple iPhone" as one single term. You CAN sort, aggregate, and do exact match with term. But searching "apple" alone won't find it.
Best practice β Multi-field mapping: Map the field as BOTH:
"name": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }
Now use name for full-text search and name.keyword for sorting/aggregation. This is actually what ES does by default with dynamic mapping!
Dynamic vs Explicit Mapping
| Dynamic Mapping (default) | Explicit Mapping (recommended) |
|---|---|
| ES auto-detects types from first document | You define every field and its type |
| "42" might become text instead of integer | You control exactly how data is indexed |
| Fine for development/prototyping | Required for production |
| Can lead to "mapping explosion" with dynamic keys | Use dynamic: 'strict' to reject unmapped fields |
"Mapping defines field types and indexing behavior. The key distinction is text (analyzed, for full-text search) vs keyword (not analyzed, for exact match/sort/aggs). In production, always use explicit mapping with dynamic:strict to prevent type-guessing issues. Multi-field mapping lets you index the same data both ways β text for search, keyword for sorting."
10. Query DSL β Complete Guide
Query DSL (Domain Specific Language) is ES's JSON-based query language. Two categories: Leaf queries (search a single field) and Compound queries (combine multiple queries).
Query Context vs Filter Context
This distinction is critical for performance:
Query context ("How WELL does this match?")
β’ Calculates a relevance _score for each document
β’ Score calculation (BM25) is CPU-intensive
β’ Results are NOT cached
β’ Use for: full-text search where ranking matters
Filter context ("Does this match YES or NO?")
β’ No scoring β just binary match/no-match
β’ Results ARE cached in a bitset cache (extremely fast on repeated queries)
β’ Much faster than query context
β’ Use for: exact values, ranges, boolean conditions
Rule: If you don't need relevance ranking, use filter. It's faster AND cached.
10.1 match β Full-Text Search
// match: Analyzes the query, then searches the inverted index
// "titanium chip" β ["titanium", "chip"] β OR search by default
{
"query": {
"match": {
"description": {
"query": "titanium chip",
"operator": "and", // "or" (default) = any term, "and" = all terms must match
"fuzziness": "AUTO", // Typo tolerance: AUTO = 0 edits for 1-2 chars, 1 for 3-5, 2 for 6+
"minimum_should_match": "75%" // At least 75% of terms must match
}
}
}
}
10.2 match_phrase β Exact Phrase (Order Matters)
// All terms must appear in EXACT order
{
"query": {
"match_phrase": {
"description": {
"query": "A17 Pro chip",
"slop": 1 // Allow 1 word between terms ("A17 powerful Pro chip" = still matches)
}
}
}
}
How match_phrase works behind the scenes:
ES doesn't just check if all terms exist β it checks their positions in the inverted index. Remember, the inverted index stores position data for each term occurrence. "A17" must be at position N, "Pro" at N+1, "chip" at N+2. Slop allows gaps: with slop:1, positions can differ by 1 extra.
10.3 multi_match β Search Multiple Fields
{
"query": {
"multi_match": {
"query": "Apple premium",
"fields": ["name^3", "brand^2", "description"], // ^N = boost that field's score by N
"type": "best_fields", // Score from best matching field (default)
"fuzziness": "AUTO" // "most_fields" = sum of all fields, "cross_fields" = treat as one field
}
}
}
10.4 term β Exact Value (No Analysis)
// term: Does NOT analyze the query β searches for exact token
// Use for: keywords, IDs, booleans, enums, numbers
// NEVER use term on text fields!
{ "query": { "term": { "brand.keyword": "Apple" } } }
// terms: Match any value (like SQL IN)
{ "query": { "terms": { "category.keyword": ["smartphones", "laptops", "tablets"] } } }
10.5 range β Number/Date Ranges
{ "query": { "range": { "price": { "gte": 500, "lte": 1500 } } } }
// Date range with relative math
{ "query": { "range": { "createdAt": {
"gte": "now-30d/d", // 30 days ago, rounded to start of day
"lte": "now/d", // today, rounded to end of day
"time_zone": "+05:30" // Adjust for timezone before comparison
} } } }
10.6 bool β Combine Queries (MOST IMPORTANT)
{
"query": {
"bool": {
"must": [ // AND + scored β all must match, affects ranking
{ "match": { "description": "premium quality" } }
],
"should": [ // OR + scored β boosts score if matched
{ "match": { "tags": "5g" } },
{ "match": { "tags": "camera" } }
],
"minimum_should_match": 1, // At least 1 should clause must match
"filter": [ // AND + NOT scored + CACHED β fastest for exact conditions
{ "term": { "brand.keyword": "Apple" } },
{ "range": { "price": { "gte": 500, "lte": 2000 } } },
{ "term": { "inStock": true } }
],
"must_not": [ // NOT + NOT scored + CACHED β exclude results
{ "term": { "category.keyword": "accessories" } }
]
}
}
}
"Bool query has 4 clauses: must (AND + scored), should (OR + scored), filter (AND + not scored + cached), must_not (NOT + not scored + cached). Use must/should for text search where ranking matters. Use filter for exact matches and ranges β it's faster because there's no scoring overhead and results are cached in a bitset."
10.7 Other Useful Queries
// wildcard: Pattern matching (* = any, ? = single char)
{ "query": { "wildcard": { "name.keyword": "*Phone*" } } }
// prefix: Starts with
{ "query": { "prefix": { "name.keyword": "Mac" } } }
// exists: Field exists and is not null
{ "query": { "exists": { "field": "ratings" } } }
// ids: Match specific document IDs
{ "query": { "ids": { "values": ["1", "2", "3"] } } }
11. Aggregations (Analytics Engine)
Aggregations are ES's analytics engine β like SQL GROUP BY, COUNT, AVG, SUM on steroids. You can nest them infinitely and combine with any query.
Three Types
| Type | What It Does | SQL Equivalent | Examples |
|---|---|---|---|
| Bucket | Groups documents into buckets | GROUP BY | terms, range, date_histogram, filters |
| Metric | Calculates values from grouped docs | AVG, SUM, COUNT | avg, sum, min, max, cardinality, percentiles |
| Pipeline | Aggregates on other aggregation results | Subqueries on aggregates | cumulative_sum, derivative, bucket_sort |
How aggregations work behind the scenes:
Aggregations use doc_values β a column-oriented data structure stored on disk alongside the inverted index. While the inverted index maps termsβdocs (good for search), doc_values map docsβvalues (good for sorting and aggregation).
When you ask "group by brand", ES reads the brand doc_values column for all matching documents, which is extremely cache-friendly because values for the same field are stored contiguously.
keyword fields have doc_values enabled by default. text fields do NOT β that's why you must use brand.keyword for aggregations, not brand.
"Aggregations have 3 types: Bucket (grouping like GROUP BY), Metric (calculations like AVG/SUM), and Pipeline (aggregations on aggregation results). They use doc_values β a columnar data structure β which is why aggregations only work on keyword and numeric fields, not analyzed text fields. You can nest aggs infinitely: group by brand β within each brand, calculate avg price."
12. Pagination Strategies
| Method | Limit | Stateful? | Use Case |
|---|---|---|---|
| from/size | 10,000 results max | No | UI pagination (page 1, 2, 3...) |
| search_after | Unlimited | No | Infinite scroll, deep pagination |
| PIT + search_after | Unlimited | Yes (snapshot) | Data exports, consistent reads |
| scroll (deprecated) | Unlimited | Yes | Legacy β use PIT instead |
Why from/size is capped at 10,000:
Requesting from:9990, size:10 means EVERY shard returns its top 10,000 results to the coordinating node. With 5 shards, the coordinator merges 50,000 results just to return 10. Memory and CPU cost grows linearly with from.
search_after is efficient because it uses the sort values of the last document as a cursor. Each shard only needs to find documents AFTER that point β no wasted work on documents you've already seen.
"from/size is simple but capped at 10K. For deep pagination, use search_after β it's cursor-based using the last document's sort values, so each shard only processes documents after the cursor. For consistent exports (no changes during pagination), combine search_after with Point in Time (PIT), which creates a frozen snapshot of the index."
13. Cluster Architecture
Node Roles
| Role | Responsibility | Hardware Needs |
|---|---|---|
| Master | Manages cluster state, shard allocation, index creation/deletion | Low CPU/RAM, reliable network |
| Data | Stores data, executes searches and aggregations | High CPU, RAM, fast SSD |
| Coordinating | Routes requests, merges results from shards | Medium CPU/RAM |
| Ingest | Pre-processes docs before indexing (transform, enrich) | Medium CPU |
| ML | Runs machine learning jobs | High CPU/RAM |
Cluster state and master election:
The cluster state is a metadata object containing: all index mappings, shard routing table (which shard is on which node), node membership. Every node has a copy but only the master can modify it.
Master election uses a quorum: you need a majority of master-eligible nodes to agree. That's why you run an ODD number (3 or 5) of master-eligible nodes β to avoid split-brain. In a split-brain scenario, two groups of nodes think they're each the cluster, leading to data corruption.
Cluster Health
| Status | Meaning | Action |
|---|---|---|
| GREEN | All primary + replica shards assigned | All good! |
| YELLOW | All primaries OK, some replicas unassigned | Add nodes or reduce replica count |
| RED | Some PRIMARY shards unassigned β data loss risk | Urgent! Check disk, node health |
15. Replication
Every primary shard can have 0+ replica shards β exact copies on different nodes.
How replication works internally:
1. Writes ALWAYS go to the primary shard first
2. After writing locally, the primary forwards the operation to all replicas in parallel
3. The client gets a response only after all in-sync replicas confirm (configurable via wait_for_active_shards)
4. Reads (searches) can be served by either primary or replica β ES round-robins between them
5. Replicas are NEVER on the same node as their primary β this is enforced by the shard allocator
Two benefits:
β’ Fault tolerance: If a node dies, replicas on other nodes get promoted to primary. Zero data loss.
β’ Read scaling: More replicas = more shards that can serve search requests in parallel
"Replicas serve two purposes: high availability (if a node dies, replica promotes to primary) and read throughput (searches can hit any replica). Writes always go to primary first, then replicate to all replicas in parallel. Replicas are never co-located with their primary. Default is 1 replica β meaning 2 copies of your data."
16. Performance Tuning
- Use filter over query for non-scoring conditions β filters are cached in bitsets
- Reduce _source β only fetch fields you need
- Bulk API for writes β 500-5000 docs per batch instead of one-by-one
- Refresh interval β set to 30s or -1 during bulk indexing, back to 1s after
- Shard sizing β 10-50GB per shard, not too many, not too few
- Index aliases β for zero-downtime reindexing
- Disable replicas during bulk load β set to 0, then back to 1 after
- Force merge read-only indices β merge to 1 segment for max read speed
- Use doc_values: false on fields you never sort/aggregate β saves disk
- Avoid wildcard queries with leading wildcards β
*phonerequires full scan
"Key tuning levers: use filter context for non-scoring queries (cached), batch writes with Bulk API, temporarily increase refresh_interval during ingestion, right-size shards to 10-50GB, use index aliases for zero-downtime mapping changes, and force-merge read-only indices to a single segment."
17. Security
ES security has 5 layers:
- Authentication β Who are you? (API keys, SAML, LDAP, native users)
- Authorization β What can you do? (RBAC β role-based access control)
- Encryption β TLS for transport (node-to-node) and HTTP (client-to-node)
- Audit logging β Who did what and when?
- Field/document-level security β Restrict WHICH data specific roles can see
"Elasticsearch security covers authentication (API keys, SAML), authorization (RBAC with index-level and field-level permissions), encryption (TLS everywhere), and audit logging. API keys are preferred over username/password because they can be scoped to specific indices and operations and are easily revokable."
18. ELK Stack (Elastic Stack)
| Component | Role | When to Use |
|---|---|---|
| Beats | Lightweight data shippers (Filebeat, Metricbeat) | Collect from servers/containers |
| Logstash | Heavy ETL pipeline (collect, transform, output) | Complex transformations, multiple outputs |
| Ingest Pipeline | Transform within ES itself | Simple transforms (grok, geoip, date parsing) |
| Elasticsearch | Store, index, search, analyze | Always β the core engine |
| Kibana | Visualize, dashboard, manage, Dev Tools | Dashboards, alerts, monitoring |
"The Elastic Stack is: Beats (lightweight collection) β Logstash (heavy transform) β Elasticsearch (store and search) β Kibana (visualize). The modern alternative to Logstash for simple transforms is Ingest Pipelines built into ES. Elastic Agent with Fleet is replacing individual Beats for centralized management."
19. Scaling & Index Lifecycle Management
Hot-Warm-Cold Architecture
| Tier | Hardware | Data Age | Purpose |
|---|---|---|---|
| Hot | Fast NVMe SSDs, high CPU/RAM | 0-7 days | Active indexing + frequent search |
| Warm | Larger, cheaper SSDs | 7-30 days | Infrequent search, read-only |
| Cold | HDD or shared storage | 30-90 days | Rare search, compliance retention |
| Frozen | S3 / blob storage | 90+ days | Archive, searchable snapshots |
ILM (Index Lifecycle Management) automates data tiering:
You define a policy: hot (7d, rollover at 50GB) β warm (shrink shards, force merge) β cold (remove replicas) β delete (after 90d)
ES automatically moves indices through these phases based on age or size. This reduces cost by 40-70% because old, rarely-searched data lives on cheap storage.
"At scale, use Hot-Warm-Cold architecture with ILM policies. Hot tier has NVMe SSDs for active data, warm tier for read-only searchable data, cold for compliance. ILM automates rollover, shrink, merge, and deletion. Combined with searchable snapshots on S3, you can achieve 60-80% cost savings on archive data."
20. Cost Optimization
| Strategy | Savings | Effort |
|---|---|---|
| ILM + Tiered Storage | 40-60% | Medium |
| Right-size Shards (avoid oversharding) | 20-30% | Low |
| Searchable Snapshots (frozen tier) | 60-80% on archive | Low |
| Remove replicas on warm/cold | 50% storage | Low |
| _source excludes (prune unused fields) | 10-30% | Low |
| best_compression codec | 10-15% | Low |
"Cost optimization: ILM for automated tiering (biggest win), right-size shards to avoid overhead, searchable snapshots on S3 for archive, reduce replicas on cold data, prune _source fields, use best_compression. Also evaluate managed (Elastic Cloud) vs self-hosted annually based on team size and usage."
PART 2 β Node.js Implementation
Every method, every argument, what it does, what happens behind the scenes
21. Setup & Connection
// Install: npm install @elastic/elasticsearch
const { Client } = require('@elastic/elasticsearch');
const client = new Client({
node: 'http://localhost:9200', // ES REST endpoint (port 9200)
maxRetries: 5, // Retry failed requests 5 times
requestTimeout: 60000, // 60s timeout per request
sniffOnStart: false, // true = discover all nodes on connect
// For Elastic Cloud:
// cloud: { id: 'deployment:base64...' },
// auth: { apiKey: 'your-key' }
});
// Test connection
async function ping() {
const info = await client.info();
console.log('Connected:', info.version.number);
}
ping();
Behind the scenes: The client uses HTTP keep-alive connections. With an array, it round-robins requests across nodes.
Behind the scenes: On connection errors or 502/503/504, the client waits with exponential backoff, then retries on a different node if available.
Behind the scenes: Sets the socket timeout. If ES is doing a heavy aggregation that takes 45s, a 30s timeout kills it prematurely.
GET _nodes/_all/http on startup to discover all cluster nodes.Behind the scenes: Builds a full node pool for optimal request distribution. Essential for multi-node clusters. Don't use with Elastic Cloud (it handles routing).
Behind the scenes: API keys are Base64-encoded id:key pairs sent in the Authorization header. They can be scoped per-index and revoked without changing passwords.
22. Create Index with Mapping
async function createProductIndex() {
await client.indices.create({
index: 'products',
settings: {
number_of_shards: 1, // 1 shard (small dataset). CANNOT change later!
number_of_replicas: 1, // 1 replica (2 total copies). CAN change later.
refresh_interval: '1s', // New data becomes searchable every 1 second
analysis: {
analyzer: {
autocomplete_analyzer: {
type: 'custom',
tokenizer: 'autocomplete_tokenizer',
filter: ['lowercase']
}
},
tokenizer: {
autocomplete_tokenizer: {
type: 'edge_ngram',
min_gram: 2, // Minimum 2-char prefix: "iP"
max_gram: 15, // Maximum 15-char prefix
token_chars: ['letter', 'digit']
}
}
}
},
mappings: {
dynamic: 'strict', // Reject docs with unmapped fields
properties: {
name: {
type: 'text',
fields: {
keyword: { type: 'keyword', ignore_above: 256 },
autocomplete: { type: 'text', analyzer: 'autocomplete_analyzer', search_analyzer: 'standard' }
}
},
brand: { type: 'keyword' },
category: { type: 'keyword' },
price: { type: 'float' },
description: { type: 'text' },
inStock: { type: 'boolean' },
ratings: { type: 'float' },
tags: { type: 'keyword' },
createdAt: { type: 'date' }
}
}
});
console.log('Index created');
}
What happens when you create an index:
1. Master node validates the mapping and settings
2. Master updates the cluster state with the new index metadata
3. Shard allocation kicks in β master decides which nodes get which shards
4. Each assigned node creates a Lucene index directory on disk for its shards
5. Each shard initializes its translog (write-ahead log) and empty segment
6. Replica shards are allocated to different nodes and start syncing
Interview: "Cannot be changed after creation because the routing formula hash(id)%shards would send existing docs to wrong shards. To change, you must reindex."
Interview: "Set to 0 during bulk import for speed, then back to 1+. More replicas = better fault tolerance + read throughput but more disk."
Interview: "Prevents accidental mapping pollution. Without strict, a typo like 'prce' creates a new field forever."
Interview: "For autocomplete: index with edge_ngram (creates prefix tokens), search with standard (search the exact typed prefix). Without this, searching 'ip' would also be edge_ngrammed into 'i','ip' causing too-broad matches."
23. CRUD Operations
CREATE β Index a Document
async function createDoc() {
const result = await client.index({
index: 'products', // Target index
id: '1', // Doc ID. Omit = ES auto-generates. Exists = REPLACES entire doc.
document: { // The JSON body to store
name: 'iPhone 15 Pro',
brand: 'Apple',
price: 999,
category: 'smartphones',
description: 'Latest iPhone with A17 Pro chip and titanium design',
inStock: true,
ratings: 4.8,
tags: ['premium', '5g', 'camera'],
createdAt: new Date().toISOString()
},
refresh: 'wait_for', // Wait until next refresh to make it searchable
});
console.log(result.result); // 'created' or 'updated'
}
Behind the scenes of client.index():
1. Client sends PUT /products/_doc/1 with JSON body
2. Coordinating node hashes ID "1" β determines shard number
3. Routes to the primary shard on the correct node
4. Primary: writes to translog β writes to in-memory buffer
5. On next refresh (1s): buffer β new immutable segment β now searchable
6. Primary replicates to replica shards
7. With refresh: 'wait_for': response waits until the refresh happens
Critical: If ID already exists, the entire document is REPLACED (like PUT, not PATCH). Use
update() for partial updates.'false' (default) = return immediately, doc searchable in ~1s.
'wait_for' = return after next scheduled refresh. Safe middle ground.
READ β Get Document
const doc = await client.get({
index: 'products',
id: '1',
_source_includes: ['name', 'price'], // Only return these fields (reduces payload)
});
console.log(doc._source); // { name: 'iPhone 15 Pro', price: 999 }
console.log(doc._version); // Version number (increments on every update)
// Get multiple docs at once
const multi = await client.mget({ index: 'products', ids: ['1', '2', '3'] });
GET does NOT use the inverted index. It uses the document's _id to route directly to the correct shard (same hash formula), then reads from the _source stored field. This is O(1) β like a key-value lookup. It can hit either primary or replica.
UPDATE β Partial Update
await client.update({
index: 'products',
id: '1',
doc: { // Only these fields change; rest stays untouched
price: 899,
inStock: false,
},
retry_on_conflict: 3, // Retry 3 times if version conflict
});
// Update by query β update all docs matching a condition
await client.updateByQuery({
index: 'products',
query: { match: { brand: 'Apple' } },
script: {
source: "ctx._source.price = ctx._source.price * 0.9", // 10% discount
lang: 'painless'
},
conflicts: 'proceed', // Skip conflicts instead of aborting
});
Updates are NOT in-place. Because segments are immutable, an update actually: 1) reads the old document, 2) applies changes in memory, 3) indexes a NEW version as a new document in a new segment, 4) marks the old document as deleted. The old version is physically removed during the next segment merge. This is why retry_on_conflict exists β two concurrent updates might read the same version.
DELETE
await client.delete({ index: 'products', id: '1' });
// Delete by query
await client.deleteByQuery({
index: 'products',
query: { range: { price: { lt: 100 } } }
});
// Delete entire index
await client.indices.delete({ index: 'products' });
Deletes don't immediately free disk space. The document is just marked as deleted in a .del file. It's still on disk in its segment. It's filtered out from search results. The space is reclaimed only when that segment gets merged with others β the merge process skips deleted docs.
24. Search & Filters
async function searchProducts({ query, brand, category, minPrice, maxPrice, inStock, page = 1, pageSize = 10 }) {
const must = [];
const filter = [];
// Full-text search (scored)
if (query) {
must.push({
multi_match: {
query,
fields: ['name^3', 'name.autocomplete^2', 'description', 'brand^2'],
type: 'best_fields',
fuzziness: 'AUTO',
}
});
}
// Exact filters (not scored, cached)
if (brand) filter.push({ term: { brand } });
if (category) filter.push({ term: { category } });
if (minPrice || maxPrice) {
const range = {};
if (minPrice) range.gte = minPrice;
if (maxPrice) range.lte = maxPrice;
filter.push({ range: { price: range } });
}
if (inStock !== undefined) filter.push({ term: { inStock } });
const result = await client.search({
index: 'products',
from: (page - 1) * pageSize, // Offset (skip first N results)
size: pageSize, // Limit (return N results)
query: {
bool: {
must: must.length ? must : [{ match_all: {} }],
filter,
}
},
highlight: { // Wrap matched words in tags
fields: { description: {}, name: {} },
pre_tags: ['<mark>'],
post_tags: ['</mark>'],
},
_source: ['name', 'brand', 'price', 'category', 'ratings', 'inStock'],
sort: ['_score', { price: 'asc' }], // Primary: relevance, secondary: price
});
return {
total: result.hits.total.value,
results: result.hits.hits.map(h => ({
id: h._id, score: h._score, ...h._source, highlight: h.highlight
})),
};
}
from+size = 10,000.Behind the scenes: Every shard returns its top
from+size docs. Coordinator merges all, takes docs from from to from+size. Deep pagination wastes work.Behind the scenes: ES re-analyzes the stored text and finds positions where query terms occur, then extracts surrounding context. Three highlighter types: unified (default, best), plain (simple), fvh (for large fields).
Behind the scenes: ES stores the entire original JSON as a compressed blob called _source. This filter happens AFTER fetch β it just strips fields from the response, saving network bandwidth.
25. Aggregations
async function getDashboard() {
const result = await client.search({
index: 'products',
size: 0, // size:0 = only return agg results, no documents
aggs: {
// BUCKET: Group by brand
brands: {
terms: { field: 'brand', size: 20 },
aggs: { // Nested: avg price per brand
avg_price: { avg: { field: 'price' } }
}
},
// BUCKET: Custom price ranges
price_ranges: {
range: { field: 'price', ranges: [
{ key: 'Budget', to: 300 },
{ key: 'Mid', from: 300, to: 1000 },
{ key: 'Premium', from: 1000 },
]}
},
// METRIC: Overall stats
price_stats: { stats: { field: 'price' } },
// METRIC: Count unique brands
unique_brands: { cardinality: { field: 'brand' } },
}
});
console.log(result.aggregations.brands.buckets);
// [{ key:'Apple', doc_count:4, avg_price:{value:1061} }, ...]
}
Behind the scenes: Skips the entire fetch phase. Only the query phase + aggregation computation runs. Much faster when you just need analytics.
Behind the scenes: Each shard returns its local top N terms. Coordinator merges them. This means counts can be approximate if a term is common on one shard but rare on another. Set
shard_size higher for accuracy.Behind the scenes: Uses the HyperLogLog++ algorithm β probabilistic, uses very little memory, but with ~0.5% error rate on large datasets. Exact counting would require loading all values in memory.
26. Pagination (search_after + PIT)
// Cursor-based deep pagination (unlimited, efficient)
async function cursorPaginate(lastSort = null) {
const body = {
index: 'products',
size: 20,
query: { match_all: {} },
sort: [{ createdAt: 'desc' }, { _id: 'asc' }], // Tiebreaker required!
};
if (lastSort) body.search_after = lastSort; // Sort values of last doc from previous page
const result = await client.search(body);
const hits = result.hits.hits;
return { hits, nextCursor: hits.length ? hits[hits.length-1].sort : null };
}
// PIT + search_after for consistent data export
async function exportAll() {
const pit = await client.openPointInTime({ index: 'products', keep_alive: '5m' });
let all = [], searchAfter;
while (true) {
const r = await client.search({
size: 1000,
pit: { id: pit.id, keep_alive: '5m' },
sort: [{ _id: 'asc' }],
...(searchAfter && { search_after: searchAfter }),
});
if (!r.hits.hits.length) break;
all.push(...r.hits.hits.map(h => h._source));
searchAfter = r.hits.hits[r.hits.hits.length-1].sort;
}
await client.closePointInTime({ id: pit.id });
return all;
}
Behind the scenes: Each shard starts scanning from AFTER this sort value β so it doesn't waste work on already-seen documents. Much more efficient than from/size for deep pages.
Behind the scenes: PIT prevents segment merges from deleting old segments, preserving a consistent view. Without PIT, a document could be updated between page requests, causing duplicates or missing results.
27. Autocomplete
async function autocomplete(query) {
const result = await client.search({
index: 'products',
size: 5,
query: { match: { 'name.autocomplete': query } },
_source: ['name', 'brand'],
});
return result.hits.hits.map(h => h._source.name);
}
// autocomplete('mac') β ['MacBook Pro M3 Max']
How this works:
At index time, "MacBook" was processed by edge_ngram: ["ma","mac","macb","macbo","macboo","macbook"]. These all live in the inverted index.
At search time, "mac" is processed by the standard analyzer (because we set search_analyzer: 'standard'), so it stays as ["mac"].
ES looks up "mac" in the inverted index β finds the edge_ngram token β matches the document. Instant.
28. Bulk Operations
async function bulkIndex(products) {
const operations = products.flatMap((doc, i) => [
{ index: { _index: 'products', _id: String(i + 1) } }, // Action line
doc // Document body
]);
const result = await client.bulk({
operations,
refresh: true, // Make all docs searchable after bulk completes
});
if (result.errors) {
const failed = result.items.filter(i => i.index?.error);
console.error('Failed items:', failed);
}
console.log(`Indexed ${result.items.length} docs`);
}
Why bulk is 10-100x faster than individual index calls:
1. One HTTP request instead of N β saves TCP overhead and round trips
2. ES batches writes to the translog and in-memory buffer together
3. Fewer refresh cycles β instead of N small segments, you get fewer larger ones
4. Internal thread pool handles the batch efficiently
Optimal batch size: 500-5000 docs or 5-15MB per request. Too large = heap pressure. Too small = not enough batching benefit.
29. Aliases & Zero-Downtime Reindex
// Step 1: Your app always uses the alias "products" (not the real index name)
await client.indices.putAlias({ index: 'products_v1', name: 'products' });
// Step 2: Need to change mapping? Create new index
await client.indices.create({ index: 'products_v2', /* new mapping */ });
// Step 3: Reindex data from v1 to v2
await client.reindex({
source: { index: 'products_v1' },
dest: { index: 'products_v2' },
});
// Step 4: Atomic alias swap β zero downtime!
await client.indices.updateAliases({
actions: [
{ remove: { index: 'products_v1', alias: 'products' } },
{ add: { index: 'products_v2', alias: 'products' } },
]
});
// Your app never noticed the switch!
Alias swap is atomic. Both actions (remove old + add new) happen in a single cluster state update. There is zero gap where the alias points to nothing. Your application keeps querying "products" and seamlessly switches to the new index.
30. Cluster Monitoring
// Cluster health
const health = await client.cluster.health({});
console.log(health.status); // 'green' | 'yellow' | 'red'
console.log(health.unassigned_shards); // > 0 means trouble
// Index stats
const stats = await client.indices.stats({
index: 'products',
metric: ['docs', 'store', 'search', 'indexing']
});
console.log(stats._all.primaries.docs.count); // Total documents
console.log(stats._all.primaries.store.size); // Disk usage
31. Complete Runnable Project
// ===== elasticsearch-demo.js =====
// Run: docker run -d -p 9200:9200 -e "discovery.type=single-node" -e "xpack.security.enabled=false" docker.elastic.co/elasticsearch/elasticsearch:8.12.0
// Then: npm install @elastic/elasticsearch && node elasticsearch-demo.js
const { Client } = require('@elastic/elasticsearch');
const client = new Client({ node: 'http://localhost:9200' });
const INDEX = 'products';
async function run() {
// 1. Delete old index if exists
try { await client.indices.delete({ index: INDEX }); } catch(e) {}
// 2. Create index with mapping
await client.indices.create({
index: INDEX,
settings: { number_of_shards: 1, number_of_replicas: 0,
analysis: { analyzer: { ac: { type:'custom', tokenizer:'ac_tok', filter:['lowercase'] }},
tokenizer: { ac_tok: { type:'edge_ngram', min_gram:2, max_gram:15, token_chars:['letter','digit'] }}}},
mappings: { properties: {
name: { type:'text', fields:{ keyword:{type:'keyword'}, ac:{type:'text',analyzer:'ac',search_analyzer:'standard'}}},
brand:{type:'keyword'}, category:{type:'keyword'}, price:{type:'float'},
description:{type:'text'}, inStock:{type:'boolean'}, ratings:{type:'float'},
tags:{type:'keyword'}, createdAt:{type:'date'}
}}
});
// 3. Bulk index sample data
const data = [
{ name:'iPhone 15 Pro', brand:'Apple', price:999, category:'smartphones', description:'A17 Pro chip titanium design', inStock:true, ratings:4.8, tags:['premium','5g'], createdAt:'2024-09-22' },
{ name:'Samsung Galaxy S24', brand:'Samsung', price:799, category:'smartphones', description:'Galaxy AI powered with S Pen', inStock:true, ratings:4.7, tags:['ai'], createdAt:'2024-01-17' },
{ name:'MacBook Pro M3', brand:'Apple', price:2499, category:'laptops', description:'Professional laptop M3 Max chip', inStock:true, ratings:4.9, tags:['pro'], createdAt:'2023-11-07' },
{ name:'Sony WH-1000XM5', brand:'Sony', price:349, category:'headphones', description:'Industry leading noise cancelling', inStock:true, ratings:4.6, tags:['wireless','anc'], createdAt:'2023-05-18' },
{ name:'iPad Air M2', brand:'Apple', price:599, category:'tablets', description:'Lightweight tablet M2 creativity', inStock:false, ratings:4.5, tags:['portable'], createdAt:'2024-03-08' },
{ name:'Dell XPS 15', brand:'Dell', price:1499, category:'laptops', description:'Premium Windows OLED display', inStock:true, ratings:4.3, tags:['oled'], createdAt:'2024-02-20' },
{ name:'AirPods Pro 2', brand:'Apple', price:249, category:'headphones', description:'Active noise cancellation adaptive audio', inStock:true, ratings:4.7, tags:['wireless'], createdAt:'2023-09-12' },
{ name:'Pixel 8 Pro', brand:'Google', price:899, category:'smartphones', description:'AI-first best camera system', inStock:true, ratings:4.5, tags:['ai','camera'], createdAt:'2023-10-12' },
];
await client.bulk({ operations: data.flatMap((d,i) => [{ index:{_index:INDEX,_id:String(i+1)}}, d]), refresh:true });
console.log('Seeded', data.length, 'docs');
// 4. Full-text search
console.log('\n--- Search: "apple premium" ---');
const s1 = await client.search({ index:INDEX, query:{ multi_match:{ query:'apple premium', fields:['name^3','brand^2','description'], fuzziness:'AUTO' }}, _source:['name','price'] });
s1.hits.hits.forEach(h => console.log(` ${h._source.name} β $${h._source.price} (score: ${h._score})`));
// 5. Filtered search
console.log('\n--- Filter: Laptops under $2000 ---');
const s2 = await client.search({ index:INDEX, query:{ bool:{ filter:[{ term:{category:'laptops'}}, { range:{price:{lte:2000}}}]}}, _source:['name','price'] });
s2.hits.hits.forEach(h => console.log(` ${h._source.name} β $${h._source.price}`));
// 6. Autocomplete
console.log('\n--- Autocomplete: "mac" ---');
const ac = await client.search({ index:INDEX, size:3, query:{ match:{ 'name.ac':'mac' }}, _source:['name'] });
ac.hits.hits.forEach(h => console.log(` ${h._source.name}`));
// 7. Aggregations
console.log('\n--- Aggregations ---');
const agg = await client.search({ index:INDEX, size:0, aggs:{
brands: { terms:{field:'brand'}, aggs:{ avg_price:{avg:{field:'price'}}} },
price_stats: { stats:{field:'price'} },
}});
agg.aggregations.brands.buckets.forEach(b => console.log(` ${b.key}: ${b.doc_count} products, avg $${b.avg_price.value.toFixed(0)}`));
console.log(' Price stats:', agg.aggregations.price_stats);
// 8. Cluster health
console.log('\n--- Cluster Health ---');
const h = await client.cluster.health({});
console.log(` Status: ${h.status}, Nodes: ${h.number_of_nodes}, Shards: ${h.active_shards}`);
}
run().catch(e => console.error(e.meta?.body?.error || e.message));
Interview Quick-Reference Cheatsheet
| Question | Answer |
|---|---|
| What is Elasticsearch? | Distributed search engine on Lucene. Stores JSON, builds inverted index, near real-time search via REST. |
| What is an inverted index? | Maps each unique term β list of documents + positions + frequency. O(1) term lookup. |
| How is it different from a DB? | Uses inverted index (not B-tree). Complements your DB as a search layer. No ACID, eventual consistency. |
| match vs term? | match = analyzes query + full-text search. term = exact value, no analysis. Never use term on text fields. |
| text vs keyword? | text = analyzed into tokens for search. keyword = stored as-is for exact match, sort, aggs. |
| What is a shard? | A partition of an index. Each shard = full Lucene index. Count fixed at creation. hash(id)%shards for routing. |
| Query vs Filter context? | Query = scored (relevance). Filter = yes/no, cached in bitset, faster. Use filter for exact conditions. |
| What are analyzers? | 3-stage pipeline: char_filter β tokenizer β token_filter. Runs at index time AND search time. |
| How does a write work? | Route to primary shard β translog β in-memory buffer β refresh (1s) β segment β replicate. |
| How does a search work? | Scatter-gather: query all shards β each returns IDs+scores β coordinator merges β fetch top N docs. |
| Why is update not in-place? | Segments are immutable. Update = read old doc + write new version + mark old deleted. Cleaned on merge. |
| Deep pagination? | search_after (cursor-based, efficient) + PIT (consistent snapshot). from/size capped at 10K. |
| Cluster health colors? | Green = all shards OK. Yellow = replicas missing. Red = primaries missing β data loss risk. |
| Hot-Warm-Cold? | Tiered storage: hot (SSD, active), warm (slow SSD, read-only), cold (HDD, archive). ILM automates movement. |
| What is ELK? | Elasticsearch + Logstash + Kibana + Beats. Ingest, store, search, visualize. |
| refresh parameter? | 'true' = immediate (expensive). 'false' = ~1s delay. 'wait_for' = waits for next refresh cycle. |
Concepts + Node.js Implementation + Behind the Scenes