Sie müssen hier ein paar Dinge tun, um Ihr Endergebnis zu erzielen, aber die ersten Schritte sind relativ einfach. Nehmen Sie das von Ihnen bereitgestellte Benutzerobjekt:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Angenommen, Sie haben diese Daten bereits abgerufen, dann läuft es darauf hinaus, die gleichen Strukturen für jeden "Freund" zu finden und den Array-Inhalt von "Künstler" in eine einzige eindeutige Liste herauszufiltern. Vermutlich wird auch hier jedes "Gewicht" insgesamt berücksichtigt.
Dies ist eine einfache Aggregationsoperation, die zuerst die Künstler herausfiltert, die bereits in der Liste für den angegebenen Benutzer enthalten sind:
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
}
);
Der "Vorfilter" ist hier der einzige wirklich knifflige Teil. Sie könnten einfach $unwind
das Array und $match
erneut, um die unerwünschten Einträge herauszufiltern. Auch wenn wir $unwind
wollen die Ergebnisse später zu kombinieren, um sie zu kombinieren, ist es effizienter, sie "zuerst" aus dem Array zu entfernen, damit weniger erweitert werden muss.
Also hier der $map
Der Operator ermöglicht die Inspektion jedes Elements des Benutzer-"Künstler"-Arrays und auch den Vergleich mit der gefilterten "Benutzer"-Künstlerliste, um nur die gewünschten Details zurückzugeben. Der $setDifference
wird verwendet, um tatsächlich alle Ergebnisse zu "filtern", die nicht als Array-Inhalt, sondern als false
zurückgegeben wurden .
Danach gibt es nur noch das $unwind
um den Inhalt im Array und der $group
insgesamt pro Künstler zusammenzubringen. Zum Spaß verwenden wir $sort
um zu zeigen, dass die Liste in der gewünschten Reihenfolge zurückgegeben wird, aber das wird zu einem späteren Zeitpunkt nicht mehr erforderlich sein.
Das ist zumindest ein Teil des Weges hierher, da die resultierende Liste nur andere Künstler enthalten sollte, die sich noch nicht in der eigenen Liste des Benutzers befinden, und sortiert nach dem summierten "Gewicht" von allen Künstlern, die möglicherweise auf mehreren Freunden erscheinen könnten.
Der nächste Teil wird Daten aus der Sammlung "Künstler" benötigen, um die Anzahl der Zuhörer zu berücksichtigen. Während Mungo einen .populate()
hat -Methode, möchten Sie dies hier wirklich nicht, da Sie nach den Zählwerten "eindeutiger Benutzer" suchen. Dies impliziert eine weitere Aggregationsimplementierung, um diese unterschiedlichen Zählungen für jeden Künstler zu erhalten.
Anknüpfend an die Ergebnisliste der vorherigen Aggregationsoperation würden Sie die $_id
verwenden Werte wie diese:
// 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
}
);
Hier wird der Trick zusammen mit $map
ausgeführt um eine ähnliche Transformation von Werten durchzuführen, die $setUnion
um sie zu einer einzigartigen Liste zu machen. Dann der $size
-Operator angewendet, um herauszufinden, wie groß diese Liste ist. Die zusätzliche Mathematik soll dieser Zahl eine Bedeutung verleihen, wenn sie auf die bereits aufgezeichneten Gewichtungen aus den vorherigen Ergebnissen angewendet wird.
Natürlich müssen Sie all dies irgendwie zusammenbringen, da es im Moment nur zwei unterschiedliche Ergebnissätze gibt. Der grundlegende Prozess ist eine „Hash-Tabelle“, in der die eindeutigen „Künstler“-ID-Werte als Schlüssel verwendet und die „Gewicht“-Werte kombiniert werden.
Sie können dies auf verschiedene Arten tun, aber da der Wunsch besteht, die kombinierten Ergebnisse zu "sortieren", würde ich etwas "MongoDBish" bevorzugen, da es den grundlegenden Methoden folgt, an die Sie bereits gewöhnt sein sollten.
Eine praktische Möglichkeit, dies zu implementieren, ist die Verwendung von nedb
, der einen Speicher im Arbeitsspeicher bereitstellt, der weitgehend die gleichen Methoden verwendet wie zum Lesen und Schreiben in MongoDB-Sammlungen.
Dies lässt sich auch gut skalieren, wenn Sie eine tatsächliche Sammlung für große Ergebnisse verwenden müssen, da alle Prinzipien gleich bleiben.
-
Der erste Aggregationsvorgang fügt neue Daten in den Speicher ein
-
Die zweite Aggregation "aktualisiert" diese Daten und erhöht das "Gewicht"-Feld
Als vollständiges Funktionslisting und mit etwas anderer Hilfe des async
Bibliothek würde es so aussehen:
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);
}
);
});
}
Nach dem Abgleich mit dem anfänglichen Quellbenutzerobjekt werden die Werte also an die erste Aggregatfunktion übergeben, die nacheinander ausgeführt wird und async.waterfall
verwendet um das Ergebnis zu übergeben.
Zuvor werden jedoch die Aggregationsergebnisse dem DataStore
hinzugefügt mit normalem .insert()
Anweisungen und achten Sie darauf, _id
umzubenennen Felder als nedb
mag nichts anderes als seine eigene selbst generierte _id
Werte. Jedes Ergebnis wird mit artist_id
eingefügt und weight
Eigenschaften aus dem Aggregationsergebnis.
Diese Liste wird dann an die zweite Aggregationsoperation weitergegeben, die jeden angegebenen "Künstler" mit einem berechneten "Gewicht" basierend auf der unterschiedlichen Benutzergröße zurückgibt. Es gibt die "updated" mit dem gleichen .update()
Anweisung auf dem DataStore
für jeden Künstler und Erhöhen des "Gewichts"-Felds.
Wenn alles gut geht, ist die letzte Operation .find()
diese Ergebnisse und .sort()
sie durch das kombinierte "Gewicht" und geben Sie das Ergebnis einfach an den übergebenen Rückruf an die Funktion zurück.
Sie würden es also folgendermaßen verwenden:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
Und es wird alle Künstler zurückgeben, die sich derzeit nicht in der Liste dieses Benutzers, aber in ihren Freundeslisten befinden, und geordnet nach der kombinierten Gewichtung der Anzahl der Freunde, die zuhören, plus der Punktzahl aus der Anzahl unterschiedlicher Benutzer dieses Künstlers.
So gehen Sie mit Daten aus zwei verschiedenen Sammlungen um, die Sie zu einem einzigen Ergebnis mit verschiedenen aggregierten Details kombinieren müssen. Es sind mehrere Abfragen und ein Arbeitsbereich, aber auch Teil der MongoDB-Philosophie, dass solche Operationen besser auf diese Weise ausgeführt werden, als sie auf die Datenbank zu werfen, um Ergebnisse zu "verknüpfen".