Прошу прощения за расплывчатое описание вопроса, но у меня довольно сложный вопрос по фильтрации в агрегациях MongoDB. Пожалуйста, посмотрите мою схему данных, чтобы лучше понять вопрос:
Company {
_id: ObjectId
name: string
}
License {
_id: ObjectId
companyId: ObjectId
userId: ObjectId
}
User {
_id: ObjectId
companyId: ObjectId
email: string
}
Цель:
Я хотел бы запросить всех нелицензированных пользователей. Для этого вам понадобятся следующие простые запросы MongoDB:
const licenses = db.licenses.find({ companyId }); // Get all licenses for specific company
const userIds = licenses.toArray().map(l => l.userId); // Collect all licensed user ids
const nonLicensedUsers = db.users.find({ _id: { $nin: userIds } }); // Query all users that don't hold a license
Эта проблема:
Приведенный выше код работает отлично. Однако в нашей системе компании могут иметь сотни тысяч пользователей. Поэтому первый и последний шаг становятся исключительно дорогими. Я уточню это. Во-первых, вам нужно получить большое количество документов из БД и передать их по сети, что довольно дорого. Затем нам нужно снова передать огромный запрос $nin в MongoDB по сети, что удваивает накладные расходы.
Итак, я хотел бы выполнить все упомянутые операции на стороне MongoDB и вернуть небольшую часть нелицензированных пользователей, чтобы избежать затрат на передачу по сети. Есть идеи, как этого добиться?
Я смог подойти довольно близко, используя следующую агрегацию (псевдокод):
db.company.aggregate([
{ $match: { _id: id } }, // Step 1. Find the company entity by id
{ $lookup: {...} }, // Step 2. Joins 'users' collection by `companyId` field
{ $lookup: {...} }, // Step 3. Joins 'licenses' collection by `companyId` field
{
$project: {
licensesMap: // Step 4. Convert 'licenses' array to the map with the shape { 'user-id': true }. Could be done with $arrayToObject operator
}
},
{
$project: {
unlicensedUsers: {
$filter: {...} // And this is the place, where I stopped
}
}
}
]);
Давайте подробнее рассмотрим заключительный этап вышеупомянутой агрегации. Я попытался использовать агрегацию $фильтр следующим образом:
{
$filter: {
input: "$users"
as: "user",
cond: {
$neq: ["$licensesMap[$$user._id]", true]
}
}
}
Но, к сожалению, это не сработало. Казалось, что MongoDB не применяла интерполяцию и просто пыталась сравнить необработанную "$licensesMap[$$user._id]" строку с true логическим значением.
Примечание №1:
К сожалению, мы не можем изменить текущую схему данных. Это было бы дорого для нас.
Заметка 2:
Я не включил это в приведенный выше пример агрегации, но преобразовал идентификаторы объектов Mongo в строки, чтобы можно было создать licensesMap. Кроме того, я упорядочил идентификаторы списка users, чтобы иметь доступ к licensesMap должным образом.
Пример данных:
Коллекция компаний:
[
{ _id: "1", name: "Acme" }
]
Коллекция лицензий
[
{ _id: "1", companyId: "1", userId: "1" },
{ _id: "2", companyId: "1", userId: "2" }
]
Коллекция пользователей:
[
{ _id: "1", companyId: "1" },
{ _id: "2", companyId: "1" },
{ _id: "3", companyId: "1" },
{ _id: "4", companyId: "1" },
]
Ожидаемый результат:
[
_id: "1", // company id
name: "Acme",
unlicensedUsers: [
{ _id: "3", companyId: "1" },
{ _id: "4", companyId: "1" },
]
]
Объяснение: список unlicensedUsers содержит третьего и четвертого пользователей, потому что у них нет соответствующих записей в коллекции licenses.
если licensesMap является объектом, подобным {userid1 : value, userid2 : value}, и вы хотите получить значение для userid2, например, вы можете использовать $getField, но только если вы используете строку, в вашем случае вы хотите использовать переменную mongodb $$user._id, поэтому это не представляется возможным с получением хэш-карты, линейный поиск выглядит как способ получить значение здесь, например, сохранить карту лицензий в виде массива и выполнить поиск по ней.
Конечно, я предоставлю образцы данных чуть позже. Говоря о хранении лицензий в виде массива. Я не думаю, что это хорошая идея, поскольку фильтрация в этом случае приведет к квадратичной асимптотической сложности.
да, я знаю, но у нас нет в mongodb функции get("$$key") у нас есть только get("key_string") см. оператор $getField
Привет @Takis, я обновил описание и включил примеры данных с желаемым результатом. Кроме того, я пытался использовать $getField, но оператор $neq внутри $filter не распознал его.

Как насчет чего-то простого, например:
db.usersCollection.aggregate([
{
$lookup: {
from: "licensesCollection",
localField: "_id",
foreignField: "userId",
as: "licensedUsers"
}
},
{$match: {"licensedUsers.0": {$exists: false}}},
{
$group: {
_id: "$companyId",
unlicensedUsers: {$push: {_id: "$_id", companyId: "$companyId"}}
}
},
{
$lookup: {
from: "companiesCollection",
localField: "_id",
foreignField: "_id",
as: "company"
}
},
{$project: {unlicensedUsers: 1, company: {$arrayElemAt: ["$company", 0]}}},
{$project: {unlicensedUsers: 1, name: "$company.name"}}
])
users коллекция и licenses коллекция, у обеих есть все, что вам нужно для пользователей, поэтому после первого $lookup, который «объединяет» их, и простого $match, чтобы оставить только нелицензированных пользователей, все, что осталось, это просто форматирование в формат, который вы запрашиваете.
Бонус: это решение может работать с любым типом идентификатора. Например детская площадка
Да, это сработает. Но, к сожалению, в этом случае первый этап $lookup будет довольно медленным (где-то O(M*N)), потому что Mongo придется сканировать и licenses, и users коллекцию, чтобы выполнить операцию соединения. У меня действительно большой набор данных и агрегация, похожая на вашу. И, к сожалению, работает довольно медленно.
На самом деле, если вы проиндексируете его, оно должно быть O (n), где n — количество пользователей. Должен перебирать пользователей и для каждого из них находить свою лицензию (O (1)), если она хеширована.
Хм, интересно. Он проиндексирован, но применяется индекс B-Tree. Я попробую перестроить индекс и сделать некоторые измерения
Хэш-индексы сделали свое дело. Спасибо!
Рад слышать :) Если это работает для вас, пожалуйста, примите это, чтобы другим было легче его найти.
Если вы столкнулись с похожей ситуацией. Имейте в виду, что приведенное выше решение будет работать быстро только с индексом hashed.
«$licensesMap[$$user._id]» — это недопустимый путь для агрегации. Поскольку ваш вопрос довольно большой, можно ли его упростить? например, сосредоточиться на этапе, который не работает (если проблема в последнем проекте), также могут помочь некоторые примеры данных до проблемного этапа и ожидаемый результат.