Registro aleatório do MongoDB

336

Estou procurando obter um registro aleatório de um enorme (100 milhões de registros) mongodb.

Qual é a maneira mais rápida e eficiente de fazer isso? Os dados já estão lá e não há campo em que eu possa gerar um número aleatório e obter uma linha aleatória.

Alguma sugestão?

Will M
fonte
2
Veja também esta pergunta do SO intitulada "Ordenando um conjunto de resultados aleatoriamente no mongo" . Pensar em encomendar aleatoriamente um conjunto de resultados é uma versão mais geral dessa pergunta - mais poderosa e mais útil.
David J.
11
Esta pergunta continua aparecendo. As informações mais recentes provavelmente podem ser encontradas na solicitação do recurso para obter itens aleatórios de uma coleção no rastreador de tickets do MongoDB. Se implementado de forma nativa, provavelmente seria a opção mais eficiente. (Se você quiser que o recurso, vá voto-lo.)
David J.
Esta é uma coleção fragmentada?
Dylan Tong
3
A resposta correta foi dada por @JohnnyHK abaixo: db.mycoll.aggregate ({$ sample: {size: 1}})
Florian
Alguém sabe o quanto isso é mais lento do que apenas gravar o primeiro disco? Estou debatendo se vale a pena tirar uma amostra aleatória para fazer algo vs apenas fazê-lo em ordem.
David Kong

Respostas:

247

A partir da versão 3.2 do MongoDB, é possível obter N documentos aleatórios de uma coleção usando o $sampleoperador de agregação de pipeline:

// Get one random document from the mycoll collection.
db.mycoll.aggregate([{ $sample: { size: 1 } }])

Se você deseja selecionar o (s) documento (s) aleatório (s) de um subconjunto filtrado da coleção, inclua um $matchestágio no pipeline:

// Get one random document matching {a: 10} from the mycoll collection.
db.mycoll.aggregate([
    { $match: { a: 10 } },
    { $sample: { size: 1 } }
])

Conforme observado nos comentários, quando sizefor maior que 1, pode haver duplicatas na amostra de documento retornada.

JohnnyHK
fonte
12
É uma boa maneira, mas lembre-se de que NÃO garante que não haja cópias do mesmo objeto na amostra.
Matheus Araujo
10
@MatheusAraujo que não importa se você quer um recorde, mas bom ponto de qualquer maneira
Toby
3
Não é pedante, mas a pergunta não especifica uma versão do MongoDB, portanto, eu suporia que ter a versão mais recente seja razoável.
precisa saber é o seguinte
2
@Nepoxx Consulte os documentos sobre o processamento envolvido.
precisa saber é o seguinte
2
@brycejl Isso teria a falha fatal de não corresponder a nada se o estágio $ sample não selecionasse nenhum documento correspondente.
JohnnyHK
115

Faça uma contagem de todos os registros, gere um número aleatório entre 0 e a contagem e faça:

db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
ceejayoz
fonte
139
Infelizmente, skip () é bastante ineficiente, pois precisa digitalizar muitos documentos. Além disso, há uma condição de corrida se as linhas forem removidas entre obter a contagem e executar a consulta.
mstearn 17/05/10
6
Observe que o número aleatório deve estar entre 0 e a contagem (exclusivo). Ou seja, se você tiver 10 itens, o número aleatório deve estar entre 0 e 9. Caso contrário, o cursor pode tentar pular o último item e nada será retornado.
Matt
4
Obrigado, funcionou perfeitamente para os meus propósitos. @mstearn, seus comentários sobre eficiência e condições de corrida são válidos, mas para coleções onde isso não importa (extração de lote único do lado do servidor em uma coleção em que os registros não são excluídos), isso é muito superior ao hacky (IMO) solução no Mongo Cookbook.
Michael Moussa
4
o que definir o limite para -1 faz?
precisa saber é o seguinte
@MonkeyBonkey docs.mongodb.org/meta-driver/latest/legacy/… "Se numberToReturn for 0, o db usará o tamanho de retorno padrão. Se o número for negativo, o banco de dados retornará esse número e fechará o cursor. "
precisa saber é o seguinte
86

