HOWTO · MongoDB

MongoDB のルックアップオペレーターでパイプラインを使用する

このチュートリアルでは、MongoDB のルックアップ演算子でパイプラインを使用する方法を示します。

このページの内容

MongoDB の $lookup ステージ内でパイプラインを使うと、shopId が一致し、collection1 のドキュメントに shopPosId フィールドがない場合に、collection2 のドキュメントへ collection1 のドキュメントを結合できます。$lookup は左外部結合なので、入力された collection2 のドキュメントはすべて結果に残り、一致するドキュメントがない行では shopDescription 配列が空になります。一致した行だけが必要な場合は、後続の $match ステージを追加します。

集約パイプラインとは

集約パイプラインでは、順序に沿って各ステージにドキュメントを渡して処理します。各ステージは入力を絞り込んだり、形を変えたりして、その結果を次のステージへ渡します。この例では $lookup が一致するドキュメントを追加し、後続の $match で結合結果を絞り込めます。MongoDB には map-reduce や単一目的の集約操作もありますが、この結合には必要ありません。

詳しくはMongoDB の集約パイプラインの概要を参照してください。

MongoDB の $lookup 演算子とは

$lookup は同じデータベース内の別のコレクションからドキュメントを結合し、一致したドキュメントを配列フィールドに格納します。from オプションで検索するコレクションを指定し、as で各入力ドキュメントに追加する配列の名前を指定します。パイプライン形式を使うと、結合するドキュメントに条件を適用できます。

利用できる構文形式についてはMongoDB の $lookup リファレンスを参照してください。以下の例では相関パイプラインを使います。let は入力ドキュメントの値をパイプライン内で参照できるようにし、$expr を使うと $match でその値とフィールドを比較できます。この記事の MongoDB シェルコマンドは MongoDB データベースで実行する例です。設定された記事用ランタイムはデータベース操作を実行しません。

MongoDB で条件付き結合を行うために $lookup でパイプラインを使う

次のドキュメントを使って collection1 と collection2 を作成します。セットアップは MongoDB シェルの例です。使うデータベースに接続した mongosh で各文を実行してください。

db.createCollection('collection1'); db.createCollection('collection2'); db.collection1.insertMany([{"shopId":"001","shopPosId":"001","description":"description for 001"},{"shopId":"002","description":"description for 002"},{"shopId":"003","shopPosId":"003","description":"description for 003"},{"shopId":"004","description":"description for 004"}]); db.collection2.insertMany([{"shopId":"001","shopPosId":"0078","clientUid":"474192"},{"shopId":"002","shopPosId":"0012","clientUid":"474193"},{"shopId":"003","shopPosId":"0034","clientUid":"474194"},{"shopId":"004","shopPosId":"0056","clientUid":"474195"}]);

次の単独の Python 例は、同じサンプルデータでの一致処理を模したものです。collection1 に shopId が一致し shopPosId がないドキュメントがあるため、店舗 002 と 004 に結合結果が含まれます。これは結果ロジックを示す例であり、MongoDB クエリではありません。後続のコードと出力ブロックは正確なバイト列を保つため英語のままです。表示される結果では店舗 002 と 004 に結合結果があります。

この例を実行します。

collection1 = [
    {"shopId": "001", "shopPosId": "001", "description": "description for 001"},
    {"shopId": "002", "description": "description for 002"},
    {"shopId": "003", "shopPosId": "003", "description": "description for 003"},
    {"shopId": "004", "description": "description for 004"},
]
collection2 = [
    {"shopId": "001", "shopPosId": "0078", "clientUid": "474192"},
    {"shopId": "002", "shopPosId": "0012", "clientUid": "474193"},
    {"shopId": "003", "shopPosId": "0034", "clientUid": "474194"},
    {"shopId": "004", "shopPosId": "0056", "clientUid": "474195"},
]

for source in collection2:
    matches = [
        item["description"]
        for item in collection1
        if item["shopId"] == source["shopId"] and "shopPosId" not in item
    ]
    print(f'{source["shopId"]}: {matches}')
001: []
002: ['description for 002']
003: []
004: ['description for 004']

MongoDB のパイプラインでは、let: {shopId: "$shopId"} を設定して、各 collection2 行の ID を lookup パイプラインに渡します。$expr: {$eq: ["$shopId", "$$shopId"]} で一致を調べます。$$ 接頭辞は let で定義した変数を参照します。この比較を $and の下にある {shopPosId: {$exists: false}} と組み合わせると、結合先の collection1 ドキュメントにそのフィールドがないことも条件にできます。

対応する最初のクエリは db.collection2.aggregate([{$lookup: {from: "collection1", let: {shopId: "$shopId"}, pipeline: [{$match: {$and: [{$expr: {$eq: ["$shopId", "$$shopId"]}}, {shopPosId: {$exists: false}}]}}], as: "shopDescription"}}]) です。このクエリでは、入力された collection2 の 4 件すべてが残ります。店舗 001 と 003 には条件に合う collection1 ドキュメントがないため、shopDescription 配列は空です。店舗 002 と 004 は、結合先のドキュメントに shopPosId がないため一致します。

一致する行だけを返すには、結合配列に 1 件以上の要素があるかを調べる $match ステージを追加します。条件は {$match: {shopDescription: {$ne: []}}} です。後続処理を含む全体のクエリは db.collection2.aggregate([{$lookup: {from: "collection1", let: {shopId: "$shopId"}, pipeline: [{$match: {$and: [{$expr: {$eq: ["$shopId", "$$shopId"]}}, {shopPosId: {$exists: false}}]}}], as: "shopDescription"}}, {$match: {shopDescription: {$ne: []}}}]) です。サンプルデータでは、店舗 002 と 004 の元の行が返されます。最初の lookup ステージは左外部結合のままです。2 番目のステージで、結合配列が空の行を除外します。