入手可能な商品の全バリエーションを探す

MongoDBBeginner
オンラインで実践に進む

はじめに

RIDGE & REED はアウトドア衣料品を販売しています。顧客は、在庫があり、5000 セント以下の、緑色の M サイズの商品を探しています。ある商品に緑色のバリエーションと、在庫のある M サイズのバリエーションがあっても、緑色の M サイズが入手できるとは限りません。正しい商品を選んだ後でも、バリエーション配列全体や、最も安くても在庫のない選択肢を表示すれば、顧客を誤解させてしまいます。

再利用可能な商品セレクターを保存し、条件を満たすすべての SKU を返して、その同じバリエーションから最低価格を計算する MongoDB 集計パイプラインを作ります。基本的なターミナル操作、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 には、次の異なる 3 つのバリエーションがあります。

SKU 色 サイズ 在庫数 価格(セント)
T-GM green M 0 1800
T-BM blue M 5 2200
T-GL green L 4 2100

緑色の M サイズは在庫切れです。青色の M サイズと緑色の L サイズには在庫がありますが、どちらもリクエスト全体を満たしません。SKU は 1 つのバリエーションを識別し、商品 ID はその親商品を識別します。この関係を保ってください。

./db は MongoDB シェルをこのコースの読み取り専用カタログに接続します。--eval は引用符内の JavaScript を実行して終了します。シェルの単一引用符は、空白と、ドル記号で始まる MongoDB の演算子をシェル展開から保護します。その内側の二重引用符は JavaScript の文字列です。"variants.color" のようなドットパスは、入れ子のフィールドを参照します。$gt: 0 はゼロより大きいこと、$lte: 5000 は 5000 以下、つまり予算の境界値も含むことを意味します。数値は数値であり、引用符で囲んだ通貨文字列ではありません。

次の通常のドットパスクエリは、意図的に条件を広くしすぎています。find の第 2 引数は、商品 ID だけを投影します。1 はフィールドを含め、_id: 0 は MongoDB の内部 ID を除外します。sort に 1 を指定すると昇順になり、toArray() は返されたドキュメントを 1 つの読みやすい配列にまとめます。

./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 は、すべての条件を 1 つの配列要素に適用します。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 だけが残ります。一致する商品には、条件を満たすバリエーションが少なくとも 1 つあります。ただし、その商品のすべてのバリエーションが条件を満たすという意味ではありません。

クエリ内に green/M/5000 を固定せず、リクエストを引数として受け取るセレクターを保存します。JavaScript 関数は再利用可能なコードです。function buildSelector(request) は関数に名前を付け、request というオブジェクトを受け取ります。request.color は、そのオブジェクトの color プロパティを読みます。return は準備済みのクライアントへクエリオブジェクトを渡します。波括弧はオブジェクトや関数本体を囲み、コロンはオブジェクトのキーと値を結び、カンマはプロパティを区切ります。必須の関数インターフェースは、クエリオブジェクトを 1 つ返す buildSelector(request) です。シェルコマンドやモジュールのエクスポートではありません。

シェルでは、cat > が保存先ファイルを置き換えます。<<'JS' は、ドル記号と引用符をそのまま保持するヒアドキュメントを開始します。最後の 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 は保存した関数を読み、その関数が返すセレクターを変更せずに MongoDB の find で実行します。3 つの引数は、色、サイズ、価格上限のセントです。クライアントは予算を数値に変換します。green や M のような文字列は、大文字・小文字を区別する厳密なラベルのままです。

./select green M 5000

実際に一致するドキュメントは CAMP-3 と RIDGE-2 です。一致しないバリエーションも含め、元の配列全体が引き続き表示されます。find ではこれが通常の動作です。選択するのはドキュメントであり、表示する配列要素ではありません。セレクターは有用な成果として保存し、次のステップで表示の問題を解決します。

ファイルがなければ、ls でこのディレクトリを一覧表示し、ファイル名と作業ディレクトリの絶対パスを確認します。JavaScript の構文エラーでは、保存したブロック全体を修正版で置き換え、クライアントを再実行します。シェルが入力待ちなら、不足している最後の JS 行を入力します。クエリが正しく見えるように、元の在庫数を変えたり、バリエーションを削除したり、データベースをリセットしたりしないでください。

条件を満たすすべての SKU を返す

このステップでは、最初の一致だけを返したり、配列全体を出してしまったりせず、商品の選択を完全なバリエーション結果へと進めます。同じ Terminal の /home/labex/project/variant-desk から始めます。

本当に一致する 2 商品を読み取ります。

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

上限 5000 セントの green/M に対して、Ridge Jacket には条件を満たす SKU が 2 つあります。4500 の R-GM1 と 5000 の R-GM2 です。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()'

この出力では、各商品にバリエーションが 1 つしかありません。在庫があるにもかかわらず、もう 1 つの正当な SKU が欠けています。元の配列全体を返すと、今度は不要なバリエーションまで含めるという逆の誤りになります。

集計パイプラインは、順番に処理するステージの配列です。各ステージは前のステージのドキュメントを受け取ります。$project は出力フィールドを選び、新しいフィールドの計算もできます。$filter は条件が真になるすべての要素を含む配列を計算します。ここでは件数制限を設けません。

$filter 内では、input が元の配列、as: "v" が現在のバリエーションの名前を指定します。"$variants" はドキュメントのフィールド、"$$v.color" は現在の配列要素の色を参照します。これらのドル記号は、JavaScript 文字列内の MongoDB の式です。request.color は、クライアントから渡された JavaScript のリクエスト値です。

