我有以下架构的待售物品集合:
var itemSchema = new Schema({
"category" : { type : Schema.Types.ObjectId, ref : 'Category' },
"merchant" : { type : Schema.Types.ObjectId, ref : 'Merchant' },
"rating" : Number
})
我继承了一个聚合查询,该查询返回与按类别划分的项目,按商家分组,并按组中的最大评级排序:
Item.aggregate([
{ "$match" : { category : categoryId, merchant : { $ne : null }}},
{ "$group" : { _id : "$merchant",
rating : { $max : "$rating" },
items : { $push : "$$ROOT" }}},
{ "$sort" : { rating : -1 }}
], { allowDiskUse : true })
.skip(skip)
.limit(limit)
在此之后,代码继续按评分对每个组中的项目进行排序,并删除除每个组中排名前2位的最高项目之外的所有项目。
是否可以在群组中执行此排序和限制作为聚合函数的一部分,以便聚合只返回每个组中评价最高的两个项目?
答案 0 :(得分:11)
在可预见的不久的将来,在当前的聚合框架中试图做到这一点并不是最明智的想法。当然,主要问题来自您已经拥有的代码中的这一行:
"items" : { "$push": "$$ROOT" }
这正是意味着,需要基本上发生的是分组键中的所有对象都需要被推入一个数组,以便在以后的任何代码中获得“前N个”结果。
这显然无法扩展,因为该数组本身的大小可能非常可能超过16MB的BSON限制,并且与分组文档中的其余数据无关。这里的主要问题是不可能“限制推动”仅限于一定数量的项目。就这样的事情有一个long standing JIRA issue。
仅凭这个原因,最实用的方法是针对每个分组键对“前N个”项运行单独查询。这些甚至不需要.aggregate()
个数据(取决于数据),并且可以真正地只是限制你想要的“前N个”值。
您的体系结构似乎位于node.js
mongoose
上,但支持异步IO和并行执行查询的任何内容都将是最佳选择。理想情况下,它拥有自己的API库,支持将这些查询的结果合并到一个响应中。
例如,这个简化的示例列表使用您的架构和可用的库(特别是async
)来完成并行和组合结果:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var data = [
{ "merchant": 1, "rating": 1 },
{ "merchant": 1, "rating": 2 },
{ "merchant": 1, "rating": 3 },
{ "merchant": 2, "rating": 1 },
{ "merchant": 2, "rating": 2 },
{ "merchant": 2, "rating": 3 }
];
var testSchema = new Schema({
merchant: Number,
rating: Number
});
var Test = mongoose.model( 'Test', testSchema, 'test' );
async.series(
[
function(callback) {
Test.remove({},callback);
},
function(callback) {
async.each(data,function(item,callback) {
Test.create(item,callback);
},callback);
},
function(callback) {
async.waterfall(
[
function(callback) {
Test.distinct("merchant",callback);
},
function(merchants,callback) {
async.concat(
merchants,
function(merchant,callback) {
Test.find({ "merchant": merchant })
.sort({ "rating": -1 })
.limit(2)
.exec(callback);
},
function(err,results) {
console.log(JSON.stringify(results,undefined,2));
callback(err);
}
);
}
],
callback
);
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
这导致输出中每个商家的前两个结果:
[
{
"_id": "560d153669fab495071553ce",
"merchant": 1,
"rating": 3,
"__v": 0
},
{
"_id": "560d153669fab495071553cd",
"merchant": 1,
"rating": 2,
"__v": 0
},
{
"_id": "560d153669fab495071553d1",
"merchant": 2,
"rating": 3,
"__v": 0
},
{
"_id": "560d153669fab495071553d0",
"merchant": 2,
"rating": 2,
"__v": 0
}
]
它确实是最有效的处理方式,虽然它会占用资源,因为它仍然是多个查询。但是,如果您尝试将所有文档存储在数组中并进行处理,那么聚合管道中的资源就会消失。
就此而言,可以考虑到文件数量不会导致BSON限制的违反,这可以做到这一点。当前版本的MongoDB的方法并不是很好,但即将发布的版本(编写时,3.1.8 dev分支这样做)至少会将一个$slice
运算符引入聚合管道。因此,如果您更了解聚合操作并首先使用$sort
,那么可以轻松选择数组中已经排序的项目:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var data = [
{ "merchant": 1, "rating": 1 },
{ "merchant": 1, "rating": 2 },
{ "merchant": 1, "rating": 3 },
{ "merchant": 2, "rating": 1 },
{ "merchant": 2, "rating": 2 },
{ "merchant": 2, "rating": 3 }
];
var testSchema = new Schema({
merchant: Number,
rating: Number
});
var Test = mongoose.model( 'Test', testSchema, 'test' );
async.series(
[
function(callback) {
Test.remove({},callback);
},
function(callback) {
async.each(data,function(item,callback) {
Test.create(item,callback);
},callback);
},
function(callback) {
Test.aggregate(
[
{ "$sort": { "merchant": 1, "rating": -1 } },
{ "$group": {
"_id": "$merchant",
"items": { "$push": "$$ROOT" }
}},
{ "$project": {
"items": { "$slice": [ "$items", 2 ] }
}}
],
function(err,results) {
console.log(JSON.stringify(results,undefined,2));
callback(err);
}
);
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
这产生了与前2个项目相同的基本结果,一旦它们被排序,它们就会从数组中“切片”。
在当前版本中它实际上也是“可能的”,但是具有相同的基本约束,这仍然涉及在首先对内容进行排序之后将所有内容推送到数组中。它只需要一种“迭代”的方法。您可以对此进行编码以生成更多条目的聚合管道,但只显示“两个”应该表明尝试不是一个好主意:
var async = require('async'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.connect('mongodb://localhost/test');
var data = [
{ "merchant": 1, "rating": 1 },
{ "merchant": 1, "rating": 2 },
{ "merchant": 1, "rating": 3 },
{ "merchant": 2, "rating": 1 },
{ "merchant": 2, "rating": 2 },
{ "merchant": 2, "rating": 3 }
];
var testSchema = new Schema({
merchant: Number,
rating: Number
});
var Test = mongoose.model( 'Test', testSchema, 'test' );
async.series(
[
function(callback) {
Test.remove({},callback);
},
function(callback) {
async.each(data,function(item,callback) {
Test.create(item,callback);
},callback);
},
function(callback) {
Test.aggregate(
[
{ "$sort": { "merchant": 1, "rating": -1 } },
{ "$group": {
"_id": "$merchant",
"items": { "$push": "$$ROOT" }
}},
{ "$unwind": "$items" },
{ "$group": {
"_id": "$_id",
"first": { "$first": "$items" },
"items": { "$push": "$items" }
}},
{ "$unwind": "$items" },
{ "$redact": {
"$cond": [
{ "$eq": [ "$items", "$first" ] },
"$$PRUNE",
"$$KEEP"
]
}},
{ "$group": {
"_id": "$_id",
"first": { "$first": "$first" },
"second": { "$first": "$items" }
}},
{ "$project": {
"items": {
"$map": {
"input": ["A","B"],
"as": "el",
"in": {
"$cond": [
{ "$eq": [ "$$el", "A" ] },
"$first",
"$second"
]
}
}
}
}}
],
function(err,results) {
console.log(JSON.stringify(results,undefined,2));
callback(err);
}
);
}
],
function(err) {
if (err) throw err;
mongoose.disconnect();
}
);
再次在早期版本中“可能”(这是因为你已经标记$$ROOT
使用2.6引入的功能来缩短),基本步骤是存储数组,然后使用“堆栈”使每个项目“ $first
并将该(以及可能的其他)与数组中的项进行比较以删除它们,然后从该堆栈中获取“下一个第一”项,直到最终完成“前N”。
直到有一天这样的操作允许$push
聚合累加器中的项目被限制为某个计数,那么这对于聚合来说实际上并不是一个实际的操作。
如果这些结果中的数据足够小,您可以这样做,如果数据库服务器具有足够的规格来提供真正的优势,它甚至可能比客户端处理更有效。但是,在大多数合理使用的实际应用中,两者都不会出现这种情况。
最好的办法是先使用“并行查询”选项。它总是可以很好地扩展,并且不需要“编码”这样的逻辑,即特定的分组可能不会返回至少所需的总“前N”项并找出如何保留它们(更长的例子省略了)因为它只是执行每个查询并组合结果。
使用并行查询。它将比你拥有的编码方法更好,并且它将超越长期证明的聚合方法。直到有一个更好的选择。
答案 1 :(得分:0)
我所做的只是$push
_id
,然后添加$slice
。
$group: {
_id: "$merchant",
items : { $push : "$_id" }
},
// ...
$project: {
items: {$slice: ["$items", offset, limit]}
}
在查询之后我填充结果:
return Item.populate(result, {path: 'items'})