Devi fare un paio di cose qui per il tuo risultato finale, ma le prime fasi sono relativamente semplici. Prendi l'oggetto utente che fornisci:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Ora supponendo che tu abbia già recuperato quei dati, questo si riduce a trovare le stesse strutture per ogni "amico" e filtrare il contenuto dell'array di "Artisti" in un unico elenco distinto. Presumibilmente ogni "peso" sarà considerato in totale anche qui.
Questa è una semplice operazione di aggregazione che filtrerà prima gli artisti già presenti nell'elenco per l'utente specificato:
var artists = user.Artists.map(function(artist) { return artist.artist_id });
User.aggregate(
[
// Find possible friends without all the same artists
{ "$match": {
"user_id": { "$in": user.Friends },
"Artists.artist_id": { "$nin": artists }
}},
// Pre-filter the artists already in the user list
{ "$project":
"Artists": {
"$setDifference": [
{ "$map": {
"input": "$Artists",
"as": "$el",
"in": {
"$cond": [
"$anyElementTrue": {
"$map": {
"input": artists,
"as": "artist",
"in": { "$eq": [ "$$artist", "$el.artist_id" ] }
}
},
false,
"$$el"
]
}
}}
[false]
]
}
}},
// Unwind the reduced array
{ "$unwind": "$Artists" },
// Group back by each artist and sum weights
{ "$group": {
"_id": "$Artists.artist_id",
"weight": { "$sum": "$Artists.weight" }
}},
// Sort the results by weight
{ "$sort": { "weight": -1 } }
],
function(err,results) {
// more to come here
}
);
Il "pre-filtro" è l'unica parte davvero difficile qui. Potresti semplicemente $unwind
l'array e $match
di nuovo per filtrare le voci non desiderate. Anche se vogliamo $unwind
i risultati in seguito per combinarli, risulta più efficiente rimuoverli dall'array "prima", quindi c'è meno da espandere.
Quindi qui $map
l'operatore consente l'ispezione di ogni elemento dell'array "Artisti" dell'utente e anche il confronto con l'elenco degli artisti "utente" filtrato per restituire solo i dettagli desiderati. Il $setDifference
viene utilizzato per "filtrare" effettivamente tutti i risultati che non sono stati restituiti come contenuto dell'array, ma piuttosto restituiti come false
.
Dopo di che c'è solo il $unwind
per denormalizzare il contenuto nell'array e il $group
per riunire un totale per artista. Per divertimento stiamo usando $sort
per mostrare che l'elenco viene restituito nell'ordine desiderato, ma ciò non sarà necessario in una fase successiva.
Questo è almeno parte del percorso qui in quanto l'elenco risultante dovrebbe essere solo di altri artisti non già presenti nell'elenco dell'utente e ordinato in base al "peso" sommato da tutti gli artisti che potrebbero apparire su più amici.
La parte successiva avrà bisogno dei dati della raccolta "artisti" per tenere conto del numero di ascoltatori. Mentre la mangusta ha un .populate()
metodo, davvero non lo vuoi qui perché stai cercando i conteggi di "utente distinto". Ciò implica un'altra implementazione dell'aggregazione per ottenere quei conteggi distinti per ogni artista.
In seguito all'elenco dei risultati della precedente operazione di aggregazione, dovresti utilizzare il $_id
valori come questo:
// First get just an array of artist id's
var artists = results.map(function(artist) {
return artist._id;
});
Artist.aggregate(
[
// Match artists
{ "$match": {
"artistID": { "$in": artists }
}},
// Project with weight for distinct users
{ "$project": {
"_id": "$artistID",
"weight": {
"$multiply": [
{ "$size": {
"$setUnion": [
{ "$map": {
"input": "$user_tag",
"as": "tag",
"in": "$$tag.user_id"
}},
[]
]
}},
10
]
}
}}
],
function(err,results) {
// more later
}
);
Qui il trucco è fatto in aggregato con $map
per eseguire una simile trasformazione di valori che viene inviata a $setUnion
per farne una lista unica. Quindi $size
viene applicato l'operatore per scoprire quanto è grande quell'elenco. La matematica aggiuntiva consiste nel dare un significato a quel numero quando applicato rispetto ai pesi già registrati dai risultati precedenti.
Ovviamente è necessario riunire tutto questo in qualche modo, poiché in questo momento ci sono solo due distinti insiemi di risultati. Il processo di base è una "tabella hash", in cui i valori univoci dell'ID "artista" vengono utilizzati come chiave e i valori del "peso" vengono combinati.
Puoi farlo in diversi modi, ma poiché c'è il desiderio di "ordinare" i risultati combinati, la mia preferenza sarebbe qualcosa di "MongoDBish" poiché segue i metodi di base a cui dovresti già essere abituato.
Un modo pratico per implementarlo è utilizzare nedb
, che fornisce un archivio "in memoria" che utilizza quasi lo stesso tipo di metodi utilizzati per leggere e scrivere nelle raccolte MongoDB.
Questo si adatta bene anche se è necessario utilizzare una raccolta effettiva per risultati di grandi dimensioni, poiché tutti i principi rimangono gli stessi.
-
La prima operazione di aggregazione inserisce nuovi dati nello store
-
La seconda aggregazione "aggiorna" i dati e incrementa il campo "peso"
Come elenco completo delle funzioni e con qualche altro aiuto di async
libreria sarebbe simile a questo:
function GetUserRecommendations(userId,callback) {
var async = require('async')
DataStore = require('nedb');
User.findOne({ "user_id": user_id},function(err,user) {
if (err) callback(err);
var artists = user.Artists.map(function(artist) {
return artist.artist_id;
});
async.waterfall(
[
function(callback) {
var pipeline = [
// Find possible friends without all the same artists
{ "$match": {
"user_id": { "$in": user.Friends },
"Artists.artist_id": { "$nin": artists }
}},
// Pre-filter the artists already in the user list
{ "$project":
"Artists": {
"$setDifference": [
{ "$map": {
"input": "$Artists",
"as": "$el",
"in": {
"$cond": [
"$anyElementTrue": {
"$map": {
"input": artists,
"as": "artist",
"in": { "$eq": [ "$$artist", "$el.artist_id" ] }
}
},
false,
"$$el"
]
}
}}
[false]
]
}
}},
// Unwind the reduced array
{ "$unwind": "$Artists" },
// Group back by each artist and sum weights
{ "$group": {
"_id": "$Artists.artist_id",
"weight": { "$sum": "$Artists.weight" }
}},
// Sort the results by weight
{ "$sort": { "weight": -1 } }
];
User.aggregate(pipeline, function(err,results) {
if (err) callback(err);
async.each(
results,
function(result,callback) {
result.artist_id = result._id;
delete result._id;
DataStore.insert(result,callback);
},
function(err)
callback(err,results);
}
);
});
},
function(results,callback) {
var artists = results.map(function(artist) {
return artist.artist_id; // note that we renamed this
});
var pipeline = [
// Match artists
{ "$match": {
"artistID": { "$in": artists }
}},
// Project with weight for distinct users
{ "$project": {
"_id": "$artistID",
"weight": {
"$multiply": [
{ "$size": {
"$setUnion": [
{ "$map": {
"input": "$user_tag",
"as": "tag",
"in": "$$tag.user_id"
}},
[]
]
}},
10
]
}
}}
];
Artist.aggregate(pipeline,function(err,results) {
if (err) callback(err);
async.each(
results,
function(result,callback) {
result.artist_id = result._id;
delete result._id;
DataStore.update(
{ "artist_id": result.artist_id },
{ "$inc": { "weight": result.weight } },
callback
);
},
function(err) {
callback(err);
}
);
});
}
],
function(err) {
if (err) callback(err); // callback with any errors
// else fetch the combined results and sort to callback
DataStore.find({}).sort({ "weight": -1 }).exec(callback);
}
);
});
}
Quindi, dopo aver abbinato l'oggetto utente di origine iniziale, i valori vengono passati alla prima funzione di aggregazione, che viene eseguita in serie e utilizzando async.waterfall
per passare il risultato.
Prima che ciò avvenga, i risultati dell'aggregazione vengono aggiunti al DataStore
con .insert()
normale istruzioni, avendo cura di rinominare il _id
campi come nedb
non gli piace nient'altro che il proprio _id
autogenerato i valori. Ogni risultato viene inserito con artist_id
e weight
proprietà dal risultato dell'aggregazione.
Tale elenco viene quindi passato alla seconda operazione di aggregazione che restituirà ogni "artista" specificato con un "peso" calcolato in base alla dimensione distinta dell'utente. Ci sono gli "aggiornati" con lo stesso .update()
dichiarazione sul DataStore
per ogni artista e incrementando il campo "peso".
Tutto procede bene, l'operazione finale è .find()
quei risultati e .sort()
in base al "peso" combinato e restituisci semplicemente il risultato alla richiamata passata alla funzione.
Quindi lo useresti in questo modo:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
E restituirà tutti gli artisti non attualmente nell'elenco di quell'utente ma nei loro elenchi di amici e ordinati in base ai pesi combinati del conteggio degli ascolti degli amici più il punteggio del numero di utenti distinti di quell'artista.
In questo modo gestisci i dati di due diverse raccolte che devi combinare in un unico risultato con vari dettagli aggregati. Sono più query e uno spazio di lavoro, ma fa anche parte della filosofia MongoDB che tali operazioni siano eseguite meglio in questo modo piuttosto che lanciarle nel database per "unire" i risultati.