mirror of
https://github.com/Steffo99/unimore-bda-4.git
synced 2024-11-21 23:54:19 +00:00
1042 lines
39 KiB
Markdown
1042 lines
39 KiB
Markdown
|
[ Stefano Pigozzi | Use case "creato da zero" | Tema Neo4J | Big Data Analytics | A.A. 2022/2023 | Unimore ]
|
|||
|
|
|||
|
# Modellazione di un database Neo4J per le dipendenze delle crates del linguaggio Rust
|
|||
|
|
|||
|
> ### Neo4j
|
|||
|
>
|
|||
|
> Studiare e provare un esempio di applicazione reale di Neo4j, scegliendone uno:
|
|||
|
>
|
|||
|
> * tra le seguenti sandbox disponibili alla pagina https://neo4j.com/sandbox/:
|
|||
|
> * `WWC2019`
|
|||
|
> * `OpenStreetMaps`
|
|||
|
> * tra gli esempi illustrati nelle guide https://neo4j.com/developer/example-data/#guide-examples:
|
|||
|
> * `UKCompanies`
|
|||
|
> * `Recipes`
|
|||
|
> * tra quelli disponibili alla pagina https://neo4j.com/graphgists/
|
|||
|
>
|
|||
|
> Quindi, partendo dallo use case scelto, descrivere lo scenario, i dati ed eventualmente le tecniche non viste a lezione esposte nello use case.
|
|||
|
>
|
|||
|
> Crearne e descriverne poi una propria versione “estesa” lavorando a uno o più dei seguenti aspetti:
|
|||
|
>
|
|||
|
> * modificare/migliorare il grafo, ad es. sostituendo i dati o aggiungendone di nuovi (es. importandoli / adattandoli da altre sorgenti esterne), modificando/arricchendo lo schema (es. aggiungendo nuovi archi o tipi di archi, property, ecc. a supporto di particolari interrogazioni);
|
|||
|
> * creare nuove interrogazioni e/o operazioni di modifica di varia complessità nell’ambito dello scenario ispirandosi agli argomenti visti a lezione e agli esercizi proposti nello usecase (opzionalmente: lavorando con codice Python);
|
|||
|
> * studiare e creare i migliori indici per alcune delle operazioni previste;
|
|||
|
> * ...
|
|||
|
>
|
|||
|
> È anche possibile proporre un proprio use case creato da zero.
|
|||
|
|
|||
|
## Sinossi
|
|||
|
|
|||
|
Si è realizzato un database a grafo Neo4J che rappresenta l'indice pubblico [Crates.io] delle crates (librerie) del linguaggio di programmazione [Rust], le loro categorie, i loro tag, i loro autori, le loro versioni e le loro dipendenze.
|
|||
|
|
|||
|
## Introduzione
|
|||
|
|
|||
|
[Rust] è un linguaggio di programmazione compilato che negli ultimi anni sta acquisendo popolarità grazie alle funzionalità innovative di cui è dotato, che facilitano la scrittura di codice corretto e parallelizzabile.
|
|||
|
|
|||
|
I progetti in Rust sono organizzati in [crate], unità indivisibili di compilazione, in modo simile ai package di altri linguaggi di programmazione.
|
|||
|
|
|||
|
Quando pronte per l'utilizzo, le crates vengono generalmente caricate su [Crates.io], repository ufficiale per librerie e eseguibili del linguaggio, che le indicizza e ne permette il download.
|
|||
|
|
|||
|
Nel workflow standard, le crate sono gestite da [Cargo], strumento a linea di comando in grado di recuperare le crates necessarie alla compilazione del progetto corrente e di organizzarne i metadati, contenuti all'interno del file `Cargo.toml` della directory del progetto, di cui si fornisce il seguente esempio:
|
|||
|
|
|||
|
```toml
|
|||
|
[package]
|
|||
|
name = "distributed_arcade"
|
|||
|
version = "0.3.0"
|
|||
|
authors = ["Stefano Pigozzi <me@steffo.eu>"]
|
|||
|
edition = "2021"
|
|||
|
description = "Fast and simple scoreboard service for games"
|
|||
|
repository = "https://github.com/Steffo99/distributed-arcade"
|
|||
|
license = "AGPL-3.0-or-later"
|
|||
|
keywords = ["game", "scoreboard", "redis", "axum", "web-api"]
|
|||
|
categories = ["games"]
|
|||
|
|
|||
|
[dependencies]
|
|||
|
redis = { version = "0.22.1", features=["r2d2", "ahash", "cluster", "tokio-comp", "connection-manager"] }
|
|||
|
axum = { version = "0.5.17" }
|
|||
|
tokio = { version = "1.21.2", features=["full"] }
|
|||
|
r2d2 = { version = "0.8.10" }
|
|||
|
lazy_static = { version = "1.4.0" }
|
|||
|
serde = { version = "1.0.147", features=["derive"] }
|
|||
|
serde_json = { version = "1.0.87" }
|
|||
|
log = { version = "0.4.17" }
|
|||
|
pretty_env_logger = { version = "0.4.0" }
|
|||
|
rand = { version = "0.8.5" }
|
|||
|
regex = { version = "1.7.0" }
|
|||
|
async-trait = { version = "0.1.58" }
|
|||
|
tower-http = { version = "0.3.4", features=["cors"] }
|
|||
|
```
|
|||
|
|
|||
|
Come si può vedere dall'esempio, tra i metadati di una crate gestita da Cargo, possiamo trovare:
|
|||
|
|
|||
|
- `name`: il nome della crate
|
|||
|
- `version`: la [versione semantica] attuale della crate
|
|||
|
- `authors`: nome e email degli autori della crate
|
|||
|
- `description`: una breve descrizione in linguaggio naturale dei contenuti della crate
|
|||
|
- `repository`: un link al repository Git contenente il sorgente della crate
|
|||
|
- `license`: l'[identificatore SPDX] della licenza utilizzata
|
|||
|
- `keywords`: fino a 5 termini liberamente scelti dagli autori per rendere più facile trovare la crate nell'indice
|
|||
|
- `categories`: fino a 5 categorie dall'[elenco delle categorie previste]
|
|||
|
- `dependencies`: nomi, range di versioni e funzionalità delle crate richieste dalla corrente per la compilazione
|
|||
|
|
|||
|
Quando una nuova versione di una crate viene caricata su Crates.io, i metadati relativi alla crate come `description` e `repository` vengono sovrascritti con quelli più recenti, mentre i metadati specifici alla versione come `dependencies` vengono salvati assieme nella versione specifica.
|
|||
|
|
|||
|
Lo scopo di questo progetto è di indicizzare questi metadati in un database Neo4J per poi effettuarvi alcune analisi, sfruttando gli [open data] resi disponibili da Crates.io come sorgenti di dati.
|
|||
|
|
|||
|
## Struttura del progetto
|
|||
|
|
|||
|
Il progetto è organizzato nelle seguenti directory:
|
|||
|
|
|||
|
- `README.md`: questo stesso file
|
|||
|
- `data/cratesio/`: dove vanno inseriti gli open data pubblicati da Crates.io
|
|||
|
- `data/neo4j/`: home del DBMS Neo4J da utilizzare
|
|||
|
- `scripts/`: contiene script Bash per l'esecuzione rapida di alcune operazioni sul database Neo4J
|
|||
|
- `scripts/import-cratesio/`: contiene query Cypher che vengono eseguite per l'importazione dei file di Crates.io
|
|||
|
|
|||
|
## Prerequisiti
|
|||
|
|
|||
|
### Scelta del database
|
|||
|
|
|||
|
È possibile scegliere di utilizzare un DBMS Neo4J generico, oppure di utilizzarne uno gestito da Neo4J Desktop.
|
|||
|
|
|||
|
Questo progetto si aspetta che la `NEO4J_HOME` del DBMS scelto si trovi nella cartella `data/neo4j/`.
|
|||
|
|
|||
|
#### Neo4J Desktop
|
|||
|
|
|||
|
Utilizzare Neo4J Desktop permette fra le varie cose di utilizzare *Neo4J Bloom*, applicazione che permette di esplorare il database grafo interattivamente, ed è quindi l'opzione consigliata.
|
|||
|
|
|||
|
Per utilizzare un database Neo4J Desktop, è possibile seguire seguiti i seguenti passi:
|
|||
|
|
|||
|
1. Si inizializzi il DBMS su Neo4J Desktop.
|
|||
|
|
|||
|
2. Si determini dove la home del DBMS inizializzato è collocata selezionandolo, cliccando l'opzione Altro `···`, e selezionando *Open Folder* → *DBMS*.
|
|||
|
|
|||
|
3. Si crei un collegamento simbolico tra la home del DBMS e la posizione `data/neo4j/`:
|
|||
|
```console
|
|||
|
$ ./scripts/create-neo4j-desktop-link "/home/$USER/.config/Neo4j Desktop/Application/relate-data/dbmss/dbms-$neo4j_uuid/"
|
|||
|
```
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
repo=$(git rev-parse --show-toplevel)
|
|||
|
unlink "$repo/data/neo4j"
|
|||
|
ln -s "$1" "$repo/data/neo4j"
|
|||
|
```
|
|||
|
|
|||
|
#### Database generico
|
|||
|
|
|||
|
Per utilizzare un database generico, è possibile seguire i seguenti passi:
|
|||
|
|
|||
|
1. Si inizializzi il DBMS e lo si esegua:
|
|||
|
```console
|
|||
|
$ ./scripts/run-db.sh
|
|||
|
```
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
repo=$(git rev-parse --show-toplevel)
|
|||
|
export NEO4J_HOME="$repo/data/neo4j"
|
|||
|
neo4j console
|
|||
|
```
|
|||
|
|
|||
|
2. Mentre il DBMS è in esecuzione, se ne cambi la password predefinita per ottenere accesso a tutte le funzionalità:
|
|||
|
```console
|
|||
|
$ ./scripts/alter-default-password.sh
|
|||
|
```
|
|||
|
|
|||
|
### APOC
|
|||
|
|
|||
|
Gli open data di Crates.io formattano le date con stringhe in formato `"2023-02-10 07:50:14.995689"` che la funzione [`datetime`] non è in grado di leggere.
|
|||
|
|
|||
|
Pertanto, per processarle correttamente, si è scelto di includere la libreria [APOC] per Neo4J, che include la funzione [`apoc.date.parse`] con supporto a formati arbitrari per le date.
|
|||
|
|
|||
|
Essa può essere abilitata su Neo4J Desktop dalla scheda *Plugins* del database, oppure attraverso lo script `scripts/setup-apoc.sh`.
|
|||
|
|
|||
|
### Heap size incrementata
|
|||
|
|
|||
|
Alcune query utilizzate in questo progetto superano il massimo di memoria utilizzabile per l'heap da Neo4J, impostato di default a 2 GB.
|
|||
|
|
|||
|
Per aumentarlo a 6 GB, è necessario aprire il file `data/neo4j/conf/neo4j.conf` e modificare nel seguente modo le proprietà:
|
|||
|
|
|||
|
```properties
|
|||
|
server.memory.heap.initial_size=6g
|
|||
|
server.memory.heap.max_size=6g
|
|||
|
```
|
|||
|
|
|||
|
## Realizzazione
|
|||
|
|
|||
|
### Download dei dati
|
|||
|
|
|||
|
Si è scaricato il [dump giornaliero del database] di Crates.io del giorno 19 Febbraio 2023, e lo si è estratto nella directory `data/cratesio/2023-02-19-020031/`.
|
|||
|
|
|||
|
Esso contiene tutti i dati disponibili sotto forma di file comma-separated-values all'interno della directory `data/cratesio/2023-02-19-020031/data/`.
|
|||
|
|
|||
|
### Ripulitura dei dati
|
|||
|
|
|||
|
I file `csv` scaricati non sono immediatamente utilizzabili in Neo4J:
|
|||
|
|
|||
|
* non effettuano correttamente escaping dei backslash presenti all'interno delle stringhe
|
|||
|
* devono trovarsi nella directory `data/neo4j/import`
|
|||
|
|
|||
|
Entrambi i problemi vengono risolti dal seguente script, che corregge l'escaping e piazza la copia corretta in `data/neo4j/import`:
|
|||
|
|
|||
|
```console
|
|||
|
$ ./scripts/fixup-data-files.sh
|
|||
|
```
|
|||
|
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
|
|||
|
repo=$(git rev-parse --show-toplevel)
|
|||
|
cwd=$(pwd)
|
|||
|
data_files=$(ls $repo/data/cratesio/*/data/*.csv)
|
|||
|
|
|||
|
cd "$repo"
|
|||
|
|
|||
|
for file in $data_files; do
|
|||
|
echo "Fixing data file $file..."
|
|||
|
basefilename=$(basename $file)
|
|||
|
sed --expression='s=\\=\\\\=g' $file > "$repo/data/neo4j/import/$basefilename"
|
|||
|
done
|
|||
|
|
|||
|
cd "$cwd"
|
|||
|
```
|
|||
|
|
|||
|
### Importazione dei dati
|
|||
|
|
|||
|
I file `csv` ripuliti vengono poi importati nel database Neo4J attraverso una serie di istruzioni Cypher collocate in `./scripts/import-cratesio`, divise in gruppi per tipo di entità importate.
|
|||
|
|
|||
|
Possono essere eseguite in gruppo o una alla volta con lo script `scripts/import-cratesio.sh`:
|
|||
|
|
|||
|
* una alla volta:
|
|||
|
|
|||
|
```console
|
|||
|
$ ./scripts/import-cratesio.sh $NUMERO-
|
|||
|
```
|
|||
|
|
|||
|
* tutte insieme, in ordine:
|
|||
|
|
|||
|
```console
|
|||
|
$ ./scripts/import-cratesio.sh
|
|||
|
```
|
|||
|
|
|||
|
Lo script esegue le seguenti istruzioni:
|
|||
|
|
|||
|
```bash
|
|||
|
#!/usr/bin/env bash
|
|||
|
set -e
|
|||
|
|
|||
|
export NEO4J_USERNAME="neo4j"
|
|||
|
export NEO4J_PASSWORD="unimore-big-data-analytics-4"
|
|||
|
|
|||
|
repo=$(git rev-parse --show-toplevel)
|
|||
|
cwd=$(pwd)
|
|||
|
import_scripts=$(echo $repo/scripts/import-cratesio/$1*.cypher | sort)
|
|||
|
|
|||
|
cd "$repo"
|
|||
|
|
|||
|
for file in $import_scripts; do
|
|||
|
echo "Executing $file..."
|
|||
|
cypher-shell --fail-at-end --format verbose < $file
|
|||
|
done
|
|||
|
|
|||
|
cd "$cwd"
|
|||
|
```
|
|||
|
|
|||
|
#### `1` - Creazione dei nodi `:Crate`
|
|||
|
|
|||
|
Si creano nel grafo nodi aventi il label `:Crate`, importando i dati delle singole crate contenuti all'interno del file `crates.csv`.
|
|||
|
|
|||
|
##### Proprietà
|
|||
|
|
|||
|
I nodi `:Crate` avranno le seguenti proprietà:
|
|||
|
|
|||
|
- `id`: l'id univoco della crate su Crates.io
|
|||
|
- `name`: nome univoco con il quale la crate è stata uploadata su Crates.io
|
|||
|
- `created_at`: data del primo upload su Crates.io ("data di creazione")
|
|||
|
- `updated_at`: data dell'ultimo upload su Crates.io ("data di aggiornamento")
|
|||
|
- `max_upload_size`: dimensione della crate massima uploadabile
|
|||
|
- `downloads`: numero di download totale
|
|||
|
|
|||
|
Le seguenti proprietà saranno ugualmente definite, ma potrebbero acquisire un valore di `null` qualora non fossero specificate nei metadati delle crate:
|
|||
|
|
|||
|
- `description`: breve descrizione della crate
|
|||
|
- `readme`: lunga descrizione multilinea della crate
|
|||
|
- `documentation`: link alla documentazione della crate
|
|||
|
- `homepage`: link alla homepage della crate
|
|||
|
- `repository`: link al repository del codice sorgente della crate
|
|||
|
|
|||
|
##### Indici
|
|||
|
|
|||
|
Per determinare gli indici da creare, si immaginano alcune semplici interrogazioni che potrebbero essere effettuate sull'insieme dei nodi `:Crate`, e si creano indici in grado di risolverle:
|
|||
|
|
|||
|
- _ricerca **per id** o **in un intervallo**_: si crea un indice `RANGE` sul campo `id`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_crate_id IF NOT EXISTS
|
|||
|
FOR (crate:Crate)
|
|||
|
ON (crate.id);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca di crate **con un numero minimo di download**_: si crea un indice `RANGE` sul campo `downloads`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_crate_downloads IF NOT EXISTS
|
|||
|
FOR (crate:Crate)
|
|||
|
ON (crate.downloads);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca di crate **create** o **aggiornate** **prima** o **dopo una certa data**_: si creano due indici `RANGE`, uno sul campo `created_at`, l'altro sul campo `updated_at`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_crate_created_at IF NOT EXISTS
|
|||
|
FOR (crate:Crate)
|
|||
|
ON (crate.created_at);
|
|||
|
|
|||
|
CREATE RANGE INDEX index_crate_updated_at IF NOT EXISTS
|
|||
|
FOR (crate:Crate)
|
|||
|
ON (crate.updated_at);
|
|||
|
```
|
|||
|
- _ricerca di crate con un **nome**, **prefisso** o **suffisso specifico**_: si crea un indice `TEXT` sul campo `name`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_crate_name IF NOT EXISTS
|
|||
|
FOR (crate:Crate)
|
|||
|
ON (crate.name);
|
|||
|
```
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
Si scrive una query per l'importazione di dati da file CSV.
|
|||
|
|
|||
|
Si utilizza l'istruzione `LOAD CSV WITH HEADERS`, che legge il file CSV dato dalla cartella `$NEO4J_HOME/import` e trasforma ogni riga in un oggetto avente come chiavi i titoli delle colonne definite nella prima riga.
|
|||
|
|
|||
|
```cypher
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///crates.csv" AS line FIELDTERMINATOR ","
|
|||
|
```
|
|||
|
|
|||
|
Essendo il file molto lungo, Neo4J accumula tutti i nodi da creare in una singola transazione, richiedendo al sistema operativo di dedicargli sempre più memoria RAM.
|
|||
|
|
|||
|
Per evitare che la memoria si esaurisca e che Neo4J venga terminato, si utilizza l'istruzione `CALL {} IN TRANSACTIONS OF N ROWS`, che applica la transazione ogni N righe del file CSV processate.
|
|||
|
|
|||
|
```cypher
|
|||
|
CALL {
|
|||
|
...
|
|||
|
} IN TRANSACTIONS OF 10000 ROWS
|
|||
|
```
|
|||
|
|
|||
|
Infine, si inserisce come argomento di `CALL` la seguente query, che può funzionare sia per inizializzare il database, sia per aggiornarlo con gli ultimi dati:
|
|||
|
|
|||
|
1. Prova a vedere se esiste già un nodo `:Crate` con il dato `id`.
|
|||
|
2. Se non esiste, lo crea.
|
|||
|
3. Al nodo, esistente o non, vengono impostate le restanti proprietà, lette dal CSV e convertite nel tipo di Neo4J che le rappresenta più accuratamente.
|
|||
|
|
|||
|
```
|
|||
|
WITH line
|
|||
|
MERGE (crate:Crate { id: toInteger(line.id) })
|
|||
|
SET
|
|||
|
crate.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"),
|
|||
|
crate.updated_at = apoc.date.parse(line.updated_at, "ms", "yyyy-MM-dd HH:mm:ss"),
|
|||
|
crate.max_upload_size = toInteger(line.max_upload_size),
|
|||
|
crate.downloads = toInteger(line.downloads),
|
|||
|
crate.description = CASE trim(line.description)
|
|||
|
WHEN ""
|
|||
|
THEN null
|
|||
|
ELSE
|
|||
|
line.description
|
|||
|
END,
|
|||
|
crate.documentation = CASE trim(line.documentation)
|
|||
|
WHEN ""
|
|||
|
THEN null
|
|||
|
ELSE
|
|||
|
line.documentation
|
|||
|
END,
|
|||
|
crate.homepage = CASE trim(line.homepage)
|
|||
|
WHEN ""
|
|||
|
THEN null
|
|||
|
ELSE
|
|||
|
line.homepage
|
|||
|
END,
|
|||
|
crate.name = CASE trim(line.name)
|
|||
|
WHEN ""
|
|||
|
THEN null
|
|||
|
ELSE
|
|||
|
line.name
|
|||
|
END,
|
|||
|
crate.readme = CASE trim(line.readme)
|
|||
|
WHEN ""
|
|||
|
THEN null
|
|||
|
ELSE
|
|||
|
line.readme
|
|||
|
END,
|
|||
|
crate.repository = CASE trim(line.repository)
|
|||
|
WHEN ""
|
|||
|
THEN null
|
|||
|
ELSE
|
|||
|
line.repository
|
|||
|
END
|
|||
|
```
|
|||
|
|
|||
|
#### `2` - Creazione dei nodi `:Keyword`
|
|||
|
|
|||
|
Come menzionato in precedenza, ciascuna crate può specificare fino a 5 keyword al fine di essere trovata più facilmente nell'indice di Crates.io.
|
|||
|
|
|||
|
![Screenshot delle keyword su Crates.io](media/cratesio-keywords.png)
|
|||
|
|
|||
|
Si identificano queste keyword come nodi, a cui si assegna il label `:Keyword`.
|
|||
|
|
|||
|
Si ripete la stessa procedura di prima per creare un nodo `:Keyword` per ciascuna keyword esistente nel file `keywords.csv`.
|
|||
|
|
|||
|
##### Proprietà
|
|||
|
|
|||
|
I nodi `:Keyword` avranno come proprietà:
|
|||
|
|
|||
|
- `id`: l'id univoco della keyword su Crates.io
|
|||
|
- `name`: il nome della keyword
|
|||
|
- `creation_date`: la data in cui la keyword è comparsa per la prima volta su Crates.io
|
|||
|
|
|||
|
##### Indici
|
|||
|
|
|||
|
Si ripete la stessa procedura di prima per creare alcuni indici relativi ai nodi `:Keyword`.
|
|||
|
|
|||
|
Query e indici identificati sono:
|
|||
|
|
|||
|
- _ricerca **per id**_: si crea un indice `RANGE` sul campo `id`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_keyword_id IF NOT EXISTS
|
|||
|
FOR (keyword:Keyword)
|
|||
|
ON (keyword.id);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca **per nome**_: si crea un indice `TEXT` sul campo `name`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_keyword_name IF NOT EXISTS
|
|||
|
FOR (keyword:Keyword)
|
|||
|
ON (keyword.name);
|
|||
|
```
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
Come prima, si crea una query per inizializzazione o aggiornamento, che, per ogni riga del file `keywords.csv`, applica il seguente algoritmo:
|
|||
|
|
|||
|
1. Prova a vedere se esiste già un nodo `:Keyword` con il dato `id`.
|
|||
|
2. Se non esiste, lo crea.
|
|||
|
3. Al nodo, esistente o non, vengono impostate le restanti proprietà, lette dal CSV e convertite nel tipo di Neo4J che le rappresenta più accuratamente.
|
|||
|
|
|||
|
```cypher
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///keywords.csv" AS line FIELDTERMINATOR ","
|
|||
|
MERGE (keyword:Keyword { id: toInteger(line.id) })
|
|||
|
SET
|
|||
|
keyword.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"),
|
|||
|
keyword.name = line.keyword;
|
|||
|
```
|
|||
|
|
|||
|
#### `3` - Creazione delle relazioni `:IS_TAGGED_WITH`
|
|||
|
|
|||
|
Il file `crates_keywords.csv` associa crate alle loro keyword specificando un'associazione tra id di una crate e id di una keyword in ogni riga.
|
|||
|
|
|||
|
Si costruisce a partire da esso la relazione `:IS_TAGGED_WITH`, diretta dalla `:Crate` alla `:Keyword`.
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
Quando viene caricata su Crates.io una nuova versione di una crate, le sue keywords possono essere cambiate.
|
|||
|
|
|||
|
Per permettere alla query di funzionare sia per l'inizializzazione del grafo, sia per il suo aggiornamento, si effettuano due passi:
|
|||
|
|
|||
|
1. Tutte le relazioni `:IS_TAGGED_WITH` sono rimosse.
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (:Crate)-[relation:IS_TAGGED_WITH]->(:Keyword)
|
|||
|
DELETE relation;
|
|||
|
```
|
|||
|
|
|||
|
2. Il file `crates_keywords.csv` viene aperto, e per ciascuna riga viene creata una nuova relazione `:IS_TAGGED_WITH`.
|
|||
|
|
|||
|
```
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///crates_keywords.csv" AS line FIELDTERMINATOR ","
|
|||
|
MATCH
|
|||
|
(crate:Crate {id: toInteger(line.crate_id)}),
|
|||
|
(keyword:Keyword {id: toInteger(line.keyword_id)})
|
|||
|
CREATE (crate)-[:IS_TAGGED_WITH]->(keyword);
|
|||
|
```
|
|||
|
|
|||
|
#### `4` - Creazione dei nodi `:Category`
|
|||
|
|
|||
|
Oltre alle keyword, ciascuna crate può scegliere di appartenere a fino a cinque categorie di un thesaurus gerarchico definito dagli amministratori di Crates.io.
|
|||
|
|
|||
|
Il file `categories.csv` contiene tutte le categorie esistenti al momento dell'esportazione dei dati da Crates.io.
|
|||
|
|
|||
|
Si identificano le categorie come nodi aventi il label `:Category`, la cui gerarchia è rappresentata da relazioni `:CONTAINS`.
|
|||
|
|
|||
|
##### Struttura
|
|||
|
|
|||
|
I nodi `:Category` avranno le seguenti proprietà, importate dal file CSV:
|
|||
|
|
|||
|
- `id`: identificatore univoco della categoria;
|
|||
|
- `name`: nome breve in inglese della categoria;
|
|||
|
- `slug`: nome breve url-encoded della categoria, mostrato nell'indirizzo web;
|
|||
|
- `path`: percorso di identificatori separati da punti che determina dove si trova la categoria nella gerarchia;
|
|||
|
- `created_at`: data di creazione della categoria.
|
|||
|
|
|||
|
Per facilitare la navigazione del grafo, si aggiunge inoltre la seguente proprietà:
|
|||
|
|
|||
|
- `leaf`: l'ultimo identificatore del `path`.
|
|||
|
|
|||
|
##### Indici
|
|||
|
|
|||
|
Si ipotizzano le seguenti query e indici per i nodi `:Category`:
|
|||
|
|
|||
|
- _ricerca **per id**_: si crea un indice `RANGE` sul campo `id`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_category_id IF NOT EXISTS
|
|||
|
FOR (category:Category)
|
|||
|
ON (category.id);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca **per nome**_: si crea un indice `TEXT` sul campo `name`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_category_name IF NOT EXISTS
|
|||
|
FOR (category:Category)
|
|||
|
ON (category.name);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca **per slug**_: si crea un indice `TEXT` sul campo `slug`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_category_slug IF NOT EXISTS
|
|||
|
FOR (category:Category)
|
|||
|
ON (category.slug);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca **per leaf**_: si crea un indice `TEXT` sul campo `leaf`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_category_leaf IF NOT EXISTS
|
|||
|
FOR (category:Category)
|
|||
|
ON (category.leaf);
|
|||
|
```
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
Essendo le categorie variabili, proprio come le keywords, per permettere l'aggiornamento del database con nuovi dati, inizialmente si eliminano tutti i nodi `:Category`, per poi ricrearli successivamente.
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (category:Category)
|
|||
|
DETACH DELETE category;
|
|||
|
```
|
|||
|
|
|||
|
Ci si è accorti che all'interno del file CSV il nodo "root" (origine) della gerarchia è assente, quindi lo si crea prima di procedere con l'importazione vera e propria dei dati:
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE (
|
|||
|
:Category {
|
|||
|
name: "Root",
|
|||
|
created_at: datetime(),
|
|||
|
description: "Root category. Does not contain any category by itself.",
|
|||
|
id: 0,
|
|||
|
path: "root",
|
|||
|
slug: "root"
|
|||
|
}
|
|||
|
);
|
|||
|
```
|
|||
|
|
|||
|
Si usa in seguito `LOAD CSV WITH HEADERS` per creare nodi `:Category` dal file `categories.csv`, lasciando non specificato il campo `leaf`:
|
|||
|
|
|||
|
```cypher
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///categories.csv" AS line
|
|||
|
CREATE (
|
|||
|
:Category {
|
|||
|
name: line.category,
|
|||
|
created_at: apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"),
|
|||
|
description: line.description,
|
|||
|
id: toInteger(line.id),
|
|||
|
path: line.path,
|
|||
|
slug: line.slug
|
|||
|
}
|
|||
|
);
|
|||
|
```
|
|||
|
|
|||
|
Per ciascun nodo `:Category`, si prende poi il campo `path` e lo si divide in un array di stringhe alle occorrenze del carattere `"."`, utilizzando successivamente la sintassi `[-1]` per accedere all'ultimo elemento, che viene impostato come valore del campo `leaf`:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (c:Category)
|
|||
|
WITH c, split(c.path, ".") AS path
|
|||
|
SET c.leaf = path[-1];
|
|||
|
```
|
|||
|
|
|||
|
Ancora, si effettua una seconda scansione su tutti i nodi `:Category`, splittando di nuovo il `path`, ma stavolta prendendo il penultimo (`[-2]`) elemento dell'array per determinare il nodo `:Category` superiore nella gerarchia e poi creando una relazione `:CONTAINS` tra i due nodi:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (c:Category)
|
|||
|
WITH c, split(c.path, ".") AS path
|
|||
|
MATCH (d:Category {leaf: path[-2]})
|
|||
|
CREATE (d)-[:CONTAINS]->(c);
|
|||
|
```
|
|||
|
|
|||
|
#### `5` - Creazione delle relazioni `:CONTAINS`
|
|||
|
|
|||
|
Il file `crates_categories.csv`, esattamente come `crates_keywords.csv`, contiene una coppia di id per riga, uno di una `:Crate` e uno di una `:Category`.
|
|||
|
|
|||
|
Si effettua quindi lo stesso esatto procedimento per creare relazioni `:CONTAINS` da `:Category` a `:Crate`.
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (:Category)-[relation:CONTAINS]->(:Crate)
|
|||
|
DELETE relation;
|
|||
|
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///crates_categories.csv" AS line FIELDTERMINATOR ","
|
|||
|
MATCH
|
|||
|
(crate:Crate {id: toInteger(line.crate_id)}),
|
|||
|
(category:Category {id: toInteger(line.category_id)})
|
|||
|
CREATE (category)-[:CONTAINS]->(crate);
|
|||
|
```
|
|||
|
|
|||
|
#### `6` - Creazione dei nodi `:User`
|
|||
|
|
|||
|
Crates.io richiede di effettuare il login con account GitHub a tutti coloro che desiderano uploadare una crate.
|
|||
|
|
|||
|
Al primo login, viene creata un'entità nel database di Crates.io, che riassume i dettagli dell'account GitHub e gli associa un id interno.
|
|||
|
|
|||
|
Si è deciso di inserire nel grafo gli utenti come nodi aventi il label `:User`.
|
|||
|
|
|||
|
##### Proprietà
|
|||
|
|
|||
|
Si definiscono le seguenti proprietà per i nodi `:User`:
|
|||
|
|
|||
|
- `id`: L'id univoco interno assegnato da Crates.io
|
|||
|
- `gh_id`: L'id univoco esterno di GitHub
|
|||
|
- `avatar`: L'avatar dell'utente su GitHub
|
|||
|
- `name`: L'username dell'utente su GitHub
|
|||
|
- `full_name`: Nome e cognome dell'utente su GitHub
|
|||
|
|
|||
|
##### Indici
|
|||
|
|
|||
|
Si ipotizzano le seguenti query per nodi `:User` e i relativi indici:
|
|||
|
|
|||
|
- _ricerca per **id di Crates.io**_: si crea un indice `RANGE` sul campo `id`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_user_id IF NOT EXISTS
|
|||
|
FOR (user:User)
|
|||
|
ON (user.id);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca per **id di GitHub**_: si crea un indice `RANGE` sul campo `gh_id`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_user_ghid IF NOT EXISTS
|
|||
|
FOR (user:User)
|
|||
|
ON (user.gh_id);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca per **username di GitHub**_: si crea un indice `TEXT` sul campo `name`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_user_name IF NOT EXISTS
|
|||
|
FOR (user:User)
|
|||
|
ON (user.name);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca per **nome proprio, cognome, o parti di essi**_: si crea un indice `TEXT` sul campo `full_name`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_user_fullname IF NOT EXISTS
|
|||
|
FOR (user:User)
|
|||
|
ON (user.full_name);
|
|||
|
```
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
Si realizza una query che effettua il merge degli utenti importati in base al loro `id`, e poi ne aggiorna i dati:
|
|||
|
|
|||
|
```cypher
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///users.csv" AS line FIELDTERMINATOR ","
|
|||
|
MERGE (user:User { id: toInteger(line.id) })
|
|||
|
SET
|
|||
|
user.avatar = line.gh_avatar,
|
|||
|
user.gh_id = toInteger(line.gh_id),
|
|||
|
user.name = line.gh_login,
|
|||
|
user.full_name = line.name;
|
|||
|
```
|
|||
|
|
|||
|
#### `7` - Creazione delle relazioni `:OWNS`
|
|||
|
|
|||
|
L'utente che carica per la prima volta una crate con un dato nome ne è detto il *proprietario*.
|
|||
|
|
|||
|
Egli, in qualsiasi momento, può aggiungere altri proprietari dall'interfaccia web di Crates.io.
|
|||
|
|
|||
|
I proprietari di una crate sono gli unici utenti che possono caricarne nuove versioni.
|
|||
|
|
|||
|
Si identifica la proprietà come una relazione tra `:Crate` e `:User` etichettata `:OWNS`.
|
|||
|
|
|||
|
Le relazioni di proprietà, assieme ad alcune informazioni ad esse collegate, sono contenute nel file `crate_owners.csv` del dataset.
|
|||
|
|
|||
|
##### Proprietà
|
|||
|
|
|||
|
Si associano le seguenti proprietà alle relazioni `:OWNS`:
|
|||
|
|
|||
|
- `created_at`: data in cui l'utente è stato aggiunto come proprietario alla crate
|
|||
|
- `created_by`: id di Crates.io dell'utente che ha aggiunto l'utente come proprietario
|
|||
|
- `owner_kind`: sconosciuto, è presente nel dataset ma per ora è sempre impostato a `1`
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
Essendo le relazioni di proprietà variabili (un proprietario "secondario" potrebbe essere rimosso dal proprietario "originale"), tutte le relazioni `:OWNS` già esistenti vengono prima eliminate e poi ricreate con i dati aggiornati.
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (:User)-[owns:OWNS]->(:Crate)
|
|||
|
DELETE owns;
|
|||
|
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///crate_owners.csv" AS line FIELDTERMINATOR ","
|
|||
|
MATCH (crate:Crate { id: toInteger(line.crate_id) })
|
|||
|
MATCH (owner:User { id: toInteger(line.owner_id) })
|
|||
|
CREATE (owner)-[ownership:OWNS {
|
|||
|
created_at: apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"),
|
|||
|
created_by: toInteger(line.created_by),
|
|||
|
owner_kind: toInteger(line.owner_kind)
|
|||
|
}]->(crate);
|
|||
|
```
|
|||
|
|
|||
|
#### `8` - Creazione dei nodi `:Version` e delle relazioni `:HAS_VERSION` e `:PUBLISHED`
|
|||
|
|
|||
|
Ogni upload di una crate a Crates.io contiene una stringa nel formato `X.Y.Z` detta *versione*, che, assieme al nome della crate, determina una tupla in grado di identificarlo.
|
|||
|
|
|||
|
Il dataset contiene informazioni sulle versioni nel file `versions.csv`.
|
|||
|
|
|||
|
Si decide di rappresentare le versioni attraverso nodi etichettati `:Version`, le crate a cui si riferiscono con relazioni `(:Crate)-[:HAS_VERSION]->(:Version)`, e gli utenti che le hanno pubblicate con relazioni `(:User)-[:PUBLISHED]->(:Version)`.
|
|||
|
|
|||
|
##### Proprietà
|
|||
|
|
|||
|
Si definiscono le seguenti proprietà per i nodi `:Version`:
|
|||
|
|
|||
|
- `id`: identificatore numerico incrementale assegnato da Crates.io
|
|||
|
- `name`: [nome semantico] della versione, come `v1.2.3`
|
|||
|
- `checksum`: hash SHA1 del commit che aggiunge la versione all'indice
|
|||
|
- `size`: dimensione in bytes della versione
|
|||
|
- `created_at`: data di upload della versione
|
|||
|
- `downloads`: numero di download della versione
|
|||
|
- `license`: stringa SPDX rappresentante le licenze open source sotto cui la versione è resa disponibile
|
|||
|
- `features`: blob JSON contenente metadati sui tag assegnati alle dipendenze opzionali della crate, detti "feature"
|
|||
|
- `links`: blob JSON di collegamenti aggiuntivi specificati nei metadati della crate
|
|||
|
- `is_yanked`: se il download della versione è stato sconsigliato da un proprietario
|
|||
|
|
|||
|
Non si definiscono invece proprietà per le relazioni `:HAS_VERSION` e `:PUBLISHED`.
|
|||
|
|
|||
|
<!-- TODO: Aggiungere un campo per maggiore, minore, e patch? -->
|
|||
|
|
|||
|
##### Indici
|
|||
|
|
|||
|
Query ipotetiche e indici creati per nodi `:Version` sono:
|
|||
|
|
|||
|
- _ricerca **per checksum**_: si crea un indice `LOOKUP` sul campo `checksum`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE LOOKUP INDEX index_version_checksum IF NOT EXISTS
|
|||
|
FOR (version:Version)
|
|||
|
ON (version.checksum);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca di versioni **più grandi o più piccole di un certo numero di bytes**_: si crea un indice `RANGE` sul campo `size`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_version_size IF NOT EXISTS
|
|||
|
FOR (version:Version)
|
|||
|
ON (version.size);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca di versioni rilasciate **prima, durante, o dopo una certa data**_: si crea un indice `RANGE` sul campo `created_at`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_version_created_at IF NOT EXISTS
|
|||
|
FOR (version:Version)
|
|||
|
ON (version.created_at);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca di versioni con **almeno un certo numero di download**_: si crea un indice `RANGE` sul campo `downloads`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_version_downloads IF NOT EXISTS
|
|||
|
FOR (version:Version)
|
|||
|
ON (version.downloads);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca **per id**_: si crea un indice `RANGE` sul campo `id`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_version_id IF NOT EXISTS
|
|||
|
FOR (version:Version)
|
|||
|
ON (version.id);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca per **versione** o **range di versioni**_: si crea un indice `TEXT` sul campo `name`, in quanto esso permette la ricerca di prefissi, suffissi, e sottostringhe, che riassumono efficientemente buona parte delle query effettuabili sul campo
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_version_name IF NOT EXISTS
|
|||
|
FOR (version:Version)
|
|||
|
ON (version.name);
|
|||
|
```
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
Essendo le versioni di una crate immutabili, si utilizza lo stesso procedimento utilizzato per i nodi `:Crate` per inizializzazione e aggiornamento del database, ovvero di effettuare un `MERGE` sull'`id` e di impostare sul nodo trovato o creato le varie proprietà aggiornate.
|
|||
|
|
|||
|
Nella stessa query si individua la crate a cui appartiene la versione, e si crea una relazione `HAS_VERSION` tra i due nodi; allo stesso modo, si individua l'utente che ha pubblicato la versione, e si crea con esso una relazione `:PUBLISHED`.
|
|||
|
|
|||
|
```cypher
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///versions.csv" AS line FIELDTERMINATOR ","
|
|||
|
CALL {
|
|||
|
WITH line
|
|||
|
MERGE (version:Version { id: toInteger(line.id) } )
|
|||
|
SET
|
|||
|
version.checksum = line.checksum,
|
|||
|
version.size = toInteger(line.crate_size),
|
|||
|
version.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"),
|
|||
|
version.downloads = toInteger(line.downloads),
|
|||
|
version.license = line.license,
|
|||
|
version.features = line.features,
|
|||
|
version.links = line.links,
|
|||
|
version.name = line.num,
|
|||
|
version.is_yanked = CASE line.yanked
|
|||
|
WHEN "t"
|
|||
|
THEN true
|
|||
|
ELSE
|
|||
|
false
|
|||
|
END
|
|||
|
WITH line, version
|
|||
|
MATCH (crate:Crate { id: toInteger(line.crate_id) })
|
|||
|
MERGE (crate)-[:HAS_VERSION]->(version)
|
|||
|
WITH line, version
|
|||
|
MATCH (user:User { id: toInteger(line.published_by) })
|
|||
|
MERGE (user)-[:PUBLISHED]->(version)
|
|||
|
} IN TRANSACTIONS OF 10000 ROWS;
|
|||
|
```
|
|||
|
|
|||
|
#### `9` - Creazione delle relazioni `:DEPENDS_ON`
|
|||
|
|
|||
|
Ciascuna versione di una crate deve specificare quali altre versioni di crate richiede per una corretta compilazione.
|
|||
|
|
|||
|
Le versioni necessarie sono specificate come [un range di versioni accettabili di una data crate in forma di stringa](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html), e sono dette *dipendenze* della versione specificante, contenute nel file `dependencies.csv` del dataset.
|
|||
|
|
|||
|
Alcuni esempi delle precedenti stringhe sono:
|
|||
|
|
|||
|
```toml
|
|||
|
[dependencies]
|
|||
|
rand = "*" # Qualsiasi versione della crate rand è accettabile
|
|||
|
serde = "^1.0.0" # Qualsiasi versione della crate serde tra 1.0.0 (inclusa) e 2.0.0 (esclusa) è accettabile
|
|||
|
quote = "~1.0.0" # Qualsiasi versione della crate quote tra 1.0.0 (inclusa) e 1.1.0 (esclusa) è accettabile
|
|||
|
itoa = "=1.0.0" # Solo la versione 1.0.0 della crate itoa è accettabile
|
|||
|
```
|
|||
|
|
|||
|
Si decide di rappresentare le dipendenze come relazioni etichettate come `:DEPENDS_ON`.
|
|||
|
|
|||
|
##### Proprietà
|
|||
|
|
|||
|
Si stabiliscono le seguenti proprietà per le relazioni di dipendenza:
|
|||
|
|
|||
|
- `id`: id univoco che identifica la relazione
|
|||
|
- `requirement`: stringa determinante il range di versioni accettabili
|
|||
|
- `target`: le piattaforme di compilazione (x86, arm, riscv) in cui è richiesta la dipendenza
|
|||
|
- `is_optional`: se la dipendenza è facoltativa, ovvero se si può usare la crate senza di essa (ma con funzionalità ridotte)
|
|||
|
- `is_default`: se la dipendenza è facoltativa, ma viene installata di default se non viene esclusa esplicitamente
|
|||
|
- `features`: blob JSON riguardo le feature di cui la dipendenza fa parte
|
|||
|
- `explicit_name`: se presente, il nome a cui deve essere rinominata la dipendenza durante la compilazione della crate
|
|||
|
|
|||
|
##### Indici
|
|||
|
|
|||
|
Si ipotizzano le seguenti query e si creano i relativi indici per processarle:
|
|||
|
|
|||
|
- _ricerca per **id**_: si crea un indice `RANGE` sul campo `id`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE RANGE INDEX index_dependency_id IF NOT EXISTS
|
|||
|
FOR ()-[dependency:DEPENDS_ON]->()
|
|||
|
ON (dependency.id);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca per **versione** o **range di versioni**_: si crea un indice `TEXT` sul campo `requirement`, con lo stesso scopo dell'indice per la ricerca per nome semantico dei nodi `:Version`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_dependency_requirement IF NOT EXISTS
|
|||
|
FOR ()-[dependency:DEPENDS_ON]->()
|
|||
|
ON (dependency.requirement);
|
|||
|
```
|
|||
|
|
|||
|
- _ricerca per **nome esplicito**_: si crea un indice `TEXT` sul campo `explicit_name`.
|
|||
|
|
|||
|
```cypher
|
|||
|
CREATE TEXT INDEX index_dependency_explicit_name IF NOT EXISTS
|
|||
|
FOR ()-[dependency:DEPENDS_ON]->()
|
|||
|
ON (dependency.explicit_name);
|
|||
|
```
|
|||
|
|
|||
|
##### Query
|
|||
|
|
|||
|
```cypher
|
|||
|
LOAD CSV WITH HEADERS FROM "file:///dependencies.csv" AS line FIELDTERMINATOR ","
|
|||
|
CALL {
|
|||
|
WITH line
|
|||
|
MATCH
|
|||
|
(version:Version { id: toInteger(line.version_id) }),
|
|||
|
(requirement:Crate { id: toInteger(line.crate_id) })
|
|||
|
MERGE (version)-[dependency:DEPENDS_ON]->(requirement)
|
|||
|
SET
|
|||
|
dependency.id = line.id,
|
|||
|
dependency.is_optional = CASE line.optional
|
|||
|
WHEN "t"
|
|||
|
THEN true
|
|||
|
ELSE
|
|||
|
false
|
|||
|
END,
|
|||
|
dependency.is_default = CASE line.default_features
|
|||
|
WHEN "t"
|
|||
|
THEN true
|
|||
|
ELSE
|
|||
|
false
|
|||
|
END,
|
|||
|
dependency.explicit_name = line.explicit_name,
|
|||
|
dependency.features = line.features,
|
|||
|
dependency.requirement = line.req,
|
|||
|
dependency.target = line.target
|
|||
|
} IN TRANSACTIONS OF 10000 ROWS;
|
|||
|
```
|
|||
|
|
|||
|
## Alcune interrogazioni interessanti
|
|||
|
|
|||
|
Per dimostrare le possibilità offerte dal grafo, si presentano alcune query effettuabili ritenute particolarmente interessanti.
|
|||
|
|
|||
|
Le risposte incluse si riferiscono al dataset del 19 Febbraio 2023, non allegato a questa relazione per motivi di dimensioni.
|
|||
|
|
|||
|
### Numero totale di crate
|
|||
|
|
|||
|
> "Quante crate sono state uploadate a Crates.io, in totale?"
|
|||
|
|
|||
|
Si propone la seguente query, che conta il numero di nodi `:Crate`, per rispondere alla domanda:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (crate:Crate)
|
|||
|
RETURN count(crate);
|
|||
|
```
|
|||
|
|
|||
|
```text
|
|||
|
105287
|
|||
|
```
|
|||
|
|
|||
|
### Numero totale di download di tutte le crate su tutte le versioni
|
|||
|
|
|||
|
> "Quante versioni di crate sono state scaricate su Crates.io?"
|
|||
|
|
|||
|
Si propone la seguente query, che somma i valori del campo `download` di tutti i nodi `:Version`, per rispondere alla domanda:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (version:Version)
|
|||
|
RETURN sum(version.downloads);
|
|||
|
```
|
|||
|
|
|||
|
```text
|
|||
|
27475924849
|
|||
|
```
|
|||
|
|
|||
|
### Albero delle categorie
|
|||
|
|
|||
|
> "Come è organizzata la gerarchia di categorie di Crates.io?"
|
|||
|
|
|||
|
Si propone la seguente query, che recupera tutti i nodi `:Category` e le loro relazioni `:CONTAINS`, per rispondere alla domanda:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (supercategory:Category)-[conts:CONTAINS*]->(category:Category)
|
|||
|
RETURN supercategory, conts, category;
|
|||
|
```
|
|||
|
|
|||
|
![Grafo con 88 nodi e 151 relazioni. Un nodo centrale da cui escono tanti altri nodi, da cui a loro volta talvolta escono altri nodi.](media/query-categorygraph-solution.svg)
|
|||
|
|
|||
|
### Informazioni relative a una specifica crate
|
|||
|
|
|||
|
> "Voglio sapere tutto il possibile sulla [crate `rand`]."
|
|||
|
|
|||
|
Si propone la seguente query, che recupera il singolo nodo `:Crate` avente `"rand"` come `name`, per rispondere alla domanda:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (crate:Crate {name: "rand"})
|
|||
|
RETURN crate;
|
|||
|
```
|
|||
|
|
|||
|
In alternativa, si propone la seguente query, che recupera la `:Crate` `rand` e tutti i nodi a cui essa si connette direttamente, e le connessioni:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (crate:Crate {name: "rand"})-[rel]->(other)
|
|||
|
RETURN crate, rel, other;
|
|||
|
```
|
|||
|
|
|||
|
![Grafo con 71 nodi e 70 relazioni. Un nodo centrale "rand" da cui escono tanti altri nodi `:Version`, e alcuni altri nodi `:Keyword`, rappresentanti "rng" e "random".](media/query-randfull-solution.svg)
|
|||
|
|
|||
|
### Dipendenze di `serde` il cui nome contiene a loro volta con `serde`
|
|||
|
|
|||
|
> "Esistono delle crate, che dipendono direttamente dalla [crate `serde`], che hanno a loro volta `serde` nel nome?
|
|||
|
>
|
|||
|
> (Potrebbero essere ad esempio delle estensioni di qualche tipo.)
|
|||
|
|
|||
|
Si propone la seguente query, che trova tutte le versioni che dipendono da `serde` e poi recupera la crate a cui appartengono, per verificare:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (dependency:Crate)-[vr:HAS_VERSION]->(version:Version)-[dr:DEPENDS_ON]->(serde:Crate {name: "serde"})
|
|||
|
WHERE dependency.name CONTAINS "serde"
|
|||
|
RETURN dependency, vr, version, dr, serde;
|
|||
|
```
|
|||
|
|
|||
|
![Visualizzazione dell'execution plan della precedente query.](media/query-serdedeps-plan.svg)
|
|||
|
|
|||
|
![Grafo parziale con 300 nodi e 671 relazioni. Tanti nodi centrali interconnettono il nodo `serde` a tanti nodi crate all'esterno del grafo.](media/query-serdedeps-solution.svg)
|
|||
|
|
|||
|
### Crate dipendenti da loro stesse
|
|||
|
|
|||
|
> "Esistono delle crate che dipendono da loro stesse?"
|
|||
|
|
|||
|
Si propone la seguente query, con funzionamento molto simile alla precedente, per verificare:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (a:Crate)-[rhv:HAS_VERSION]->(v:Version)-[rdo:DEPENDS_ON]->(b:Crate)
|
|||
|
WHERE a = b
|
|||
|
RETURN a, rhv, v, rdo, b
|
|||
|
ORDER BY v.downloads DESC;
|
|||
|
```
|
|||
|
|
|||
|
> (Approfondendo la ricerca, si è scoperto che [è una tecnica usata per migliorare la compatibilità delle crate](https://github.com/dtolnay/semver-trick)!)
|
|||
|
|
|||
|
![Grafo parziale con 300 nodi e 633 relazioni. Tanti nodi sono sparsi per il grafo, e spesso hanno doppi collegamenti.](media/query-selfdeps-solution.svg)
|
|||
|
|
|||
|
### Le crate di un utente e tutti i loro dettagli
|
|||
|
|
|||
|
> "Quali crate ha rilasciato l'utente Steffo99, e come sono collegate tra loro?
|
|||
|
|
|||
|
Si propone la seguente query, che recupera tutte le informazioni interessanti riguardo l'utente specificato, per rispondere alla domanda:
|
|||
|
|
|||
|
```cypher
|
|||
|
MATCH (user:User {name: "Steffo99"})-[owns:OWNS]->(crate:Crate)
|
|||
|
MATCH (crate:Crate)-[is_tagged_with:IS_TAGGED_WITH]->(keyword:Keyword)
|
|||
|
MATCH (crate:Crate)-[has_version:HAS_VERSION]->(version:Version)
|
|||
|
MATCH (crate:Crate)<-[contains:CONTAINS]-(category:Category)
|
|||
|
MATCH (category:Category)<-[supercontains:CONTAINS]-(supercategory:Category)
|
|||
|
RETURN user, owns, crate, is_tagged_with, keyword, has_version, version, contains, category, supercontains, supercategory;
|
|||
|
```
|
|||
|
|
|||
|
![Grafo con 57 nodi e 870 relazioni. Un nodo rosso centrale "Steffo99" è connesso a tanti altri nodi.](media/query-selfdeps-solution.svg)
|
|||
|
|
|||
|
|
|||
|
<!-- Collegamenti -->
|
|||
|
|
|||
|
[Crates.io]: https://crates.io
|
|||
|
[Rust]: https://www.rust-lang.org
|
|||
|
[crate]: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html
|
|||
|
[versione semantica]: https://semver.org
|
|||
|
[identificatore SPDX]: https://spdx.org/licenses/
|
|||
|
[Cargo]: https://doc.rust-lang.org/cargo/
|
|||
|
[elenco delle categorie previste]: https://github.com/rust-lang/crates.io/blob/master/src/boot/categories.toml
|
|||
|
[open data]: https://crates.io/data-access
|
|||
|
[`apoc.date.parse`]: https://neo4j.com/labs/apoc/4.3/overview/apoc.date/apoc.date.parse/
|
|||
|
[`datetime`]: https://neo4j.com/docs/cypher-manual/5/syntax/temporal/
|
|||
|
[dump giornaliero del database]: https://static.crates.io/db-dump.tar.gz
|
|||
|
[nome semantico]: https://semver.org/
|
|||
|
[crate `rand`]: https://crates.io/crates/rand
|
|||
|
[crate `serde`]: https://crates.io/crates/serde
|