TLDR Özeti
Modern MongoDB sürümlerinde $slice
, temel toplama sonucunun hemen dışında bunu kaba kuvvet uygulayabilirsiniz. "Büyük" sonuçlar için, her gruplama yerine paralel sorguları çalıştırın (listeleyen bir gösteri cevap sonundadır) ya bekleyin SERVER-9377 gidermek için, öğelerin sayısı için bir "sınır" izin verecek $push
bir karşı dizi.
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
}}
])
MongoDB 3.6 Önizleme
Hala SERVER-9377'yi çözmüyor , ancak bu sürümde $lookup
bir "pipeline"
ifadeyi "localFields"
ve "foreignFields"
seçenekleri yerine argüman olarak alan yeni bir "ilişkisiz" seçeneğe izin veriyor . Bu daha sonra $limit
"top-n" sonuçlarını döndürmek için başvurabileceğimiz başka bir ardışık düzen ifadesi ile "kendi kendine birleşmeye" izin verir .
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"
}}
])
Buradaki diğer ek, elbette, "birleşim" içindeki eşleşen öğeleri seçmek için $expr
kullanarak değişkeni ara değerleme yeteneğidir $match
, ancak genel öncül, iç içeriğin üst öğeden eşleşmelerle filtrelenebildiği "bir ardışık düzen içinde bir ardışık düzen" dir. . Her ikisi de "boru hattı" olduklarından, $limit
her biri ayrı ayrı sonuçlanabilir.
Bu, paralel sorguları çalıştırmak için bir sonraki en iyi seçenek olacaktır ve aslında $match
"alt boru hattı" işleminde bir indeks kullanmasına izin verilseydi ve mümkün olsaydı daha iyi olurdu . Dolayısıyla $push
, başvurulan sorunun sorduğu gibi "sınırla" seçeneğini kullanmayan, aslında daha iyi çalışması gereken bir şey sunar.
Orijinal İçerik
Görünüşe göre ilk "N" problemine rastlamışsınız. Bir bakıma probleminizin çözülmesi oldukça kolaydır, ancak tam olarak istediğiniz sınırlamayla değil:
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 }
])
Şimdi bu size şöyle bir sonuç verecektir:
{
"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
}
Dolayısıyla, bu sizin sorduğunuzdan farklıdır, ancak adres değerleri için en iyi sonuçları alırken, temel alınan "kitaplar" seçimi yalnızca gerekli sonuç miktarı ile sınırlı değildir.
Bunu yapmak çok zor görünüyor, ancak karmaşıklık eşleştirmeniz gereken öğelerin sayısı ile artsa da yapılabilir. Basitleştirmek için bunu en fazla 2 maçta tutabiliriz:
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 } }
])
Bu size aslında ilk iki "adres" girişinden ilk 2 "kitabı" verecektir.
Ama param için, ilk formda kalın ve sonra ilk "N" elemanlarını almak için döndürülen dizinin elemanlarını basitçe "dilimleyin".
Gösteri Kodu
Gösterim kodu, v8.x ve v10.x sürümlerinden NodeJS'nin mevcut LTS sürümleriyle kullanım için uygundur. Bu çoğunlukla async/await
sözdizimi içindir, ancak genel akış içinde böyle bir kısıtlamaya sahip olan ve düz vaatlere veya hatta basit geri arama uygulamasına geri dönüldüğünde çok az değişiklik yaparak uyum sağlayan hiçbir şey yoktur.
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" }