MongoDB按子文档值排序

时间:2014-05-06 15:53:41

标签: ruby-on-rails mongodb sorting mongoid

我尝试根据子字段的子文档字段对字段集合进行排序。

这是我的文档的简化版本:

{
  "_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 个答案:

答案 0 :(得分:1)

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

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