Elasticsearch: join e query bonus

Elasticsearch è un database NoSQL molto valido per effettuare ricerche efficienti su dati testuali e strutturati. Nonostante ciò, non supporta nativamente il join tra documenti. Esistono però delle query che, mediante una definizione dello schema appropriata, permettono di effettuare ricerche su documenti correlati tra di loro. Scopriremo come scrivere le query di join e anche alcune query particolari che potrebbero essere utili nei nostri progetti.

Share

Reading time: 7 minutes

Negli articoli Elasticsearch: uso delle match queryElasticsearch: uso delle term queryElasticsearch: query compound abbiamo visto come interrogare sia i campi testuali che i dati strutturati imponendo la verifica di più condizioni contemporaneamente su un indice di Elasticsearch. Inoltre, nelle compound query abbiamo visto come modificare il calcolo dello score per adattarlo ai dati di ciascun documento. Queste query, però, non rispondono alla necessità di ricercare documenti che sono collegati tra di loro mediante relazioni di parentela. Si consideri che Elasticsearch è un database NoSQL e quindi le operazioni di join tra documenti non sono native e, seppur disponibili, sono limitate ed onerose.

In questo articolo vedremo come definire una gerarchia tra i documenti per utilizzarla nelle ricerche. Inoltre, vi forniremo alcune query “bonus” che potrebbero tornare utili per alcuni progetti.

Come nei tutorial precedenti, useremo lo stesso ambiente. Pertanto vi invitiamo a leggere attentamente le istruzioni per installare lo stack di Elasticsearch sul vostro PC mediante il repository Docker e come importare correttamente i dati.

Di seguito riportiamo i principali esempi di query trattati in questa guida, per un rapido riferimento:

Categoria Tipo Criterio di match Query Match No Match
has_child join Interroga i documenti figli e restituisce i documenti genitori corrispondenti (dei figli corrispondenti). N/A N/A N/A
has_parent join Esegue una query sui documenti padre e restituisce i documenti padre corrispondenti (dei genitori corrispondenti). N/A N/A N/A
query_string full-text Query polivalente che "può fare da collettore" all'uso di altre query come "match", "multi-match", "regexp", "wildcard" ecc. Ha una formattazione rigorosa (position:engineer) OR (salary:(>=10000 AND <=52000)) i documenti con il testo "engineer" nel campo "position" OPPURE i documenti che hanno un intervallo di retribuzione compreso tra 10.000 e 52.000 (inclusi 10.000 e 52.000) N/A
simple_query_string full-text Come query_string, ma non rigoroso (position:engineer) | (country:china) Documenti con "engineer" nel campo "position" OPPURE china nel campo "country". N/A

Parent-Child Query

Le relazioni uno a molti possono essere gestite utilizzando il metodo genitore-figlio (ora chiamato operazione di join) in Elasticsearch. Dimostriamo questo con un esempio di scenario. Consideriamo di avere un forum, in cui chiunque può pubblicare qualsiasi argomento (diciamo i post). Gli utenti possono commentare i singoli post. Quindi, in questo scenario, possiamo considerare i singoli post come documenti padre e i commenti ad essi come figli. Questo è spiegato meglio nella figura seguente:

Per questa operazione, verrà creato un indice separato, con una mappatura speciale (schema) applicata.

Creare l’indice con il tipo di dati join con la richiesta seguente

PUT post-comments
{
  "mappings": {
    "properties": {
      "document_type": { 
        "type": "join",
        "relations": {
          "post": "comment" 
        }
      }
    }
  }
} 

Nello schema sopra riportato, si può notare la presenza di un tipo denominato “join”, che indica che questo indice avrà documenti correlati a genitori e figli. Inoltre, nell’oggetto “relations” sono definiti i nomi degli identificatori di genitore e figlio.

Cioè post:comment si riferisce alla relazione genitore:figlio. Ogni documento sarà composto da un campo chiamato “tipo_documento”, che avrà il valore “post” o “comment”. Il valore “post” indicherà che il documento è un genitore e il valore “comment” indicherà che il documento è un “figlio”.

Indicizziamo alcuni documenti:

