Elastic search
A relational database finds rows by key; a search engine finds documents by words and ranks them. This page builds Elasticsearch from the ground up: the inverted index, how text is analysed, how mappings and the Query DSL work, how BM25 ranks results, how shards, segments, refresh and the translog behave, how to size and operate a cluster, why it must not be your system of record, and how to keep it in sync, finishing with a Docker lab and a Java client example.
What a Search Engine Solves
Imagine a products table with ten million rows and a search box on top. A user types waterproof hiking boots. In a relational database the obvious query is WHERE name LIKE '%waterproof%'. It has four problems: a leading wildcard cannot use a normal B-tree index, so the database scans every row; LIKE knows nothing about words (boot does not match boots, Boots needs case handling); it returns rows in no meaningful order; and it cannot say which product is the best match.
A search engine is built for exactly that job: find the documents that contain these words, rank them by relevance, and do it in milliseconds over millions of documents. Elasticsearch is a distributed search and analytics engine built on the Apache Lucene library. You send it JSON documents over a REST API, and it builds the data structures that make text search and aggregations fast.
| Need | Relational database | Search engine (Elasticsearch) |
|---|---|---|
| Exact lookup by key | Excellent (B-tree) | Possible, but not what it is for |
| Transactions across rows | Yes (ACID) | No multi-document transactions |
| Full text: words, stems, synonyms, typos | Basic (PostgreSQL has built-in full-text search, see page 07) | Core feature: analyzers, fuzziness, phrase queries |
| Rank results by relevance | Rarely | Built in (BM25 by default, chapter 06) |
| Facets and counts ("brand: Acme (120)") | GROUP BY, can be slow at scale | Aggregations, designed for it |
| Search over logs / events at huge volume | Poor fit | Strong fit (data streams, chapter 09) |
| Source of truth for business data | Yes | No (chapter 10) |
Typical uses: product and site search, log and event analytics (the "ELK" pattern), application and security observability, autocomplete, and in recent versions vector search for semantic retrieval (chapter 09). Across all of them the pattern is the same: a copy of data from somewhere else, shaped for fast reading.
This page targets Elasticsearch 9.x (the Elastic docs list 9.5.5 as the current release on 2026-10-31). Most concepts are identical in 8.x; where a behaviour differs by version it is stated. The brief reference docs are at elastic.co/docs.
The Inverted Index
The data structure behind every search engine is the inverted index. A normal ("forward") index maps document to its words. The inverted index flips it: it maps each term to the list of documents that contain it. That list is called a postings list.
Lucene stores a sorted term dictionary (so a term can be found by binary-search-like lookups, and prefix or range of terms can be enumerated) and, for every term, a compressed postings list of document ids. To answer red boots it looks up two terms and intersects two sorted integer lists, which is very cheap. The cost is paid at index time, when every document is broken into terms.
| Stored per term | What it is used for |
|---|---|
| Postings: the ids of documents containing the term | Deciding which documents match |
| Term frequency: how often the term occurs in each document | Relevance scoring (a document mentioning "boots" 5 times scores higher than one mentioning it once, with diminishing returns) |
| Positions: where in the field each occurrence is | Phrase queries: "red boots" must be adjacent |
| Document frequency: in how many documents the term occurs | Rare terms matter more than common ones (the IDF idea) |
Search needs more than "which documents match". Lucene keeps several structures per field, and which ones exist depends on the field type (chapter 04):
- Inverted index - term to documents, for matching text and exact values.
- Doc values - a column-oriented, on-disk structure (document to value) used for sorting and aggregations. Most field types except analyzed
texthave it on by default. - **Stored fields /
_source** - the original JSON, returned in search hits. Elasticsearch stores the original document in the_sourcefield. - Vectors and BKD trees - used for dense vector search and for numeric/geo range queries respectively.
- Each field you index creates several on-disk structures, so the same data costs more space and write time than in a table. Index only what you search or aggregate on, and disable what you do not need (for example
index: falseordoc_values: falseon a field you only display).
Analysis: Character Filters, Tokenizer, Token Filters
Before text reaches the inverted index it goes through analysis. An analyzer is a pipeline of exactly three kinds of component, in this order:
| Stage | How many | Job | Examples |
|---|---|---|---|
| Character filters | 0 or more | Clean the raw character stream before tokenising | html_strip, mapping, pattern_replace |
| Tokenizer | Exactly 1 | Split text into tokens and record their positions and offsets | standard, whitespace, keyword (one token), ngram, edge_ngram |
| Token filters | 0 or more | Add, remove or modify tokens | lowercase, stop, stemmer, synonym, asciifolding |
If you do not choose an analyzer, a text field uses the **standard analyzer**: the Unicode-text-segmentation standard tokenizer plus the lowercase token filter (no stop words by default in current versions, no stemming). That is why Quick and quick match each other by default but foxes and fox do not.
The _analyze API is the single most useful debugging tool in Elasticsearch: it shows exactly which terms an analyzer produces. Request (Kibana Dev Tools style):
The response lists one entry per token with its position and character offsets. Abridged and shown in a compact layout; this is the documented shape, not a captured run:
Note the hyphen split Brown-Foxes into two tokens, the digit became a token, the punctuation disappeared, and everything was lower-cased. The stop word The was kept: the standard analyzer does not remove stop words unless you configure it.
With this analyzer, <b>Running</b> shoes is indexed as the terms run and shoe, and a query for runs shoe produces the same terms, so they match. Test it with POST /products-v1/_analyze and "analyzer": "english_clean" before you rely on it.
run, shoe are written to the inverted indexmatch query "runs shoe" on the same fieldrun, shoe, both found, document matchesterm query for "Running"term query is not analysed. It looks for the literal term Running, which is not in the index (the stored term is run), so it finds nothing- The most common beginner bug:
termsearches for the exact token, but atextfield stores analysed (lower-cased, stemmed) tokens.{"term": {"title": "Red Boots"}}finds nothing because no stored term contains a capital or a space. Usematchfor text fields andtermforkeyword, numbers and dates.
Mappings: text vs keyword, Multi-Fields, Dynamic Mapping
A mapping defines the fields of an index and how each is stored and indexed. Elasticsearch is "schemaless" only in the sense that it will guess a mapping if you do not give one. In production you should define it.
| Type | Analysed? | Use it for | Query with |
|---|---|---|---|
text | Yes (analyzer) | Full-text search: titles, descriptions, messages | match, match_phrase |
keyword | No (exact value, optionally a normalizer) | IDs, enums, tags, emails, status, anything you filter, sort or aggregate on | term, terms, prefix, aggregations |
long / integer / double / scaled_float | No | Numbers, prices | term, range |
date | No | Timestamps | range, date_histogram |
boolean | No | Flags | term |
object / nested | n/a | Structured sub-documents (nested keeps array elements separate) | Normal queries; nested query for nested |
dense_vector | No | Embeddings for vector search | knn |
The name field above is indexed as text for searching and as name.raw (keyword) for sorting, exact matching and aggregations. This is the standard answer to "I need both full text and exact": one source value, two indexed fields. You search name, you sort and aggregate on name.raw. Sorting or aggregating on an analysed text field is not allowed by default (it has no doc values).
If you index a document with an unknown field and dynamic is true (the default), Elasticsearch adds the field to the mapping, guessing the type from the first value it sees. Strings typically become a text field with a keyword sub-field; numbers and dates are detected by JSON shape and date-pattern rules. Three risks follow:
| Risk | What happens | Mitigation |
|---|---|---|
| Wrong type, locked in | The first document decides. A field that first appears as "1" may become a type you did not want, and an existing field type cannot be changed in place | Explicit mappings; dynamic: strict (reject unknown fields) or false (ignore them for indexing) |
| Mapping explosion | Free-form keys (user-supplied maps, tag names as keys, request headers) create thousands of fields. The cluster state grows, memory use grows, and indexing slows | Do not use data as keys. The index.mapping.total_fields.limit setting is a safety net; the flattened type stores a whole object as one field |
| Double cost for strings | Every string gets both text and keyword | Declare which one you need |
Elasticsearch counts every mapper toward index.mapping.total_fields.limit: object levels, leaf fields and each multi-field sub-field all count individually (Elastic documentation, 9.x). The limit exists precisely to stop accidental explosions; treat hitting it as a modelling bug, not as a setting to raise.
- You can add fields to an existing mapping, but you cannot change the type of an existing field or its analyzer. The fix is to create a new index (
products-v2), copy data with the_reindexAPI, and switch an alias (products) from the old index to the new one. Using an alias from day one makes that switch invisible to clients.
The Query DSL
Searches are JSON documents sent to GET /<index>/_search. The Query DSL is a tree of query clauses. Leaf clauses look at one field; compound clauses (mostly bool) combine them.
| Query | Analysed input? | Meaning | Typical field |
|---|---|---|---|
match | Yes | Analyse the text, match documents containing any (default or) of the terms; operator: and requires all | text |
match_phrase | Yes | The terms must appear adjacent and in order (uses positions) | text |
multi_match | Yes | match over several fields, with optional boosts | text |
term / terms | No | The exact stored term (or any of several) | keyword, number, date, boolean |
range | No | gt, gte, lt, lte on numbers, dates, keywords | number, date |
exists | No | The field has a value | any |
prefix, wildcard, regexp | No | Pattern match on terms (can be slow, see chapter 10) | keyword |
bool | n/a | Combines clauses with must, should, filter, must_not | compound |
| Clause | Must match? | Affects score? | Cached? |
|---|---|---|---|
must | Yes (AND) | Yes | No |
should | Optional (at least one required if no must or filter; otherwise only boosts the score, tune with minimum_should_match) | Yes | No |
filter | Yes (AND) | No (yes/no only) | Eligible for caching |
must_not | Must not match | No | Eligible for caching |
Read the example as a sentence: find products whose name matches the words, that are Acme, in stock and at most 200, prefer those with the exact phrase "hiking boots", excluding discontinued ones. Only must and should contribute to the relevance score.
Every clause runs in one of two contexts. In query context the question is "how well does this document match?" and the answer is a score. In filter context the question is "does this document match, yes or no?" and no score is computed. Clauses in bool.filter, bool.must_not, and the filter parameter of some queries run in filter context.
- Use filter for everything that is not relevance: brand, status, price range, date window, tenant id. It is cheaper because no scoring is done.
- Filter results can be cached per segment (a bitset of matching documents) and reused by later queries. Elasticsearch decides what to cache based on how often a filter is used; you do not control it per query.
- Put the text clause in
mustand the rest infilter: the standard shape of a search request.
- A
termorrangeinsidemuststill works but computes a score you do not need and cannot be cached the same way. Moving it tofiltergives the same hits with less work. A search that returns correct results but is slow often has its structured conditions in the wrong clause.
Relevance (BM25) and Aggregations
Results are sorted by _score unless you say otherwise. The default scoring function is BM25: the Elastic similarity docs list BM25 as the default similarity (it replaced classic TF/IDF in Elasticsearch 5.0). You do not need the formula to use it, but you should know the three ideas it combines.
| Idea | Why it makes sense | Tuning knob |
|---|---|---|
| Term frequency saturates | The 10th mention of "boots" adds much less than the 1st | k1 in the similarity settings |
| Rare terms weigh more | "waterproof" tells you more than "the" | None needed; comes from index statistics |
| Field length normalisation | A hit in a 3-word title is stronger than in a 3000-word body | b in the similarity settings |
| Boosts you control | A title match matters more than a description match | name^3 in multi_match, boost on a clause |
Use "explain": true on a search to see how a score was computed, term by term. Use the Profile API to see where query time was spent.
- A score of 12.4 has no absolute meaning. It depends on the query and the index statistics (and, per shard, on that shard's local statistics, so tiny indices can score oddly). Use scores only to order the results of one query, and never as a "confidence" threshold.
Aggregations compute summaries over the documents that match the query: counts per category, histograms over time, averages. They are the engine behind facets and dashboards. They run on doc values, which is why you aggregate on keyword, number and date fields, not on analysed text.
| Family | Examples | Gives you |
|---|---|---|
| Bucket | terms, date_histogram, range, filters | Groups of documents with a count each |
| Metric | sum, avg, min, max, stats, cardinality, percentiles | A number computed over a bucket or all hits |
| Pipeline | derivative, cumulative_sum, bucket_sort | Computations over other aggregations' results |
"size": 0 means "no hits, aggregations only". Buckets nest: the revenue sum above runs once per daily bucket. Two accuracy caveats worth knowing: terms on a high-cardinality field across many shards is approximate (each shard returns its top terms, then they are merged), and cardinality is an estimate by design.
Nodes, Indices, Shards and Replicas
An Elasticsearch cluster is a set of nodes (JVM processes). Data is stored in indices. An index is split into shards, and each shard is a self-contained Lucene index. A shard is the unit of distribution: shards are spread across nodes, so an index can be larger than any one machine and a search can use many machines in parallel.
| Term | Meaning | Notes |
|---|---|---|
| Primary shard | The shard that receives a write first | Count fixed at index creation (number_of_shards) |
| Replica shard | A copy of a primary on a different node | number_of_replicas defaults to 1 (Elastic docs); changeable at any time. Serves reads and gives failover |
| Index | A logical collection of documents with one mapping | Made of N primary shards plus replicas |
| Alias | A name pointing to one or more indices | Lets you swap indices without client changes |
| Document routing | Which shard a document lives on | Chosen from hash(_routing) % number_of_shards, default _routing is the document _id |
A node can have one or more roles, set with node.roles. A small cluster often gives every node several roles; a large one specialises them.
| Role | Job |
|---|---|
master (master-eligible) | Manages cluster state: index creation and deletion, shard allocation. One elected master at a time. Keep it light and stable |
data (and tiered: data_hot, data_warm, data_cold, data_frozen) | Holds shards, executes searches and indexing. Tiers put newer data on faster hardware (chapter 09) |
ingest | Runs ingest pipelines that transform documents before indexing |
ml | Machine learning jobs and inference |
remote_cluster_client, transform | Cross-cluster search or replication; continuous transforms |
| (coordinating) | Every node can act as a coordinating node: it receives a request, fans it out and merges results. A node with no roles is a dedicated coordinating node |
from + size hits_source of only the winning documents and returns themBecause every shard must return its top from + size hits, deep pagination is expensive (chapter 09), and searching 1000 tiny shards is slower than searching 10 sized shards (chapter 09).
- You cannot change
number_of_shardsof an existing index except by creating a new index (reindex, or the split and shrink APIs under rules). Too few shards limits how far one index can scale; too many wastes memory and slows searches. Chapter 09 gives the sizing guidance.
Segments, Refresh, Translog, Flush and Merges
Understanding the write path explains most surprises: why a document is not searchable instantly, why updates are costly, why deleted data does not free space at once, and why heavy indexing competes with search.
| Step | What happens | Default / note |
|---|---|---|
| Index request | The document is routed to its primary shard, added to an in-memory buffer and written to the translog | Request returns after the translog is fsynced on primary and replicas (index.translog.durability: request, the default) |
| Refresh | The buffer is written as a new segment that becomes searchable. Not yet fsynced to disk | index.refresh_interval default 1s (see note below) |
| Flush | Segments are fsynced, a Lucene commit point is written, and the translog is trimmed | Triggered automatically; index.translog.flush_threshold_size defaults to 10 GB |
| Merge | Small segments are merged into larger ones in the background; deleted documents are dropped for real | Automatic; throttled |
A segment is an immutable Lucene index fragment. Once written, it is never modified. A shard is made of many segments, and a search must look in all of them and combine the results, which is why having fewer, larger segments is better for search and why merges exist.
The Elastic docs for index.refresh_interval say that if you do not set it explicitly, shards that have received **no search request for at least index.search.idle.after (default 30s)** skip background refreshes until a search arrives; a search on an idle shard triggers a refresh itself. Setting any explicit value, even 1s, turns this idle optimisation off. The default value is 1s in the Elastic Stack (checked against the 9.x docs).
Segments are immutable, so there is no way to change a field inside an existing document. To update a document Elasticsearch marks the old version as deleted (a bit in a per-segment deleted-documents set) and indexes a new complete document into the buffer. The old version still occupies disk space until a merge rewrites that segment. Consequences:
- An update of one field reindexes the whole document (the
_updateAPI fetches_source, applies the change and reindexes it). Frequently updating tiny fields of big documents is expensive. - Deleting or updating many documents does not shrink the index immediately; space is reclaimed by merges.
- The same document id after an update gets a new internal version; concurrency control uses
_seq_noand_primary_term(if_seq_no,if_primary_term) for optimistic locking.
Indexing APIs take a refresh parameter. refresh=true refreshes the affected shards immediately, which is expensive if used per request. refresh=wait_for makes the request wait until the next regular refresh makes the change visible. The Elastic docs advise using neither by default and designing the application to tolerate a short delay; if you need a read-your-own-write, fetch the document by id with the get API, which is real-time.
By default a document goes to shard hash(_id) % number_of_shards, which spreads data evenly but means a search must ask all shards. With custom routing (?routing=customer-42) all documents of one customer land on one shard, and a search that passes the same routing value asks only that shard. It is a strong optimisation for multi-tenant data, at the price of possible uneven shard sizes. In Elasticsearch 9.4 and later the docs describe the routing function for new indices as hash(_routing) % number_of_shards; indices created earlier keep the legacy function.
- Forcing a refresh per request produces a flood of tiny segments, high merge load, and slower indexing and search for everyone. For bulk loads, increase the refresh interval (or set it to
-1) during the load and restore it afterwards, and always use the Bulk API instead of one request per document.
Shard Sizing, Data Streams, ILM, Pagination and Vector Search
The Elastic documentation (shard sizing, 9.x) gives these as guidance, not hard limits:
| Guidance | Value (from the Elastic docs) | Why |
|---|---|---|
| Target shard size | 10 GB to 50 GB for many use cases | Smaller shards add per-shard overhead; very large shards recover and rebalance slowly |
| Documents per shard | Keep below about 200 million | Search latency and merge cost grow with document count |
| Heap per master-eligible node | About 1 GB of heap per 3000 indices | Cluster state lives in master heap |
| Delete whole indices, not documents | Prefer deleting an index over deleting documents | Deletes only mark documents; whole-index deletion frees space at once |
| Hard cap | A single index may not exceed 1024 primary shards by default | Safety limit (index.max_number_of_shards system property) |
Each shard costs memory and file handles whether it holds data or not. The cluster also enforces cluster.max_shards_per_node; hitting it produces a "maximum shards open" error. Hundreds of tiny shards is the single most common cause of an unhealthy cluster: fewer, bigger shards (within the range above) beat many small ones.
Logs and metrics are append-only and have a retention period. A data stream is a named, append-only abstraction over a series of hidden backing indices: writes go to the newest index, reads search all of them. Index Lifecycle Management (ILM) moves backing indices through phases (hot, warm, cold, frozen, delete) and rolls over to a new backing index when the current one reaches a size or age. Retention then becomes "delete the old index", which is instant and cheap.
The rollover threshold max_primary_shard_size is how you keep shards in the 10-50 GB band automatically. The Elastic docs recommend this over choosing a fixed index per day.
from + size paging works by having every shard return from + size hits, so the work grows with the page number. By default you cannot page past 10,000 hits with from and size: the limit is the index setting index.max_result_window, default 10000. For more, use **search_after with a point in time (PIT). The PIT freezes the view of the index so that pages are consistent even if a refresh happens between requests, and the sort values of the last hit tell Elasticsearch where to continue. The Elastic docs state that the scroll API is no longer recommended** for deep pagination.
Always include a tie-breaker field (unique per document, such as an id) in the sort, otherwise documents with equal sort values can be skipped or repeated. Users almost never need page 500 of a search; offering "next page" and refining filters is a better design.
Elasticsearch can store embeddings in a **dense_vector field and answer k-nearest-neighbour** queries: given a query vector, return the documents with the most similar vectors. This is the basis of semantic search and retrieval-augmented generation (RAG). The Elastic docs (9.x) describe dense_vector with the knn search option and approximate search using the HNSW algorithm. Indexing is on by default for dense vectors, and the docs list automatic quantization defaults that vary by version (for example, int8_hnsw in 9.0; bbq_hnsw for vectors of 384 or more dimensions from 9.1; bbq_disk as default from 9.4 where the licence allows). Quantization trades a little accuracy for much lower memory use.
You can combine knn with a text query for hybrid search. Some vector features are marked preview or have licence requirements in the docs: check the page for the exact version you run before relying on them. Two practical notes: vectors are excluded from _source in search responses by default, and embeddings must come from a model you run or call yourself (or an inference endpoint Elasticsearch hosts).
Not a Primary Database, Staying in Sync, and OpenSearch
Most Elasticsearch trouble is not a bug in Elasticsearch but a mismatch between the tool and the job. The diagram lists the five recurring mistakes with their fixes.
Elasticsearch stores your documents durably enough for a search index, but it is designed as a derived, rebuildable view. Reasons to keep the authoritative copy elsewhere:
| Concern | What Elasticsearch gives you | Why that is weaker than a system of record |
|---|---|---|
| Transactions | Atomic single-document writes only | No multi-document or multi-index transactions, no rollback |
| Visibility | Near real time (chapter 08) | A write is not searchable until a refresh; reads by id are real time |
| Updates | Delete plus insert of the whole document | Cost grows with document size; no cheap partial update |
| Schema change | Fields cannot change type in place | Any such change means building a new index and reindexing everything |
| Durability | Translog fsync by default, replicas for copies | Operational mistakes (replicas set to 0, async translog, lost disks) lose data; the engine assumes you can rebuild |
| Consistency under failure | Replica-based, designed for availability of search | Not a substitute for a database with a documented, tested recovery story for the business data |
A good test: if the whole cluster vanished, could you rebuild it from another store? If yes, you are using it correctly. If no, you have made it your database.
Because Elasticsearch holds a copy, you need a reliable way to push changes from the system of record. Options, from weakest to strongest:
| Approach | How | Problem or strength |
|---|---|---|
| Dual write | The application writes to the database and then to Elasticsearch | If the second write fails, the two disagree forever. No atomicity between two systems |
| Batch re-sync | A scheduled job copies recent changes | Simple and a good safety net; stale until the next run; deletes are hard to detect |
| Transactional outbox | Write the business row and an event row in one database transaction; a relay publishes events (to Kafka) and a consumer updates the index | Atomic on the database side; delivery is at-least-once, so the consumer must be idempotent |
| Change data capture (CDC) | Read the database's transaction log (for example with Debezium) and stream the changes | No application changes; captures every change including deletes |
Both outbox and CDC rely on a durable log between the database and the index; page 07 of the messaging section, Event-Driven Patterns, covers the outbox pattern, idempotent consumers and the dual-write problem in detail. The Elasticsearch side has one nice property: **indexing a document with a fixed _id is idempotent**. Replaying the same event just overwrites the same document. Use the database primary key as _id, and use version_type=external with a version taken from the source row when events can arrive out of order.
- Design the pipeline so the index can be rebuilt from scratch: create
products-v2, bulk-load from the database (or replay the Kafka topic), run both in parallel, then flip the alias. Page 07 of this section, Polyglot Persistence, uses exactly this pattern for projections.
- Leading wildcard (
*boots,*oo*): the term dictionary is sorted by the first characters, so a leading wildcard cannot seek and must test every term. Index withedge_ngramor thewildcardfield type if you need it. - **
scriptqueries and scripted sorting** on large result sets: the script runs per document. - **Deep
from + size** and very largesizevalues (chapter 09). - Aggregations on high-cardinality fields over huge result sets: heavy on memory.
Both are search engines with a shared ancestry. Facts that are verifiable from the projects' own sites:
- OpenSearch describes itself as a fully open source search and analytics suite; its FAQ states that OpenSearch is derived from Elasticsearch 7.10.2 and OpenSearch Dashboards from Kibana 7.10.2.
- Elastic changed Elasticsearch's licence from Apache 2.0 to a dual SSPL / Elastic License 2.0 in 2021 (which is the background of the fork). Elastic later announced it would add the AGPL as another licence option alongside those two, and described Elasticsearch as open source again (Elastic blog, "Elasticsearch Is Open Source. Again!").
- Since the fork the two have diverged: APIs, query features, clients and vector search implementations are not interchangeable. Pick one, use its own client library and docs, and do not assume a feature or a setting from this page exists in OpenSearch.
The licence terms matter if you host the software for others or modify it; read the current licence text rather than relying on a summary, including this one.
- Teams start by logging "just a few documents" and end with orders or user profiles that exist only in an index. Then a mapping change, an accidental
DELETE /index, or a cluster loss turns into a business incident. Keep the data in a database, treat the index as a cache with a rebuild procedure, and snapshot it anyway.
A Single-Node Lab with Docker, and the Java Client
This lab follows the official single-node Docker instructions for the Elastic Stack 9.x (the docs list image tag 9.5.5 as the latest on 2026-10-31). It is for development and learning only; the Elastic docs state it is not suitable for production. Commands below are from the docs and were not all re-run for this page; the output is described instead of pasted.
-m 1GBcaps container memory; the docs note this means you do not have to set the JVM heap by hand. Machine-learning features need more memory.- Security is on by default since Elasticsearch 8.0 (
xpack.security.enableddefaults totrue). On the first start the node auto-configures security: it generates TLS certificates for the transport and HTTP layers and prints a generated password for theelasticuser and an enrollment token for Kibana to the console. These are shown only on first start. - Copy the password. If you lose it, reset it with
docker exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic.
- Setting
xpack.security.enabled=falseremoves authentication and TLS; the docs call disabling security not recommended. It is tempting in a lab but becomes a habit, and an open Elasticsearch on a network is a classic data leak. Keep the generated password and use the CA certificate as below.
The response is a small JSON document with the cluster name and the version number. --cacert makes curl trust the self-signed CA that the node generated, so TLS is verified instead of disabled with -k.
Use the mapping from chapter 04 saved as mapping.json (the body of the PUT, without the PUT /products-v1 line) and a two-document bulk file. The Bulk API takes newline-delimited JSON: an action line, then the document line, each ending with a newline.
A single node with the default number_of_replicas: 1 reports yellow cluster health: the replica shard has no other node to live on, so it is unassigned. That is expected in a one-node lab. Set the replicas to 0 with PUT /products-v1/_settings and {"index": {"number_of_replicas": 0}} to see green, and read this as a demonstration of why replicas exist, not as a setting to copy to production.
The official Elasticsearch Java API Client is published as co.elastic.clients:elasticsearch-java on Maven Central (its latest release at the time of writing is 9.5.5; the docs example uses 9.5.0 and requires Java 17 or later). Match the client's major version to the server.
The builder-lambda style (b -> b.host(...)) and the nested query builders mirror the JSON DSL of chapter 05. The API shape for connecting (host, usernameAndPassword, sslContext, TransportUtils.sslContextFromCaFingerprint), index, search with match and range(r -> r.number(...)) follows the Elastic Java client documentation; compile it against the client version you choose, because generated builders can vary slightly between releases. In production prefer an API key (.apiKey(...)) over the elastic superuser.
If your application is built on Spring, Spring Data Elasticsearch offers the familiar repository and template abstractions (ElasticsearchOperations, repository interfaces, entity annotations) on top of the Elasticsearch client. It is released on the Spring Data train (the Spring docs list 6.1.x as the current stable line at the time of writing). Each Spring Data Elasticsearch line supports particular Elasticsearch versions, so check the version compatibility matrix in its reference documentation before choosing a Spring Boot and server pair. See also Spring Data and persistence for the wider Spring picture.
Questions You Should Be Able to Answer
Crisp answers for the questions that come up in interviews on this topic.
- Q: What problem does Elasticsearch solve that a relational database does not?
Fast, relevance-ranked full-text search and analytics over large volumes. A databaseLIKEscan is slow and unranked; Elasticsearch uses an inverted index so it looks up terms instead of scanning rows, and it scores results with BM25. - Q: What is an inverted index?
A mapping from each term to the list of documents containing it (a postings list), along with frequencies and positions. A query looks up its terms and combines the postings lists. - Q: What does an analyzer consist of?
Zero or more character filters, exactly one tokenizer and zero or more token filters, run in that order. The default fortextfields is thestandardanalyzer (standard tokenizer plus lowercase). The same analysis is applied to the query text formatchqueries. - Q:
textvskeyword?textis analysed for full-text search and cannot be sorted or aggregated by default.keywordstores the exact value for filtering, sorting and aggregations. A multi-field indexes one value both ways. - Q: Why does a
termquery on a text field often return nothing?
Becausetermis not analysed and a text field stores lower-cased, tokenised terms. Usematchfor text, or query akeywordsub-field. - Q: Query context vs filter context?
Query context computes a relevance score; filter context is yes/no, computes no score and can be cached. Put relevance clauses inmust/shouldand structured conditions infilter. - Q: What is the default scoring algorithm and what does it consider?
BM25. It combines term frequency (with saturation), inverse document frequency (rarer terms matter more) and field-length normalisation. - Q: What is a shard and how many should I have?
A shard is a Lucene index and the unit of distribution. Primary count is fixed at creation. Elastic guidance: shards of roughly 10-50 GB and below about 200 million documents; avoid many tiny shards; use rollover for time series. - Q: Why is Elasticsearch "near real time"?
New documents sit in an in-memory buffer and the translog until a refresh writes them as a searchable segment. The default refresh interval is 1s. A GET by id is real time. - Q: What is the translog for?
It records operations not yet in a Lucene commit point so they can be replayed after a crash. With the defaultrequestdurability it is fsynced before the write is acknowledged. - Q: Why are updates expensive?
Segments are immutable. An update marks the old document deleted and indexes a whole new one; space is reclaimed on merge. Updating a tiny field of a large document reindexes all of it. - Q: How do you page beyond 10,000 results?
from + sizeis capped byindex.max_result_window(10000). Usesearch_afterwith a point in time and a unique tie-breaker in the sort. Scroll is no longer recommended for this. - Q: Why should Elasticsearch not be your primary database?
No multi-document transactions, near-real-time visibility, delete-plus-insert updates, fixed field types requiring reindexing for changes, and it is designed as a rebuildable derived view. Keep the source of truth in a database and sync to it. - Q: How do you keep Elasticsearch in sync with a database?
Avoid dual writes. Use a transactional outbox or CDC (such as Debezium) through a log like Kafka, with an idempotent consumer that indexes with the database key as the_id. Support a full rebuild by reindexing into a new index and switching an alias. - Q: Your mapping has a wrong field type. What now?
You cannot change an existing field type in place. Create a new index with the right mapping,_reindexinto it, and move the alias. - Q: What is a mapping explosion?
Too many distinct fields, usually from using data values as field names under dynamic mapping. It bloats cluster state and memory. Use explicit or strict mappings, theflattenedtype, and the field limit as a safety net. - Q: Elasticsearch vs OpenSearch?
OpenSearch is a fork derived from Elasticsearch 7.10.2 after Elastic moved from Apache 2.0 to SSPL/ELv2. Elastic later added AGPL as an option. They have diverged in APIs and features, so do not assume compatibility.
- "Add search to our product catalogue." PostgreSQL stays the system of record; an outbox or CDC stream feeds an
productsalias on Elasticsearch; text inmust, filters infilter; facets via aggregations; a reindex runbook; the page degrades to database queries (or a clear message) if search is down. - "Store 1 TB of logs per day with 30-day retention." Data stream with ILM rollover at about 50 GB primary shard size, hot nodes for ingest, delete phase at 30 days, Bulk API with batching, and few fields mapped explicitly.
- "Search results are slow after we added a tenant filter." Check query shape (filter context), shard count, and whether routing by tenant id can cut the shards searched.
Cheat Sheet and Recap
| Concept | Remember | Default / value (Elasticsearch 9.x docs) |
|---|---|---|
| Core structure | Inverted index: term to postings list | Plus doc values for sort and aggregations |
| Default analyzer | Standard tokenizer plus lowercase | Applied to text fields and match query text |
text vs keyword | Search vs filter/sort/aggregate | Use a multi-field for both |
| Filter vs must | Filter: no score, cacheable | Structured conditions go in filter |
| Scoring | BM25: TF (saturating), IDF, field length | Default similarity |
| Replicas | Copies on other nodes | number_of_replicas = 1 |
| Refresh | Makes new docs searchable | refresh_interval = 1s (idle-shard optimisation applies) |
| Translog durability | Fsync before ack | request; flush threshold 10 GB |
| Shard size guidance | 10-50 GB, under about 200M docs | Guidance, not a limit |
from + size limit | Use search_after + PIT beyond it | index.max_result_window = 10000 |
| Updates | Delete + insert, space freed by merge | Whole document is reindexed |
| Security on first start (8.x and later) | Auto-configured TLS and elastic password | xpack.security.enabled = true |
| Java client | co.elastic.clients:elasticsearch-java | Java 17+; docs example 9.5.0 |
- It is an index, not a database. The source of truth lives elsewhere and the index can be rebuilt.
- Analysis decides what you can find. Test it with
_analyze; query text must be analysed like the indexed text. - Model with explicit mappings:
textfor search,keywordfor exact, multi-fields for both, no dynamic surprises. - **
mustfor relevance,filterfor the rest.** - Writes are near real time: buffer, refresh to a segment, translog for safety, merges in the background.
- Keep shards sized and few; use rollover and ILM for time series.
- Sync with outbox or CDC and idempotent writes; deep paging with
search_afterand PIT.
Checked against the Elastic documentation (elastic.co/docs, Elasticsearch 9.x, current release listed as 9.5.5) on 2026-10-31: BM25 as default similarity, refresh_interval behaviour, translog defaults, max_result_window of 10000 and the PIT guidance, shard sizing guidance, number_of_replicas default, Docker 9.5.5 image and first-start security, dense vector quantization defaults, the Java client coordinates and API shape; and against the OpenSearch FAQ and the Elastic AGPL announcement for the fork and licence statements. Not run here: the Docker lab and the Java sample were written from the docs and not executed or compiled. Search results shown as responses are illustrative.
- 04 MongoDB - the document database you would keep as a system of record next to a search index.
- 06 Caching Patterns and 02 Redis - caching in front of search and database.
- 07 Polyglot Persistence - running a database, a cache, a search index and a log together.
- Event-Driven Patterns - outbox, CDC and idempotent consumers.