Ho usato Elasticsearch in produzione per log analytics, ricerca di prodotti e pipeline di recupero documenti. Gli stessi errori emergono ogni volta che un team lo tratta come un database relazionale. Questo articolo copre ciò che bisogna davvero sapere: mapping, semantica delle query, scoring di rilevanza e aggregazioni come funzionano realmente.
Concetti fondamentali: Index, Document, Shard, Replica#
Un index è un namespace logico. Un document è un oggetto JSON memorizzato in un indice. Elasticsearch distribuisce i documenti su shard (partizioni orizzontali), e ogni shard può avere una o più replica per la fault tolerance.
Non usare Elasticsearch come datastore primario. Non supporta le transazioni, e il suo modello di consistenza near-real-time (i documenti diventano ricercabili dopo un intervallo di refresh di circa 1 secondo per default) significa che si può scrivere un documento e non vederlo subito. Conserva i dati autorevoli in PostgreSQL o un altro store ACID, e sincronizza su Elasticsearch per la ricerca.
Una topologia di cluster tipica per la produzione: 3 nodi master dedicati, N data node dimensionati in base al numero di shard e al volume di dati. Evita di co-locare i master con i data node in produzione.
Mapping degli indici: Esplicito vs Dinamico#
Elasticsearch può inferire i tipi dei campi dal primo documento indicizzato (dynamic mapping), ma in produzione si dovrebbero sempre definire mapping espliciti.
La decisione più importante sul tipo di campo è text vs keyword:
- I campi
textvengono analizzati: la stringa viene tokenizzata, convertita in minuscolo e stemmatizzata. Usarli per la ricerca full-text. - I campi
keywordvengono memorizzati verbatim. Usarli per filtri exact-match, ordinamento e aggregazioni.
La pipeline dell’analyzer per un campo text è: character filter (rimozione HTML, normalizzazione unicode) -> tokenizer (split su spazi, punteggiatura) -> token filter (lowercase, stop words, stemming).
curl -X PUT "localhost:9200/products" \
-H "Content-Type: application/json" \
-u "elastic:changeme" \
-d '{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"english_custom": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "english_stop", "english_stemmer"]
}
},
"filter": {
"english_stop": { "type": "stop", "stopwords": "_english_" },
"english_stemmer": { "type": "stemmer", "language": "english" }
}
}
},
"mappings": {
"properties": {
"name": { "type": "text", "analyzer": "english_custom" },
"sku": { "type": "keyword" },
"price": { "type": "double" },
"category": { "type": "keyword" },
"description": { "type": "text", "analyzer": "english_custom" },
"created_at": { "type": "date", "format": "strict_date_optional_time" },
"in_stock": { "type": "boolean" }
}
}
}'Una volta creato un indice, non è possibile cambiare il tipo di un campo esistente. Bisogna re-indicizzare in un nuovo indice. Pianifica i mapping attentamente prima di andare in produzione, o usa gli alias di indice per nascondere l’operazione di re-indicizzazione ai client.
Indicizzazione dei documenti: PUT vs POST e Bulk per le prestazioni#
Usa PUT /<index>/_doc/<id> quando controlli l’ID del documento (upsert idempotente). Usa POST /<index>/_doc quando vuoi che Elasticsearch generi l’ID.
Per l’ingest massivo, usa sempre l’API _bulk. Inviare documenti singoli via HTTP per dataset di grandi dimensioni è un collo di bottiglia significativo.
# Ogni riga di azione deve essere seguita dalla riga del documento.
# La riga vuota alla fine è obbligatoria.
curl -X POST "localhost:9200/_bulk" \
-H "Content-Type: application/x-ndjson" \
-u "elastic:changeme" \
--data-binary '
{"index": {"_index": "products", "_id": "1"}}
{"name": "Widget Pro", "sku": "WGT-001", "price": 29.99, "category": "widgets", "in_stock": true}
{"index": {"_index": "products", "_id": "2"}}
{"name": "Gadget Max", "sku": "GDG-002", "price": 49.99, "category": "gadgets", "in_stock": false}
'Per pipeline ad alto throughput, imposta refresh_interval a 30s o -1 durante i bulk load, poi riportalo a 1s. Questo evita che i merge dei segmenti Lucene competano con l’ingest.
Match query, bool query, e filter vs must#
La query match esegue la ricerca full-text: analizza l’input e assegna un punteggio ai documenti per rilevanza. La query bool compone più clausole:
must: il documento deve corrispondere, contribuisce al punteggiofilter: il documento deve corrispondere, NON contribuisce al punteggio (viene memorizzata in cache)should: aumenta il punteggio se corrisponde, opzionale a meno che non ci sia unmustmust_not: il documento non deve corrispondere, nessun punteggio
curl -X GET "localhost:9200/products/_search" \
-H "Content-Type: application/json" \
-u "elastic:changeme" \
-d '{
"query": {
"bool": {
"must": [
{ "match": { "name": "widget" } }
],
"filter": [
{ "term": { "category": "widgets" } },
{ "term": { "in_stock": true } },
{ "range": { "price": { "lte": 50.0 } } }
]
}
}
}'Usa filter per criteri strutturati (categoria, range di prezzo, range di date, flag booleani). Le clausole filter sono memorizzate in cache a livello di segmento e riutilizzate tra le query, rendendo le ricerche filtrate ripetute molto più veloci. Usa must solo per la parte di testo libero dove il punteggio di rilevanza ha importanza.
Scoring di rilevanza: BM25, boost e explain#
Elasticsearch usa BM25 (Best Match 25) come funzione di similarità predefinita. BM25 assegna un punteggio a un documento basandosi su:
- Term frequency: quante volte il termine appare nel campo (con rendimenti decrescenti)
- Inverse document frequency: quanto è raro il termine nell’indice
- Normalizzazione per lunghezza del campo: campi più corti con lo stesso termine ottengono un punteggio più alto
Si può influenzare lo scoring con boost:
{
"query": {
"bool": {
"should": [
{ "match": { "name": { "query": "widget", "boost": 3.0 } } },
{ "match": { "description": { "query": "widget", "boost": 1.0 } } }
]
}
}
}Per capire perché un documento ha ottenuto un certo punteggio, aggiungi "explain": true alla query. La risposta include un albero dei contributi al punteggio per ogni documento.
curl -X GET "localhost:9200/products/_search" \
-H "Content-Type: application/json" \
-u "elastic:changeme" \
-d '{
"explain": true,
"query": { "match": { "name": "widget" } }
}'Aggregazioni: analytics a tempo di query#
Le aggregazioni vengono calcolate a tempo di query sul result set. Fanno parte del corpo della richiesta di ricerca, non della creazione dell’indice. Un errore comune è pensare di poter integrare i risultati delle aggregazioni nella struttura dell’indice al momento della scrittura. Non è possibile.
# terms + date_histogram aggregazione annidata
curl -X GET "localhost:9200/products/_search" \
-H "Content-Type: application/json" \
-u "elastic:changeme" \
-d '{
"size": 0,
"query": { "term": { "in_stock": true } },
"aggs": {
"by_category": {
"terms": { "field": "category", "size": 10 },
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
},
"sales_over_time": {
"date_histogram": {
"field": "created_at",
"calendar_interval": "month"
}
}
}
}'Impostare "size": 0 dice a Elasticsearch di non restituire i documenti, solo i risultati delle aggregazioni. Questa è un’ottimizzazione significativa per le dashboard che hanno bisogno solo di conteggi aggregati.
Index template per pattern multi-indice#
Quando si hanno indici time-series (logs-2025-01, logs-2025-02, ecc.), si usano i template di indice per applicare automaticamente mapping e impostazioni coerenti.
curl -X PUT "localhost:9200/_index_template/logs_template" \
-H "Content-Type: application/json" \
-u "elastic:changeme" \
-d '{
"index_patterns": ["logs-*"],
"priority": 100,
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"level": { "type": "keyword" },
"service": { "type": "keyword" },
"message": { "type": "text" }
}
}
}
}'Combinare con ILM (Index Lifecycle Management) per ruotare gli indici al raggiungimento di una dimensione o un’eta e cancellare automaticamente i dati vecchi.
Elasticsearch vs OpenSearch#
AWS ha fatto un fork di Elasticsearch 7.10.2 nel 2021 per creare OpenSearch, dopo che Elastic ha cambiato la licenza in SSPL. Le API sono altamente compatibili a livello di query, ma divergono in:
- Si hanno bisogno delle ultime funzionalità Lucene (ricerca vettoriale con HNSW, linguaggio di query ES|QL)
- Si sta eseguendo on-premises o su un cloud che offre Elastic gestito
- Si fa affidamento su Kibana o sulle integrazioni Elastic APM
- Il team ha esperienza esistente con Elastic
- Si sta eseguendo su AWS e si vuole un servizio gestito senza problemi di licenza
- Si ha bisogno di uno stack completamente open-source (Apache 2.0)
- Si usa AWS Cognito / IAM per il controllo degli accessi (integrazione nativa con OpenSearch Service)
- Il costo è una priorita: OpenSearch Serverless scala a zero
Per i nuovi progetti su AWS, OpenSearch è il default pragmatico. Il costo operativo della gestione autonoma dei cluster Elasticsearch raramente vale la pena.
Errori comuni#
Usare match dove term è corretto
match analizza la stringa di query. Se si esegue match su un campo keyword con un valore come "widgets", si possono ottenere risultati inattesi perché la pipeline di analisi trasforma la query. Per il matching di valori esatti su campi keyword, boolean o numeric, usare sempre term o terms all’interno di una clausola filter.Esplosione del dynamic mapping
dynamic: "strict" per rifiutare i campi sconosciuti.Non usare filter per criteri non-scoring
must invece di filter costringe Elasticsearch a calcolare i punteggi BM25 per ogni documento corrispondente, per poi scartarli. Le clausole filter non vengono punteggiare e vengono memorizzate in cache. La differenza di prestazioni è di 2-10x per le query tipiche delle dashboard.Memorizzare dati binari di grandi dimensioni nei documenti
Confondere le aggregazioni con le strutture dati dell'indice
"aggs" nel corpo di un mapping PUT. Non è valido. Le aggregazioni sono operazioni a tempo di query inviate con GET /<index>/_search. Non è possibile pre-aggregare al momento della scrittura tramite le API di mapping. Se si ha bisogno di dati pre-aggregati per le prestazioni, usare i transform job o gli indici roll-up.Se vuoi approfondire questi argomenti, offro sessioni di coaching 1:1 per ingegneri che lavorano su integrazione AI, architettura cloud e platform engineering. Prenota una sessione (50 EUR / 60 min) o scrivimi a manuel.fedele+website@gmail.com.