Возвращает ограниченное количество записей определенного типа, но неограниченное количество других записей?

У меня есть запрос, когда мне нужно вернуть 10 записей типа «A», возвращая все остальные записи. Как я могу это сделать?

Обновление: По общему признанию, я мог бы сделать это с двумя запросами, но я хотел избежать этого, если возможно, подумать, что это будет меньше накладных расходов и, возможно, более результативным. Мой запрос уже является агрегирующим запросом, в котором учитываются оба типа записей, мне просто нужно ограничить количество записей одного типа в результатах.

Обновление: ниже приведен пример запроса, который подчеркивает проблему:

db.books.aggregate([ {$geoNear: {near: [-118.09771, 33.89244], distanceField: "distance", spherical: true}}, {$match: {"type": "Fiction"}}, {$project: { 'title': 1, 'author': 1, 'type': 1, 'typeSortOrder': {$add: [ {$cond: [{$eq: ['$type', "Fiction"]}, 1, 0]}, {$cond: [{$eq: ['$type', "Science"]}, 0, 0]}, {$cond: [{$eq: ['$type', "Horror"]}, 3, 0]} ]}, }}, {$sort: {'typeSortOrder'}}, {$limit: 10} ]) db.books.aggregate([ {$geoNear: {near: [-118.09771, 33.89244], distanceField: "distance", spherical: true}}, {$match: {"type": "Horror"}}, {$project: { 'title': 1, 'author': 1, 'type': 1, 'typeSortOrder': {$add: [ {$cond: [{$eq: ['$type', "Fiction"]}, 1, 0]}, {$cond: [{$eq: ['$type', "Science"]}, 0, 0]}, {$cond: [{$eq: ['$type', "Horror"]}, 3, 0]} ]}, }}, {$sort: {'typeSortOrder'}}, {$limit: 10} ]) db.books.aggregate([ {$geoNear: {near: [-118.09771, 33.89244], distanceField: "distance", spherical: true}}, {$match: {"type": "Science"}}, {$project: { 'title': 1, 'author': 1, 'type': 1, 'typeSortOrder': {$add: [ {$cond: [{$eq: ['$type', "Fiction"]}, 1, 0]}, {$cond: [{$eq: ['$type', "Science"]}, 0, 0]}, {$cond: [{$eq: ['$type', "Horror"]}, 3, 0]} ]}, }}, {$sort: {'typeSortOrder'}}, {$limit: 10} ]) 

Я хотел бы, чтобы все эти записи были возвращены в одном запросе, но ограничьте тип не более чем на 10 из любой категории. Я понимаю, что typeSortOrder не обязательно должен быть условным, когда запросы разбиваются, как это, я имел его там, когда запросы были одним запросом, изначально (к чему я хотел бы вернуться).

проблема


Результаты здесь не невозможны, но также могут быть непрактичными. Были сделаны общие замечания о том, что вы не можете «срезать» массив или иначе «ограничить» количество результатов, нажатых на один. И метод для этого для «типа» заключается в использовании массивов.

«Нецелесообразная» часть обычно относится к числу результатов, когда слишком большой набор результатов взорвет лимит документа BSON при «группировке». Но, я собираюсь рассмотреть это с некоторыми другими рекомендациями по вашему «гео-поиску» вместе с конечной целью – вернуть 10 результатов каждого «типа» максимум.

Принцип


Чтобы сначала рассмотреть и понять проблему, давайте рассмотрим упрощенный «набор» данных и код конвейера, необходимых для возврата «лучших 2 результатов» от каждого типа:

 { "title": "Title 1", "author": "Author 1", "type": "Fiction", "distance": 1 }, { "title": "Title 2", "author": "Author 2", "type": "Fiction", "distance": 2 }, { "title": "Title 3", "author": "Author 3", "type": "Fiction", "distance": 3 }, { "title": "Title 4", "author": "Author 4", "type": "Science", "distance": 1 }, { "title": "Title 5", "author": "Author 5", "type": "Science", "distance": 2 }, { "title": "Title 6", "author": "Author 6", "type": "Science", "distance": 3 }, { "title": "Title 7", "author": "Author 7", "type": "Horror", "distance": 1 } 