Atualização para o MongoDB 3.2

3.2 introduziu $ sample no pipeline de agregação.

Há também um bom post sobre como colocá-lo em prática.

Para versões mais antigas (resposta anterior)

Na verdade, era uma solicitação de recurso: http://jira.mongodb.org/browse/SERVER-533, mas foi arquivada em "Não será corrigido".

O livro de receitas tem uma receita muito boa para selecionar um documento aleatório de uma coleção: http://cookbook.mongodb.org/patterns/random-attribute/

Parafraseando a receita, atribua números aleatórios aos seus documentos:

db.docs.save( { key : 1, ..., random : Math.random() } )

Em seguida, selecione um documento aleatório:

rand = Math.random()
result = db.docs.findOne( { key : 2, random : { $gte : rand } } )
if ( result == null ) {
  result = db.docs.findOne( { key : 2, random : { $lte : rand } } )
}

Consultando com ambos $gtee $lteé necessário encontrar o documento com um número aleatório mais próximo rand.

E é claro que você deseja indexar no campo aleatório:

db.docs.ensureIndex( { key : 1, random :1 } )

Se você já estiver consultando um índice, basta soltá-lo, anexá random: 1-lo e adicioná-lo novamente.

Michael
fonte
7
E aqui está uma maneira simples de adicionar o campo aleatório a todos os documentos da coleção. função setRandom () {db.topics.find (). forEach (função (obj) {obj.random = Math.random (); db.topics.save (obj);}); } db.eval (setRandom);
Geoffrey
8
Isso seleciona um documento aleatoriamente, mas se você o fizer mais de uma vez, as pesquisas não são independentes. É mais provável que você obtenha o mesmo documento duas vezes seguidas do que o acaso ditaria.
lacker
12
Parece uma má implementação do hash circular. É ainda pior do que diz a falta: até uma pesquisa é tendenciosa porque os números aleatórios não são distribuídos uniformemente. Para fazer isso corretamente, você precisará de um conjunto de, digamos, 10 números aleatórios por documento. Quanto mais números aleatórios você usa por documento, mais uniforme a distribuição de saída se torna.
Thomas
4
O ticket do MongoDB JIRA ainda está ativo: jira.mongodb.org/browse/SERVER-533 Vá comentar e vote se quiser o recurso.
David J.
11
Tome nota do tipo de advertência mencionado. Isso não funciona de maneira eficiente com pequena quantidade de documentos. Dados dois itens com chave aleatória de 3 e 63. O documento # 63 será escolhido com mais frequência onde $gteestá o primeiro. Solução alternativa stackoverflow.com/a/9499484/79201 funcionaria melhor nesse caso.
Ryan Schumacher
56

Você também pode usar o recurso de indexação geoespacial do MongoDB para selecionar os documentos 'mais próximos' de um número aleatório.

Primeiro, ative a indexação geoespacial em uma coleção:

db.docs.ensureIndex( { random_point: '2d' } )

Para criar vários documentos com pontos aleatórios no eixo X:

for ( i = 0; i < 10; ++i ) {
    db.docs.insert( { key: i, random_point: [Math.random(), 0] } );
}

Então você pode obter um documento aleatório da coleção como este:

db.docs.findOne( { random_point : { $near : [Math.random(), 0] } } )

Ou você pode recuperar vários documentos mais próximos de um ponto aleatório:

db.docs.find( { random_point : { $near : [Math.random(), 0] } } ).limit( 4 )

Isso requer apenas uma consulta e nenhuma verificação nula, além do código ser limpo, simples e flexível. Você pode até usar o eixo Y do ponto geográfico para adicionar uma segunda dimensão aleatória à sua consulta.

