🍃

[MongoDB] ドキュメント内の配列をソートする

2022/06/24に公開

はじめに

MongoDBはひとつのドキュメント(RDBでいうところのレコード)にサブドキュメントや配列をネストして保持することが可能です。そのため、「ひとつのドキュメント内に含まれる配列を特定のキーでソートしたい」というニーズがあり、少々手こずったので方法を記載します。

具体的には以下のようなドキュメントを想定します。Aliceが投稿した本のレビューをreviewsとして配列で保持していますが、このドキュメントをstarやtitleでソートした状態で取得するにはどうすればよいか?という内容になります。

{
    "_id": "62b448342a2920dbf51973d0",
    "name": "Alice",
    "reviews": [
      {
        "_id": "62b448342a2920dbf51973d1",
        "title": "Perfect JavaScript",
        "star": 3
      },
      {
        "_id": "62b448342a2920dbf51973d2",
        "title": "Readable Code",
        "star": 5
      },
      {
        "_id": "62b448342a2920dbf51973d3",
        "title": "A Philosophy of Software Design",
        "star": 5
      },
      {
        "_id": "62b448342a2920dbf51973d4",
        "title": "Code Complete",
        "star": 4
      }
    ],
}

結論

aggregationを利用します。評価したい配列を$unwindしたうえで$sortします。-1は降順(DESC)を意味しており、以下のクエリはスターの高い順にソートします。1なら昇順(ASC)になります。

query.ts
const sorted = await UserModel.aggregate([
  {
    $match: {
      _id: "62b448342a2920dbf51973d0"
    }
  },
  {
    $unwind: "$reviews"
  },
  {
    $sort: {
      "reviews.star": -1
    }
  },
  {
    $group: {
      _id: "$_id",
      name: {
        $first: "$name"
      },
      reviews: {
        $push: "$reviews"
      }
    }
  }
]);

結果は以下のようになります。

  {
    "_id": "62b448342a2920dbf51973d0",
    "name": "Alice",
    "reviews": [
      {
        "_id": "62b448342a2920dbf51973d2",
        "star": 5,
        "title": "Readable Code"
      },
      {
        "_id": "62b448342a2920dbf51973d3",
        "star": 5,
        "title": "A Philosophy of Software Design"
      },
      {
        "_id": "62b448342a2920dbf51973d4",
        "star": 4,
        "title": "Code Complete"
      },
      {
        "_id": "62b448342a2920dbf51973d1",
        "star": 3,
        "title": "Perfect JavaScript"
      }
    ]
  }

内部的な動作としては取得したドキュメントを$unwindで一旦バラして、指定のキーでソートしたうえで$groupでまとめ直しているのかなと想像していますが、このあたりはあまり理解できていません。。。

通常のソート

通常、複数のドキュメントをソートするのであればsortによるクエリが一般的で、これはSQLにおけるORDER BY と同様に利用することが可能です。以下のようなコレクションから、ドキュメントをソートして取り出したい、というケースです。

  {
    "_id": "62b448342a2920dbf51973d2",
    "star": 5,
    "title": "Readable Code"
  },
  {
    "_id": "62b448342a2920dbf51973d3",
    "star": 5,
    "title": "A Philosophy of Software Design"
  },
  {
    "_id": "62b448342a2920dbf51973d4",
    "star": 4,
    "title": "Code Complete"
  },
  {
    "_id": "62b448342a2920dbf51973d1",
    "star": 3,
    "title": "Perfect JavaScript"
  }
sort.ts
const sorted = await Reviews.find().sort({"title":1});

非常に簡潔ですが、このsortは「ドキュメント」が対象になるので、findOneなどドキュメントがひとつに絞られるケースでは利用できませんし(エラーは起きませんがソートされない)、ドキュメント内のネストした配列を対象にソートすることはできません。

ソートする対象がドキュメントなのか、ドキュメント内にネストしたオブジェクトなのか、によってソートの方法が異なるので注意が必要です。

配列の要素をフィルターしてからソートする

ソートに加えてフィルターする必要がでてきたので、そちらの方法も記載しておきます。

query.ts
const filteredAndSorted = await UserModel.aggregate([
  {
    $match: {
      _id: "62b448342a2920dbf51973d0"
    }
  },
  {
    $project: {
      name: "$name",
      reviews: {
        $filter: {
          input: "$reviews",
          as: "review",
          cond: {
            $eq: [
              "$$review.star",
              5
            ]
          }
        }
      }
    }
  },
  {
    $unwind: "$reviews"
  },
  {
    $sort: {
      "reviews.title": 1
    }
  },
  {
    $group: {
      _id: "$_id",
      name: {
        $first: "$name"
      },
      reviews: {
        $push: "$reviews"
      }
    }
  }
]);

startが5のレビューをフィルタしてタイトルの昇順で取得してみました。結果は以下のようになります。

  {
    "_id": "62b448342a2920dbf51973d0",
    "name": "Alice",
    "reviews": [
      {
        "_id": "62b448342a2920dbf51973d3",
        "star": 5,
        "title": "A Philosophy of Software Design"
      },
      {
        "_id": "62b448342a2920dbf51973d2",
        "star": 5,
        "title": "Readable Code"
      }
    ]
  }

Discussion