MongoDB
 sql >> база данни >  >> NoSQL >> MongoDB

Mongodb агрегиране $група, ограничаване на дължината на масива

Модерен

От MongoDB 3.6 има "нов" подход към това чрез използване на $lookup за да извършите "самосъединяване" почти по същия начин като оригиналната обработка на курсора, показана по-долу.

Тъй като в тази версия можете да посочите "pipeline" аргумент за $lookup като източник за "join", това по същество означава, че можете да използвате $match и $limit за събиране и "ограничаване" на записите за масива:

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"
  }}
])

По желание можете да добавите допълнителна проекция след $lookup за да направи елементите на масива просто стойности, а не документи с _id ключ, но основният резултат е налице, като просто направите горното.

Все още има изключителния SERVER-9277, който всъщност изисква директно "лимит за натискане", но използвайки $lookup по този начин е жизнеспособна алтернатива междувременно.

ЗАБЕЛЕЖКА :Има също $slice който беше въведен след написването на оригиналния отговор и споменат от "нерешен проблем с JIRA" в оригиналното съдържание. Въпреки че можете да получите същия резултат с малки набори от резултати, той все още включва „натискане на всичко“ в масива и след това по-късно ограничаване на крайния изход на масива до желаната дължина.

Така че това е основното разграничение и защо обикновено не е практично да се $slice за големи резултати. Но, разбира се, може да се използва алтернативно в случаите, когато е така.

Има още няколко подробности за стойностите на групата на mongodb по множество полета относно алтернативното използване.

Оригинал

Както бе посочено по-рано, това не е невъзможно, но със сигурност е ужасен проблем.

Всъщност, ако основната ви грижа е, че получените ви масиви ще бъдат изключително големи, тогава най-добрият подход е да подадете за всеки отделен "conversation_ID" като индивидуална заявка и след това да комбинирате резултатите си. В много синтаксис на MongoDB 2.6, който може да се нуждае от някои настройки в зависимост от това какво всъщност е езиковата ви реализация:

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 );
    });
});

Но всичко зависи от това дали се опитвате да избегнете това. И така към истинския отговор:

Първият проблем тук е, че няма функция за "ограничаване" на броя на елементите, които се "избутват" в масив. Това със сигурност е нещо, което бихме искали, но функционалността в момента не съществува.

Вторият проблем е, че дори когато натискате всички елементи в масив, не можете да използвате $slice , или всеки подобен оператор в конвейера за агрегиране. Така че няма начин да получите само „топ 10“ резултати от произведен масив с проста операция.

Но всъщност можете да създадете набор от операции за ефективно „нарязване“ на вашите граници на групиране. Това е доста ангажирано и например тук ще намаля елементите на масива "нарязани" само до "шест". Основната причина тук е да демонстрирате процеса и да покажете как да направите това, без да бъдете разрушителни с масиви, които не съдържат общата сума, която искате да „нарежете“.

Даден е извадка от документи:

{ "_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 }

Там можете да видите, че при групиране по вашите условия ще получите един масив с десет елемента и друг с "пет". Това, което искате да направите тук, намалите и двете до първите „шест“, без да „унищожавате“ масива, който ще съвпада само с „пет“ елемента.

И следната заявка:

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" }
    }}
])

Получавате най-добрите резултати в масива, до шест записа:

{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }

Както можете да видите тук, много забавно.

След като първоначално сте групирали, вие по същество искате да "изкарате" $first стойност извън стека за резултатите от масива. За да направим този процес малко опростен, ние всъщност правим това в първоначалната операция. Така процесът става:

  • $unwind масива
  • Сравнете със стойностите, които вече сте виждали с $eq равенство съвпадение
  • $sort резултатите да "float" false невиждани стойности до върха (това все още запазва реда)
  • $group обратно и „изкарайте“ $first невиждана стойност като следващ член в стека. Също така това използва $cond оператор за замяна на "виждани" стойности в стека на масива с false за помощ при оценката.

Последното действие с $cond има ли, за да се уверите, че бъдещите итерации не просто добавят последната стойност на масива отново и отново, където броят на „срезовете“ е по-голям от членовете на масива.

Целият този процес трябва да се повтори за толкова елементи, колкото искате да „нарежете“. Тъй като вече намерихме "първия" елемент в първоначалното групиране, това означава n-1 повторения за желания резултат от срез.

Последните стъпки всъщност са само незадължителна илюстрация за преобразуване на всичко обратно в масиви за резултата, както е показано накрая. Така че наистина просто условно натискане на елементи или false обратно по тяхната съвпадаща позиция и накрая "филтриране" на всички false стойности, така че крайните масиви имат съответно "шест" и "пет".

Така че няма стандартен оператор, който да поеме това и не можете просто да „ограничите“ натискането до 5 или 10 или каквито и да било елементи в масива. Но ако наистина трябва да го направите, тогава това е най-добрият ви подход.

Възможно е да подходите към това с mapReduce и да изоставите рамката за агрегиране заедно. Подходът, който бих възприел (в разумни граници), би бил ефективно да имам хеш-карта в паметта на сървъра и да натрупам масиви към нея, като същевременно използвам JavaScript фрагмент за „ограничаване“ на резултатите:

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 }
    }
)

Така че това просто изгражда обекта "в паметта", съответстващ на излъчените "ключове" с масив, който никога не надвишава максималния размер, който искате да извлечете от вашите резултати. Освен това това дори не си прави труда да "излъчи" елемента, когато се покрие максималният стек.

Частта за намаляване всъщност не прави нищо друго освен по същество просто да намали до "ключ" и една стойност. Така че, само в случай, че нашият редуктор не бъде извикан, както би било вярно, ако съществуваше само 1 стойност за ключ, функцията finalize се грижи за съпоставянето на ключовете „скрито“ към крайния изход.

Ефективността на това варира в зависимост от размера на изхода и JavaScript оценката със сигурност не е бърза, но вероятно по-бърза от обработката на големи масиви в конвейер.

Гласувайте за проблемите на JIRA, за да имате действително оператор "slice" или дори "лимит" за "$push" и "$addToSet", което и двете биха били полезни. Лично се надявам, че може да се направи поне някаква модификация на $map оператор, за да изложи стойността на "текущ индекс" при обработка. Това ефективно би позволило „нарязване“ и други операции.

Наистина бихте искали да кодирате това, за да "генерирате" всички необходими итерации. Ако отговорът тук получи достатъчно любов и/или друго изчакващо време, което имам в такси, тогава бих могъл да добавя някакъв код, за да демонстрирам как да направя това. Това вече е доста дълъг отговор.

Код за генериране на конвейер:

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" }
    }
}); 

Това изгражда основния итеративен подход до maxLen със стъпките от $unwind към $group . Също така са вградени подробности за необходимите окончателни прогнози и „вложеното“ условно изявление. Последният е основно подходът, възприет по този въпрос:

Гарантира ли клаузата $in на MongoDB?



  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Как да експортирате всички колекции в MongoDB?

  2. Spring Boot свързва Mysql и MongoDb

  3. Дължина на стойността на низовото поле в mongoDB

  4. Как да използвате Spring за свързване с MongoDB, което изисква удостоверяване

  5. Актуализирайте стойност в MongoDB въз основа на текущата й стойност