Tóm tắt TLDR
Trong các bản phát hành MongoDB hiện đại, bạn có thể thực hiện điều này bằng $slice
chỉ ra khỏi kết quả tổng hợp cơ bản. Đối với các kết quả "lớn", hãy chạy các truy vấn song song thay cho mỗi nhóm (danh sách minh họa ở cuối câu trả lời) hoặc đợi SERVER-9377 giải quyết, điều này sẽ cho phép "giới hạn" số lượng mục đối với $push
vào một mảng.
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$project": {
"books": { "$slice": [ "$books", 2 ] },
"count": 1
}}
])
Bản xem trước MongoDB 3.6
Vẫn không giải quyết được SERVER-9377, nhưng trong bản phát hành này $lookup
cho phép tùy chọn "không tương quan" mới có "pipeline"
biểu thức dưới dạng đối số thay vì "localFields"
và "foreignFields"
tùy chọn. Sau đó, điều này cho phép "tự tham gia" với một biểu thức đường ống khác, trong đó chúng tôi có thể áp dụng $limit
để trả về kết quả "top-n".
db.books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$lookup": {
"from": "books",
"let": {
"addr": "$_id"
},
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$addr", "$$addr"] }
}},
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
],
"as": "books"
}}
])
Sự bổ sung khác ở đây tất nhiên là khả năng nội suy biến thông qua $expr
sử dụng $match
để chọn các mục phù hợp trong "nối", nhưng tiền đề chung là "đường dẫn trong đường ống" nơi nội dung bên trong có thể được lọc theo các kết quả phù hợp từ mục gốc. Vì bản thân chúng đều là "đường ống" nên chúng ta có thể $limit
từng kết quả riêng biệt.
Đây sẽ là lựa chọn tốt nhất tiếp theo để chạy các truy vấn song song và thực sự sẽ tốt hơn nếu $match
đã được phép và có thể sử dụng một chỉ mục trong quá trình xử lý "đường dẫn phụ". Vì vậy, cái nào không sử dụng "giới hạn cho $push
"như vấn đề được tham chiếu yêu cầu, nó thực sự mang lại thứ gì đó sẽ hoạt động tốt hơn.
Nội dung gốc
Có vẻ như bạn đã vấp phải vấn đề "N" hàng đầu. Theo một cách nào đó, vấn đề của bạn khá dễ giải quyết mặc dù không có giới hạn chính xác mà bạn yêu cầu:
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
])
Bây giờ sẽ cho bạn một kết quả như thế này:
{
"result" : [
{
"_id" : "address1",
"books" : [
{
"book" : "book4",
"count" : 1
},
{
"book" : "book5",
"count" : 1
},
{
"book" : "book1",
"count" : 3
}
],
"count" : 5
},
{
"_id" : "address2",
"books" : [
{
"book" : "book5",
"count" : 1
},
{
"book" : "book1",
"count" : 2
}
],
"count" : 3
}
],
"ok" : 1
}
Vì vậy, điều này khác với những gì bạn đang yêu cầu, trong khi chúng tôi nhận được kết quả hàng đầu cho các giá trị địa chỉ, lựa chọn "sách" cơ bản không chỉ giới hạn ở một lượng kết quả bắt buộc.
Điều này hóa ra là rất khó thực hiện, nhưng nó có thể được thực hiện mặc dù độ phức tạp chỉ tăng lên với số lượng các mục bạn cần kết hợp. Để đơn giản, chúng ta có thể giữ điều này tối đa là 2 trận đấu:
db.books.aggregate([
{ "$group": {
"_id": {
"addr": "$addr",
"book": "$book"
},
"bookCount": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.addr",
"books": {
"$push": {
"book": "$_id.book",
"count": "$bookCount"
},
},
"count": { "$sum": "$bookCount" }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$unwind": "$books" },
{ "$sort": { "count": 1, "books.count": -1 } },
{ "$group": {
"_id": "$_id",
"books": { "$push": "$books" },
"count": { "$first": "$count" }
}},
{ "$project": {
"_id": {
"_id": "$_id",
"books": "$books",
"count": "$count"
},
"newBooks": "$books"
}},
{ "$unwind": "$newBooks" },
{ "$group": {
"_id": "$_id",
"num1": { "$first": "$newBooks" }
}},
{ "$project": {
"_id": "$_id",
"newBooks": "$_id.books",
"num1": 1
}},
{ "$unwind": "$newBooks" },
{ "$project": {
"_id": "$_id",
"num1": 1,
"newBooks": 1,
"seen": { "$eq": [
"$num1",
"$newBooks"
]}
}},
{ "$match": { "seen": false } },
{ "$group":{
"_id": "$_id._id",
"num1": { "$first": "$num1" },
"num2": { "$first": "$newBooks" },
"count": { "$first": "$_id.count" }
}},
{ "$project": {
"num1": 1,
"num2": 1,
"count": 1,
"type": { "$cond": [ 1, [true,false],0 ] }
}},
{ "$unwind": "$type" },
{ "$project": {
"books": { "$cond": [
"$type",
"$num1",
"$num2"
]},
"count": 1
}},
{ "$group": {
"_id": "$_id",
"count": { "$first": "$count" },
"books": { "$push": "$books" }
}},
{ "$sort": { "count": -1 } }
])
Vì vậy, điều đó thực sự sẽ cung cấp cho bạn 2 "cuốn sách" hàng đầu từ hai mục "địa chỉ" hàng đầu.
Nhưng đối với tiền của tôi, hãy ở lại với biểu mẫu đầu tiên và sau đó chỉ cần "cắt" các phần tử của mảng được trả về để lấy các phần tử "N" đầu tiên.
Mã trình diễn
Mã trình diễn thích hợp để sử dụng với các phiên bản LTS hiện tại của NodeJS từ các bản phát hành v8.x và v10.x. Đó là chủ yếu cho async/await
cú pháp, nhưng không có gì thực sự trong quy trình chung có bất kỳ hạn chế nào như vậy và điều chỉnh với một chút thay đổi đối với các lời hứa đơn giản hoặc thậm chí quay trở lại triển khai gọi lại đơn giản.
index.js
const { MongoClient } = require('mongodb');
const fs = require('mz/fs');
const uri = 'mongodb://localhost:27017';
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const client = await MongoClient.connect(uri);
const db = client.db('bookDemo');
const books = db.collection('books');
let { version } = await db.command({ buildInfo: 1 });
version = parseFloat(version.match(new RegExp(/(?:(?!-).)*/))[0]);
// Clear and load books
await books.deleteMany({});
await books.insertMany(
(await fs.readFile('books.json'))
.toString()
.replace(/\n$/,"")
.split("\n")
.map(JSON.parse)
);
if ( version >= 3.6 ) {
// Non-correlated pipeline with limits
let result = await books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 },
{ "$lookup": {
"from": "books",
"as": "books",
"let": { "addr": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$addr", "$$addr" ] }
}},
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 },
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]
}}
]).toArray();
log({ result });
}
// Serial result procesing with parallel fetch
// First get top addr items
let topaddr = await books.aggregate([
{ "$group": {
"_id": "$addr",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]).toArray();
// Run parallel top books for each addr
let topbooks = await Promise.all(
topaddr.map(({ _id: addr }) =>
books.aggregate([
{ "$match": { addr } },
{ "$group": {
"_id": "$book",
"count": { "$sum": 1 }
}},
{ "$sort": { "count": -1 } },
{ "$limit": 2 }
]).toArray()
)
);
// Merge output
topaddr = topaddr.map((d,i) => ({ ...d, books: topbooks[i] }));
log({ topaddr });
client.close();
} catch(e) {
console.error(e)
} finally {
process.exit()
}
})()
books.json
{ "addr": "address1", "book": "book1" }
{ "addr": "address2", "book": "book1" }
{ "addr": "address1", "book": "book5" }
{ "addr": "address3", "book": "book9" }
{ "addr": "address2", "book": "book5" }
{ "addr": "address2", "book": "book1" }
{ "addr": "address1", "book": "book1" }
{ "addr": "address15", "book": "book1" }
{ "addr": "address9", "book": "book99" }
{ "addr": "address90", "book": "book33" }
{ "addr": "address4", "book": "book3" }
{ "addr": "address5", "book": "book1" }
{ "addr": "address77", "book": "book11" }
{ "addr": "address1", "book": "book1" }