Доступ к карте корневого документа в агрегации $filter (MongoDb)

Прошу прощения за расплывчатое описание вопроса, но у меня довольно сложный вопрос по фильтрации в агрегациях 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[$$user._id]» — это недопустимый путь для агрегации. Поскольку ваш вопрос довольно большой, можно ли его упростить? например, сосредоточиться на этапе, который не работает (если проблема в последнем проекте), также могут помочь некоторые примеры данных до проблемного этапа и ожидаемый результат.

Takis 13.05.2022 14:44

если licensesMap является объектом, подобным {userid1 : value, userid2 : value}, и вы хотите получить значение для userid2, например, вы можете использовать $getField, но только если вы используете строку, в вашем случае вы хотите использовать переменную mongodb $$user._id, поэтому это не представляется возможным с получением хэш-карты, линейный поиск выглядит как способ получить значение здесь, например, сохранить карту лицензий в виде массива и выполнить поиск по ней.

Takis 13.05.2022 15:05

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

TK-95 13.05.2022 16:36

да, я знаю, но у нас нет в mongodb функции get("$$key") у нас есть только get("key_string") см. оператор $getField

Takis 13.05.2022 17:31

Привет @Takis, я обновил описание и включил примеры данных с желаемым результатом. Кроме того, я пытался использовать $getField, но оператор $neq внутри $filter не распознал его.

TK-95 13.05.2022 22:19
Использование JavaScript и MongoDB
Использование JavaScript и MongoDB
Сегодня я собираюсь вкратце рассказать о прототипах в JavaScript, а также представить и объяснить вам работу с базой данных MongoDB.
0
5
58
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Как насчет чего-то простого, например:

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 коллекцию, чтобы выполнить операцию соединения. У меня действительно большой набор данных и агрегация, похожая на вашу. И, к сожалению, работает довольно медленно.

TK-95 16.05.2022 10:59

На самом деле, если вы проиндексируете его, оно должно быть O (n), где n — количество пользователей. Должен перебирать пользователей и для каждого из них находить свою лицензию (O (1)), если она хеширована.

nimrod serok 16.05.2022 11:10

Хм, интересно. Он проиндексирован, но применяется индекс B-Tree. Я попробую перестроить индекс и сделать некоторые измерения

TK-95 16.05.2022 22:30

Хэш-индексы сделали свое дело. Спасибо!

TK-95 17.05.2022 17:11

Рад слышать :) Если это работает для вас, пожалуйста, примите это, чтобы другим было легче его найти.

nimrod serok 17.05.2022 17:14

Если вы столкнулись с похожей ситуацией. Имейте в виду, что приведенное выше решение будет работать быстро только с индексом hashed.

Другие вопросы по теме