Nico de Poel
fonte
8
Eu gosto desta resposta, é a mais eficiente que já vi e não exige muita bagunça no servidor.
Tony Million
4
Isso também é tendencioso em relação aos documentos que possuem poucos pontos nas proximidades.
Thomas
6
Isso é verdade e também existem outros problemas: os documentos estão fortemente correlacionados em suas chaves aleatórias; portanto, é altamente previsível quais documentos serão retornados como um grupo se você selecionar vários documentos. Além disso, os documentos próximos aos limites (0 e 1) têm menos probabilidade de serem escolhidos. O último poderia ser resolvido usando geomapping esférico, que envolve as bordas. No entanto, você deve ver esta resposta como uma versão aprimorada da receita do livro de receitas, não como um mecanismo de seleção aleatória perfeito. É aleatório o suficiente para a maioria dos propósitos.
Nico de Poel
@NicodePoel, eu gosto da sua resposta e do seu comentário! E eu tenho algumas perguntas para você: 1- Como você sabe que pontos próximos aos limites 0 e 1 são menos propensos a serem escolhidos, isso se baseia em algum terreno matemático ?, 2- Você pode elaborar mais sobre geomapping esférico, como será melhor a seleção aleatória e como fazê-lo no MongoDB? ... Estimado!
securecurve 10/09/2015
Aprecie sua ideia. Finalmente, eu tenho um ótimo código que é muito amigável para CPU e RAM! Obrigado
Qais Bsharat 03/03
21

A receita a seguir é um pouco mais lenta que a solução do livro de receitas mongo (adicione uma chave aleatória em todos os documentos), mas retorna documentos aleatórios distribuídos de maneira mais uniforme. É um pouco menos uniformemente distribuído que a skip( random )solução, mas muito mais rápido e com mais segurança contra falhas caso os documentos sejam removidos.

function draw(collection, query) {
    // query: mongodb query object (optional)
    var query = query || { };
    query['random'] = { $lte: Math.random() };
    var cur = collection.find(query).sort({ rand: -1 });
    if (! cur.hasNext()) {
        delete query.random;
        cur = collection.find(query).sort({ rand: -1 });
    }
    var doc = cur.next();
    doc.random = Math.random();
    collection.update({ _id: doc._id }, doc);
    return doc;
}

Também requer que você adicione um campo "aleatório" aleatório aos seus documentos, portanto, não se esqueça de adicioná-lo ao criá-los: pode ser necessário inicializar sua coleção, como mostra Geoffrey

function addRandom(collection) { 
    collection.find().forEach(function (obj) {
        obj.random = Math.random();
        collection.save(obj);
    }); 
} 
db.eval(addRandom, db.things);

Resultados de referência

Esse método é muito mais rápido que o skip()método (de ceejayoz) e gera documentos aleatórios mais uniformes que o método "livro de receitas" relatado por Michael:

Para uma coleção com 1.000.000 de elementos:

  • Esse método leva menos de um milissegundo na minha máquina

  • o skip()método leva 180 ms em média

O método do livro de receitas fará com que um grande número de documentos nunca seja escolhido porque o número aleatório deles não os favorece.

  • Este método seleciona todos os elementos uniformemente ao longo do tempo.

  • No meu benchmark, era apenas 30% mais lento que o método do livro de receitas.

  • a aleatoriedade não é 100% perfeita, mas é muito boa (e pode ser melhorada se necessário)

Esta receita não é perfeita - a solução perfeita seria um recurso interno, como outros observaram.
No entanto, deve ser um bom compromisso para muitos propósitos.

spam_eggs
fonte
10

Aqui está uma maneira de usar os ObjectIdvalores padrão para _ide um pouco de matemática e lógica.

// Get the "min" and "max" timestamp values from the _id in the collection and the 
// diff between.
// 4-bytes from a hex string is 8 characters

var min = parseInt(db.collection.find()
        .sort({ "_id": 1 }).limit(1).toArray()[0]._id.str.substr(0,8),16)*1000,
    max = parseInt(db.collection.find()
        .sort({ "_id": -1 })limit(1).toArray()[0]._id.str.substr(0,8),16)*1000,
    diff = max - min;

