【问题标题】:MongoDB sort by sub-sub document valueMongoDB按子子文档值排序
【发布时间】:2014-06-23 08:16:59
【问题描述】:

我正在尝试根据一个字段对文档集合进行排序,该字段是子文档的子文档的字段。

这是我的文档的一个非常简化的版本:

{
  "_id": ObjectId("536900cdb4f805efff8b075b"),
  "name": "el1",
  "versions": [{
    "releases": [{
      "rd": ISODate("2064-05-05T15:36:10.098Z")
    }, {
      "rd": ISODate("2014-05-01T16:00:00Z")
    }]
  }, {
    "releases": [{
      "rd": ISODate("2064-05-04T15:36:10.098Z")
    }, {
      "rd": ISODate("2014-05-01T14:00:00Z")
    }]
  }]
}, {
  "_id": ObjectId("536900f2b4f805efff8b075c"),
  "name": "el2",
  "versions": [{
    "releases": [{
      "rd": ISODate("2064-05-05T15:36:10.098Z")
    }, {
      "rd": ISODate("2014-05-01T17:00:00Z")
    }]
  }]
}

如您所见,每个文档可能有名为version 的子文档,每个version 可能有多个名为release 的子文档。我想根据rd 字段对主要文档进行排序,同时从sort 计算中排除所有超过一年的日期。我不关心在主文档中对子文档进行排序。

ISODate("2064-05-05T15:36:10.098Z") 应该被忽略,因为太远了,而 ISODate("2014-05-01T16:00:00Z") 很好。 “忽略”是指:不要在排序计算中使用该值,也不要:从结果中删除该文档。

我尝试了几种方法,包括map-reduceaggregation framework,但都失败了。

这应该是成功排序的输出:

{
  "_id": ObjectId("536900f2b4f805efff8b075c"),
  "name": "el2",
  "versions": [{
    "releases": [{
      "rd": ISODate("2064-05-05T15:36:10.098Z")
    }, {
      "rd": ISODate("2014-05-01T17:00:00Z")
    }]
  }]
}, {
  "_id": ObjectId("536900cdb4f805efff8b075b"),
  "name": "el1",
  "versions": [{
    "releases": [{
      "rd": ISODate("2064-05-05T15:36:10.098Z")
    }, {
      "rd": ISODate("2014-05-01T16:00:00Z")
    }]
  }, {
    "releases": [{
      "rd": ISODate("2064-05-04T15:36:10.098Z")
    }, {
      "rd": ISODate("2014-05-01T14:00:00Z")
    }]
  }]
}

