Padroneggiare Elasticsearch Query DSL: Comandi Essenziali per il Recupero dei Dati
Sblocca la potenza del recupero dati di Elasticsearch padroneggiando il Query DSL. Questa guida analizza le strutture JSON essenziali delle query, concentrandosi sull'uso pratico delle query `match`, `term` e range. Impara la differenza critica tra le clausole `must` (con punteggio) e `filter` (con caching) all'interno della query `bool` fondamentale, permettendoti di costruire ricerche dati complesse e ad alte prestazioni in modo efficiente.
Padroneggiare Elasticsearch Query DSL: Comandi Essenziali per il Recupero dei Dati
Elasticsearch Query DSL è il linguaggio JSON che usi quando una semplice casella di ricerca non è sufficiente. Ti permette di combinare ricerca full-text, filtri esatti, intervalli di date, ordinamento, paginazione e aggregazioni in un'unica richiesta. Questa flessibilità è utile, ma rende anche facile scrivere una query che restituisce i documenti sbagliati o che funziona bene in fase di test e rallenta in produzione.
Il modo migliore per imparare Query DSL è tenere a mente due domande: "Sto cercando testo per pertinenza?" e "Sto filtrando valori esatti?" La maggior parte delle scelte di query deriva da questa suddivisione.
L'Anatomia di una Richiesta di Ricerca Elasticsearch
Tutte le ricerche Elasticsearch vengono eseguite sull'endpoint _search di un indice specifico (o di più indici). Una richiesta di ricerca di base è una richiesta POST contenente un corpo JSON che definisce i parametri della query. La parte più critica di questo corpo è l'oggetto query.
Struttura di Base:
POST /nome_del_tuo_indice/_search
{
"query": { ... Definisci qui la struttura della tua query ... },
"size": 10,
"from": 0
}
Tipi di Query Fondamentali: Precisione e Pertinenza
Il Query DSL offre un'ampia gamma di query su misura per diversi tipi di dati ed esigenze di corrispondenza. La scelta della query ha un impatto significativo sia sul punteggio di pertinenza che sulle prestazioni.
1. Ricerca Full-Text: La Query match
La query match è lo standard per la ricerca full-text su campi analizzati. Tokenizza il termine di ricerca e verifica la presenza di token corrispondenti nel/i campo/i specificato/i.
Caso d'Uso: Ricerca di testo in linguaggio naturale dove il punteggio di pertinenza è importante.
Esempio: Trovare documenti in cui il campo 'description' contiene la parola 'cloud' o 'computing'.
GET /products/_search
{
"query": {
"match": {
"description": "cloud computing"
}
}
}
2. Corrispondenza di Valori Esatti: La Query term
La query term cerca documenti contenenti il termine esatto specificato. A differenza di match, non esegue analisi sulla stringa di ricerca, rendendola ideale per corrispondenze esatte su parole chiave, ID o campi indicizzati numericamente.
Caso d'Uso: Filtrare per valori esatti in campi non analizzati (come campi keyword o numeri).
Esempio: Recuperare un prodotto con l'ID esatto SKU10021.
GET /products/_search
{
"query": {
"term": {
"product_id": "SKU10021"
}
}
}
3. Query di Intervallo (Range)
Le query di intervallo permettono di filtrare documenti in cui il valore di un campo rientra in un intervallo specificato (numerico, data o stringa).
Sintassi: Usa gt (maggiore di), gte (maggiore o uguale a), lt (minore di) e lte (minore o uguale a).
Esempio: Trovare ordini effettuati dopo il 1° gennaio 2024.
GET /orders/_search
{
"query": {
"range": {
"order_date": {
"gte": "2024-01-01",
"lt": "2025-01-01"
}
}
}
}
4. Filtrare per Presenza: La Query exists
La query exists identifica documenti in cui un campo specifico è presente (cioè non nullo o mancante).
Esempio: Trovare tutti gli utenti che hanno fornito un indirizzo email.
GET /users/_search
{
"query": {
"exists": {
"field": "email_address"
}
}
}
Costruire Logiche Complesse con la Query bool
Per praticamente tutte le applicazioni di ricerca reali, è necessario combinare più criteri. La query bool è lo strumento essenziale per questo, permettendoti di combinare altre clausole di query usando la logica booleana.
Clausole all'interno di bool
La query bool accetta quattro clausole principali:
must: Tutte le clausole in questo array devono corrispondere. Le clausole inmustcontribuiscono al punteggio di pertinenza.filter: Tutte le clausole in questo array devono corrispondere, ma vengono eseguite in un contesto senza punteggio. Questo le rende molto più veloci per criteri di inclusione/esclusione rigorosi.should: Almeno una clausola in questo array dovrebbe corrispondere. Queste clausole influenzano il punteggio di pertinenza ma sono opzionali per la corrispondenza.must_not: Nessuna delle clausole in questo array deve corrispondere (l'equivalente di un NOT logico).
Esempio Pratico di Query bool
Combiniamo diversi concetti per trovare documenti ad alta priorità che menzionano 'security' ma escludono bozze e sono disponibili nella regione 'US'.
GET /logs/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "security breach"
}
}
],
"filter": [
{
"term": {
"region.keyword": "US"
}
}
],
"should": [
{
"term": {
"priority": 5
}
}
],
"must_not": [
{
"term": {
"status.keyword": "DRAFT"
}
}
]
}
}
}
Spiegazione dell'Esempio:
- Must: Il documento deve contenere la frase "security breach" nel campo content analizzato.
- Filter: Il documento deve essere etichettato per la regione 'US' (una corrispondenza esatta e veloce).
- Should: I documenti che corrispondono a
priority: 5riceveranno un aumento nel loro punteggio di pertinenza, ma i documenti con priorità inferiori che soddisfano le clausolemustefilterverranno comunque restituiti. - Must Not: I documenti contrassegnati come 'DRAFT' sono rigorosamente esclusi.
Migliori Pratiche per la Costruzione delle Query
Per garantire che le tue ricerche siano sia accurate che performanti, segui queste linee guida:
- Preferisci
filteramustper criteri senza punteggio. Se stai solo verificando l'inclusione/esclusione (ad esempio, filtrando per ID, data esatta o stato), usa sempre la clausolafilterall'interno di una querybool. Questo sfrutta la memorizzazione nella cache ed evita costosi calcoli del punteggio. - Usa le Query Esatte con Saggezza: Per i campi mappati come
text(analizzati), usamatch. Per i campi mappati comekeyword(non analizzati), usatermo query di intervallo. - Evita Annidamenti Profondi: Sebbene possibile, le query
boolprofondamente annidate possono diventare difficili da leggere e debuggare, e talvolta possono portare a un degrado delle prestazioni. - Sfrutta
minimum_should_match: Per le clausoleshould, impostareminimum_should_match(ad esempio, a1o2) forza il soddisfacimento di un certo numero di quei criteri opzionali, trasformandoli di fatto in criteri obbligatori pur permettendo loro di contribuire al punteggio.
Il Mapping Decide Quale Query Ha Senso
La maggior parte degli errori con Query DSL inizia dal mapping. Una query può sembrare corretta e restituire comunque risultati confusi se il campo è mappato diversamente da come pensi.
Uno schema comune è un campo di testo con un sottocampo keyword:
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"status": { "type": "keyword" },
"created_at": { "type": "date" },
"price": { "type": "double" }
}
}
}
Usa match su title quando vuoi un comportamento full-text analizzato. Usa term su title.keyword quando hai bisogno del valore esatto del titolo. Usa term su status perché è già una keyword. Usa range su created_at o price perché quei campi sono valori di data e numerici.
Se una query term su un campo di testo non funziona come previsto, il problema è spesso l'analisi. I token memorizzati potrebbero essere in minuscolo, suddivisi, derivati o altrimenti modificati. Controlla il mapping prima di modificare la query.
GET /products/_mapping
Per problemi di analisi del testo, _analyze è utile:
GET /products/_analyze
{
"field": "description",
"text": "Cloud Computing"
}
Questo mostra quali token Elasticsearch cercherà.
match, match_phrase e multi_match
match è la query full-text di tutti i giorni, ma non è l'unica che userai.
Usa match_phrase quando l'ordine delle parole è importante:
GET /products/_search
{
"query": {
"match_phrase": {
"description": "wireless charging stand"
}
}
}
Questo è utile per nomi di prodotti, messaggi di log, titoli di documenti e frasi in cui la sequenza esatta ha un significato. È più restrittiva di match, quindi potrebbe restituire meno documenti.
Usa multi_match quando lo stesso input utente dovrebbe cercare in diversi campi:
GET /products/_search
{
"query": {
"multi_match": {
"query": "noise cancelling headphones",
"fields": ["title^3", "description", "brand^2"]
}
}
}
I boost ^3 e ^2 dicono a Elasticsearch che le corrispondenze in title e brand dovrebbero contare di più di quelle in description. Il boosting non è una garanzia che un documento si classifichi per primo; è un suggerimento per il punteggio. Testa con query reali prima di ottimizzare i boost in modo troppo aggressivo.
Paginazione Senza Danneggiare il Cluster
I parametri di base from e size vanno bene per la paginazione superficiale:
GET /products/_search
{
"from": 20,
"size": 10,
"query": {
"match": {
"description": "laptop sleeve"
}
}
}
La paginazione profonda è diversa. Richiedere la pagina 1.000 costringe Elasticsearch a ordinare e saltare molti risultati. Per la ricerca front-end, evita la paginazione profonda illimitata. Per esportazioni o scansioni in background, usa search_after con un ordinamento stabile:
GET /products/_search
{
"size": 100,
"sort": [
{ "created_at": "asc" },
{ "_id": "asc" }
],
"search_after": ["2025-01-10T12:00:00Z", "abc123"],
"query": {
"term": {
"status": "active"
}
}
}
I valori in search_after provengono dall'array sort dell'ultimo risultato nella risposta precedente. Questo approccio è più stabile per scorrere grandi set di risultati.
Il Filtraggio dell'Origine Mantiene le Risposte Utili
Le prestazioni di ricerca non riguardano solo l'esecuzione della query. Restituire documenti enormi può rallentare il client, la rete e il nodo coordinatore. Se l'interfaccia utente ha bisogno solo di pochi campi, richiedi solo quei campi:
GET /orders/_search
{
"_source": ["order_id", "customer_id", "total", "created_at", "status"],
"query": {
"bool": {
"filter": [
{ "term": { "status": "paid" } },
{ "range": { "created_at": { "gte": "now-7d/d" } } }
]
}
}
}
Questo rende la risposta più facile da leggere e può ridurre la dimensione del payload. Non sostituisce un buon design dell'indice, ma aiuta quando i documenti contengono descrizioni lunghe, blob di metadati o array annidati di cui la pagina corrente non ha bisogno.
Ordinamento e Aggregazioni Necessitano dei Campi Giusti
Ordinare su testo analizzato è di solito un errore. Ordina su campi keyword, numerici o di data:
GET /products/_search
{
"sort": [
{ "price": "asc" },
{ "title.keyword": "asc" }
],
"query": {
"term": {
"status": "active"
}
}
}
Lo stesso vale per molte aggregazioni. Se vuoi conteggi per stato, aggrega su un campo keyword:
GET /orders/_search
{
"size": 0,
"aggs": {
"orders_by_status": {
"terms": {
"field": "status"
}
}
},
"query": {
"range": {
"created_at": {
"gte": "now-30d/d"
}
}
}
}
size: 0 dice a Elasticsearch che vuoi solo i risultati dell'aggregazione, non i documenti corrispondenti. È una piccola abitudine che mantiene le risposte più pulite.
Debug delle Query con explain e profile
Quando un risultato si classifica in modo strano, usa explain su un singolo documento:
GET /products/_explain/SKU10021
{
"query": {
"match": {
"description": "cloud computing"
}
}
}
Quando una query è lenta, usa profile in un ambiente non di produzione o in un test di produzione attentamente controllato:
GET /products/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "description": "cloud computing" } }
],
"filter": [
{ "term": { "status": "active" } }
]
}
}
}
L'output del profilo è verboso, ma può mostrare se il tempo viene speso in una query di testo, un filtro, uno script o un'altra parte della richiesta. Non lasciare la profilazione abilitata nel codice dell'applicazione; usala come strumento di debug.
Un'abitudine Sensata per Costruire Query
Per la maggior parte delle ricerche applicative, costruisci la richiesta in questo ordine:
- Metti i vincoli esatti in
filter: ID tenant, stato, regione, finestra temporale, permessi. - Metti il testo inserito dall'utente in
mustconmatch,match_phraseomulti_match. - Usa
shouldper preferenze di ranking, non requisiti rigidi, a meno che tu non impostiminimum_should_match. - Limita
_sourceai campi di cui il chiamante ha bisogno. - Aggiungi un ordinamento stabile se la paginazione o le esportazioni sono importanti.
- Controlla il mapping prima di incolpare Elasticsearch.
Il Query DSL è potente perché separa filtraggio, punteggio, ordinamento e modellazione della risposta. Una volta che mantieni questi compiti separati, le query diventano più facili da leggere, più facili da ottimizzare e meno sorprendenti in produzione.
Un Piccolo Esempio di Risoluzione dei Problemi
Supponiamo che un utente cerchi ACME-1000 e non ottenga risultati, anche se il prodotto esiste. Non aggiungere immediatamente wildcard. Prima controlla il mapping. Se sku è una keyword, questo dovrebbe funzionare:
GET /products/_search
{
"query": {
"term": {
"sku": "ACME-1000"
}
}
}
Se sku è stato accidentalmente mappato come text, l'analisi potrebbe aver suddiviso o modificato il valore. Puoi comunque interrogarlo in alcuni casi, ma la soluzione migliore è di solito una modifica del mapping per gli indici futuri. Identificatori esatti, stati, regioni e ID tenant dovrebbero essere campi di tipo keyword. Descrizioni e titoli scritti da umani dovrebbero essere campi di testo. Il Query DSL diventa molto più facile quando il mapping corrisponde al modo in cui le persone recuperano effettivamente i dati.