Bạn cần thực hiện một số điều ở đây để có kết quả cuối cùng, nhưng các giai đoạn đầu tiên tương đối đơn giản. Lấy đối tượng người dùng mà bạn cung cấp:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
Bây giờ, giả sử bạn đã truy xuất dữ liệu đó, sau đó điều này đi đến việc tìm các cấu trúc giống nhau cho từng "người bạn" và lọc ra nội dung mảng của "Nghệ sĩ" thành một danh sách riêng biệt. Có lẽ mỗi "trọng lượng" cũng sẽ được xem xét tổng cộng ở đây.
Đây là một hoạt động tổng hợp mô phỏng, trước tiên sẽ lọc ra các nghệ sĩ đã có trong danh sách cho người dùng nhất định:
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
}
);
"Bộ lọc trước" là phần thực sự khó khăn duy nhất ở đây. Bạn chỉ có thể $unwind
mảng và $match
một lần nữa để lọc ra các mục bạn không muốn. Mặc dù chúng tôi muốn $unwind
các kết quả sau đó để kết hợp chúng với nhau, việc xóa chúng khỏi mảng "đầu tiên" sẽ hoạt động hiệu quả hơn, vì vậy sẽ ít phải mở rộng hơn.
Vì vậy, đây là $map
toán tử cho phép kiểm tra từng phần tử của mảng "Nghệ sĩ" người dùng và cũng để so sánh với danh sách nghệ sĩ "người dùng" đã lọc để chỉ trả về các chi tiết mong muốn. $setDifference
được sử dụng để thực sự "lọc" bất kỳ kết quả nào không được trả về dưới dạng nội dung mảng mà được trả về dưới dạng false
.
Sau đó chỉ có $unwind
để hủy chuẩn hóa nội dung trong mảng và nhóm $group
để mang lại tổng số cho mỗi nghệ sĩ. Để giải trí, chúng tôi đang sử dụng $sort
để cho thấy rằng danh sách được trả về theo thứ tự mong muốn, nhưng điều đó sẽ không cần thiết ở giai đoạn sau.
Đó ít nhất là một phần của chặng đường ở đây vì danh sách kết quả chỉ nên là những nghệ sĩ khác chưa có trong danh sách của chính người dùng và được sắp xếp theo "trọng số" tổng hợp từ bất kỳ nghệ sĩ nào có thể xuất hiện trên nhiều bạn bè.
Phần tiếp theo sẽ cần dữ liệu từ bộ sưu tập "nghệ sĩ" để tính đến số lượng người nghe. Trong khi mongoose có .populate()
, bạn thực sự không muốn điều này ở đây vì bạn đang tìm kiếm số lượng "người dùng riêng biệt". Điều này ngụ ý một triển khai tổng hợp khác để có được những số lượng riêng biệt đó cho từng nghệ sĩ.
Tiếp theo từ danh sách kết quả của thao tác tổng hợp trước đó, bạn sẽ sử dụng $_id
các giá trị như thế này:
// 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
}
);
Đây là thủ thuật được thực hiện tổng hợp với $map
để thực hiện một biến đổi tương tự của các giá trị được cung cấp cho $setUnion
để biến chúng thành một danh sách duy nhất. Sau đó, $size
toán tử được áp dụng để tìm ra danh sách đó lớn như thế nào. Phép toán bổ sung là cung cấp cho con số đó một số ý nghĩa khi áp dụng với các trọng số đã được ghi lại từ các kết quả trước đó.
Tất nhiên bạn cần phải kết hợp tất cả những điều này lại với nhau bằng cách nào đó, vì hiện tại chỉ có hai bộ kết quả khác nhau. Quy trình cơ bản là "Bảng băm", trong đó các giá trị id "nghệ sĩ" duy nhất được sử dụng làm khóa và các giá trị "trọng số" được kết hợp.
Bạn có thể thực hiện việc này theo một số cách, nhưng vì có mong muốn "sắp xếp" các kết quả được kết hợp nên ưu tiên của tôi sẽ là "MongoDBish" vì nó tuân theo các phương pháp cơ bản mà bạn đã từng sử dụng.
Một cách hữu ích để triển khai điều này là sử dụng nedb
, cung cấp một kho lưu trữ "trong bộ nhớ" sử dụng nhiều phương thức giống như được sử dụng để đọc và ghi vào các bộ sưu tập MongoDB.
Điều này cũng có quy mô tốt nếu bạn cần sử dụng một bộ sưu tập thực tế để có kết quả lớn, vì tất cả các nguyên tắc vẫn như cũ.
-
Thao tác tổng hợp đầu tiên sẽ chèn dữ liệu mới vào cửa hàng
-
Tổng hợp thứ hai "cập nhật" dữ liệu đó làm tăng trường "trọng số"
Dưới dạng danh sách chức năng đầy đủ và với một số trợ giúp khác của async
thư viện nó sẽ giống như thế này:
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);
}
);
});
}
Vì vậy, sau khi khớp với đối tượng người dùng nguồn ban đầu, các giá trị được chuyển vào hàm tổng hợp đầu tiên, hàm này đang thực thi theo chuỗi và sử dụng async.waterfall
để vượt qua kết quả.
Trước khi điều đó xảy ra, mặc dù kết quả tổng hợp được thêm vào DataStore
với .insert()
thông thường , chú ý đổi tên _id
các trường dưới dạng nedb
không thích bất cứ thứ gì khác ngoài _id
do chính nó tạo ra các giá trị. Mỗi kết quả được chèn bằng artist_id
và weight
thuộc tính từ kết quả tổng hợp.
Sau đó, danh sách đó được chuyển đến thao tác tổng hợp thứ hai sẽ trả về từng "nghệ sĩ" được chỉ định với "trọng lượng" được tính toán dựa trên quy mô người dùng riêng biệt. Có "cập nhật" với cùng một .update()
trên DataStore
cho từng nghệ sĩ và tăng trường "trọng số".
Mọi việc suôn sẻ, thao tác cuối cùng là .find()
các kết quả đó và .sort()
chúng bằng "trọng số" được kết hợp và chỉ cần trả lại kết quả cho hàm được truyền trong lệnh gọi lại.
Vì vậy, bạn sẽ sử dụng nó như thế này:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
Và nó sẽ trả về tất cả các nghệ sĩ hiện không có trong danh sách của người dùng đó nhưng trong danh sách bạn bè của họ và được sắp xếp theo trọng số tổng hợp của số lượt nghe của bạn bè cộng với điểm số từ số lượng người dùng riêng biệt của nghệ sĩ đó.
Đây là cách bạn xử lý dữ liệu từ hai bộ sưu tập khác nhau mà bạn cần kết hợp thành một kết quả duy nhất với các chi tiết tổng hợp khác nhau. Đó là nhiều truy vấn và một không gian làm việc, nhưng cũng là một phần của triết lý MongoDB rằng các hoạt động như vậy được thực hiện theo cách này tốt hơn so với việc ném chúng vào cơ sở dữ liệu để "kết hợp" kết quả.