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

FeatureBenefit
Inverted indexFast full-text search across millions of documents
Distributed by designShards and replicas scale horizontally
Schema-flexibleDynamic mapping for evolving data
Near real-timeDocuments searchable about one second after indexing
AggregationsAnalytics without a separate OLAP system

Core Concepts

TermRelational equivalentDescription
IndexTableCollection of documents
DocumentRowJSON object with fields
FieldColumnA named value in a document
MappingSchemaField types and analyzers
ShardPartitionHorizontal slice of an index
ReplicaRead replicaCopy 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

QueryPurpose
matchAnalyzed full-text search
termExact value on a keyword field
rangeNumeric or date range
boolCombine with must, should, filter, must_not
wildcardPattern match (use sparingly)
multi_matchSearch 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" }
      }
    }
  }'
TypeUse for
textFull-text search; analyzed and tokenized
keywordExact match, sorting, aggregations
integer, long, doubleNumeric values
dateTimestamps and dates
booleanTrue/false
nestedArrays 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 filter over must when 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

MistakeResultFix
Aggregating on a text fieldError or unexpected bucketsUse the .keyword subfield
Over-sharding small indicesCluster overheadAim for 10–50 GB per shard
Using Elasticsearch as the primary databaseDurability and transaction limitsKeep the source of truth in a relational DB; sync to ES
Deep pagination with fromSlow queries, memory spikesUse search_after