PUT post-comments/_doc/1
{
"document_type": {
    "name": "post" 
  },
"post_title" : "Angel Has Fallen"
} 
PUT post-comments/_doc/2
{
"document_type": {
    "name": "post" 
  },
"post_title" : "Beauty and the beast - a nice movie"
} 

Indicizziamo ora dei documenti figli

PUT post-comments/_doc/A?routing=1
{
"document_type": {
    "name": "comment",
    "parent": "1"
  },
"comment_author": "Neil Soans",
"comment_description": "'Angel has Fallen' has some redeeming qualities, but they're too few and far in between to justify its existence"
} 
PUT post-comments/_doc/B?routing=1
{
"document_type": {
    "name": "comment",
    "parent": "1"
  },
"comment_author": "Exiled Universe",
"comment_description": "Best in the trilogy! This movie wasn't better than the Rambo movie but it was very very close."
} 
PUT post-comments/_doc/D?routing=1
{
"document_type": {
    "name": "comment",
    "parent": "2"
  },
"comment_author": "Emma Cochrane",
"comment_description": "There's the sublime beauty of a forgotten world and the promise of happily-ever-after to draw you to one of your favourite fairy tales, once again. Give it an encore."
} 
PUT post-comments/_doc/E?routing=1
{
"document_type": {
    "name": "comment",
    "parent": "2"
  },
"comment_author": "Common Sense Media Editors",
"comment_description": "Stellar music, brisk storytelling, delightful animation, and compelling characters make this both a great animated feature for kids and a great movie for anyone"
} 

Query has_child

Questa query interroga i documenti figlio e restituisce come risultati i genitori ad essi associati. Supponiamo di dover cercare il termine “musica” nel campo “commenti_descrizione” nei documenti figli e di dover ottenere i documenti genitori corrispondenti ai risultati della ricerca, possiamo usare la query has_child come segue:

GET post-comments/_search
{
  "query": {
    "has_child": {
      "type": "comment",
      "query": {
        "match": {
          "comment_description": "music"
        }
      }
    }
  }
} 

Per la query di cui sopra, i documenti figli che corrispondono alla ricerca sono solo il documento con id=E, per il quale il genitore è il documento con id=2. Il risultato della ricerca ci porterà al documento padre come di seguito indicato:

{
  "took": 46,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "post-comments",
        "_id": "2",
        "_score": 1,
        "_source": {
          "document_type": {
            "name": "post"
          },
          "post_title": "Beauty and the beast - a nice movie"
        }
      }
    ]
  }
} 

Query has_parent

La query has_parent esegue l’opposto della query has_child, cioè restituisce i documenti figli dei documenti genitori che corrispondono alla query.

Cerchiamo la parola “Beauty” nel documento padre e restituiamo i documenti figli dei genitori corrispondenti. A tale scopo, possiamo utilizzare la seguente query

GET post-comments/_search
{
  "query": {
    "has_parent": {
      "parent_type": "post",
      "query": {
        "match": {
          "post_title": "Beauty"
        }
      }
    }
  }
} 

Il documento genitore corrispondente alla query di cui sopra è quello con id documento =1. Come si può vedere dalla risposta sottostante, i documenti figli corrispondenti al documento id=1 vengono restituiti dalla query precedente:

{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "post-comments",
        "_id": "D",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "document_type": {
            "name": "comment",
            "parent": "2"
          },
          "comment_author": "Emma Cochrane",
          "comment_description": "There's the sublime beauty of a forgotten world and the promise of happily-ever-after to draw you to one of your favourite fairy tales, once again. Give it an encore."
        }
      },
      {
        "_index": "post-comments",
        "_id": "E",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "document_type": {
            "name": "comment",
            "parent": "2"
          },
          "comment_author": "Common Sense Media Editors",
          "comment_description": "Stellar music, brisk storytelling, delightful animation, and compelling characters make this both a great animated feature for kids and a great movie for anyone"
        }
      }
    ]
  }
} 

Restituzione dei documenti child con i parent

A volte, nei risultati della ricerca è necessario visualizzare sia il documento padre che quello figlio. Ad esempio, se stiamo elencando i post, sarebbe bello visualizzare anche alcuni commenti al di sotto di essi.

