DIANA

L'acronimo sta per: DIANA Is Another News Aggregator

Salvatore Pappalardo

Specifica del problema

Il sistema DIANA prova a semplificare l'esperienza di lettura delle notizie postate su Facebook dalle principali testate giornalistiche italiane.

L'applicazione monitora periodicamente i feed delle pagine dei suddetti giornali per estrarre articoli rilevanti, aggregandoli nel caso in cui trattino argomenti simili.

La loro importanza viene calcolata in base al seguito "social" generato (in pratica, in base al numero di likes, condivisioni e commenti che l'articolo ottiene).

Architettura: Data Warehouse

Architettura: Data Warehouse

L'architettura Data Warehouse è stata scelta principalmente per restituire velocemente dati ad applicazioni esterne (e quindi agli utenti).

  • Lo strato di data extraction (avviato ciclicamente) ha il compito di estrapolare i dati dalle fonti tramite i wrapper, unire le informazioni trovate ed inviare i dati finali al data warehouse.

  • Per risparmiare iterazioni superflue, gli articoli esaminati vengono conservati per due giorni all'interno di BoltDB (un database chiave-valore), per ogni link viene conservata l'azione da compiere alla prossima iterazione; le azioni sono due: ignora e aggiorna.

  • Il Data Warehouse immagazzina i dati sotto forma di grafo (grazie a Neo4J) e li espone ad eventuali client (con oppurtune limitazioni) tramite una REST API.

Fonti: API

facebook graph api: permette di interrogare e manipolare i dati pubblici generati su facebook.

dandelion api: permette di estrapolare entità a partire da un testo o da un link.

Fonti: siti

repubblica

la stampa

il fatto quotidiano

corriere della sera

Operazioni di join

  • I link ricavati dai feed delle pagine facebook dei quotidiani vengono utilizzati per estrapolare ulteriori informazioni dai meta tag presenti nei documenti html a cui puntano i link stessi.
  • Se i dati ricavati dai link sopracitati non presentano errori, vengono utilizzati per ricavare le entità (città, stati, personaggi, associazioni, ecc. ecc.) associate agli articoli attraverso dandelion.

Schemi locali (datalog)

  • Facebook Graph API:
    • post(postURL,reactionCount,shareCount,commentCount,createdTime)
  • Dandelion API:
    • entity(name,image,summary,accuracy,relatedURL)
  • Siti dei quotidiani:
    • repubblica(articleURL,ogTitle,ogDescription,ogImage,ogSection,publisher)
    • corriere(articleURL,ogTitle,ogDescription,ogImage,ogSection,publisher)
    • laStampa(articleURL,ogTitle,ogDescription,ogImage,ogSection,publisher)
    • ilFattoQuotidiano(articleURL,ogTitle,ogDescription,ogImage,ogSection,publisher)

Wrapper: FB Graph API

esempio di endpoint: <fbNodeName>/feed?fields=link,reactions.limit(0).summary(true),shares,created_time

esempio di risposta:

Wrapper: FB Graph API

Per permettere lo scambio di dati tra l'API di facebook e l'extraction layer è stato implementato un package in go appositamente per quest'applicazione (chiamato graphapi).

Le funzioni principali sono definite nelle seguenti interfacce

Wrapper: FB Graph API

esempi d'utilizzo:

Wrapper: Dandelion API

esempio di endpoint: /nex/v1/?lang=it&min_confidence=0.5&url=<PH>&include=image%2Csummary%2Ccategories&token=<PH>

esempio di risposta:

Wrapper: Dandelion API

Come per l'api di facebook, anche per dandelion è stato scritto un wrapper che non si appoggia a librerie esterne. Di seguito un esempio di come la libreria viene utilizzata

Wrapper: Meta crawler

Per estrapolare i dati dalle pagine html è stato utilizzato il pacchetto goQuery, che permette di manipolare le fonti attraverso selettori css, utilizzando una sintassi simile a quella di jQuery.

Wrapper: Meta crawler

goQuery è stato utilizzato all'interno del package crawler per estrarre i dati dai meta tag delle pagine web.

Wrapper: Meta crawler

di seguito un esempio di utilizzo delle funzioni del package crawler

Schema globale

  • article(articleURL,title,description,imageURL,section,publisher)
  • socialInfo(articleURL,reactions,shares,comments,createdTime)
  • articleEntity(articleURL,name,image,summary,accuracy)

GAV

socialInfo(articleURL,reactions,shares,comments,createdTime)
post(articleURL,reactions,shares,comments,createdTime),
article(articleURL,title,description,imageURL,section,publisher)
repubblica(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
article(articleURL,title,description,imageURL,section,publisher)
corriere(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
article(articleURL,title,description,imageURL,section,publisher)
laStampa(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
article(articleURL,title,description,imageURL,section,publisher)
ilFattoQuotidiano(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
articleEntity(articleURL,name,image,summary,accuracy)
entity(name,image,summary,accuracy,articleURL),
post(articleURL,reactions,shares,comments,createdTime)

LAV - 1

post(articleURL,reactions,shares,comments,createdTime)
socialInfo(articleURL,reactions,shares,comments,createdTime)
repubblica(articleURL,title,description,imageURL,section,publisher)
article(articleURL,title,description,imageURL,section,publisher),
socialInfo(articleURL,reactions,shares,comments,createdTime),
publisher='repubblica.it'
corriere(articleURL,title,description,imageURL,section,publisher)
article(articleURL,title,description,imageURL,section,publisher),
socialInfo(articleURL,reactions,shares,comments,createdTime),
publisher='corriere.it'
laStampa(articleURL,title,description,imageURL,section,publisher)
article(articleURL,title,description,imageURL,section,publisher),
socialInfo(articleURL,reactions,shares,comments,createdTime),
publisher='lastampa.it'

LAV - 2

ilFattoQuotidiano(articleURL,title,description,imageURL,section,publisher)
article(articleURL,title,description,imageURL,section,publisher),
socialInfo(articleURL,reactions,shares,comments,createdTime),
publisher='ilfattoquotidiano.it'
entity(name,image,summary,accuracy,articleURL)
articleEntity(articleURL,name,image,summary,accuracy),
socialInfo(articleURL,reactions,shares,comments,createdTime),

Query SQL/Datalog

Articoli riguardanti gli esteri pubblicati il 21/07/2017

SELECT a.articleURL, a.title, a.description, a.imageURL, si.reactions, si.shares, si.comments
FROM article AS a,socialInfo AS si
WHERE a.section = "esteri"
AND a.articleURL=si.articleURL
AND si.createdTime>=1500595200
AND si.createdTime<=1500681599
articleEsteri(articleURL,title,description,imageURL,reactions,shares,comments):-
article(articleURL,title,description,imageURL,section,publisher),
socialInfo(articleURL,reactions,shares,comments,createdTime),
section="esteri",
createdTime>=1500595200,
createdTime<=1500681599

Query SQL/Datalog

Entità collegate agli articoli pubblicati il 21/07/2017 in cronaca

SELECT ae.name
FROM article AS a,socialInfo AS si,article AS ae
WHERE a.section = "cronaca"
AND si.createdTime>=1500595200
AND si.createdTime<=1500681599
AND a.articleURL = si.articleURL
AND ae.articleURL = si.articleURL
entityCronaca(name):-
articleEntity(articleURL,name,image,summary,accuracy),
socialInfo(articleURL,reactions,shares,comments,createdTime),
article(articleURL,title,description,imageURL,section,publisher),
section="cronaca",
createdTime>=1500595200,
createdTime<=1500681599

Query SQL/Datalog

Articoli collegati all'entità Napoli pubblicati il 21/07/2017

SELECT a.articleURL, a.title, a.imageURL, si.reactions, si.shares, si.comments
FROM article AS a, socialInfo AS si, articleEntity AS ae
WHERE ae.name = "napoli"
AND a.createdTime>=1500595200
AND a.createdTime<=1500681599
AND a.articleURL = si.articleURL
AND a.articleURL = ae.articleURL
artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
articleEntity(articleURL,name,image,summary,accuracy),
socialInfo(articleURL,reactions,shares,comments,createdTime),
article(articleURL,title,description,imageURL,section,publisher),
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

GAV query unfolding - 1

artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
articleEntity(articleURL,name,image,summary,accuracy),
socialInfo(articleURL,reactions,shares,comments,createdTime),
article(articleURL,title,description,imageURL,section,publisher),
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599
artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
post(articleURL,reactions,shares,comments,createdTime)
post(articleURL,reactions,shares,comments,createdTime)
repubblica(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

Il procedimento è simile per gli altri giornali...

GAV query unfolding - 2

artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
post(articleURL,reactions,shares,comments,createdTime)
repubblica(articleURL,title,description,imageURL,section,publisher),
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599
artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
post(articleURL,reactions,shares,comments,createdTime)
corriere(articleURL,title,description,imageURL,section,publisher),
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

GAV query unfolding - 3

artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
post(articleURL,reactions,shares,comments,createdTime)
laStampa(articleURL,title,description,imageURL,section,publisher),
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599
artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
post(articleURL,reactions,shares,comments,createdTime)
ilFattoQuotidiano(articleURL,title,description,imageURL,section,publisher),
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

LAV - bucket algorithm - 1

artNapoli(articleURL,title,imageURL,reactions,shares,comments):-
articleEntity(articleURL,name,image,summary,accuracy),
socialInfo(articleURL,reactions,shares,comments,createdTime),
article(articleURL,title,description,imageURL,section,publisher),
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599
articleEntity(A,N,I,S,AC) article(A,T,D,IU,S,P) socialInfo(A,R,SH,C,CT)
entity(A,N,I,S,AC) repubblica(A,T,D,IU,S,P) post(A,R,SH,C,CT)
corriere(A,T,D,IU,S,P)
laStampa(A,T,D,IU,S,P)
ilFattoQuotidiano(A,T,D,IU,S,P)

LAV - bucket algorithm - 2

applicando il prodotto cartesiano...

artNapoli'(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
repubblica(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599
artNapoli'(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
corriere(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

LAV - bucket algorithm - 3

artNapoli'(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
laStampa(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599
artNapoli'(articleURL,title,imageURL,reactions,shares,comments):-
entity(name,image,summary,accuracy,articleURL),
ilFattoQuotidiano(articleURL,title,description,imageURL,section,publisher),
post(articleURL,reactions,shares,comments,createdTime)
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

LAV - bucket algorithm - 4

testando il containment (solo per l'ultima, le altre sono pressoché uguali)

artNapoli'(articleURL,title,imageURL,reactions,shares,comments):-
articleEntity(articleURL,name,image,summary,accuracy),
socialInfo(articleURL,reactions,shares,comments,createdTime),
article(articleURL,title,description,imageURL,section,publisher),
socialInfo(articleURL,reactions,shares,comments,createdTime),
socialInfo(articleURL,reactions,shares,comments,createdTime),
publisher='ilfattoquotidiano.it',
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

LAV - bucket algorithm - 5

risulta che la query artNapoli' è contenuta in artNapoli.

artNapoli'(articleURL,title,imageURL,reactions,shares,comments):-
articleEntity(articleURL,name,image,summary,accuracy),
socialInfo(articleURL,reactions,shares,comments,createdTime),
article(articleURL,title,description,imageURL,section,publisher),
publisher='ilfattoquotidiano.it',
name="napoli",
createdTime>=1500595200,
createdTime<=1500681599

Accorpamento delle notizie

Quando il processo di data extraction (nel progetto chiamato retriever) ha terminato la sua iterazione, manda i dati raccolti al data warehouse (neowr).

Prima di salvare gli articoli e le entità all'interno di neo4j, viene eseguito un ulteriore controllo per cercare di accorpare gli articoli che parlano di argomenti simili in un unico cluster.

In pratica, vengono cercati gli articoli (inseriti durante la giornata corrente) che abbiano in comune, con quello che sta per essere immagazzinato, la sezione ed almeno un'entità correlata.

Accorpamento delle notizie

Se esistono articoli con tali caratteristiche, vengono prelevati insieme ad un massimo di 5 entità correlate.

Le 5 entità vengono scelte in base al grado di accuratezza che dandelion ha precedentemente calcolato per quell'articolo (nel caso in cui il valori siano uguali, prevale quella esaminata più volte da quando il server è in funzione).

Un procedimento simile viene applicato all'articolo prossimo all'inserimento.

Accorpamento delle notizie

L'ultimo passaggio compiuto, è quello di calcolare un indice di similarità tra le due liste risultanti

L'indice di similarità scelto è Jaccard

Se l'indice supera una certa soglia, vuol dire che ci sono abbastanza entità in comune da effettuare un accorpamento tra le due notizie

Ricerca

Oltre all'accorpamento delle notizie, prima di inserire articoli ed entità all'interno del data warehouse viene compiuto un ulteriore passo.

Sia le informazioni riguardanti gli articoli e le informazioni riguardanti le entità vengono date in pasto ad un analyzer (scritto in go), in modo di generare una serie di parole chiave che servano a rendere la ricerca più efficace.

Nel caso degli articoli, per ogni parola viene generato anche il suo score tf (salvato sull'arco che collega l'articolo e la parola chiave).

Quando l'utente immette una query, anche questa viene data in input all'analyzer e le parole risultanti vengono processate in OR

Ricerca articoli (neo4j)

  • Vengono selezionati gli articoli in base alla sezione ed al periodo indicato dall'utente.
  • Di questi, vengono selezionati gli articoli collegati a keyword che matchano almeno uno degli input.
  • In seguito, ogni parola viene associata ad una lista, i cui elementi sono organizzati nella forma {key: idDocumento, score: tf*Idf}
  • Gli articoli vengono in seguito raggruppati ordinandoli per la somma degli score * il numero di occorrenze del documento nella ricerca

Ricerca entità (neo4j+go)

  • Vengono selezionate le entità in base alla sezione ed al periodo indicato dall'utente.
  • Di queste, vengono selezionate le entità collegate a keyword che matchano almeno uno degli input.
  • Le entità vengono in seguito ordinate in base al grado di somiglianza tra il loro nome e la query (in caso di valori uguali prevale l'entità che ha più occorrenze nel periodo selezionato).

Tecnologie utilizzate

  • Golang
  • Neo4j
  • Neoism (driver go-neo4j)
  • GoQuery (per scraping)
  • AngularJS (per il front-end)
  • HTML5
  • JSON