Это упрощенное представление данных и несколько репрезентативное состояние документов после первоначального запроса. Теперь идет трюк, как использовать конвейер агрегации для получения «ближайших» двух результатов для каждого «типа»:

 db.books.aggregate([ { "$sort": { "type": 1, "distance": 1 } }, { "$group": { "_id": "$type", "1": { "$first": { "_id": "$_id", "title": "$title", "author": "$author", "distance": "$distance" } }, "books": { "$push": { "_id": "$_id", "title": "$title", "author": "$author", "distance": "$distance" } } }}, { "$project": { "1": 1, "books": { "$cond": [ { "$eq": [ { "$size": "$books" }, 1 ] }, { "$literal": [false] }, "$books" ] } }}, { "$unwind": "$books" }, { "$project": { "1": 1, "books": 1, "seen": { "$eq": [ "$1", "$books" ] } }}, { "$sort": { "_id": 1, "seen": 1 } }, { "$group": { "_id": "$_id", "1": { "$first": "$1" }, "2": { "$first": "$books" }, "books": { "$push": { "$cond": [ { "$not": "$seen" }, "$books", false ] } } }}, { "$project": { "1": 1, "2": 2, "pos": { "$literal": [1,2] } }}, { "$unwind": "$pos" }, { "$group": { "_id": "$_id", "books": { "$push": { "$cond": [ { "$eq": [ "$pos", 1 ] }, "$1", { "$cond": [ { "$eq": [ "$pos", 2 ] }, "$2", false ]} ] } } }}, { "$unwind": "$books" }, { "$match": { "books": { "$ne": false } } }, { "$project": { "_id": "$books._id", "title": "$books.title", "author": "$books.author", "type": "$_id", "distance": "$books.distance", "sortOrder": { "$add": [ { "$cond": [ { "$eq": [ "$_id", "Fiction" ] }, 1, 0 ] }, { "$cond": [ { "$eq": [ "$_id", "Science" ] }, 0, 0 ] }, { "$cond": [ { "$eq": [ "$_id", "Horror" ] }, 3, 0 ] } ] } }}, { "$sort": { "sortOrder": 1 } } ]) 

Конечно, это всего лишь два результата, но он описывает процесс получения n результатов, что, естественно, выполняется в генерируемом коде трубопровода. Прежде чем перейти к коду, процесс заслуживает прохода.

После любого запроса первое, что нужно сделать здесь, это $sort results, и это вы в основном должны делать как с «ключом группировки», который является «типом», так и «расстоянием», так что «ближайшие» элементы наверху.

Причина этого показана на этапах $group которые будут повторяться. То, что сделано, по существу, «выталкивает $first результат из каждого стека группировки. Таким образом, другие документы не теряются, они помещаются в массив с помощью $push .

Чтобы быть в безопасности, следующий этап действительно требуется только после «первого шага», но при необходимости может быть добавлен для аналогичной фильтрации в повторении. Основная проверка здесь заключается в том, что полученный «массив» больше, чем один элемент. Там, где это не так, содержимое заменяется на одно значение false. Причина для этого станет очевидной.

После этого «первого шага» существа реального цикла повторения, где этот массив затем «де-нормируется» с помощью $unwind а затем $project сделанного для «сопоставления» документа, который был последним «замеченным».

Поскольку только один из документов будет соответствовать этому условию, результаты снова «сортируются», чтобы всплывать «невидимые» документы вверху, в то же время сохраняя порядок группировки. Следующая вещь похожа на первый шаг $group , но там, где сохраняются поддерживаемые позиции, и снова «первый невидимый» документ «выскочил со стека».

Документ, который был «замечен», затем возвращается к массиву не как самому, а как значение false . Это не будет соответствовать сохраненной ценности, и это, как правило, способ справиться с этим, не будучи «разрушительным» для содержимого массива, где вы не хотите, чтобы операции завершились с ошибкой, если не было достаточного количества совпадений для покрытия необходимых n результатов.

Очистка по завершении следующей «проекции» добавляет массив к окончательным документам, теперь сгруппированным по «типу», представляющему каждую позицию в n требуемых результатах. Когда этот массив разматывается, документы снова могут быть сгруппированы вместе, но теперь все в одном массиве, который, возможно, содержит несколько false значений, но содержит n элементов.