// Get a random value from diff and divide/multiply be 1000 for The "_id" precision:
var random = Math.floor(Math.floor(Math.random(diff)*diff)/1000)*1000;

// Use "random" in the range and pad the hex string to a valid ObjectId
var _id = new ObjectId(((min + random)/1000).toString(16) + "0000000000000000")

// Then query for the single document:
var randomDoc = db.collection.find({ "_id": { "$gte": _id } })
   .sort({ "_id": 1 }).limit(1).toArray()[0];

Essa é a lógica geral na representação de shell e facilmente adaptável.

Assim, em pontos:

  • Encontre os valores mínimo e máximo da chave primária na coleção

  • Gere um número aleatório que fique entre os carimbos de data e hora desses documentos.

  • Adicione o número aleatório ao valor mínimo e encontre o primeiro documento maior ou igual a esse valor.

Isso usa "preenchimento" do valor do carimbo de data e hora em "hex" para formar um ObjectIdvalor válido, pois é isso que estamos procurando. Usar números inteiros como _idvalor é essencialmente mais simples, mas é a mesma idéia básica nos pontos.

Blakes Seven
fonte
Eu tenho uma coleção de 300 000 000 linhas. Esta é a única solução que funciona e é rápida o suficiente.
Nikos
8

Em Python usando pymongo:

import random

def get_random_doc():
    count = collection.count()
    return collection.find()[random.randrange(count)]
Jabba
fonte
5
Vale ressaltar que internamente, isso usará ignorar e limitar, assim como muitas das outras respostas.
precisa saber é o seguinte
Sua resposta está correta. No entanto, substitua count()por o estimated_document_count()que count()está obsoleto no Mongdo v4.2.
user3848207 11/06
8

Agora você pode usar o agregado. Exemplo:

db.users.aggregate(
   [ { $sample: { size: 3 } } ]
)

Veja o doc .

dbam
fonte
3
Nota: $ amostra pode obter o mesmo documento mais de uma vez
Saman Shafigh
6

é difícil se não houver dados para digitar. quais são os campos _id? eles são ids de objetos mongodb? Nesse caso, você pode obter os valores mais alto e mais baixo:

lowest = db.coll.find().sort({_id:1}).limit(1).next()._id;
highest = db.coll.find().sort({_id:-1}).limit(1).next()._id;

se você assumir que os IDs são distribuídos uniformemente (mas não são, mas pelo menos é um começo):

unsigned long long L = first_8_bytes_of(lowest)
unsigned long long H = first_8_bytes_of(highest)

V = (H - L) * random_from_0_to_1();
N = L + V;
oid = N concat random_4_bytes();

randomobj = db.coll.find({_id:{$gte:oid}}).limit(1);
dm.
fonte
11
Alguma idéia de como isso seria no PHP? ou pelo menos qual idioma você usou acima? é Python?
Marcin
6

Usando Python (pymongo), a função agregada também funciona.

collection.aggregate([{'$sample': {'size': sample_size }}])

Essa abordagem é muito mais rápida do que executar uma consulta para um número aleatório (por exemplo, collection.find ([random_int])) .É o caso especialmente de coleções grandes.

Daniel
fonte
5

Você pode escolher um carimbo de data e hora aleatório e procurar o primeiro objeto que foi criado posteriormente. Ele digitalizará apenas um único documento, embora não necessariamente forneça uma distribuição uniforme.

var randRec = function() {
    // replace with your collection
    var coll = db.collection
    // get unixtime of first and last record
    var min = coll.find().sort({_id: 1}).limit(1)[0]._id.getTimestamp() - 0;
    var max = coll.find().sort({_id: -1}).limit(1)[0]._id.getTimestamp() - 0;

    // allow to pass additional query params
    return function(query) {
        if (typeof query === 'undefined') query = {}
        var randTime = Math.round(Math.random() * (max - min)) + min;
        var hexSeconds = Math.floor(randTime / 1000).toString(16);
        var id = ObjectId(hexSeconds + "0000000000000000");
        query._id = {$gte: id}
        return coll.find(query).limit(1)
    };
}();
Martin Nowak
fonte
Seria facilmente possível inclinar a data aleatória para explicar o crescimento superlinear do banco de dados.
Martin Nowak
este é o melhor método para coleções muito grandes, ele funciona em O (1), unline pular () ou count () usado nas outras soluções aqui
marmor
4

