Короче, не может.Но если кто-то сказал вам это, то он заслуживает объяснения, чтобы объяснить, почему такая концепция неверна.
Почему НЕ $ facet
Как прокомментировано, $facet
не можетсделать что-нибудь для вас здесь и, вероятно, было неверное представление о том, что ваш запрос должен делать.Во всяком случае, стадия конвейера $facet
вызовет больше проблем с пределом BSON из-за того факта, что единственным выходом стадии конвейера $facet
является «Единый документ» , что означает, что, если вы фактически не используете его для намеченной цели «сводных результатов», вы почти наверняка нарушите этот предел в реальных условиях.
Самая большая причина, по которой онпросто не применяется, потому что ваш источник $lookup
извлекает данные из другой коллекции.Этап $facet
применяется только к «той же коллекции», поэтому вы не можете иметь выходные данные из одной коллекции в одном «фасете» и другой коллекции в другом фасете.Для той же коллекции, в которой выполняется .aggregate()
, могут быть определены только «конвейеры».
$ lookup - это STILL, что вы хотите
Точка ограничения размера BSON, однако, совершеннодопустимо, поскольку основной сбой в текущем конвейере агрегации - использование оператора $size
в возвращаемом массиве.«Массив» на самом деле является проблемой здесь, так как «unbound» имеет «потенциал» для извлечения документов из связанной коллекции, что фактически приводит к тому, что родительский документ, содержащий этот массив в выходных данных, превышает предел BSON.
Таким образом, есть два основных подхода к этому, которые вы можете использовать для того, чтобы просто получить «размер» без необходимости создания массива «целых» связанных документов.
MongoDB3.6 и выше
Здесь вы будете использовать $lookup
с синтаксисом выражения «sub-pipe», введенным в этой версии, чтобы просто возвращать «уменьшенное количество» без фактического возврата каких-либо документов:
const pipeline = [
{ "$match": { "category": query.category } },
{ "$sort": { "timestamp": -1 } },
{ "$skip": (query.page - 1) * query.count },
{ "$limit": query.count },
{ "$lookup": {
"from": "posts",
"let": { "id": "$_id" },
"pipeline": [
{ "$match": {
"$expr": { "$eq": [ "$$id", "$parent" ] }
}},
{ "$count": "count" }
],
"as": "comments",
}},
{ $addFields: {
"comments": {
"$ifNull": [ { "$arrayElemAt": ["$comments.count", 0] }, 0 ]
},
"id": "$_id"
}}
];
Проще говоря, новый "под-конвейер" возвращает в целевой "массив" (который всегда является массивом) только выходные данные конвейерного выражения.Здесь мы не только $match
на значениях локального и внешнего ключа (что фактически делает другая форма $lookup
теперь внутри), но мы продолжаем конвейер, используя$count
stage, который опять-таки фактически является синонимом для:
{ "$group": { "_id": null, "count": { "$sum": 1 } } },
{ "$project": { "_id": 0, "count": 1 } }
Дело в том, что в ответе массива вы когда-либо получаете только «один» документ, который мы можем затемлегко преобразовать в единственное значение с помощью $arrayElemAt
и с использованием $ifNull
в случае отсутствия совпадений в иностранной коллекции для получения счетчика 0
Более ранние версии
Для более ранних версий, чем MongoDB 3.6, общая идея - $unwind
непосредственно после $lookup
.Это на самом деле имеет специальное действие, которое описано в $ lookup + $ unwind Coalescence в более широком разделе руководства по Агрегационная оптимизация конвейера .Лично я считаю это скорее «помехой», чем «оптимизацией», так как вы действительно должны быть способны «выразить то, что вы имеете в виду» вместо того, чтобы что-то делать для вас «за спиной».Но основы таковы:
const pipeline = [
{ "$match": { "category": query.category } },
{ "$sort": { "timestamp": -1 } },
{ "$skip": (query.page - 1) * query.count },
{ "$limit": query.count },
{ "$lookup": {
"from": "posts",
"localField": "_id",
"foreignField": "parent",
"as": "comments"
}},
{ "$unwind": "$comments" },
{ "$group": {
"_id": "$_id",
"otherField": { "$first": "$otherField" },
"comments": { "$sum": 1 }
}}
];
Важная часть здесь - это то, что на самом деле происходит с этапами $lookup
и $unwind
, как можетбыть просмотренным с помощью explain()
для просмотра проанализированного конвейера как фактически выраженного сервером:
{
"$lookup" : {
"from" : "posts",
"as" : "comments",
"localField" : "_id",
"foreignField" : "parent",
"unwinding" : {
"preserveNullAndEmptyArrays" : false
}
}
}
То, что unwinding
по существу "свернуто" в $lookup
, а само $unwind
"исчезает". Это происходит потому, что комбинация переводится таким «особым образом», что фактически приводит к «размотанным» результатам $lookup
вместо нацеливания на массив. По сути, это делается для того, чтобы «массив» никогда не создавался, тогда предел BSON никогда не может быть нарушен.
Остальное, конечно, довольно просто: вы просто используете $group
, чтобы «сгруппировать» исходный документ. Вы можете использовать $first
в качестве аккумулятора для хранения любых полей документа, который вы хотите в ответе, и просто $sum
для подсчета возвращенных внешних данных.
Поскольку это мангуста, я уже обрисовал процесс «автоматизации» построения всех полей для включения в $first
как часть моего ответа на Запросы после заполнения в Mongoose который показывает, как проверить «схему», чтобы получить эту информацию.
Еще одна «морщина» - это $unwind
, отменяющая «LEFT JOIN», присущую $lookup
, поскольку там, где нет совпадений с родительским содержимым, тогда это " родительский документ "исключен из результатов. Я не совсем уверен в этом на момент написания (и должен посмотреть это позже), но опция preserveNullAndEmptyArrays
имела ограничение в том смысле, что она не может применяться в этой форме "Объединения", однако это не так по крайней мере, MongoDB 3.6:
const pipeline = [
{ "$match": { "category": query.category } },
{ "$sort": { "timestamp": -1 } },
{ "$skip": (query.page - 1) * query.count },
{ "$limit": query.count },
{ "$lookup": {
"from": "posts",
"localField": "_id",
"foreignField": "parent",
"as": "comments"
}},
{ "$unwind": { "path": "$comments", "preserveNullAndEmptyArrays": true } },
{ "$group": {
"_id": "$_id",
"otherField": { "$first": "$otherField" },
"comments": {
"$sum": {
"$cond": {
"if": { "$eq": [ "$comments", null ] },
"then": 0,
"else": 1
}
}
}
}}
];
Так как я не могу на самом деле подтвердить, что он работает должным образом во всем, кроме MongoDB 3.6, то это бессмысленно, так как в более новой версии вы все равно должны использовать другую форму $lookup
. Я знаю, что была, по крайней мере, первоначальная проблема с MongoDB 3.2 в том, что preserveNullAndEmptyArrays
отменял «Объединение» и, следовательно, $lookup
все еще возвращал вывод в виде «массива», и только после этой стадией был массив "размотан". Который побеждает цель сделать это, чтобы избежать предела BSON.
Сделай это в коде
Все это говорит о том, что в конечном итоге вы просто ищете "подсчеты", которые будут добавлены к вашим результатам для "связанных" комментариев. Пока вы не тянете страницы с «сотнями элементов», тогда ваше условие $limit
должно поддерживать это при разумном результате, чтобы просто запускать count()
запросов для получения соответствующий документ учитывается для каждого ключа без «слишком много» * 1159 * накладных расходов, что делает его необоснованным:
// Get documents
let posts = await Post.find({ "category": query.category })
.sort({ "timestamp": -1 })
.skip((query.page - 1) * query.count)
.limit(query.count)
.lean().exec();
// Map counts to each document
posts = (await Promise.all(
posts.map(post => Comment.count({ "parent": post._id }) )
)).map((comments,i) => ({ ...posts[i], comments }) );
Здесь «компромисс» заключается в том, что хотя «параллельное» выполнение всех этих запросов count()
означает дополнительные запросы к серверу, издержки каждого запроса сами по себе действительно низкие. Получение «счетчика курсоров» результата запроса намного эффективнее, чем использование чего-то вроде $count
этапа конвейера агрегации, показанного выше.
Это накладывает нагрузку на соединения с базой данных при выполнении, но у него нет той же «нагрузки по обработке», и, конечно, вы когда-либо просматриваете только «подсчеты», и никакие документы не возвращаются по проводам или даже «извлекаются». "из коллекции при обработке курсора результатов.
Так что последнее - это, по сути, «оптимизация» процесса mongoose populate()
, когда мы на самом деле не запрашиваем «документы», а просто получаем счетчик для каждого запроса. Технически populate()
будет использовать здесь «один» запрос с $in
для всех документов в предыдущем результате. Но это не сработает, потому что вам нужно общее количество на «родителя», которое по сути является агрегацией в одном запросе и ответе. Следовательно, почему здесь выдается несколько запросов.
Резюме
Итак, чтобы избежать проблем BSON , вы действительно ищете какой-либо из тех методов, который позволяет избежать возврата «массива» связанных документов из вашего $lookup
этап конвейера, который используется для "соединения", либо путем получения "сокращенных данных", либо с помощью метода "счетчика курсоров".
Существует немного больше "глубины" в пределе размера BSON и обработки в:
Совокупный $ lookup Общий размер документов в соответствующем конвейере превышает максимальный размер документа на этом сайте.Обратите внимание, что те же методы, которые продемонстрированы там, чтобы вызвать ошибку, могут также применяться к стадии $facet
, так как ограничение в 16 МБ является константой для всего, что является «документом».И почти "все" в MongoDB является документом BSON, поэтому работа в указанных пределах чрезвычайно важна.
ПРИМЕЧАНИЕ : чисто из "производительности"«В перспективе самая большая проблема за пределами потенциального нарушения BSON Size Limit, присущего вашему текущему запросу, на самом деле - обработка $skip
и $limit
.Если то, что вы на самом деле реализуете, - это больше функциональность типа «Загрузить больше результатов ...», то что-то вроде Реализация нумерации страниц в mongodb , где вы бы использовали «диапазон» для запуска следующей «страницы»выбор, исключая предыдущие результаты, намного более ориентирован на производительность, чем $skip
и $limit
.
Пейджинг с $skip
и$limit
следует использовать только там, где у вас нет другого выбора.Находясь на «пронумерованных страницах», где вы можете перейти на любую пронумерованную страницу.И даже тогда все же гораздо лучше вместо этого «кэшировать» результаты в заранее определенные наборы.
Но это действительно «совсем другой вопрос», чем существенный вопрос здесь о пределе размера BSON.