【问题讨论】:

    标签: ruby-on-rails mongodb sorting mongoid


    【解决方案1】:

    请在下面的测试用例中为您的问题找到两个解决方案。 第一个解决方案使用 MongoDB 聚合框架。 对于每个文档,排序键会根据您的时间限制从 rd 值中投影出来。 通过展开两次然后对最大排序键进行分组来减少嵌套排序键结构。 对文档进行排序后,最后一个“项目”阶段会删除排序键。 第二种解决方案在客户端排序。 为了提高效率,它会处理每个文档以确定排序键并将其合并。 对文档进行排序后,它会从每个文档中删除排序键。 如果可以容忍排序键的存在,您可以消除删除它们。

    MongoDB 的一个主要优势是文档可以很好地映射到编程语言数据结构。 因此,我建议在寻找数据库解决方案之前,先使用 Ruby 来尝试解决方案。 请注意,在 Ruby 解决方案中,rd_sort_key 方法虽然简单,但并不重要, 表明您尝试使用条件和嵌套数组的内容相当复杂, 即使没有尝试在 MongoDB 的聚合框架中这样做。

    如果您无限制地获取整个结果集,则客户端解决方案是可以的。 如果您使用限制,服务器端解决方案可能会为您节省一些传输时间。 但与往常一样,您应该衡量和比较。

    我希望这会有所帮助,并且它很有趣并且可能很有启发性。

    test.rb

    require 'mongo'
    require 'date'
    require 'test/unit'
    
    def iso_date_to_time(s)
      DateTime.parse(s).to_time
    end
    
    class MyTest < Test::Unit::TestCase
      def setup
        @pipeline = [
            {'$project' => {
                'name' => '$name',
                'versions' => '$versions',
                'rd_sort_key' => {
                    '$map' => {
                        'input' => '$versions', 'as' => 'version', 'in' => {
                            '$map' => {
                                'input' => '$$version.releases', 'as' => 'release', 'in' => {
                                    '$cond' => [
                                        {'$lt' => ['$$release.rd', @year_from_now]},
                                        '$$release.rd',
                                        nil
                                    ]}}}}}}},
            {'$unwind' => '$rd_sort_key'},
            {'$unwind' => '$rd_sort_key'},
            {'$group' => {
                '_id' => '$_id',
                'name' => {'$first' => '$name'},
                'versions' => {'$first' => '$versions'},
                'rd_sort_key' => {'$max' => '$rd_sort_key'}}},
            {'$sort' => {'rd_sort_key' => -1}},
            {'$project' => {
                '_id' => '$_id',
                'name' => '$name',
                'versions' => '$versions'}}
        ]
        @coll = Mongo::MongoClient.new['test']['events_h']
        @docs = [
            {"_id" => BSON::ObjectId("536900cdb4f805efff8b075b"),
             "name" => "el1",
             "versions" => [{"releases" => [{"rd" => iso_date_to_time("2064-05-05T15:36:10.098Z")},
                                            {"rd" => iso_date_to_time("2014-05-01T16:00:00Z")}]},
                            {"releases" => [{"rd" => iso_date_to_time("2064-05-04T15:36:10.098Z")},
                                            {"rd" => iso_date_to_time("2014-05-01T14:00:00Z")}]}]
            },
            {"_id" => BSON::ObjectId("536900f2b4f805efff8b075c"),
             "name" => "el2",
             "versions" => [{"releases" => [{"rd" => iso_date_to_time("2064-05-05T15:36:10.098Z")},
                                            {"rd" => iso_date_to_time("2014-05-01T17:00:00Z")}]}]
            }]
        @expected_names = [@docs.last['name'], @docs.first['name']]
        @coll.remove
        @coll.insert(@docs)
        @year_from_now = Time.now + 60*60*24*365
      end
    
      test "aggregation sort with map and conditional" do
        result = @coll.aggregate(@pipeline)
        assert_equal(@expected_names, result.collect{|doc| doc['name']})
      end
    
      def rd_sort_key(doc, future_time_limit)
        sort_key = nil
        doc['versions'].each do |version|
          version['releases'].each do |release|
            rd = release['rd']
            sort_key = sort_key ? [sort_key, rd].max : rd if rd < future_time_limit
          end
        end
        sort_key
      end
    
      test "client sort with conditional" do
        result = @coll.find.to_a
        result.each{|doc| doc['rd_sort_key'] = rd_sort_key(doc, @year_from_now)}
        result = result.sort{|a, b| b['rd_sort_key'] ? b['rd_sort_key'] <=> a['rd_sort_key'] : -1}
        result.each{|doc| doc.delete('rd_sort_key')}
        assert_equal(@expected_names, result.collect{|doc| doc['name']})
      end
    end
    

    $ ruby​​ test.rb

    Loaded suite test
    Started
    ..
    
    Finished in 0.008794 seconds.
    
    2 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
    100% passed
    
    227.43 tests/s, 227.43 assertions/s
    

    【讨论】:

    • 哇,sort_key 方法看起来很有趣,以前从未使用过。肯定会尝试一下。实际上,我通过创建一个哈希集合来使用 mongoid 在 Rails 中完成此操作,每个哈希的名称和最大 rd 不大于 1y 并按此排序。我接受您的回答,因为您提供了 2 个可行的解决方案:D
    • 很高兴能提供帮助并听到您正在向前推进。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-07-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-07
    • 2016-04-10
    • 2014-08-14
    相关资源
    最近更新 更多