Minha solução em php:

/**
 * Get random docs from Mongo
 * @param $collection
 * @param $where
 * @param $fields
 * @param $limit
 * @author happy-code
 * @url happy-code.com
 */
private function _mongodb_get_random (MongoCollection $collection, $where = array(), $fields = array(), $limit = false) {

    // Total docs
    $count = $collection->find($where, $fields)->count();

    if (!$limit) {
        // Get all docs
        $limit = $count;
    }

    $data = array();
    for( $i = 0; $i < $limit; $i++ ) {

        // Skip documents
        $skip = rand(0, ($count-1) );
        if ($skip !== 0) {
            $doc = $collection->find($where, $fields)->skip($skip)->limit(1)->getNext();
        } else {
            $doc = $collection->find($where, $fields)->limit(1)->getNext();
        }

        if (is_array($doc)) {
            // Catch document
            $data[ $doc['_id']->{'$id'} ] = $doc;
            // Ignore current document when making the next iteration
            $where['_id']['$nin'][] = $doc['_id'];
        }

        // Every iteration catch document and decrease in the total number of document
        $count--;

    }

    return $data;
}
code_turist
fonte
3

Para obter um número determinado de documentos aleatórios sem duplicatas:

  1. primeiro obtenha todos os IDs
  2. obter tamanho dos documentos
  3. loop obter índice aleatório e pular duplicado

    number_of_docs=7
    db.collection('preguntas').find({},{_id:1}).toArray(function(err, arr) {
    count=arr.length
    idsram=[]
    rans=[]
    while(number_of_docs!=0){
        var R = Math.floor(Math.random() * count);
        if (rans.indexOf(R) > -1) {
         continue
          } else {           
                   ans.push(R)
                   idsram.push(arr[R]._id)
                   number_of_docs--
                    }
        }
    db.collection('preguntas').find({}).toArray(function(err1, doc1) {
                    if (err1) { console.log(err1); return;  }
                   res.send(doc1)
                });
            });
Fabio Guerra
fonte
2

Eu sugeriria usar o mapa / reduzir, onde você usa a função de mapa para emitir apenas quando um valor aleatório está acima de uma determinada probabilidade.

function mapf() {
    if(Math.random() <= probability) {
    emit(1, this);
    }
}

function reducef(key,values) {
    return {"documents": values};
}

res = db.questions.mapReduce(mapf, reducef, {"out": {"inline": 1}, "scope": { "probability": 0.5}});
printjson(res.results);

A função reduzem-se acima funciona porque apenas uma tecla ('1') é emitida a partir da função de mapa.

O valor da "probabilidade" é definido no "escopo", ao chamar mapRreduce (...)

Usar o mapReduce como esse também deve ser usado em um banco de dados fragmentado.

Se você quiser selecionar exatamente n de m documentos no banco de dados, faça o seguinte:

function mapf() {
    if(countSubset == 0) return;
    var prob = countSubset / countTotal;
    if(Math.random() <= prob) {
        emit(1, {"documents": [this]}); 
        countSubset--;
    }
    countTotal--;
}

function reducef(key,values) {
    var newArray = new Array();
for(var i=0; i < values.length; i++) {
    newArray = newArray.concat(values[i].documents);
}

return {"documents": newArray};
}

res = db.questions.mapReduce(mapf, reducef, {"out": {"inline": 1}, "scope": {"countTotal": 4, "countSubset": 2}})
printjson(res.results);

Onde "countTotal" (m) é o número de documentos no banco de dados e "countSubset" (n) é o número de documentos a serem recuperados.

Essa abordagem pode causar alguns problemas em bancos de dados fragmentados.