Наконец, размотайте массив, используйте $match чтобы отфильтровать false значения и выполните проект в требуемой форме документа.

Практичность


Проблема, о которой говорилось выше, заключается в том, что количество фильтруемых результатов определяется как реальный предел количества результатов, которые могут быть перенесены в массив. Это в основном предел BSON, но вы также не хотите, чтобы 1000 предметов, даже если они все еще находятся под лимитом.

Трюк здесь заключается в том, что первоначальный «матч» достаточно мал, чтобы «операции нарезки» стали практичными. Есть некоторые вещи с $geoNear который может сделать это возможным.

Очевидным является limit . По умолчанию это 100, но вы явно хотите иметь что-то в диапазоне:

(количество категорий, которые вы можете сопоставить) X (требуемые совпадения)

Но если это по существу число не в 1000-х годах, то здесь уже есть какая-то помощь.

Другие – maxDistance и minDistance , где вы, по сути, ставите верхнюю и нижнюю границы того, как «далеко» искать. Максимальная граница – это общий ограничитель, в то время как минимальная граница полезна при «пейджинге», который является следующим помощником.

При «восходящем подкачке» вы можете использовать аргумент query , чтобы исключить значения _id документов «уже видели» с помощью запроса $nin . Точно так же minDistance может быть заполнен наибольшим расстоянием «последнего увиденного» или, по крайней мере, наименьшим по величине расстоянием «тип». Это позволяет некоторым понятиям отфильтровывать вещи, которые уже были «замечены» и получить другую страницу.

На самом деле тема сама по себе, но это общие вещи, которые нужно искать в сокращении этого первоначального соответствия, чтобы сделать процесс практичным.

Внедрение


Общая проблема возвращения «10 результатов не более, для каждого типа» явно хочет получить некоторый код для генерации этапов конвейера. Никто не хочет набирать это, и практически говоря, вы, вероятно, захотите изменить это число в какой-то момент.

