ひとりMongoDB University / M121 Aggregation Framework(2)

9 min read読了の目安(約8800字

この記録は、アドベントカレンダー形式ではじめた、MongoDB Universityの学習コースの記録の続きになります!

ただいまのコース

このコースでは、Aggregationの深堀りをしていきます。
前回の記事は、ひとりMongoDB University / M121 Aggregation Framework(1) でした。

Chapter 1: Basic Aggregation - $match and $project

Lab - Changing Document Shape with $project

練習問題です。

Problem:

ISPから、帯域幅の制限(クォータ)に近くなっていますよ(気をつけて)との連絡が。
でも、他の映画のレコメンドをしないといけません。
前の課題で、パイプラインでmatchのステージを使ったので、さらにproject stageで必要な列だけに制限してデータを抽出してください
タイトル、映画のレーディングだけを取り出してください (title and rated fields)

_id は除外でやってみる。

// 前回の自分の答え(プロジェクションでのフィールド絞り込みはしていない)
var pipeline = [
  { $match: { $and:[
      { "imdb.rating": { $gte: 7 } },
      { genres: { $nin: [ "Crime", "Horror" ] } },
      { rated: { $in: [ "PG", "G" ] } },
      { languages: { $all: [ "English", "Japanese" ] } }
  ] } }
]

// 今回はプロジェクションのステージを追加
// 抽出するフィールドを絞り込みます
var pipeline = [
  { $match: { $and:[
      { "imdb.rating": { $gte: 7 } },
      { genres: { $nin: [ "Crime", "Horror" ] } },
      { rated: { $in: [ "PG", "G" ] } },
      { languages: { $all: [ "English", "Japanese" ] } }
      ]
    }
  },
  {
    $project: { title: 1, rated: 1, _id: 0 }
  }
]

// aggregate() にオプション指定で確認
// res = db.movie.explain().aggregate(pipeline) も同じ
res = db.movies.aggregate(pipeline, { explain: true })
res.stages.pop()["$project"]
{ "_id" : false, "title" : true, "rated" : 1 }


> validateLab2(pipeline)
false
true
true
Answer is 15 // これが結果

リファレンス

Lab - Computing Fields

ひきつづき練習問題です。
プロジェクションでのフィールドの抽出だけでなく、加工したフィールドを取り出します。

Problem:

movieコレクションには様々な形式のドキュメントが含まれます。タイトルが複雑なものがあったりします。タイトルが単語1つで構成されたものを分析したい場合、まずはデータベースから全てのデータを取り出し、クライアントアプリケーションで処理したりします。
アグリゲーションフレームワークを使うと、サーバ側でそう言った条件を加味した結果を返してくれます。

タイトルが単語一個だけのものを取り出してみましょう。

やってみる。

  • ヒント
    • $split String expression を使って配列に加工
    • $size Array expression を使ってカウントする

// 今回はプロジェクションのステージを追加
// 抽出するフィールドを絞り込みます
var pipeline = [
  {
    $project: {
      title: 1,
      rated: 1,
      _id: 0,
      "titleSize": { $size: { $split: [ "$title", " " ] } }
    }
  },
  { $match: { titleSize: 1 } }
]

MongoDB Enterprise > db.movies.aggregate(pipeline).itcount()
8066


// これでもいいみたい(まず先にstringを指定抽出)
db.movies.aggregate([
  {
    $match: {
      title: {
        $type: "string"
      }
    }
  },
  {
    $project: {
      title: { $split: ["$title", " "] },
      _id: 0
    }
  },
  {
    $match: {
      title: { $size: 1 }
    }
  }
]).itcount()
8066

できました!
ただし、パイプラインはステージの順番通りの処理なので、順番が適切でないと、結果が正しく出ません。


var pipeline = [
  { $match: { titleSize: 1 } },
  {
    $project: {
      title: 1,
      rated: 1,
      _id: 0,
      "titleSize": { $size: { $split: [ "$title", " " ] } }
    }
  }
]

// これだと0件
MongoDB Enterprise > db.movies.aggregate(pipeline).itcount()
0

Optional Lab - Expressions with $project

Problem:

movieのドキュメントのWritersフィールドがある場合とない場合があります
Writersには配列でライターの名前が列挙されています

writersフィールドの配列が空っぽでない、というものを抽出するならこの条件で可能です。

// writersフィールドの配列が空っぽでない、というものを抽出するならこの条件
{ $match: { writers: { $elemMatch: { $exists: true } } }

ただし、結果の中にはこのような表記がある場合も!

"writers" : [ "Vincenzo Cerami (story)", "Roberto Benigni (story)" ]

また、 "Roberto Benigni"さんは、Castの中にも登場します。

// データの例
> db.movies.findOne({title: "Life Is Beautiful"}, { _id: 0, cast: 1, writers: 1})
{
        "cast" : [
                "Roberto Benigni",
                "Nicoletta Braschi",
                "Giustino Durano",
                "Giorgio Cantarini"
        ],
        "writers" : [
                "Vincenzo Cerami (story)",
                "Roberto Benigni (story)"
        ]
}

// Roberto Benigni さんがCastにもWritersにも両方出てくる。。。

こういったケースでは、$map を利用することができる。
$mapは配列をイテレートして、要素を変換してくれる。(JavaScriptのmap的な!)
オリジナルの配列を変更する、もしくは新しい配列を生成してくれる、どちらでもできます。

以下の例:

  • $map で、input で指定されたフィールド(配列)に対して、1つ1つ処理を行う
  • as はいわゆるループでの変数名
  • in は、加工する対象
// この例では、$arrayElemAt で、$$writer (ループで取り出した変数)を分割
// [ "Roberto Benigni", "story)" ] という配列になるので、そのうち氏名(インデックス0)のみを取り出すことになる
writers: {
  $map: {
    input: "$writers",
    as: "writer",
    in: {
      $arrayElemAt: [
        {
          $split: [ "$$writer", " (" ]
        },
        0
      ]
    }
  }
}

db.movies.aggregate(
  [
    { $match: { title: "Life Is Beautiful" } },
    { $project:
      {
        _id: 0,
        cast: 1,
        writers: 1
      }
    }
  ]
).pretty()

// 結果がfindOneとおなじなのを確認
{
        "cast" : [
                "Roberto Benigni",
                "Nicoletta Braschi",
                "Giustino Durano",
                "Giorgio Cantarini"
        ],
        "writers" : [
                "Vincenzo Cerami (story)",
                "Roberto Benigni (story)"
        ]
}

// 実験
var condition = writers: {
  $map: {
    input: "$writers",
    as: "writer",
    in: {
      $arrayElemAt: [
        {
          $split: [ "$$writer", " (" ]
        },
        0
      ]
    }
  }
}

db.movies.aggregate(
  [
    { $match: { title: "Life Is Beautiful" } },
    { $project:
      {
        _id: 0,
        cast: 1,
        writers:  {
          $map: {
            input: "$writers",
            as: "writer",
            in: {
              $arrayElemAt: [
                {
                  $split: [ "$$writer", " (" ]
                },
                0
              ]
            }
          }
        }
      }
    }
  ]
).pretty()

// 加工されました!
{
        "cast" : [
                "Roberto Benigni",
                "Nicoletta Braschi",
                "Giustino Durano",
                "Giorgio Cantarini"
        ],
        "writers" : [
                "Vincenzo Cerami",
                "Roberto Benigni"
        ]
}

つぎの問題:

Let's find how many movies in our movies collection are a "labor of love", where the same person appears in cast, directors, and writers
"labor of love"の映画について抽出しましょう!(cast, directors, and writers)
また、そのなかで「同じ人物が登場する」ものを見つけましょう

Hint: You will need to use $setIntersection operator in the aggregation pipeline to find out the result.

$setIntersectionを使うのがヒント。


// まず基本
db.movies.aggregate(
  [
    { $project:
      {
        _id: 0,
        cast: 1,
        directors: 1,
        writers:  {
          $map: {
            input: "$writers",
            as: "writer",
            in: {
              $arrayElemAt: [
                {
                  $split: [ "$$writer", " (" ]
                },
                0
              ]
            }
          }
        },
      }
    },
    { $count: "labors of love" }
  ]
).pretty()

// 兼任だけじゃない場合はこれだけ
{ "labors of love" : 44488 }

// $setIntersection を使ってみます
// 引数に取れる配列は可変。フィールド名を渡すなら、"$フイールド名" で
// $matchも$projectも最初だけじゃない!! つぎつぎ絞り込む形なら良い!
db.movies.aggregate(
  [
    { $match:
      {
        writers: { $elemMatch: { $exists: true } },
        cast: { $elemMatch: { $exists: true } },
        directors: { $elemMatch: { $exists: true } }
      }
    },
    { $project:
      {
        _id: 0,
        cast: 1,
        directors: 1,
        writers:  {
          $map: {
            input: "$writers",
            as: "writer",
            in: {
              $arrayElemAt: [
                {
                  $split: [ "$$writer", " (" ]
                },
                0
              ]
            }
          }
        },
      }
    },
  {
    $project: {
      labor_of_love: {
        $gt: [
          { $size: { $setIntersection: ["$cast", "$directors", "$writers"] } },
          0
        ]
      }
    }
  },
  {
    $match: { labor_of_love: true }
  },
    { $count: "labors of love" }
  ]
).pretty()

// 結果は1596
{ "labors of love" : 1596 }

なかなか回答につながるMQLが書けずに、今回はてこずりました....!

Chapter: 1 / $match and $project のおさらい

今回は、もっと実践的な $match$project の利用になりました。
基本的には、$match は絞り込み、$project はフィールドの指定を行います。

ただし、実際はqueryオペレータを使って、データを加工したり条件をより絞り込んだりしています。

とくに今回の課題では、以下もわかりました。

  • 基本はパイプラインは前から順に処理
  • 一度matchのステージやprojectのステージを通ったら、もう使えないわけではない
  • 加工をしながら、さらに$match$projectを何回でも利用することができる
  • queryオペレータも本当にいろいろあるので、リファレンスが欠かせない!

さて今回は、再びVSCodeでのデータ確認をしていました。
課題の確認用のMongoDBは、MongoDBが用意しているレプリカセットを使っています。

レプリカセットの場合は、複数のMongod (MongoDBのサービス)をURLとして指定するのですが、VSCodeのプラグインでは、以下のような形でした。

Chapter1までできりが良いので、ここまで。
5月中旬の締め切りまで、週一くらいで進めないと間に合わない....!