Modern
Dari MongoDB 3.6 ada pendekatan "baru" untuk ini dengan menggunakan $lookup
untuk melakukan "penggabungan mandiri" dengan cara yang sama seperti pemrosesan kursor asli yang ditunjukkan di bawah ini.
Karena dalam rilis ini Anda dapat menentukan "pipeline"
argumen ke $lookup
sebagai sumber untuk "bergabung", ini pada dasarnya berarti Anda dapat menggunakan $match
dan $limit
untuk mengumpulkan dan "membatasi" entri untuk larik:
db.messages.aggregate([
{ "$group": { "_id": "$conversation_ID" } },
{ "$lookup": {
"from": "messages",
"let": { "conversation": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$conversation_ID", "$$conversation" ] } }},
{ "$limit": 10 },
{ "$project": { "_id": 1 } }
],
"as": "msgs"
}}
])
Anda dapat secara opsional menambahkan proyeksi tambahan setelah $lookup
untuk membuat item array hanya nilai daripada dokumen dengan _id
kunci, tetapi hasil dasarnya ada hanya dengan melakukan hal di atas.
Masih ada SERVER-9277 yang luar biasa yang sebenarnya meminta "batas untuk mendorong" secara langsung, tetapi menggunakan $lookup
dengan cara ini merupakan alternatif yang layak untuk sementara.
CATATAN :Ada juga
$slice
yang diperkenalkan setelah menulis jawaban asli dan disebutkan oleh "masalah JIRA yang luar biasa" dalam konten aslinya. Meskipun Anda bisa mendapatkan hasil yang sama dengan kumpulan hasil kecil, hal itu tetap melibatkan "mendorong semuanya" ke dalam larik dan kemudian membatasi keluaran larik akhir ke panjang yang diinginkan.Jadi itulah perbedaan utama dan mengapa biasanya tidak praktis untuk
$slice
untuk hasil yang besar. Tapi tentu saja dapat digunakan secara bergantian dalam kasus di mana itu.Ada beberapa detail lebih lanjut tentang nilai grup mongodb menurut beberapa bidang tentang penggunaan alternatif.
Asli
Seperti yang dinyatakan sebelumnya, ini bukan tidak mungkin tetapi tentu saja masalah yang mengerikan.
Sebenarnya jika perhatian utama Anda adalah bahwa array yang dihasilkan akan menjadi sangat besar, maka pendekatan terbaik Anda adalah mengirimkan untuk setiap "conversation_ID" yang berbeda sebagai kueri individual dan kemudian menggabungkan hasil Anda. Dalam sintaksis MongoDB 2.6 yang mungkin memerlukan beberapa penyesuaian tergantung pada implementasi bahasa Anda sebenarnya:
var results = [];
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID"
}}
]).forEach(function(doc) {
db.messages.aggregate([
{ "$match": { "conversation_ID": doc._id } },
{ "$limit": 10 },
{ "$group": {
"_id": "$conversation_ID",
"msgs": { "$push": "$_id" }
}}
]).forEach(function(res) {
results.push( res );
});
});
Tapi itu semua tergantung pada apakah itu yang Anda coba hindari. Jadi ke jawaban sebenarnya:
Masalah pertama di sini adalah bahwa tidak ada fungsi untuk "membatasi" jumlah item yang "didorong" ke dalam array. Ini tentu sesuatu yang kami inginkan, tetapi fungsinya tidak ada saat ini.
Masalah kedua adalah bahwa meskipun memasukkan semua item ke dalam larik, Anda tidak dapat menggunakan $slice
, atau operator serupa dalam pipeline agregasi. Jadi tidak ada cara saat ini untuk mendapatkan hanya hasil "10 teratas" dari larik yang dihasilkan dengan operasi sederhana.
Tetapi Anda benar-benar dapat menghasilkan satu set operasi untuk secara efektif "mengiris" pada batas pengelompokan Anda. Ini cukup terlibat, dan misalnya di sini saya akan mengurangi elemen array "diiris" menjadi "enam" saja. Alasan utama di sini adalah untuk mendemonstrasikan proses dan menunjukkan bagaimana melakukan ini tanpa merusak dengan array yang tidak berisi total yang ingin Anda "potong".
Diberikan contoh dokumen:
{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 6, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 123 }
{ "_id" : 8, "conversation_ID" : 123 }
{ "_id" : 9, "conversation_ID" : 123 }
{ "_id" : 10, "conversation_ID" : 123 }
{ "_id" : 11, "conversation_ID" : 123 }
{ "_id" : 12, "conversation_ID" : 456 }
{ "_id" : 13, "conversation_ID" : 456 }
{ "_id" : 14, "conversation_ID" : 456 }
{ "_id" : 15, "conversation_ID" : 456 }
{ "_id" : 16, "conversation_ID" : 456 }
Anda dapat melihat di sana bahwa ketika mengelompokkan berdasarkan kondisi Anda, Anda akan mendapatkan satu larik dengan sepuluh elemen dan lainnya dengan "lima". Apa yang ingin Anda lakukan di sini mengurangi keduanya menjadi "enam" teratas tanpa "menghancurkan" larik yang hanya akan cocok dengan elemen "lima".
Dan pertanyaan berikut:
db.messages.aggregate([
{ "$group": {
"_id": "$conversation_ID",
"first": { "$first": "$_id" },
"msgs": { "$push": "$_id" },
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"seen": { "$eq": [ "$first", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"seen": { "$eq": [ "$second", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"seen": { "$eq": [ "$third", "$msgs" ] },
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"seen": { "$eq": [ "$forth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$msgs" }
}},
{ "$unwind": "$msgs" },
{ "$project": {
"msgs": 1,
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"seen": { "$eq": [ "$fifth", "$msgs" ] }
}},
{ "$sort": { "seen": 1 }},
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
},
"first": { "$first": "$first" },
"second": { "$first": "$second" },
"third": { "$first": "$third" },
"forth": { "$first": "$forth" },
"fifth": { "$first": "$fifth" },
"sixth": { "$first": "$msgs" },
}},
{ "$project": {
"first": 1,
"second": 1,
"third": 1,
"forth": 1,
"fifth": 1,
"sixth": 1,
"pos": { "$const": [ 1,2,3,4,5,6 ] }
}},
{ "$unwind": "$pos" },
{ "$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [
{ "$eq": [ "$pos", 1 ] },
"$first",
{ "$cond": [
{ "$eq": [ "$pos", 2 ] },
"$second",
{ "$cond": [
{ "$eq": [ "$pos", 3 ] },
"$third",
{ "$cond": [
{ "$eq": [ "$pos", 4 ] },
"$forth",
{ "$cond": [
{ "$eq": [ "$pos", 5 ] },
"$fifth",
{ "$cond": [
{ "$eq": [ "$pos", 6 ] },
"$sixth",
false
]}
]}
]}
]}
]}
]
}
}
}},
{ "$unwind": "$msgs" },
{ "$match": { "msgs": { "$ne": false } }},
{ "$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}}
])
Anda mendapatkan hasil teratas dalam larik, hingga enam entri:
{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }
Seperti yang Anda lihat di sini, sangat menyenangkan.
Setelah Anda mengelompokkan, pada dasarnya Anda ingin "meletakkan" $first
nilai dari tumpukan untuk hasil array. Untuk membuat proses ini sedikit disederhanakan, kami benar-benar melakukan ini di operasi awal. Sehingga prosesnya menjadi:
$unwind
susunan- Bandingkan dengan nilai yang sudah terlihat dengan
$eq
pertandingan kesetaraan $sort
hasilnya "mengambang"false
nilai tak terlihat ke atas ( ini masih mempertahankan urutan )$group
kembali lagi dan "letakkan"$first
nilai tak terlihat sebagai anggota berikutnya di tumpukan. Ini juga menggunakan$cond
operator untuk mengganti nilai "terlihat" di tumpukan array denganfalse
untuk membantu dalam evaluasi.
Tindakan terakhir dengan $cond
apakah ada untuk memastikan bahwa iterasi di masa depan tidak hanya menambahkan nilai terakhir dari array berulang-ulang di mana jumlah "irisan" lebih besar dari anggota array.
Seluruh proses itu perlu diulang untuk sebanyak mungkin item yang ingin Anda "iris". Karena kami sudah menemukan item "pertama" di pengelompokan awal, itu berarti n-1
iterasi untuk hasil irisan yang diinginkan.
Langkah terakhir sebenarnya hanyalah ilustrasi opsional untuk mengubah semuanya kembali menjadi array untuk hasil seperti yang akhirnya ditunjukkan. Jadi benar-benar hanya mendorong item secara kondisional atau false
kembali dengan posisi yang cocok dan akhirnya "memfilter" semua false
nilai sehingga larik akhir memiliki anggota "enam" dan "lima".
Jadi tidak ada operator standar untuk mengakomodasi ini, dan Anda tidak bisa hanya "membatasi" push ke 5 atau 10 atau item apa pun dalam array. Tetapi jika Anda benar-benar harus melakukannya, maka ini adalah pendekatan terbaik Anda.
Anda mungkin bisa mendekati ini dengan mapReduce dan mengabaikan kerangka kerja agregasi bersama-sama. Pendekatan yang akan saya ambil ( dalam batas yang wajar ) adalah memiliki peta hash di dalam memori secara efektif di server dan mengakumulasikan array untuk itu, sambil menggunakan potongan JavaScript untuk "membatasi" hasil:
db.messages.mapReduce(
function () {
if ( !stash.hasOwnProperty(this.conversation_ID) ) {
stash[this.conversation_ID] = [];
}
if ( stash[this.conversation_ID.length < maxLen ) {
stash[this.conversation_ID].push( this._id );
emit( this.conversation_ID, 1 );
}
},
function(key,values) {
return 1; // really just want to keep the keys
},
{
"scope": { "stash": {}, "maxLen": 10 },
"finalize": function(key,value) {
return { "msgs": stash[key] };
},
"out": { "inline": 1 }
}
)
Jadi pada dasarnya hanya membangun objek "dalam memori" yang cocok dengan "kunci" yang dipancarkan dengan array yang tidak pernah melebihi ukuran maksimum yang ingin Anda ambil dari hasil Anda. Selain itu, ini bahkan tidak repot untuk "memancarkan" item ketika tumpukan maksimum terpenuhi.
Bagian reduksi sebenarnya tidak melakukan apa-apa selain hanya mengurangi menjadi "kunci" dan satu nilai. Jadi untuk berjaga-jaga jika peredam kami tidak dipanggil, seperti yang akan benar jika hanya ada 1 nilai untuk sebuah kunci, fungsi finalisasi menangani pemetaan kunci "simpanan" ke hasil akhir.
Efektivitas ini bervariasi pada ukuran output, dan evaluasi JavaScript tentu saja tidak cepat, tetapi mungkin lebih cepat daripada memproses array besar dalam sebuah pipeline.
Pilih masalah JIRA untuk benar-benar memiliki operator "slice" atau bahkan "batas" pada "$push" dan "$addToSet", yang keduanya akan berguna. Secara pribadi berharap bahwa setidaknya beberapa modifikasi dapat dilakukan pada $map
operator untuk mengekspos nilai "indeks saat ini" saat memproses. Itu akan secara efektif memungkinkan "pengirisan" dan operasi lainnya.
Sungguh Anda ingin membuat kode ini untuk "menghasilkan" semua iterasi yang diperlukan. Jika jawabannya di sini mendapat cukup cinta dan/atau waktu lain yang tertunda yang saya miliki dalam pelajaran, maka saya dapat menambahkan beberapa kode untuk menunjukkan bagaimana melakukan ini. Ini sudah merupakan respons yang cukup lama.
Kode untuk menghasilkan saluran:
var key = "$conversation_ID";
var val = "$_id";
var maxLen = 10;
var stack = [];
var pipe = [];
var fproj = { "$project": { "pos": { "$const": [] } } };
for ( var x = 1; x <= maxLen; x++ ) {
fproj["$project"][""+x] = 1;
fproj["$project"]["pos"]["$const"].push( x );
var rec = {
"$cond": [ { "$eq": [ "$pos", x ] }, "$"+x ]
};
if ( stack.length == 0 ) {
rec["$cond"].push( false );
} else {
lval = stack.pop();
rec["$cond"].push( lval );
}
stack.push( rec );
if ( x == 1) {
pipe.push({ "$group": {
"_id": key,
"1": { "$first": val },
"msgs": { "$push": val }
}});
} else {
pipe.push({ "$unwind": "$msgs" });
var proj = {
"$project": {
"msgs": 1
}
};
proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$msgs" ] };
var grp = {
"$group": {
"_id": "$_id",
"msgs": {
"$push": {
"$cond": [ { "$not": "$seen" }, "$msgs", false ]
}
}
}
};
for ( n=x; n >= 1; n-- ) {
if ( n != x )
proj["$project"][""+n] = 1;
grp["$group"][""+n] = ( n == x ) ? { "$first": "$msgs" } : { "$first": "$"+n };
}
pipe.push( proj );
pipe.push({ "$sort": { "seen": 1 } });
pipe.push(grp);
}
}
pipe.push(fproj);
pipe.push({ "$unwind": "$pos" });
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": stack[0] }
}
});
pipe.push({ "$unwind": "$msgs" });
pipe.push({ "$match": { "msgs": { "$ne": false } }});
pipe.push({
"$group": {
"_id": "$_id",
"msgs": { "$push": "$msgs" }
}
});
Itu membangun pendekatan iteratif dasar hingga maxLen
dengan langkah-langkah dari $unwind
ke $group
. Juga tertanam di dalamnya ada rincian proyeksi akhir yang diperlukan dan pernyataan kondisional "bersarang". Yang terakhir pada dasarnya adalah pendekatan yang diambil untuk pertanyaan ini:
Apakah klausa $in MongoDB menjamin pesanan?