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