Итак, теперь к коду, который может генерировать контур монстров. Весь код в JavaScript, но легко перевести в принципы:

 var coords = [-118.09771, 33.89244]; var key = "$type"; var val = { "_id": "$_id", "title": "$title", "author": "$author", "distance": "$distance" }; var maxLen = 10; var stack = []; var pipe = []; var fproj = { "$project": { "pos": { "$literal": [] } } }; pipe.push({ "$geoNear": { "near": coords, "distanceField": "distance", "spherical": true }}); pipe.push({ "$sort": { "type": 1, "distance": 1 }}); for ( var x = 1; x <= maxLen; x++ ) { fproj["$project"][""+x] = 1; fproj["$project"]["pos"]["$literal"].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 }, "books": { "$push": val } }}); pipe.push({ "$project": { "1": 1, "books": { "$cond": [ { "$eq": [ { "$size": "$books" }, 1 ] }, { "$literal": [false] }, "$books" ] } }}); } else { pipe.push({ "$unwind": "$books" }); var proj = { "$project": { "books": 1 } }; proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$books" ] }; var grp = { "$group": { "_id": "$_id", "books": { "$push": { "$cond": [ { "$not": "$seen" }, "$books", false ] } } } }; for ( n=x; n >= 1; n-- ) { if ( n != x ) proj["$project"][""+n] = 1; grp["$group"][""+n] = ( n == x ) ? { "$first": "$books" } : { "$first": "$"+n }; } pipe.push( proj ); pipe.push({ "$sort": { "_id": 1, "seen": 1 } }); pipe.push(grp); } } pipe.push(fproj); pipe.push({ "$unwind": "$pos" }); pipe.push({ "$group": { "_id": "$_id", "msgs": { "$push": stack[0] } } }); pipe.push({ "$unwind": "$books" }); pipe.push({ "$match": { "books": { "$ne": false } }}); pipe.push({ "$project": { "_id": "$books._id", "title": "$books.title", "author": "$books.author", "type": "$_id", "distance": "$books", "sortOrder": { "$add": [ { "$cond": [ { "$eq": [ "$_id", "Fiction" ] }, 1, 0 ] }, { "$cond": [ { "$eq": [ "$_id", "Science" ] }, 0, 0 ] }, { "$cond": [ { "$eq": [ "$_id", "Horror" ] }, 3, 0 ] }, ] } } }); pipe.push({ "$sort": { "sortOrder": 1, "distance": 1 } }); 

чередовать


Конечно, конечный результат здесь и общая проблема со всем выше, это то, что вы действительно хотите, чтобы «топ-10» каждого «типа» возвращались. Конвейер агрегации сделает это, но за счет сохранения более 10, а затем «выталкивания стека» до достижения 10.

Альтернативный подход заключается в «грубой силе» с помощью переменных mapReduce и «глобально ограниченных». Не так приятно, поскольку результаты все в массивах, но это может быть практический подход:

 db.collection.mapReduce( function () { if ( !stash.hasOwnProperty(this.type) ) { stash[this.type] = []; } if ( stash[this.type.length < maxLen ) { stash[this.type].push({ "title": this.title, "author": this.author, "type": this.type, "distance": this.distance }); emit( this.type, 1 ); } }, function(key,values) { return 1; // really just want to keep the keys }, { "query": { "location": { "$nearSphere": [-118.09771, 33.89244] } }, "scope": { "stash": {}, "maxLen": 10 }, "finalize": function(key,value) { return { "msgs": stash[key] }; }, "out": { "inline": 1 } } ) 

Это настоящий чит, который просто использует «глобальную область» для хранения одного объекта, ключи которого являются ключами группировки. Результаты будут перенесены на массив в этом глобальном объекте до тех пор, пока не будет достигнута максимальная длина. Результаты уже отсортированы по ближайшим, поэтому картограф просто отказывается делать что-либо с текущим документом после того, как 10 достигнут за ключ.

Редуктор не будет вызван, поскольку испускается только один документ на клавишу. Финализация затем просто «вытягивает» значение из глобального и возвращает его в результате.

Простой, но, конечно, у вас нет всех опций $geoNear , если они вам действительно нужны, и эта форма имеет жесткий предел 100 документов в качестве результата из исходного запроса.

Я не думаю, что это в настоящее время (2.6) возможно сделать с одним конвейером агрегации. Трудно дать точный аргумент, почему нет, но в основном конвейер агрегации выполняет преобразования потоков документов, по одному документу за раз. Внутри конвейера состояния самого потока нет никакой осведомленности, что вам нужно будет определить, что вы достигли предела для A, B и т. Д., И вам нужно бросить дополнительные документы того же типа. $group объединяет несколько документов и позволяет их значения полей в совокупности влиять на итоговый групповой документ ( $sum , $avg и т. д.). Возможно, это имеет смысл, но это необязательно строго, потому что есть простые операции, которые вы могли бы добавить, чтобы можно было ограничить на основе типов, например, добавление $push x аккумулятора в $group которая только толкает значение, если массив толкается на менее чем х элементов.

Даже если бы у меня был способ сделать это, я бы порекомендовал просто сделать две скопления. Будь проще.

Это классический случай для подзапроса / соединения, который не поддерживается MongoDB. Все операции присоединения и подзапросы должны быть реализованы в логике приложения. Таким образом, множество запросов – ваш лучший выбор. Производительность подхода с несколькими запросами должна быть хорошей, если у вас есть индекс по типу.

В качестве альтернативы вы можете написать один запрос агрегирования за вычетом ограничений на соответствие типов и ограничений, а затем обработать поток в логике приложения для ограничения количества документов на тип. Такой подход будет низким по производительности для больших наборов результатов, поскольку документы могут быть возвращены в случайном порядке. Затем ваша предельная логика должна пройти по всему набору результатов.

я думаю, вы можете использовать cursor.limit () на курсоре, чтобы указать максимальное количество документов, которые вернет курсор. limit () аналогичен оператору LIMIT в базе данных SQL. Вы должны применить limit () к курсору перед извлечением любых документов из базы данных.

Предельная функция в курсорах может использоваться для ограничения количества записей в поиске.

Я думаю, этот пример должен помочь:

 var myCursor = db.bios.find( ); db.bios.find().limit( 5 )