2011-10-13 10 views
9

Diciamo che ho una collezione di utenti. C'è un modo di usare mongoid per trovare n utenti casuali nella raccolta in cui non restituisce lo stesso utente due volte? Per ora diciamo che la collezione utente assomiglia a questa:Documento casuale mongolo

class User 
    include Mongoid::Document 
    field :name 
end 

Semplice eh?

Grazie

+1

Questo viene considerato dal team MongoDB. Assegnano priorità ai problemi in base alla domanda; quindi se vuoi questa funzione, dai un'occhiata a [Ticket # 533: Get random item (s) from Collection] (https://jira.mongodb.org/browse/SERVER-533), leggi e vota di conseguenza. –

+0

Il ticket è stato chiuso e ora c'è un operatore '$ sample' per MongoDB. Non sembra ancora integrato a Mongoid, la query deve essere eseguita manualmente. Si potrebbe anche voler dare un'occhiata a 'snapshot' per evitare realmente i duplicati dalla concorrenza. –

risposta

13

La soluzione migliore è andare a dipendere dalle dimensioni atteso della collezione.

Per collezioni molto piccole, basta avere tutti loro e .shuffle.slice!

Per le piccole dimensioni di n, è possibile ottenere via con qualcosa di simile:

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

Per le grandi dimensioni di n, consiglio la creazione di una colonna di "random" per l'ordinamento. Vedi qui per maggiori dettagli: http://cookbook.mongodb.org/patterns/random-attribute/ https://github.com/mongodb/cookbook/blob/master/content/patterns/random-attribute.txt

+1

Grazie ... questo potrebbe essere eccessivo, ma mi chiedevo se ci fosse un modo semplice per riconvertirlo in un Mongoid :: Criteria – GTDev

+0

SQL ha ORDER BY RAND() ma per quanto ne so non c'è un equivalente di quello in mongodb . Quindi puoi creare la colonna "Random" e poi User.order_by quella, che sarebbe una singola query. –

+1

In base a [SO: MongoDB trova le prestazioni del set di dati casuali] (http://stackoverflow.com/questions/9434969/mongodb-find-random-dataset-performance), 'skip' non è molto efficiente per valori di grandi dimensioni:" Salta forza Mongo a camminare attraverso il set di risultati fino a quando non arriva al documento che stai cercando, quindi più grande è il set di risultati di quella query, più tempo ci vorrà. " (Questo supporta la risposta di Dan.) –

2

È possibile fare questo

  1. generare casuale di offset che sarà ulteriormente soddisfare di scegliere il successivo n elementi (senza superare il limite)
  2. Assumere conteggio è 10, e il n è 5
  3. per eseguire questa verifica il dato n è inferiore al conteggio totale
  4. se non impostato l'offset su 0 e passare al punto 8
  5. se sì, sottrarre il n dal conteggio totale, e si otterrà un numero di 5
  6. Utilizzare questo per trovare un numero casuale, il numero sicuramente sarà da 0 a 5 (assumere 2)
  7. Utilizzare il casuale il numero 2 come offset
  8. ora si può prendere il casuali 5 utenti semplicemente passando questo offset e la n (5) come un limite.
  9. Ora si arriva agli utenti di 3 al 7

codice

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

e si può mettere questo in un metodo

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

e chiamarlo come

rand_users = get_random_users(5) 

spero che lui lps

+0

grazie. Ma sarà davvero casuale. Immagino che questo fornirà un intervallo casuale da cnt a cnt + n ma non creerà un condizionale. come se l'utente 5 è selezionato ... c'è un'alta probabilità che l'utente 6 sarà mentre una probabilità zero che l'utente 11 sarà? – GTDev

+0

Giusto, questo è un compromesso con la mia risposta. Se riesci a farla franca partendo da un punto a caso e selezionando solo i successivi n record sequenziali, puoi eseguirla in una query anziché in queries. È quindi possibile mischiare il risultato per renderlo casuale all'interno di quella selezione. Ma no, questo non è davvero casuale. –

3

Se si vuole veramente la semplicità è possibile utilizzare questo invece:

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 

Basta chiamare User.random(5) e avrai 5 utenti casuali. Funzionerà anche con il filtraggio, quindi se vuoi solo utenti registrati puoi fare User.where(:registered => true).random(5).

Questa operazione richiederà un po 'di tempo per le raccolte di grandi dimensioni, pertanto è consigliabile utilizzare un metodo alternativo in cui eseguire una divisione casuale del conteggio (ad esempio 25.000 - 30.000) e randomizzare tale intervallo.

+0

Su quale posto e con quale nome salvare questo file ?. Come ti chiami a questo file? Grazie! – hyperrjas

+0

@hyperrjas Puoi mettere questo file nella cartella lib della tua applicazione. Quindi assicurarsi che l'applicazione sia configurata per il caricamento automatico dei file all'interno di quella cartella. Il nome del file non ha importanza. – Moox

+0

Grazie. Ho aggiunto all'interno della cartella '/ app/lib' il file' random.rb' con questo codice, ma per esempio, se corro in console 'User.random (5)' Ricevo l'errore 'NoMethodError: metodo non definito' casuale "per utente: classe". Come posso risolvere questo? – hyperrjas

0

Dato che voglio mantenere un criterio, lo faccio:

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]]) 
} 
1

appena incontrato un tale problema. Provato

Model.all.sample 

e funziona per me

+9

Abbastanza sicuro che questo caricherà ogni singolo modello dal database e quindi utilizzerà il metodo 'Array # sample' per scegliere un oggetto casuale. Suppongo che sia OK se stai curiosando in console, ma non è raccomandato per le applicazioni di produzione. – steve

+0

Prende molto tempo se il numero di elementi è più nel modello – Sairam

+0

Funziona ma con più di 20'000 documenti come nel mio caso ci vuole molto, come detto. –

-2

penso che sia meglio concentrarsi su randomizzazione risultato restituito set così ho provato:

Model.all.to_a.shuffle 

Spero che questo aiuti.

+3

Sarebbe orrendo se nel tuo database ci siano un milione di istanze di 'Model'. –

+0

È solo un esempio. Dovresti assolutamente ottenere il set di risultati che stai cercando per primo. –

15

Se si desidera solo un documento, e non si vuole definire un nuovo metodo di criteri, si può solo fare questo:

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

Se si vuole trovare un modello casuale basato su alcuni criteri:

0

L'approccio di @moox è davvero interessante ma dubito che la monkeypatching dell'intero Mongoid sia una buona idea qui. Quindi il mio approccio è solo per scrivere una preoccupazione Randomizable che può includere in ogni modello che usi questa funzione. Questo va a 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 

Allora il vostro modello di User sarebbe simile a questa:

class User 
    include Mongoid::Document 
    include Randomizable 

    field :name 
end 

e le prove ....

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 
2

MongoDB 3.2 viene in soccorso con $sample (link to doc)

EDIT: Il più recente di Mongoid ha implementato $ campione, in modo da poter chiamare YourCollection.all.sample(5)

versioni precedenti di mongoid

Mongoid non supporta sample fino Mongoid 6, in modo da avere a eseguire questo query di aggregazione con il driver Mongo:

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

che cosa si può fare con quel

Credo che il functionnality deve fare la sua strada presto per Mongoid, ma nel frattempo

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)