Overview
Elasticsearch is a distributed search and analytics engine built on Apache Lucene. It powers full-text search, log analysis, and real-time dashboards. This tutorial covers installation, the document model, and the query DSL you will use most.
What Makes Elasticsearch Different
| Feature | Benefit |
|---|---|
| Inverted index | Fast full-text search across millions of documents |
| Distributed by design | Shards and replicas scale horizontally |
| Schema-flexible | Dynamic mapping for evolving data |
| Near real-time | Documents searchable about one second after indexing |
| Aggregations | Analytics without a separate OLAP system |
Core Concepts
| Term | Relational equivalent | Description |
|---|---|---|
| Index | Table | Collection of documents |
| Document | Row | JSON object with fields |
| Field | Column | A named value in a document |
| Mapping | Schema | Field types and analyzers |
| Shard | Partition | Horizontal slice of an index |
| Replica | Read replica | Copy of a shard for availability |
Running Elasticsearch Locally
docker network create elastic
docker run -d --name elasticsearch \
--net elastic \
-p 9200:9200 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
docker.elastic.co/elasticsearch/elasticsearch:8.15.0
Verify it is up:
curl http://localhost:9200
For a native install, see the Elasticsearch download page.
Indexing Documents
# Index with an explicit ID
curl -X PUT "localhost:9200/products/_doc/1" \
-H "Content-Type: application/json" \
-d '{
"name": "Wireless Headphones",
"brand": "Acme",
"price": 129.99,
"tags": ["audio", "bluetooth"],
"in_stock": true
}'
# Let Elasticsearch generate the ID
curl -X POST "localhost:9200/products/_doc" \
-H "Content-Type: application/json" \
-d '{"name":"USB-C Cable","price":12.99}'
Bulk Indexing
curl -X POST "localhost:9200/_bulk" \
-H "Content-Type: application/x-ndjson" \
--data-binary @bulk.json
Each action line is followed by a document line:
{"index":{"_index":"products","_id":"2"}}
{"name":"Bluetooth Speaker","price":59.99}
{"index":{"_index":"products","_id":"3"}}
{"name":"Noise-Cancelling Headphones","price":249.00}
Bulk requests are dramatically faster than individual indexing calls. Batch sizes between 1,000 and 5,000 documents are typical.
Basic Search
# Match all
curl "localhost:9200/products/_search?pretty"
# Search a field
curl -X GET "localhost:9200/products/_search?pretty" \
-H "Content-Type: application/json" \
-d '{
"query": {
"match": { "name": "headphones" }
}
}'
Query DSL Essentials
| Query | Purpose |
|---|---|
match | Analyzed full-text search |
term | Exact value on a keyword field |
range | Numeric or date range |
bool | Combine with must, should, filter, must_not |
wildcard | Pattern match (use sparingly) |
multi_match | Search the same term across several fields |
Boolean Query Example
{
"query": {
"bool": {
"must": [
{ "match": { "name": "headphones" } }
],
"filter": [
{ "range": { "price": { "lte": 200 } } },
{ "term": { "in_stock": true } }
],
"must_not": [
{ "term": { "brand": "Discontinued" } }
]
}
}
}
Use filter for conditions that do not affect scoring. Filters are cached and faster than must clauses.
Sorting and Pagination
{
"query": { "match_all": {} },
"sort": [
{ "price": "asc" }
],
"from": 0,
"size": 20
}
For deep pagination, from and size become expensive beyond a few thousand results. Use search_after with a sort cursor instead.
Aggregations
{
"size": 0,
"aggs": {
"avg_price": { "avg": { "field": "price" } },
"by_brand": {
"terms": { "field": "brand.keyword", "size": 10 },
"aggs": {
"avg_per_brand": { "avg": { "field": "price" } }
}
},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 50 },
{ "from": 50, "to": 200 },
{ "from": 200 }
]
}
}
}
}
Setting "size": 0 skips document hits and returns only aggregation results.
Mapping and Analyzers
curl -X PUT "localhost:9200/articles" \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"properties": {
"title": { "type": "text" },
"slug": { "type": "keyword" },
"body": { "type": "text", "analyzer": "english" },
"views": { "type": "integer" },
"published":{ "type": "date" }
}
}
}'
| Type | Use for |
|---|---|
text | Full-text search; analyzed and tokenized |
keyword | Exact match, sorting, aggregations |
integer, long, double | Numeric values |
date | Timestamps and dates |
boolean | True/false |
nested | Arrays of objects that must be queried independently |
Use text for fields people search and keyword for fields used in filters, sorting, and aggregations. A field can be both via multi-fields:
"brand": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
}
Performance Tips
- Use bulk indexing for initial loads.
- Disable refresh during bulk loads with
"refresh": false, then refresh once at the end. - Prefer
filterovermustwhen scoring does not matter. - Avoid leading-wildcard queries; they scan the entire index.
- Keep shard size between 10 and 50 GB for balanced performance.
- Use index templates and ILM policies to roll over time-series indices automatically.
Common Mistakes
| Mistake | Result | Fix |
|---|---|---|
Aggregating on a text field | Error or unexpected buckets | Use the .keyword subfield |
| Over-sharding small indices | Cluster overhead | Aim for 10–50 GB per shard |
| Using Elasticsearch as the primary database | Durability and transaction limits | Keep the source of truth in a relational DB; sync to ES |
Deep pagination with from | Slow queries, memory spikes | Use search_after |
