【问题标题】:Mongoid random documentMongoid 随机文档
【发布时间】:2011-12-07 05:43:03
【问题描述】:

假设我有一个用户集合。有没有一种方法可以使用 mongoid 在集合中找到 n 个随机用户,但它不会两次返回同一个用户?现在假设用户集合如下所示:

class User
  include Mongoid::Document
  field :name
end

简单吧?

谢谢

【问题讨论】:

  • MongoDB 团队正在考虑这个问题。他们根据需求对问题进行优先排序;所以如果你想要这个功能,请查看Ticket #533: Get random item(s) from Collection,阅读并相应地投票。
  • 票证已关闭,现在有一个 $sample 用于 MongoDB 的运算符。似乎还没有集成到 Mongoid,查询必须手动完成。您可能还想看看snapshot 以真正避免并发重复。

标签: ruby-on-rails ruby mongodb mongoid


【解决方案1】:

如果您只想要一个文档,并且不想定义新的标准方法,您可以这样做:

random_model = Model.skip(rand(Model.count)).first

如果您想根据某些标准找到随机模型:

criteria = Model.scoped_whatever.where(conditions) # query example
random_model = criteria.skip(rand(criteria.count)).first

【讨论】:

    【解决方案2】:

    最佳解决方案将取决于集合的预期大小。

    对于小型集合,只需获取所有集合和 .shuffle.slice!

    对于小尺寸的 n,您可以使用以下方法:

    result = (0..User.count-1).sort_by{rand}.slice(0, n).collect! do |i| User.skip(i).first end
    

    对于较大的 n,我建议创建一个“随机”列进行排序。详情见这里:http://cookbook.mongodb.org/patterns/random-attribute/https://github.com/mongodb/cookbook/blob/master/content/patterns/random-attribute.txt

    【讨论】:

    • 谢谢...这可能有点矫枉过正,但想知道是否有一种简单的方法可以将其转换回 Mongoid::Criteria
    • SQL 有 ORDER BY RAND() 但据我所知,在 mongodb 中没有等价的。因此,您可以创建“随机”列,然后创建 User.order_by,这将是一个查询。
    • 根据SO: MongoDB find random dataset performanceskip 对于大值不是很有效:“Skip 强制 Mongo 遍历结果集,直到它到达您要查找的文档,所以该查询的结果集越大,所需的时间就越长。” (这支持丹的回答。)
    • @DanHealy 链接已更改。我只找到了这个:github.com/mongodb/cookbook/blob/master/content/patterns/… 这个等价吗?
    • @FernandoKosh 是的,看起来非常相似。感谢您的更新!
    【解决方案3】:

    MongoDB 3.2 借助 $sample (link to doc) 来救援

    EDIT : 最新的 Mongoid 已经实现了 $sample,所以你可以调用YourCollection.all.sample(5)

    以前版本的 mongoid

    在 Mongoid 6 之前,Mongoid 不支持 sample,因此您必须使用 Mongo 驱动程序运行此聚合查询:

    samples = User.collection.aggregate([ { '$sample': { size: 3 } } ])
    # call samples.to_a if you want to get the objects in memory
    

    你可以用它做什么

    我相信这个功能应该很快就会进入 Mongoid,但与此同时

    module Utility
      module_function
      def sample(model, count)
        ids = model.collection.aggregate([ 
          { '$sample': { size: count } }, # Sample from the collection
          { '$project': { _id: 1} }       # Keep only ID fields
        ]).to_a.map(&:values).flatten     # Some Ruby magic
    
        model.find(ids)
      end
    end
    
    Utility.sample(User, 50)
    

    【讨论】:

    • 警告:似乎在某些 Mongoid 版本上,调用 .all.sample 会在调用 sample 之前解析为一个数组(而不是执行 MongoDB $sample),因此 User.all.sample 可能会将您的整个数据库加载到sample数组之前的内存。你最好在实现这个时检查你的 mongo 日志,以确保不会发生太糟糕的事情。
    【解决方案4】:

    如果你真的想要简单,你可以改用这个:

    class Mongoid::Criteria
    
      def random(n = 1)
        indexes = (0..self.count-1).sort_by{rand}.slice(0,n).collect!
    
        if n == 1
          return self.skip(indexes.first).first
        else
          return indexes.map{ |index| self.skip(index).first }
        end
      end
    
    end
    
    module Mongoid
      module Finders
    
        def random(n = 1)
          criteria.random(n)
        end
    
      end
    end
    

    您只需拨打User.random(5) 即可获得5 个随机用户。 它也适用于过滤,因此如果您只想要注册用户,您可以使用User.where(:registered => true).random(5)

    对于大型集合,这将需要一段时间,因此我建议您使用另一种方法,您可以对计数进行随机除法(例如:25 000 到 30 000)并随机化该范围。

    【讨论】:

    • 在什么地方,用什么名字保存这个文件?。怎么叫你到这个文件?谢谢!
    • @hyperrjas 您可以将此文件放在应用程序的 lib 文件夹中。然后确保您的应用程序配置为自动加载该文件夹中的文件。文件名无关紧要。
    • 谢谢。我已经在/app/lib 文件夹中添加了带有此代码的random.rb 文件,但是例如,如果我在控制台User.random(5) 中运行,我会收到错误NoMethodError: undefined method random' for User:Class`。我该如何解决这个问题?
    • 您的 lib 文件夹应该在应用程序根目录下,不确定这是否是您所说的应用程序,因为 rails 在应用程序根文件夹中也有一个应用程序文件夹。如果您的意思是 APP_ROOT/lib,请确保您在配置文件中设置了自动加载。 stackoverflow.com/questions/3356742/…
    【解决方案5】:

    你可以这样做

    1. 生成随机偏移,这将进一步满足选择下一个 n 元素(不超过限制)
    2. 假设count为10,n为5
    3. 为此检查给定的 n 是否小于总计数
    4. 如果没有设置偏移量为0,并转到步骤8
    5. 如果是,从总数中减去n,你会得到一个数字5
    6. 用这个来找一个随机数,这个数肯定是从0到5(假设2)
    7. 使用随机数 2 作为偏移量
    8. 现在您可以通过简单地传递此偏移量和 n (5) 作为限制来随机获取 5 个用户。
    9. 现在您的用户数从 3 到 7

    代码

    >> cnt = User.count
    => 10
    >> n = 5
    => 5
    >> offset = 0
    => 0
    >> if n<cnt
    >>    offset = rand(cnt-n)
    >>  end
    >> 2
    >> User.skip(offset).limit(n)
    

    你可以把它放在一个方法中

    def get_random_users(n)
      offset = 0
      cnt = User.count
      if n < cnt
        offset = rand(cnt-n)
      end
      User.skip(offset).limit(n)
    end
    

    然后这样称呼它

    rand_users = get_random_users(5)
    

    希望对你有帮助

    【讨论】:

    • 谢谢。但这真的会是随机的吗?我想这将提供从 cnt 到 cnt+n 的随机范围,但这不会创建条件。比如如果用户 5 被选中......用户 6 的可能性很高,而用户 11 的可能性为零?
    • 对,这是我回答的一个折衷方案。如果您可以从一个随机点开始并只选择接下来的 n 个连续记录,那么您可以在一个查询而不是 n 个查询中执行它。然后,您可以对结果进行洗牌,使其在该选择中随机化。但是不,这并不是随机的。
    【解决方案6】:

    因为我想保留一个标准,所以我这样做:

    scope :random, ->{
      random_field_for_ordering = fields.keys.sample
      random_direction_to_order = %w(asc desc).sample
      order_by([[random_field_for_ordering, random_direction_to_order]])
    }
    

    【讨论】:

      【解决方案7】:

      刚遇到这样的问题。试过了

      Model.all.sample
      

      它对我有用

      【讨论】:

      • 很确定这将从数据库中加载每个模型,然后使用Array#sample 方法选择一个随机项目。如果您只是在控制台中闲逛,我想可以,但不建议用于生产应用程序。
      • 如果模型中的项目数量更多,则需要很多时间
      • 它可以工作,但在我的情况下,它需要超过 20,000 个文档,如前所述。
      【解决方案8】:

      @moox 的方法非常有趣,但我怀疑猴子修补整个 Mongoid 是否是个好主意。所以我的方法只是写一个关注Randomizable,它可以包含在您使用此功能的每个模型中。这转到app/models/concerns/randomizeable.rb

      module Randomizable
        extend ActiveSupport::Concern
      
        module ClassMethods
          def random(n = 1)
            indexes = (0..count - 1).sort_by { rand }.slice(0, n).collect!
      
            return skip(indexes.first).first if n == 1
            indexes.map { |index| skip(index).first }
          end
        end
      end
      

      那么您的 User 模型将如下所示:

      class User
        include Mongoid::Document
        include Randomizable
      
        field :name
      end
      

      还有测试......

      require 'spec_helper'
      
      class RandomizableCollection
        include Mongoid::Document
        include Randomizable
      
        field :name
      end
      
      describe RandomizableCollection do
        before do
          RandomizableCollection.create name: 'Hans Bratwurst'
          RandomizableCollection.create name: 'Werner Salami'
          RandomizableCollection.create name: 'Susi Wienerli'
        end
      
        it 'returns a random document' do
          srand(2)
      
          expect(RandomizableCollection.random(1).name).to eq 'Werner Salami'
        end
      
        it 'returns an array of random documents' do
          srand(1)
      
          expect(RandomizableCollection.random(2).map &:name).to eq ['Susi Wienerli', 'Hans Bratwurst']
        end
      end
      

      【讨论】:

        【解决方案9】:

        我认为最好专注于随机化返回的结果集,所以我尝试了:

        Model.all.to_a.shuffle
        

        希望这会有所帮助。

        【讨论】:

        • 如果您的数据库中有一百万个 Model 实例,那将是可怕的。
        • 这只是一个例子。您绝对应该首先获得您正在寻找的结果集。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-08-19
        • 2016-10-02
        • 2012-05-29
        • 2014-11-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多