torbenl
fonte
4
Fazendo uma varredura completa da coleção para retornar 1 elemento ... essa deve ser a técnica menos eficiente para fazer isso.
Thomas
11
O truque é que é uma solução geral para retornar um número arbitrário de elementos aleatórios - nesse caso, seria mais rápido do que as outras soluções ao obter> 2 elementos aleatórios.
torbenl
2

Você pode escolher _id aleatório e retornar o objeto correspondente:

 db.collection.count( function(err, count){
        db.collection.distinct( "_id" , function( err, result) {
            if (err)
                res.send(err)
            var randomId = result[Math.floor(Math.random() * (count-1))]
            db.collection.findOne( { _id: randomId } , function( err, result) {
                if (err)
                    res.send(err)
                console.log(result)
            })
        })
    })

Aqui você não precisa gastar espaço armazenando números aleatórios na coleção.

Vijay13
fonte
1

Eu sugiro adicionar um campo int aleatório para cada objeto. Então você pode simplesmente fazer uma

findOne({random_field: {$gte: rand()}}) 

para escolher um documento aleatório. Apenas certifique-se de garantir o Índice ({random_field: 1})

mstearn
fonte
2
Se o primeiro registro da sua coleção tiver um valor random_field relativamente alto, ele não será retornado quase o tempo todo?
thehiatus
2
thehaitus está correta, ele vai - não é adequado para qualquer fim
Heptic
7
Esta solução está completamente errada, adicionar um número aleatório (vamos imaginar entre 0 a 2 ^ 32-1) não garante uma boa distribuição e o uso de $ gte a torna ainda pior, devido à sua seleção aleatória nem chegar perto para um número pseudo-aleatório. Eu sugiro nunca usar esse conceito sempre.
Maximiliano Rios
1

Quando me deparei com uma solução semelhante, voltei atrás e descobri que a solicitação de negócios era realmente para criar alguma forma de rotação do inventário que estava sendo apresentado. Nesse caso, existem opções muito melhores, que têm respostas de mecanismos de pesquisa como o Solr, não de repositórios de dados como o MongoDB.

Em resumo, com o requisito de "alternar inteligentemente" o conteúdo, o que devemos fazer em vez de um número aleatório em todos os documentos é incluir um modificador pessoal de pontuação q. Para implementar isso você mesmo, assumindo uma pequena população de usuários, você pode armazenar um documento por usuário que possua o ID do produto, a contagem de impressões, a contagem de cliques, a data da última visualização e quaisquer outros fatores que a empresa considere significativos para calcular a pontuação daq modificador. Ao recuperar o conjunto para exibição, normalmente você solicita mais documentos do armazenamento de dados do que o solicitado pelo usuário final, aplica o modificador q score, pega o número de registros solicitados pelo usuário final e randomiza a página de resultados, uma pequena quantidade definido, basta classificar os documentos na camada do aplicativo (na memória).

Se o universo de usuários for muito grande, você poderá categorizar os usuários em grupos de comportamento e indexar por grupo de comportamento, em vez de usuário.

Se o universo de produtos for pequeno o suficiente, você poderá criar um índice por usuário.

Eu descobri que essa técnica é muito mais eficiente, mas mais importante ainda, mais eficaz na criação de uma experiência relevante e interessante do uso da solução de software.

paegun
fonte
1

nenhuma das soluções funcionou bem para mim. especialmente quando existem muitas lacunas e o conjunto é pequeno. isso funcionou muito bem para mim (em php):

$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
Mantas Karanauskas
fonte
Você especifica o idioma, mas não a biblioteca que está usando?
Benjamin
Para sua informação, existe uma condição de corrida aqui se um documento for removido entre a primeira e a terceira linha. Além disso, find+ skipé muito ruim, você está devolvendo todos os documentos apenas para escolher um: S.
Martin Konecny
1

Minha classificação / ordem do PHP / MongoDB por solução RANDOM. Espero que isso ajude alguém.

Nota: Eu tenho IDs numéricos na minha coleção do MongoDB que se referem a um registro do banco de dados MySQL.

