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:

  1. must: Tutte le clausole in questo array devono corrispondere. Le clausole in must contribuiscono al punteggio di pertinenza.
  2. 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.
  3. should: Almeno una clausola in questo array dovrebbe corrispondere. Queste clausole influenzano il punteggio di pertinenza ma sono opzionali per la corrispondenza.
  4. 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: 5 riceveranno un aumento nel loro punteggio di pertinenza, ma i documenti con priorità inferiori che soddisfano le clausole must e filter verranno 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 filter a must per criteri senza punteggio. Se stai solo verificando l'inclusione/esclusione (ad esempio, filtrando per ID, data esatta o stato), usa sempre la clausola filter all'interno di una query bool. 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), usa match. Per i campi mappati come keyword (non analizzati), usa term o query di intervallo.
  • Evita Annidamenti Profondi: Sebbene possibile, le query bool profondamente annidate possono diventare difficili da leggere e debuggare, e talvolta possono portare a un degrado delle prestazioni.
  • Sfrutta minimum_should_match: Per le clausole should, impostare minimum_should_match (ad esempio, a 1 o 2) 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:

  1. Metti i vincoli esatti in filter: ID tenant, stato, regione, finestra temporale, permessi.
  2. Metti il testo inserito dall'utente in must con match, match_phrase o multi_match.
  3. Usa should per preferenze di ranking, non requisiti rigidi, a meno che tu non imposti minimum_should_match.
  4. Limita _source ai campi di cui il chiamante ha bisogno.
  5. Aggiungi un ordinamento stabile se la paginazione o le esportazioni sono importanti.
  6. 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.