Anda perlu melakukan beberapa hal di sini untuk hasil akhir Anda, tetapi tahap pertama relatif sederhana. Ambil objek pengguna yang Anda berikan:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Sekarang anggaplah Anda sudah memiliki data yang diambil, maka ini turun untuk menemukan struktur yang sama untuk setiap "teman" dan memfilter konten array "Artis" ke dalam satu daftar yang berbeda. Agaknya setiap "bobot" juga akan dipertimbangkan secara total di sini.
Ini adalah operasi agregasi sederhana yang pertama-tama akan menyaring artis yang sudah ada dalam daftar untuk pengguna tertentu:
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
}
);
"Pra-filter" adalah satu-satunya bagian yang sangat rumit di sini. Anda cukup $unwind
array dan $match
lagi untuk menyaring entri yang tidak Anda inginkan. Meskipun kami ingin $unwind
hasilnya nanti untuk menggabungkannya, akan lebih efisien untuk menghapusnya dari larik "pertama", jadi lebih sedikit untuk memperluasnya.
Jadi di sini $map
operator memungkinkan pemeriksaan setiap elemen larik "Artis" pengguna dan juga untuk perbandingan dengan daftar artis "pengguna" yang difilter untuk hanya mengembalikan detail yang diinginkan. $setDifference
digunakan untuk benar-benar "memfilter" hasil apa pun yang tidak dikembalikan sebagai konten array, melainkan dikembalikan sebagai false
.
Setelah itu tinggal $unwind
untuk mendenormalisasi konten dalam larik dan $group
untuk mengumpulkan total per artis. Untuk bersenang-senang kami menggunakan $sort
untuk menunjukkan bahwa daftar dikembalikan dalam urutan yang diinginkan, tetapi itu tidak diperlukan pada tahap selanjutnya.
Itu setidaknya sebagian dari jalan di sini karena daftar yang dihasilkan seharusnya hanya artis lain yang belum ada dalam daftar pengguna sendiri, dan diurutkan berdasarkan "bobot" yang dijumlahkan dari artis mana pun yang mungkin dapat muncul di beberapa teman.
Bagian selanjutnya akan membutuhkan data dari koleksi "artis" untuk memperhitungkan jumlah pendengar. Sementara luwak memiliki .populate()
metode, Anda benar-benar tidak menginginkan ini di sini karena Anda mencari jumlah "pengguna berbeda". Ini menyiratkan penerapan agregasi lain untuk mendapatkan jumlah yang berbeda untuk setiap artis.
Mengikuti dari daftar hasil operasi agregasi sebelumnya, Anda akan menggunakan $_id
nilai seperti ini:
// 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
}
);
Di sini trik dilakukan secara agregat dengan $map
untuk melakukan transformasi nilai serupa yang diumpankan ke $setUnion
untuk membuat mereka daftar yang unik. Kemudian $size
operator diterapkan untuk mengetahui seberapa besar daftar itu. Matematika tambahan adalah untuk memberikan angka itu beberapa arti ketika diterapkan terhadap bobot yang sudah tercatat dari hasil sebelumnya.
Tentu saja Anda perlu menyatukan semua ini, karena saat ini hanya ada dua set hasil yang berbeda. Proses dasarnya adalah "Tabel Hash", di mana nilai id "artis" unik digunakan sebagai kunci dan nilai "bobot" digabungkan.
Anda dapat melakukan ini dengan beberapa cara, tetapi karena ada keinginan untuk "mengurutkan" hasil gabungan, maka preferensi saya adalah sesuatu "MongoDBish" karena mengikuti metode dasar yang seharusnya sudah Anda gunakan.
Cara praktis untuk menerapkan ini adalah menggunakan nedb
, yang menyediakan penyimpanan "dalam memori" yang menggunakan banyak jenis metode yang sama seperti yang digunakan untuk membaca dan menulis ke koleksi MongoDB.
Ini juga dapat diskalakan dengan baik jika Anda perlu menggunakan koleksi aktual untuk hasil yang besar, karena semua prinsipnya tetap sama.
-
Operasi agregasi pertama memasukkan data baru ke penyimpanan
-
Agregasi kedua "memperbarui" data itu dan menambah bidang "bobot"
Sebagai daftar fungsi yang lengkap, dan dengan bantuan lain dari async
perpustakaan akan terlihat seperti ini:
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);
}
);
});
}
Jadi setelah mencocokkan objek pengguna sumber awal, nilai diteruskan ke fungsi agregat pertama, yang dijalankan secara seri dan menggunakan async.waterfall
untuk menyampaikan hasilnya.
Sebelum itu terjadi meskipun hasil agregasi ditambahkan ke DataStore
dengan .insert()
biasa pernyataan, berhati-hatilah untuk mengganti nama _id
bidang sebagai nedb
tidak menyukai apa pun selain _id
yang dihasilkan sendiri nilai-nilai. Setiap hasil disisipkan dengan artist_id
dan weight
properti dari hasil agregasi.
Daftar itu kemudian diteruskan ke operasi agregasi kedua yang akan mengembalikan setiap "artis" yang ditentukan dengan "bobot" yang dihitung berdasarkan ukuran pengguna yang berbeda. Ada yang "diperbarui" dengan .update()
yang sama pernyataan pada DataStore
untuk setiap artis dan menambah bidang "berat".
Semua berjalan dengan baik, operasi terakhir adalah .find()
hasil tersebut dan .sort()
mereka dengan gabungan "bobot", dan cukup kembalikan hasilnya ke panggilan balik yang diteruskan ke fungsi.
Jadi Anda akan menggunakannya seperti ini:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
Dan itu akan mengembalikan semua artis yang saat ini tidak ada di daftar pengguna itu tetapi di daftar teman mereka dan diurutkan berdasarkan bobot gabungan dari jumlah teman yang mendengarkan ditambah skor dari jumlah pengguna berbeda dari artis itu.
Ini adalah cara Anda menangani data dari dua koleksi berbeda yang perlu Anda gabungkan menjadi satu hasil dengan berbagai detail agregat. Ini adalah beberapa kueri dan ruang kerja, tetapi juga bagian dari filosofi MongoDB bahwa operasi semacam itu lebih baik dilakukan dengan cara ini daripada melemparkannya ke database untuk "bergabung" dengan hasil.