Primeiro, crio uma matriz com 10 números gerados aleatoriamente

    $randomNumbers = [];
    for($i = 0; $i < 10; $i++){
        $randomNumbers[] = rand(0,1000);
    }

Na minha agregação, uso o operador de pipeline $ addField combinado com $ arrayElemAt e $ mod (módulo). O operador do módulo me fornecerá um número de 0 a 9, que eu uso para escolher um número da matriz com números gerados aleatoriamente.

    $aggregate[] = [
        '$addFields' => [
            'random_sort' => [ '$arrayElemAt' => [ $randomNumbers, [ '$mod' => [ '$my_numeric_mysql_id', 10 ] ] ] ],
        ],
    ];

Depois disso, você pode usar a classificação Pipeline.

    $aggregate[] = [
        '$sort' => [
            'random_sort' => 1
        ]
    ];
feskr
fonte
0

Se você tiver uma chave de identificação simples, poderá armazenar todos os IDs em uma matriz e escolher um ID aleatório. (Resposta Ruby):

ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
Mr. Demetrius Michael
fonte
0

Usando Map / Reduce, você certamente pode obter um registro aleatório, mas não necessariamente de maneira muito eficiente, dependendo do tamanho da coleção filtrada resultante com a qual você trabalha.

Testei esse método com 50.000 documentos (o filtro o reduz para cerca de 30.000) e é executado em aproximadamente 400ms em um Intel i3 com 16 GB de RAM e um disco rígido SATA3 ...

db.toc_content.mapReduce(
    /* map function */
    function() { emit( 1, this._id ); },

    /* reduce function */
    function(k,v) {
        var r = Math.floor((Math.random()*v.length));
        return v[r];
    },

    /* options */
    {
        out: { inline: 1 },
        /* Filter the collection to "A"ctive documents */
        query: { status: "A" }
    }
);

A função Mapa simplesmente cria uma matriz dos IDs de todos os documentos que correspondem à consulta. No meu caso, testei isso com aproximadamente 30.000 dos 50.000 documentos possíveis.

A função Reduzir simplesmente seleciona um número inteiro aleatório entre 0 e o número de itens (-1) na matriz e retorna esse _id da matriz.

400ms parece muito tempo, e realmente é, se você tivesse cinquenta milhões de registros em vez de cinquenta mil, isso poderá aumentar a sobrecarga a ponto de se tornar inutilizável em situações de multiusuários.

Há um problema em aberto para o MongoDB incluir esse recurso no núcleo ... https://jira.mongodb.org/browse/SERVER-533

Se essa seleção "aleatória" fosse incorporada a uma pesquisa de índice em vez de coletar IDs em uma matriz e selecionar uma, isso ajudaria incrivelmente. (vá votar!)

doublehelix
fonte
0

Isso funciona bem, é rápido, funciona com vários documentos e não requer randcampo preenchido, o que acabará se preenchendo:

  1. adicione índice ao campo .rand da sua coleção
  2. use find e refresh, algo como:
// Install packages:
//   npm install mongodb async
// Add index in mongo:
//   db.ensureIndex('mycollection', { rand: 1 })

var mongodb = require('mongodb')
var async = require('async')

// Find n random documents by using "rand" field.
function findAndRefreshRand (collection, n, fields, done) {
  var result = []
  var rand = Math.random()

  // Append documents to the result based on criteria and options, if options.limit is 0 skip the call.
  var appender = function (criteria, options, done) {
    return function (done) {
      if (options.limit > 0) {
        collection.find(criteria, fields, options).toArray(
          function (err, docs) {
            if (!err && Array.isArray(docs)) {
              Array.prototype.push.apply(result, docs)
            }
            done(err)
          }
        )
      } else {
        async.nextTick(done)
      }
    }
  }

  async.series([

    // Fetch docs with unitialized .rand.
    // NOTE: You can comment out this step if all docs have initialized .rand = Math.random()
    appender({ rand: { $exists: false } }, { limit: n - result.length }),

    // Fetch on one side of random number.
    appender({ rand: { $gte: rand } }, { sort: { rand: 1 }, limit: n - result.length }),

    // Continue fetch on the other side.
    appender({ rand: { $lt: rand } }, { sort: { rand: -1 }, limit: n - result.length }),

    // Refresh fetched docs, if any.
    function (done) {
      if (result.length > 0) {
        var batch = collection.initializeUnorderedBulkOp({ w: 0 })
        for (var i = 0; i < result.length; ++i) {
          batch.find({ _id: result[i]._id }).updateOne({ rand: Math.random() })
        }
        batch.execute(done)
      } else {
        async.nextTick(done)
      }
    }

  ], function (err) {
    done(err, result)
  })
}