集計の比較は、オペランドの配列を使います。{ $eq: [left, right] } は等しいこと、$gt はより大きいこと、$lte は以下であることを調べます。$and は、同じ v についてすべての比較が真であることを要求します。業務上の条件は同じでも、$elemMatch のクエリオブジェクトとは書き方が異なります。

$ifNull: ["$variants", []] は、元のフィールドが null または欠落している場合、空の配列に置き換えます。[] は JavaScript の空配列です。文字列などの配列以外の値は、提供された入力契約の範囲外です。このパイプラインは、任意の不正なカタログを修復できるというものではありません。

絞り込み後、$match が条件を満たすバリエーションのない商品を除外します。"variants.0" はインデックスゼロ、つまり最初の配列要素を参照し、$exists: true はその存在を要求します。これは match ステージのクエリ述語であり、filter 条件内の式ではありません。最後の $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

結果はちょうど 2 商品になるはずです。CAMP-3 は C-GM1 と C-GM2、RIDGE-2 は R-GM1 と R-GM2 を含みます。返される各バリエーションは、SKU、色、サイズ、在庫数、セント単位の価格を厳密に保ちます。商品は product_id と title を厳密に保ちます。最低価格のフィールドはまだありません。次のステップで追加します。

パイプラインは、同額の Camp Vest の 2 バリエーションと、ちょうど予算上限にある Ridge の SKU を含め、条件を満たす 4 SKU をすべて返します。TRAIL-1、空配列、null・欠落した配列からは、結果の商品は生じません。この結果を作るために元のドキュメントを削除・編集してはいません。

クライアントが buildPipeline はパイプライン配列を返す必要があると報告したら、保存したファイルに関数全体と return [ ... ]; が含まれるか確認します。構文エラーなら、完全なブロックを置き換えて再試行します。クエリの JavaScript は保存したファイル内に保ち、シェルコマンド、対話用の use コマンド、モジュールのエクスポートを追加しないでください。

一致した集合の価格を求め、検索を再利用する

このステップでは、条件を満たす最低価格を追加し、該当なしを正直に示す空の結果も含め、同じ保存済みクエリを異なる顧客リクエストに使います。Terminal の /home/labex/project/variant-desk から始めます。

Ridge Jacket の元の最安バリエーションは 900 セントですが、在庫数はゼロです。別の安いバリエーションは色が違います。商品自体を正しく選んでいても、どちらかを green/M の最低価格として宣伝すれば、顧客を誤解させてしまいます。

$set は集計ステージの出力フィールドを追加・置換するもので、保存された商品を更新しません。ここでは min_price_cents を追加します。配列オペランドを 1 つ取る $min 式は、その最小の数値を返します。"$variants.price_cents" は、現在のドキュメントのバリエーション配列内の価格を参照します。このステージは絞り込みの後に置きます。元の配列ではなく、$project が作った一致する配列を読む必要があるためです。後続のステージにすることで、変更後のフィールドをあいまいさなく参照できます。

finder.js を以下の完全なパイプラインで置き換えます。新しいステージは 1 つだけで、空配列を除外した後に最小値を計算します。

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

結果には、条件を満たす 4 SKU をすべて残し、CAMP-3 の最低価格 4000 と RIDGE-2 の最低価格 4500 を追加する必要があります。min_price_cents は、返される各バリエーション集合から求めます。同額の価格を重複除去したり、商品全体の最低価格を使ったり、この 2 つの数値を手書きしたりしないでください。

同じ関数で予算を下げます。リクエストの値は変わりますが、保存したクエリは変えません。

./finder green M 4499

CAMP-3 だけが残り、4000 セントの両 SKU と最低価格 4000 はそのままです。Ridge Jacket には、この新しい上限以内で条件を満たす green/M のバリエーションがありません。

色と上限を変えます。

./finder blue M 2500

今度は RIDGE-2 が 1000 の R-BM だけ、TRAIL-1 が 2200 の T-BM だけを含みます。あるリクエストで正しく除外された商品も、別のリクエストでは含まれることがあります。入手可能性は特定のバリエーションとリクエストの関係であり、商品の永続的なフラグではありません。

最後に、一致しない予算を試します。

./finder green M 500

実際の結果は [] です。クライアントは画面を埋めるためにデモ結果を代入したり、入手できない商品を表示したりしません。クエリの欠落やデータベースエラーは、正常な空配列に見せかけず、エラーとして報告されます。

最終的なパイプライン結果には、ちょうど product_id、title、variants、min_price_cents が含まれます。一致する各商品を 1 回ずつ、条件を満たす各 SKU を 1 回ずつ、元の 5 フィールドすべてとともに返してください。商品・バリエーションの順序は自由ですが、識別情報と値は業務上の契約です。クエリは、同じスキーマで ID・価格・在庫数が異なる別のカタログにも対応する必要があります。準備されたクライアントは実際の MongoDB の結果を読み取り、選択、配列、価格を修正しません。

保存した両 JavaScript ファイルと、元のすべての商品・ツールを保全します。作ったのは再利用可能なカタログクエリであり、予約システムではありません。同時に起こる在庫変更や後の購入手続きで、入手可能性は変わることがあります。次の Challenge には独立したレンタル在庫があり、異なる属性名とリクエスト値で同じクエリの仕組みを使います。

まとめ

異なるバリエーションにまたがる条件一致を拒否し、最初の一致だけでなく条件を満たすすべての SKU を残し、絞り込み後の配列から表示価格を計算しました。保存した関数は、元のカタログを保全したまま、変更されたリクエスト、同額の価格、空の結果に対応します。