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 $slice
chỉ với kết quả tổng hợp cơ bản. Đối với các kết quả "lớn", thay vào đó hãy chạy các truy vấn song song 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 $push
với 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 một tùy chọn "không tương quan" mới lấy một "pipeline"
biểu thức làm đố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 ta 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
việc sử dụng $match
để chọn các mục phù hợp trong "kết hợp", 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 bằng các kết quả phù hợp từ nguồn gốc. . Vì bản thân chúng đều là "đường ống", chúng ta có thể $limit
cho mỗi kết quả một cách 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 ống con". Vì vậy, không sử dụng "giới hạn đối với $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ư sau:
{
"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 mục bạn cần kết hợp. Để đơn giản, chúng tôi có thể giữ tối đa 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" }