Per ottenere questo risultato, utilizziamo la query has_child per restituire i genitori mediante il parametro “inner_hits” restituiamo anche i child.

GET post-comments/_search
{
  "query": {
    "has_child": {
      "type": "comment",
      "query": {
        "match": {
          "comment_description": "music"
        }
      },
      "inner_hits": {}
    }
    
  }
} 

Il risultato della query sarà il seguente:

{
  "took": 43,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "post-comments",
        "_id": "2",
        "_score": 1,
        "_source": {
          "document_type": {
            "name": "post"
          },
          "post_title": "Beauty and the beast - a nice movie"
        },
        "inner_hits": {
          "comment": {
            "hits": {
              "total": {
                "value": 1,
                "relation": "eq"
              },
              "max_score": 1.1829326,
              "hits": [
                {
                  "_index": "post-comments",
                  "_id": "E",
                  "_score": 1.1829326,
                  "_routing": "1",
                  "_source": {
                    "document_type": {
                      "name": "comment",
                      "parent": "2"
                    },
                    "comment_author": "Common Sense Media Editors",
                    "comment_description": "Stellar music, brisk storytelling, delightful animation, and compelling characters make this both a great animated feature for kids and a great movie for anyone"
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
} 

Query bonus

Di seguito riportiamo alcune query che potrebbero tornare utile in alcuni contesti.

query_string

La query “query_string” è una query speciale multiuso, che può raggruppare l’uso di diverse altre query come “match”, “multi-match”, “wildcard”, regexp” ecc. La query “query_string” segue un formato rigoroso e la sua violazione produce messaggi di errore. Per questo motivo, nonostante le sue capacità, è raramente utilizzata per l’implementazione di caselle di ricerca rivolte all’utente.

Vediamo un esempio di query in azione:

POST employees/_search
{
  "query": {
    "query_string": {
      "query": "(roots heuristic systems) OR (enigneer~) OR (salary:(>=10000 AND <=52000)) ",
      "fields": [
        "position",
        "phrase^3"
      ]
    }
  }
} 

La query di cui sopra cercherà le parole “radici” O “euristica” O “sistemi” O “ingegnere” (l’uso di ~ nella query indica l’uso di una query fuzzy) nei campi “posizione” e “frase” e restituirà i risultati. “phrase^3” indica che le corrispondenze trovate nel campo “phrase” devono essere incrementate di un fattore 3. Salario:(>10000 AND <=52000), indica di recuperare i documenti che hanno il valore del campo “salario” compreso tra 10000 e 52000.

La query simple_query_string

La query “simple_query_string” è una forma semplificata della query_string con due differenze principali

È più tollerante agli errori, il che significa che non restituisce errori se la sintassi è sbagliata. Anzi, ignora la parte difettosa della query. Questo lo rende più facile da usare per le caselle di ricerca dell’interfaccia utente.

Gli operatori AND/OR/NOT ecc. sono sostituiti da +/|/-.

Un semplice esempio potrebbe essere:

POST employees/_search
{
  "query": {
    "simple_query_string": {
      "query": "(roots) | (resources manager) + (male) ",
      "fields": [
        "gender",
        "position",
        "phrase^3"
      ]
    }
  }
} 

La query di cui sopra cercherebbe “roots” OR “resources” OR “manager” AND “male” in tutti i campi indicati nell’array “fields”.

Query nominative

Le query nominative, come suggerisce il nome, riguardano la denominazione delle query. In alcuni casi è utile per identificare quali parti della query corrispondono al documento. Elasticsearch ci fornisce proprio questa funzione, consentendoci di assegnare un nome alla query o a parti della query in modo da vedere questi nomi con i documenti corrispondenti.

Ad esempio sottomettiamo la seguente query

POST employees/_search
{
  "query": {
    "match": {
      "phrase": {
        "query": "roots" ,
        "_name": "phrase_field_name" }
    }
  }
} 

Il risultato ottenuto sarà il seguente

{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.6785375,
    "hits": [
      {
        "_index": "employees",
        "_id": "2",
        "_score": 0.6785375,
        "_source": {
          "id": 2,
          "name": "Othilia Cathel",
          "email": "[email protected]",
          "gender": "female",
          "ip_address": "3.164.153.228",
          "date_of_birth": "22/07/1987",
          "company": "Edgepulse",
          "position": "Structural Engineer",
          "experience": 11,
          "country": "China",
          "phrase": "Grass-roots heuristic help-desk",
          "salary": 193530
        },
        "matched_queries": [
          "phrase_field_name"
        ]
      },
      {
        "_index": "employees",
        "_id": "4",
        "_score": 0.62577873,
        "_source": {
          "id": 4,
          "name": "Alan Thomas",
          "email": "[email protected]",
          "gender": "male",
          "ip_address": "200.47.210.95",
          "date_of_birth": "11/12/1985",
          "company": "Yamaha",
          "position": "Resources Manager",
          "experience": 12,
          "country": "China",
          "phrase": "Emulation of roots heuristic coherent systems",
          "salary": 300000
        },
        "matched_queries": [
          "phrase_field_name"
        ]
      }
    ]
  }
} 

Nell’esempio precedente, la query match è fornita con un parametro “_name”, che ha il nome della query come “phrase_field_name”. Nei risultati, abbiamo i documenti che sono stati abbinati ai risultati con un campo array chiamato “matched_queries”, che contiene i nomi delle query abbinate (qui “phrase_field_name”).

L’esempio seguente mostra l’uso delle query nominative in una bool query, che è uno dei casi più comuni di utilizzo delle query nominative.

POST employees/_search
{
  "query": {
    "bool":{
        "should": [
            {"match": {
                "phrase": {
                    "query": "roots",
                    "_name": "phrase_field_name"}
                }
            },
            {"match": {
                "gender": {
                    "query": "female" ,
                    "_name": "gender_field_name"}
                }
            }
        ]
    }
  }
} 

Il risultato sarà il seguente:

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.8825103,
    "hits": [
      {
        "_index": "employees",
        "_id": "2",
        "_score": 1.8825103,
        "_source": {
          "id": 2,
          "name": "Othilia Cathel",
          "email": "[email protected]",
          "gender": "female",
          "ip_address": "3.164.153.228",
          "date_of_birth": "22/07/1987",
          "company": "Edgepulse",
          "position": "Structural Engineer",
          "experience": 11,
          "country": "China",
          "phrase": "Grass-roots heuristic help-desk",
          "salary": 193530
        },
        "matched_queries": [
          "phrase_field_name",
          "gender_field_name"
        ]
      },
      {
        "_index": "employees",
        "_id": "4",
        "_score": 0.62577873,
        "_source": {
          "id": 4,
          "name": "Alan Thomas",
          "email": "[email protected]",
          "gender": "male",
          "ip_address": "200.47.210.95",
          "date_of_birth": "11/12/1985",
          "company": "Yamaha",
          "position": "Resources Manager",
          "experience": 12,
          "country": "China",
          "phrase": "Emulation of roots heuristic coherent systems",
          "salary": 300000
        },
        "matched_queries": [
          "phrase_field_name"
        ]
      }
    ]
  }
} 

More To Explore

Elasticsearch

Elasticsearch: aggregazioni a bucket [parte 1]

Con le aggregazioni a bucket di Elasticsearch possiamo creare gruppi di documenti. In questo articolo ci concentreremo principalmente sulle aggregazioni basate sui campi di tipo keyword presenti negli indici. Utilizzeremo diversi esempi per capire le principali differenze tra le funzioni di aggregazione disponibili.

Elasticsearch

Elasticsearch: aggregazioni metriche

Oltre alla ricerca testuale, Elasticsearch permette di effettuare analisi sui dati mediante le aggregazioni. Tra le varie tipologie di aggregazione disponibili quelle metriche sono orientate proprio a calcolare statistiche su uno o più campi. Mediante degli esempi vedremo quali informazioni possiamo estrarre con questa tipologia di aggregazione.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Design with MongoDB

Design with MongoDB!!!

Buy the new book that will help you to use MongoDB correctly for your applications. Available now on Amazon!