最終結果を得るには、ここでいくつかのことを行う必要がありますが、最初の段階は比較的単純です。提供するユーザーオブジェクトを取得します:
var user = {
user_id : 1,
Friends : [3,5,6],
Artists : [
{artist_id: 10 , weight : 345},
{artist_id: 17 , weight : 378}
]
};
すでにそのデータが取得されていると仮定すると、これは、「友達」ごとに同じ構造を見つけ、「アーティスト」の配列コンテンツを1つの別個のリストに除外することになります。おそらく、ここでは各「重量」も合計で考慮されます。
これは、特定のユーザーのリストにすでに含まれているアーティストを最初に除外する単純な集計操作です。
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
}
);
ここで本当に注意が必要なのは「プレフィルター」だけです。 $unwind
配列と$match
もう一度、不要なエントリを除外します。 $unwind
したいのに 後で結果を結合するために、それらを「最初に」配列から削除する方が効率的であるため、拡張する必要が少なくなります。
したがって、ここでは $map
演算子を使用すると、ユーザーの「Artists」配列の各要素を検査したり、フィルター処理された「user」アーティストリストと比較して必要な詳細を返すことができます。 $setDifference
配列コンテンツとして返されなかった結果を実際に「フィルタリング」するために使用されますが、false
として返されます。 。
その後、$unwind
だけがあります 配列と$group
アーティストごとの合計をまとめます。楽しみのために、$sort
を使用しています リストが希望の順序で返されることを示しますが、後の段階では必要ありません。
結果のリストは、ユーザー自身のリストにまだ含まれていない他のアーティストのみであり、複数の友達に表示される可能性のあるアーティストの「重み」の合計で並べ替えられるため、これは少なくともここでの途中です。
次の部分では、リスナーの数を考慮に入れるために、「アーティスト」コレクションからのデータが必要になります。マングースには.populate()
があります メソッドでは、「個別のユーザー」の数を探しているので、ここではこれは本当に必要ありません。これは、アーティストごとに個別のカウントを取得するための別の集計実装を意味します。
前の集計操作の結果リストに続いて、$_id
を使用します このような値:
// 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
}
);
ここでは、トリックは$map
を使用してまとめて行われます。 $setUnion
それらを一意のリストにします。次に、 $size
そのリストの大きさを調べるために演算子が適用されます。追加の計算は、前の結果からすでに記録された重みに対して適用されたときに、その数値に何らかの意味を与えることです。
もちろん、これらすべてを何らかの方法でまとめる必要があります。現在、2つの異なる結果セットしかないためです。基本的なプロセスは「ハッシュテーブル」であり、一意の「アーティスト」ID値がキーとして使用され、「重み」値が組み合わされます。
これはさまざまな方法で実行できますが、組み合わせた結果を「並べ替え」たいという要望があるため、すでに慣れている基本的な方法に従っているため、「MongoDBish」を使用することをお勧めします。
これを実装する便利な方法は、 nedb
を使用することです。
、MongoDBコレクションの読み取りと書き込みに使用されるのと同じタイプのメソッドの多くを使用する「メモリ内」ストアを提供します。
すべての原則は同じままであるため、大きな結果を得るために実際のコレクションを使用する必要がある場合にも、これは適切に拡張されます。
-
最初の集計操作により、ストアに新しいデータが挿入されます
-
2番目の集計では、データが「重み」フィールドを増分して「更新」します
完全な関数リストとして、および async
のその他の助けを借りて
ライブラリは次のようになります:
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);
}
);
});
}
したがって、最初のソースユーザーオブジェクトを照合した後、値は最初の集計関数に渡されます。この集計関数は、連続して実行され、async.waterfall
を使用します。 結果を渡すために。
その前に、集計結果がDataStore
に追加されますが 通常の.insert()
を使用 ステートメント、_id
の名前を変更するように注意してください nedb
としてのフィールド 自分で生成した_id
以外は好きではありません 値。各結果はartist_id
で挿入されます およびweight
集計結果のプロパティ。
次に、そのリストは2番目の集計操作に渡され、指定された各「アーティスト」が、個別のユーザーサイズに基づいて計算された「重み」とともに返されます。同じ.update()
で「更新された」ものがあります DataStore
のステートメント アーティストごとに、「ウェイト」フィールドをインクリメントします。
すべて順調に進んでおり、最後の操作は.find()
です。 それらの結果と.sort()
それらを組み合わせた「重み」で表し、関数に渡されたコールバックに結果を返すだけです。
したがって、次のように使用します:
GetUserRecommendations(1,function(err,results) {
// results is the sorted list
});
そして、現在そのユーザーのリストではなく、友達のリストにあるすべてのアーティストを返し、友達のリスニングカウントとそのアーティストの個別のユーザーの数のスコアを組み合わせた重みで並べ替えます。
これは、さまざまな集計詳細を含む単一の結果に結合する必要がある2つの異なるコレクションからのデータを処理する方法です。複数のクエリと作業スペースですが、そのような操作はデータベースにスローして結果を「結合」するよりも、この方法で実行する方がよいというMongoDBの哲学の一部でもあります。