2014-05-06 74 views
0

我想基於字段是一個子文檔的子文檔字段排序文檔的集合。MongoDB按子文檔值排序

這裏是我的文檔的一個非常簡化的版本:

{ 
    "_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") 
    }] 
    }] 
} 

回答

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 

$紅寶石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 
+0

哇,'sort_key'方法看起來很有趣,從來沒有使用過這一點。肯定會嘗試一下。實際上,我通過創建一個名稱和max不大於1y的哈希集合並按它排序的方式,在mongoid的rails中完成此操作。我接受你的答案,因爲你提供了2個工作解決方案:D – Oktav

+0

很高興能夠幫助並聽到你正在放大。 –