MongoDB 3.2 で追加された $lookup ステージを使ってみる

ややずるい気もしますが，仕事でどうしても使いたいというニーズも多く，ハンズオン的にやってみます．
利用するのは 2015/11/13時点 (原稿書き始め) で最新の 3.2.0-rc2 です．
これ専用に建てた CentOS 7.0 on Vagrant で動かしているので，バイナリの置き場などは雑です．

```bash
$ wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-3.2.0-rc2.tgz
$ tar -zxvf mongodb-linux-x86_64-rhel70-3.2.0-rc2.tgz
$ sudo mkdir -p /opt/mongodb
$ cp -R -n mongodb-linux-x86_64-rhel70-3.2.0-rc2 /opt/mongodb
$ echo PATH=/opt/mongodb/mongodb-linux-x86_64-rhel70-3.2.0-rc2/bin:$PATH
$ mongod --version
# 以下の様な感じで表示されれば OK!
# db version v3.2.0-rc2
# git version: 8a3acb42742182c5e314636041c2df368232bbc5
# OpenSSL version: OpenSSL 1.0.1e-fips 11 Feb 2013
# allocator: tcmalloc
# modules: none
# build environment:
#     distmod: rhel70
#     distarch: x86_64
#     target_arch: x86_64
```

よし，起動しよう．

```bash
$ mkdir -p ~/mongodb/data ~/mongodb/log
$ mongod --dbpath ~/mongodb/data --logpath ~/mongodb/log/mongod.log --fork
$ mongo # シェルが始まれば良い．
```

サンプル分析用のファイルを投入する．今回は，2015年のプロ野球セントラルリーグの規定打席到達選手の打撃成績データと，選手のプロフィールデータです．

データは以下に配置しました．

players.json: https://gist.github.com/Mura-Mi/a492ecce742d86c5e5bd
hitters_stats.json: https://gist.github.com/Mura-Mi/40ce800ec6f3f2372129

```bash
$ mongoimport --db central --collection players --file players.json # 選手データの投入
# 2015-11-13T16:12:35.839+0000	connected to: localhost
# 2015-11-13T16:12:35.864+0000	imported 24 documents
$ 
$ mongoimport --db central --collection stats --file hitters_stats.json 
$
$ mongo localhost:27017 # MongoDB Shell を起動する．
```

 ちゃんと入っているか，クエリを打って調べてみる．

```javascript
use central
db.teams.find();
// 以下の様な感じで出力されれば OK
//  { "_id" : ObjectId("56460d554936431a75165ab2"), "name" : "Yakult Swallows", "abbreviation" : "ヤ" }
// { "_id" : ObjectId("56460d554936431a75165ab3"), "name" : "Yomiuri Giants", "abbreviation" : "巨" }
// ...
db.teams.count()
// => 6
db.batters.find({}, {_id: 0, name: 1, team: 1})
// 以下のようになれば OK．
// { "name" : "川端慎吾", "team" : "ヤ" }
// { "name" : "山田哲人", "team" : "ヤ" }
// { "name" : "筒香嘉智", "team" : "Ｄ" }
// ...
db.batters.count()
// => 24
```

では，$lookup してみる．まずは，単純に Look Up するだけで，そこからの集計処理をしない．

```javascript
db.hitting_stats.aggregate(
  [
    {$lookup: {
        from: 'players', // どのコレクションを結合するか
        localField: 'name', // 集計対象のコレクション (hitting_stats) のどのフィールド？
        foreignField: 'name', // 結合対象のコレクション (player) のどのフィールド？
        as: 'profile' // 集計対象のコレクションのなんというフィールドをキーとして結合する？
      }
    }
  ]
);
```

この結果は，例えば1つのドキュメントは以下のようになる．

```json
{ 
  "_id" : ObjectId("564764654936431a75165af4"), 
  "name" : "川端慎吾", 
  "games" : 143,
  "plate" : 632,
  ...(中略)..., 
  "profile" : [ 
    { 
      "_id" : ObjectId("564761794936431a75165ac4"), 
      "name" : "川端慎吾", 
      "team" : "Ys", 
      "number" : 5, 
      "born_in" : "大阪府" 
    } 
  ] 
}
```

なるほど，きちんと `profile` フィールドに `player` コレクションのドキュメントが埋め込まれていますが，`profile` キーに紐づく値は配列になっています．条件の当てはまったドキュメントが複数あった場合にはすべてこの配列に含まれるわけですね．
(業務的に一意に定められることがわかっている場合は，この後に `$unwind` ステージを挟んで配列からスカラ値に噛み砕けば良いのかな？しかし，一意になるべきなのに実は一意ではないデータがストアされているときに，一見わかりづらいバグになりそうですね… Schema Validation で制御すればよいのかな？)

よく `$lookup` をして `ついに MongoDB で JOIN が出来るようになった！`と言われることもありますが，実際には一対多でドキュメント同士を紐付けるし，フィールドに紐づくキー同士を紐付けます．また，指定されたフィールドが存在しない場合，そのフィールドの値は暗黙的に `null` として扱われ，`null` 同士でマッチしたら JOIN されてしまうので，SQL の JOIN ほど柔軟なわけではないようですね．

では，選手の出身地ごとの本塁打数合計を出して，多い順に並べてみます．

```javascript
db.hitting_stats.aggregate(
  [
    {
      $lookup: {
        from: 'players',
        localField: 'name',
        foreignField: 'name',
        as: 'profile'
      }
    },
    { $unwind: "$profile" },
    {
      $group: {
        _id: "$profile.born_in",
        "HR": {$sum: "$homerun"}
      }
    },
    {
      $sort: {
        HR: -1
      }
    }
  ]
);

```

すると，こんな結果が得られます．

```json
{ "_id" : [ "兵庫県" ], "HR" : 50 }
{ "_id" : [ "ベネズエラ" ], "HR" : 38 }
{ "_id" : [ "ドミニカ共和国" ], "HR" : 36 }
{ "_id" : [ "岩手県" ], "HR" : 26 }
{ "_id" : [ "和歌山県" ], "HR" : 24 }
{ "_id" : [ "大阪府" ], "HR" : 21 }
{ "_id" : [ "鹿児島県" ], "HR" : 20 }
{ "_id" : [ "千葉県" ], "HR" : 19 }
{ "_id" : [ "神奈川県" ], "HR" : 16 }
{ "_id" : [ "佐賀県" ], "HR" : 15 }
{ "_id" : [ "東京都" ], "HR" : 14 }
{ "_id" : [ "島根県" ], "HR" : 13 }
{ "_id" : [ "広島県" ], "HR" : 11 }
{ "_id" : [ "アメリカ合衆国" ], "HR" : 9 }
{ "_id" : [ "愛知県" ], "HR" : 6 }
{ "_id" : [ "福井県" ], "HR" : 2 }
```
選手数が多くないのでちょっと微妙ですが…やはり山田哲人が兵庫を引っ張ってますね．外国人事情は北米でなく中南米なんですね．

ということで，`$lookup` を使ってみるの巻き，でした．