MongoDB
 sql >> Teknologi Basis Data >  >> NoSQL >> MongoDB

Agregasi Mongodb $group, batasi panjang array

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 dengan false 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?



  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Jarak cetak MongoDB antara dua titik

  2. Masukkan Nilai pada Posisi Tertentu dalam Array di MongoDB

  3. Bagaimana menerapkan pembaruan menggunakan operator posisi yang Difilter dengan arrayFilters

  4. ScaleGrid Mengumumkan Hosting MongoDB Bersama di Amazon AWS

  5. Perbarui bidang MongoDB menggunakan nilai bidang lain