// Example usage
mongodb.MongoClient.connect('mongodb://localhost:27017/core-development', function (err, db) {
  if (!err) {
    findAndRefreshRand(db.collection('profiles'), 1024, { _id: true, rand: true }, function (err, result) {
      if (!err) {
        console.log(result)
      } else {
        console.error(err)
      }
      db.close()
    })
  } else {
    console.error(err)
  }
})

ps. Como encontrar registros aleatórios na pergunta mongodb está marcado como duplicado desta pergunta. A diferença é que esta questão pede explicitamente sobre registro único quanto o outro explicitamente sobre a obtenção de documentos aleatórios s .

Mirek Rusin
fonte
-2

Se você estiver usando mongoid, o invólucro de documento para objeto, você pode fazer o seguinte no Ruby. (Supondo que seu modelo seja Usuário)

User.all.to_a[rand(User.count)]

No meu .irbrc, eu tenho

def rando klass
    klass.all.to_a[rand(klass.count)]
end

então no console do rails, eu posso fazer, por exemplo,

rando User
rando Article

para obter documentos aleatoriamente de qualquer coleção.

Zack Xu
fonte
11
Isso é terrivelmente ineficiente, pois ele lê toda a coleção em uma matriz e escolhe um registro.
perfil completo de JohnnyHK
Ok, talvez ineficiente, mas certamente conveniente. tente isso se o seu tamanho de dados não é muito grande
Zack Xu
3
Claro, mas a pergunta original era para uma coleção com 100 milhões de documentos, portanto, essa seria uma solução muito ruim para esse caso!
perfil completo de JohnnyHK
-2

você também pode usar o shuffle-array após executar sua consulta

var shuffle = require ('shuffle-array');

Accounts.find (qry, função (err, matriz de resultados)) {newIndexArr = shuffle (matriz de resultados);

rabie jegham
fonte
-7

O que funciona de maneira eficiente e confiável é o seguinte:

Adicione um campo chamado "aleatório" a cada documento e atribua um valor aleatório a ele, adicione um índice para o campo aleatório e proceda da seguinte maneira:

Vamos supor que temos uma coleção de links da web chamados "links" e queremos um link aleatório a partir dele:

link = db.links.find().sort({random: 1}).limit(1)[0]

Para garantir que o mesmo link não apareça uma segunda vez, atualize seu campo aleatório com um novo número aleatório:

db.links.update({random: Math.random()}, link)
naufrágio
fonte
2
por que atualizar o banco de dados quando você pode apenas selecionar uma chave aleatória diferente?
Jason S
Você pode não ter uma lista das teclas para selecionar aleatoriamente.
Mike
Então você tem que ordenar toda a coleção toda vez? E os infelizes registros que obtiveram grandes números aleatórios? Eles nunca serão selecionados.
Fantius
11
Você precisa fazer isso porque as outras soluções, principalmente a sugerida no livro do MongoDB, não funcionam. Se a primeira descoberta falhar, a segunda descoberta sempre retornará o item com o menor valor aleatório. Se você indexar aleatoriamente descendente, a primeira consulta sempre retornará o item com o maior número aleatório.
trainwreck
Adicionando um campo em cada documento? Eu acho que não é aconselhável.
CS_noob 16/07