Analisi di dati con Aggregation Framework di MongoDB

di Moreno Gentili, in ASP.NET Core,

Se usiamo MongoDB per conservare una grossa mole di informazioni, è probabile che prima o poi vorremo inviare delle query a scopo di analisi. Grazie all'Aggregation Framework di MongoDB possiamo configurare una pipeline per trasformare ed aggregare documenti. Quelli rappresentati nell'immagine seguente sono solo alcuni dei tipi di operazione configurabili nella pipeline.

Importare i dati di esempio


Per iniziare ad usare Aggregation Framework, procuriamoci innanzitutto un dataset di esempio, come quello che troviamo nel sito mongodb.org.
https://docs.mongodb.com/getting-started/csharp/import-data/

Dopo aver seguito le istruzioni per importare tale dataset nella nostra istanza di MongoDB, usiamo Robo 3T ([url]https://robomongo.org/[/url]) per verificare che la nuova collezione restaurants sia presente nel database test.


La collezione contiene i documenti di circa 25.000 ristoranti della città di New York. In ciascun documento si trovano i recapiti, il distretto di appartenenza, la posizione geografica e i giudizi di gradimento del ristorante. A differenza di quanto troveremmo in un database relazionale, tutte queste informazioni sono "vicine tra loro", pronte per essere visualizzate in una eventuale pagina web o per essere impiegate in attività di business intelligence.

Ora che i dati sono pronti, creiamo una nuova applicazione .NET Core ed installiamo il pacchetto MongoDB.Driver con il seguente comando:

dotnet add package MongoDB.Driver

Creare il modello


Prima di interrogare i dati, è preferibile creare le classi che ci permetteranno di interrogare il dataset in maniera fortemente tipizzata, usando lambda expression.

Data la complessità di alcuni documenti JSON, creare manualmente le classi può essere un lavoro dispendioso e propenso ad errori. Non dimentichiamo, però, che grazie a Visual Studio possiamo generare automaticamente le classi che rappresentano l'intero grafo.
Quindi creiamo un nuovo file Restaurant.cs nel nostro progetto, poi copiamo il contenuto JSON di un ristorante qualsiasi e clicchiamo il menu Modifica - Incolla speciale - Incolla JSON come classi.


La classe principale verrà chiamata Rootobject ma siamo ovviamente liberi di rinominarla in Restaurant. Di solito sono necessari anche altri piccoli aggiustamenti, come aggiungere una proprietà per l'_id generato da MongoDb, di tipo ObjectId.

Match per filtrare


Supponiamo che, per la nostra analisi, ci interessino soltanto i ristoranti che si trovano nel distretto più grande e in quello più piccolo di New York, ovvero Brooklyn e Staten Island.
Usiamo il metodo Match per filtrare l'intero dataset ma non prima di aver iniziato a costruire la nostra pipeline di aggregazione grazie al metodo Aggregate.

//Prima colleghiamoci alla collection restaurants del database test
var client = new MongoClient();
var database = client.GetDatabase("test");
var collection = database.GetCollection<Restaurant>("restaurants");

//Quindi avviamo l'aggregazione ed applichiamo il filtro
var restaurants = collection
  .Aggregate()
  .Match(r => r.borough == "Brooklyn" || r.borough == "Staten Island");

Ovviamente potremmo già stampare a video i risultati ma il nostro obiettivo, in questo caso, è di continuare la pipeline di aggregazione inserendo ulteriori stadi che ci portino ad ottenere dei dati sintetici.

Per configurare la nostra pipeline, il MongoDB Driver offre un'interfaccia fluente che ci permette di definire i vari stadi semplicemente continuando ad invocare i metodi l'uno dopo l'altro, analogamente a quanto già facciamo con LINQ, nell'interrogare collezioni di oggetti.

Group per raggruppare


Ora raggruppiamo i ristoranti per il loro distretto di appartenenza usando il metodo Group. Come argomenti, forniamo due lambda expression:

  • la prima indichierà la chiave di aggregazione, ovvero la proprietà borough;
  • la seconda indicherà la proiezione, ovvero descriverà la forma dell'oggetto risultante dall'aggregazione. In esso inseriremo il nome del distretto e il conteggio dei ristoranti esistenti nel distretto.

var groups = restaurants.Group(r => 
  r.borough,
  group => new {
    borough = group.Key,
    count = group.Count()    
});

Da questo raggruppamento otterremo due risultati, uno per ogni distretto.

{ borough: "Staten Island", count: 969 }
{ borough: "Brooklyn", count: 6086 }

SortBy per ordinare


Grazie al metodo Sort andiamo ad ordinare i risultati per nome del distretto:

var sortedGroups = groups.SortBy(s => s.borough);

I risultati appariranno riordinati in questo modo:

{ borough: "Brooklyn", count: 6086 }
{ borough: "Staten Island", count: 969 }

Se avessimo voluto ordinare in maniera decrescente, avremmo usato SortByDescending. Inoltre, ThenBy e ThenByDescending ci permettono di ordinare per altre proprietà oltre la prima.

Lookup per il join con altre collezioni


Ipotizziamo di voler conoscere quanto sia esteso il bacino di utenza di ogni ristorante e come questo cambi in base al distretto cittadino. Per farlo, ci occorrono quindi dei dati demografici sulla città di New York che possiamo reperire dal sito data.gov ([url]https://catalog.data.gov/dataset/new-york-city-population-by-boroughs-fd2c0[/url]). Trasformiamo il dataset e salviamolo in un file demographics.json così che sia facilmente importabile in MongoDB.

{ borough: "Bronx", population: 1385108 }
{ borough: "Brooklyn", population: 2504700 }
{ borough: "Manhattan", population: 1585873 }
{ borough: "Queens", population: 2230722 }
{ borough: "Staten Island", population: 468730 }

Ora importiamo il file nella collection demographics del database test.

mongoimport --db test --collection demographics --drop --file c:\percorso\demographics.json

Ora che abbiamo un nuovo tipo di documento, facciamo creare a Visual Studio la classe Demographic, proprio come già fatto in precedenza per Restaurant.
Non resta che usare il metodo Lookup per eseguire la join tra i due insiemi di dati.

var demographics = database.GetCollection<Demographic>("demographics");
var joined = sortedGroups.Lookup(
  demographics,
  g => g.borough,
  d => d.borough,
  (LookedUpRestaurantGroup g) => g.demographics);

Nel metodo Lookup abbiamo indicato un nuovo tipo: la classe LookedUpRestaurantGroup che va anch'essa creata nel nostro progetto e che conterrà il risultato della join.

public class LookedUpRestaurantGroup
{
  public string _id {get; set;}
  public int count {get; set;}
  public IEnumerable<Demographic> demographics { get; set;}
}

Finalmente eseguiamo un ciclo per stampare il risultato della nostra aggregazione.

var results = await joined.ToListAsync();
foreach (var result in results) {
  Console.WriteLine($"{result._id}: {result.demographics.First().population/result.count} abitanti/ristorante");
}

Ed ecco l'output finale.

Brooklyn: 411 abitanti/ristorante
Staten Island: 483 abitanti/ristorante

Il Lookup è una funzionalità gradita, che è stata introdotta solo dalla versione 3.2 di MongoDb. Grazie ad essa è possibile creare query di aggregazione complesse, che possono avvalersi di vari dataset per estrarre conoscenza dalla mole di dati eterogenea a nostra disposizione.

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi