Найдите каждый доступный вариант товара

MongoDBBeginner
Практиковаться сейчас

Введение

RIDGE & REED продаёт одежду для активного отдыха. Покупателю нужна зелёная вещь среднего размера, которая есть в наличии и стоит не более 5000 центов. У товара могут быть зелёный вариант и вариант среднего размера в наличии, но не быть доступного зелёного варианта среднего размера. Даже после выбора правильного товара показ всего массива его вариантов или самого дешёвого недоступного варианта всё равно ввёл бы покупателя в заблуждение.

Вы сохраните селектор товаров для повторного использования, а затем создадите конвейер агрегирования MongoDB, который возвращает все подходящие SKU и вычисляет самую низкую цену по тем же вариантам. Вам понадобятся базовые навыки навигации в терминале, подключения к MongoDB, работы с объектами и массивами, а также базовое знание find/update. Шаблоны функций JavaScript, выражения и стадии агрегирования объясняются перед использованием. На протяжении всей работы используйте Terminal; MongoDB, синтетические складские данные и клиенты только для чтения уже подготовлены.

Меняйте только selector.js и finder.js. Сохраните все исходные товары, ID вариантов, атрибуты, остатки и цены, а также предоставленные инструменты. Доступность — это подготовленное количество на складе, а не гарантия бронирования или оформления покупки. Каждый фактический вариант содержит текстовые SKU, цвет и размер и неотрицательные целые значения остатка и цены в центах. Варианты товара могут быть массивом, null или отсутствовать; другие некорректные типы входных данных выходят за рамки этого упражнения.

Примените все условия к одному варианту

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

cd /home/labex/project/variant-desk

./catalog — предоставленное средство чтения в этом каталоге; ./ означает текущий каталог. Передайте ему ID товара, чтобы прочитать фактически сохранённый документ:

./catalog TRAIL-1

У Trail Jacket есть три отдельных варианта:

SKU Цвет Размер Остаток Цена в центах
T-GM green M 0 1800
T-BM blue M 5 2200
T-GL green L 4 2100

Зелёного варианта среднего размера нет в наличии. Синий вариант среднего размера и зелёный большого размера доступны, но ни один не удовлетворяет запросу целиком. SKU идентифицирует один вариант; ID товара идентифицирует его родительский товар. Сохраните эти связи.

./db подключает оболочку MongoDB к каталогу этого курса только для чтения. --eval выполняет JavaScript в кавычках и завершает работу. Одинарные кавычки оболочки защищают пробелы и операторы MongoDB с префиксом доллара от подстановок оболочки; двойные кавычки внутри относятся к строкам JavaScript. Пути с точкой, например "variants.color", обращаются к вложенным полям. $gt: 0 означает больше нуля; $lte: 5000 означает меньше или равно 5000, включая границу бюджета. Числовые значения — это числа, а не строки денежных сумм в кавычках.

Этот обычный запрос с путями через точку намеренно слишком широк. Второй аргумент find проецирует только ID товара: 1 включает поле, а _id: 0 исключает внутренний ID MongoDB. sort с 1 сортирует по возрастанию; toArray() собирает возвращённые документы в один удобочитаемый массив:

./db --eval 'db.products.find({"variants.color":"green","variants.size":"M","variants.stock":{$gt:0},"variants.price_cents":{$lte:5000}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'

Ожидаются CAMP-3, RIDGE-2 и TRAIL-1. TRAIL-1 — ложное совпадение: разные элементы массива обеспечивают выполнение разных условий. Запрос выполнился успешно, но его ответ с точки зрения задачи неверен.

$elemMatch помещает все критерии на один элемент массива. Возможно, этот оператор уже знаком вам из основ работы с массивами MongoDB; здесь это первый компонент полноценного результата для покупателя:

./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1}).sort({product_id:1}).toArray()'

Теперь остаются только CAMP-3 и RIDGE-2. У подходящего товара есть хотя бы один подходящий вариант. Это ещё не означает, что каждый вариант этого товара подходит.

Сохраните селектор, который принимает запрос как аргумент, вместо того чтобы закреплять green/M/5000 внутри запроса. Функция JavaScript — код для повторного использования: function buildSelector(request) задаёт её имя и принимает объект с именем request; request.color читает свойство цвета этого объекта. return передаёт объект запроса подготовленному клиенту. Фигурные скобки содержат объекты или тело функции, двоеточия связывают ключи объектов со значениями, а запятые разделяют свойства. Требуемый интерфейс — функция buildSelector(request), возвращающая один объект запроса. Это не команда оболочки и не экспорт модуля.

В оболочке cat > перезаписывает файл назначения. <<'JS' начинает буквальный here-document, сохраняя знаки доллара и кавычки. Вставьте завершающий JS на отдельной строке; весь блок вводите в приглашении оболочки:

cat > selector.js <<'JS'
function buildSelector(request) {
  return {
    variants: { $elemMatch: {
      color: request.color,
      size: request.size,
      stock: { $gt: 0 },
      price_cents: { $lte: request.max_cents }
    } }
  };
}
JS

./select читает эту сохранённую функцию и без изменений выполняет возвращённый ею селектор через find MongoDB. Три его аргумента — цвет, размер и максимальная цена в центах. Клиент преобразует бюджет в число; строки вроде green и M остаются точными значениями с учётом регистра:

./select green M 5000

Фактически подходящие документы — CAMP-3 и RIDGE-2. Их исходные массивы по-прежнему отображаются целиком, включая неподходящие варианты. Это нормально для find: выборка выбирает документы, а не элементы для отображения. Сохраните селектор как полезный результат, затем решите проблему отображения на следующем шаге.

Если файл отсутствует, используйте ls, чтобы вывести содержимое каталога, и проверьте имя файла и абсолютный рабочий каталог. При синтаксической ошибке JavaScript замените весь сохранённый блок исправленной версией и снова запустите клиент. Если оболочка всё ещё ждёт ввод, введите недостающую завершающую строку JS. Не меняйте исходные остатки, не удаляйте варианты и не сбрасывайте базу данных, чтобы запрос казался правильным.

Верните каждый подходящий SKU

На этом шаге вы превратите выбор товаров в полный результат по вариантам, не ограничиваясь первым совпадением и не включая весь массив. Начните в той же вкладке Terminal в /home/labex/project/variant-desk.

Прочитайте два настоящих совпадения:

./catalog RIDGE-2
./catalog CAMP-3

Для green/M с максимумом 5000 центов у Ridge Jacket есть два подходящих SKU: R-GM1 за 4500 и R-GM2 за 5000. R-GM3 слишком дорог; другие размеры и цвета, а также вариант с нулевым остатком не подходят. У Camp Vest есть C-GM1 и C-GM2, оба за 4000. Равные цены не делают разные SKU дубликатами.

Позиционная проекция "variants.$": 1 возвращает только первый подходящий элемент массива. Посмотрите, почему она не может предоставить полный набор для этого запроса:

./db --eval 'db.products.find({variants:{$elemMatch:{color:"green",size:"M",stock:{$gt:0},price_cents:{$lte:5000}}}},{_id:0,product_id:1,"variants.$":1}).sort({product_id:1}).toArray()'

В этом выводе у каждого товара только один вариант. Второй допустимый SKU отсутствует, хотя он доступен. Возврат всего исходного массива привёл бы к противоположной ошибке — включению ненужных вариантов.

Конвейер агрегирования — массив стадий, обрабатываемых по порядку. Каждая стадия получает документы предыдущей стадии. $project выбирает выходные поля и может вычислять новое поле. $filter вычисляет массив, содержащий каждый элемент, для которого условие истинно; здесь ограничение количества не применяется.

Внутри $filter свойство input задаёт исходный массив, а as: "v" именует текущий вариант. "$variants" ссылается на поле документа; "$$v.color" — на цвет текущего элемента массива. Эти знаки доллара — выражения MongoDB внутри строк JavaScript. request.color — значение запроса JavaScript, переданное клиентом.

Сравнения при агрегировании используют массив операндов: { $eq: [left, right] } проверяет равенство, $gt — больше ли значение, а $lte — меньше ли оно или равно. $and требует истинности каждого сравнения для того же самого v. Это отличается от записи объекта запроса в $elemMatch, хотя условия задачи идентичны.

$ifNull: ["$variants", []] подставляет пустой массив для исходного поля, равного null или отсутствующего. [] — пустой массив JavaScript. Значение, не являющееся массивом, например строка, выходит за рамки предоставленного контракта входных данных; этот конвейер не предназначен для очистки произвольно некорректных каталогов.

После фильтрации $match убирает товары без подходящих вариантов. "variants.0" ссылается на нулевой индекс, первый элемент массива; $exists: true требует его наличия. Это предикат запроса на стадии match, а не выражение внутри условия фильтра. Последний $sort делает фактический вывод удобным для чтения.

Сохраните полный шаблон в finder.js. Его требуемый интерфейс — функция buildPipeline(request), возвращающая массив стадий. Клиент выполнит этот возвращённый конвейер без изменений; после выполнения он не фильтрует ответ:

cat > finder.js <<'JS'
function buildPipeline(request) {
  return [
    { $project: {
        _id: 0, product_id: 1, title: 1,
        variants: { $filter: {
          input: { $ifNull: ["$variants", []] },
          as: "v",
          cond: { $and: [
            { $eq: ["$$v.color", request.color] },
            { $eq: ["$$v.size", request.size] },
            { $gt: ["$$v.stock", 0] },
            { $lte: ["$$v.price_cents", request.max_cents] }
          ] }
        } }
    } },
    { $match: { "variants.0": { $exists: true } } },
    { $sort: { product_id: 1 } }
  ];
}
JS
./finder green M 5000

Ожидаются ровно два товара. CAMP-3 содержит C-GM1 и C-GM2; RIDGE-2 содержит R-GM1 и R-GM2. Каждый возвращённый вариант сохраняет точные исходные SKU, цвет, размер, остаток и цену в центах. Товар сохраняет точные product_id и title. Поля минимальной цены пока нет; это следующий шаг.

Теперь конвейер возвращает все четыре подходящих SKU, включая два варианта Camp Vest с одинаковой ценой и SKU Ridge точно на границе бюджета. TRAIL-1, пустые массивы и массивы, равные null или отсутствующие, не дают товаров в результате. Для получения этого результата ни один исходный документ не удалялся и не редактировался.

Если клиент сообщает, что buildPipeline должна возвращать массив конвейера, проверьте, что сохранённый файл включает всю функцию и return [ ... ];. Если он сообщает о синтаксической ошибке, замените полный блок и повторите попытку. Сохранённый файл должен содержать JavaScript запроса; не добавляйте команды оболочки, интерактивную команду use или экспорты модулей.

Рассчитайте цену подходящего набора и повторно используйте поиск

На этом шаге вы добавите самую низкую подходящую цену и используете тот же сохранённый запрос для разных запросов покупателей, включая честный пустой результат. Начните во вкладке Terminal в /home/labex/project/variant-desk.

Самый дешёвый исходный вариант Ridge Jacket стоит 900 центов, но имеет нулевой остаток. Другой дешёвый вариант — неподходящего цвета. Рекламировать любой из них как начальную цену для green/M значило бы вводить покупателя в заблуждение, даже если сам товар выбран правильно.

$set добавляет или заменяет выходное поле на стадии агрегирования; сохранённый товар он не обновляет. Здесь он добавляет min_price_cents. Выражение $min с одним операндом-массивом возвращает его наименьшее числовое значение. "$variants.price_cents" обращается к ценам внутри массива вариантов текущего документа. Поместите эту стадию после фильтрации: она должна читать подходящий массив, созданный $project, а не исходный массив. Более поздняя стадия также делает это изменённое поле однозначно доступным.

Замените finder.js полным конвейером ниже. Единственная новая стадия вычисляет минимум после исключения пустых массивов:

cat > finder.js <<'JS'
function buildPipeline(request) {
  return [
    { $project: {
        _id: 0, product_id: 1, title: 1,
        variants: { $filter: {
          input: { $ifNull: ["$variants", []] },
          as: "v",
          cond: { $and: [
            { $eq: ["$$v.color", request.color] },
            { $eq: ["$$v.size", request.size] },
            { $gt: ["$$v.stock", 0] },
            { $lte: ["$$v.price_cents", request.max_cents] }
          ] }
        } }
    } },
    { $match: { "variants.0": { $exists: true } } },
    { $set: { min_price_cents: { $min: "$variants.price_cents" } } },
    { $sort: { product_id: 1 } }
  ];
}
JS
./finder green M 5000

Результат должен сохранить все четыре подходящих SKU и добавить минимум CAMP-3, равный 4000, и минимум RIDGE-2, равный 4500. min_price_cents вычисляется по возвращённому набору вариантов каждого товара. Не удаляйте повторы цен, не используйте минимум по всему товару и не вписывайте эти два числа вручную.

Теперь уменьшите бюджет, используя ту же функцию. Меняются значения запроса, а не сохранённый запрос:

./finder green M 4499

Остаётся только CAMP-3, по-прежнему с обоими SKU по 4000 центов и минимумом 4000. У Ridge Jacket нет подходящего варианта green/M ниже этого нового предела.

Измените цвет и предел цены:

./finder blue M 2500

RIDGE-2 теперь содержит только R-BM за 1000; TRAIL-1 — только T-BM за 2200. Товар, правильно исключённый для одного запроса, может быть включён для другого. Доступность относится к конкретной связи варианта и запроса, а не к постоянному флагу товара.

Наконец, попробуйте бюджет без совпадений:

./finder green M 500

Фактический результат — []. Клиент не подставляет демонстрационный результат и не показывает недоступные товары для заполнения экрана. Отсутствующий запрос или ошибка базы данных сообщаются как ошибка, а не маскируются под этот успешный пустой массив.

Итоговый результат конвейера содержит ровно product_id, title, variants и min_price_cents. Возвращайте каждый подходящий товар один раз и каждый подходящий SKU один раз со всеми пятью исходными полями варианта. Порядок товаров и вариантов может различаться; идентификаторы и значения составляют контракт задачи. Запрос также должен обрабатывать другой каталог с той же схемой и другими ID, ценами и остатками. Подготовленные клиенты читают настоящие результаты MongoDB и не исправляют выборку, массивы или цены.

Сохраните оба файла JavaScript и все исходные товары и инструменты. Вы создали запрос к каталогу для повторного использования, а не систему бронирования: одновременные изменения остатков или последующее оформление покупки всё ещё могут влиять на доступность. Следующий Challenge имеет собственные независимые данные проката и использует те же механизмы запросов с другими именами атрибутов и значениями запросов.

Резюме

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