Losowy rekord z MongoDB

336

Szukam losowego rekordu z ogromnego (100 milionów rekordów) mongodb.

Jaki jest najszybszy i najbardziej efektywny sposób? Dane już tam są i nie ma pola, w którym mogę wygenerować losową liczbę i uzyskać losowy wiersz.

Jakieś sugestie?

Will M.
źródło
2
Zobacz także to SO pytanie zatytułowane „Losowe zestaw wyników w mongo” . Myślenie o losowym zamawianiu zestawu wyników jest bardziej ogólną wersją tego pytania - mocniejszą i bardziej przydatną.
David J.,
11
To pytanie się pojawia. Najnowsze informacje można prawdopodobnie znaleźć na żądanie funkcji, aby uzyskać losowe przedmioty z kolekcji w narzędziu do śledzenia biletów MongoDB. Jeśli zostanie wdrożony natywnie, prawdopodobnie będzie to najbardziej wydajna opcja. (Jeśli chcesz tę funkcję, głosuj w górę.)
David J.
Czy to kolekcja podzielona na kawałki?
Dylan Tong
3
Prawidłowa odpowiedź została udzielona przez @JohnnyHK poniżej: db.mycoll.aggregate ({$ sample: {size: 1}})
Florian
Czy ktoś wie, ile to wolniej niż nagrywanie pierwszego rekordu? Zastanawiam się, czy warto pobrać losową próbkę, aby coś zrobić, czy po prostu zrobić to w kolejności.
David Kong

Odpowiedzi:

248

Począwszy od wersji 3.2 MongoDB, możesz pobrać N losowych dokumentów z kolekcji za pomocą $sampleoperatora potoku agregacji:

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

Jeśli chcesz wybrać losowe dokumenty z odfiltrowanego podzbioru kolekcji, dołącz $matchetap do potoku:

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

Jak zauważono w komentarzach, gdy wartość sizejest większa niż 1, w zwróconej próbce dokumentu mogą występować duplikaty.

JohnnyHK
źródło
12
To dobry sposób, ale pamiętaj, że NIE gwarantuje, że nie ma kopii tego samego obiektu w próbce.
Matheus Araujo
10
@MatheusAraujo, co nie będzie miało znaczenia, jeśli chcesz jeden rekord, ale i tak dobry punkt
Toby
3
Nie chcę być pedantyczny, ale pytanie nie określa wersji MongoDB, więc zakładam, że najnowsza wersja jest rozsądna.
dalanmiller
2
@Nepoxx Zobacz dokumenty dotyczące wymaganego przetwarzania.
JohnnyHK,
2
@brycejl Miałoby to fatalną wadę polegającą na niepasowaniu niczego, gdyby etap próbkowania $ nie wybrał żadnych pasujących dokumentów.
JohnnyHK
115

Zliczyć wszystkie rekordy, wygenerować losową liczbę od 0 do zliczenia, a następnie:

db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
ceejayoz
źródło
139
Niestety pomijanie () jest raczej nieefektywne, ponieważ musi skanować tyle dokumentów. Ponadto istnieje warunek wyścigu, jeśli wiersze zostaną usunięte między uzyskaniem liczby a uruchomieniem zapytania.
mstearn
6
Pamiętaj, że liczba losowa powinna wynosić od 0 do liczby (wyłączne). To znaczy, jeśli masz 10 elementów, liczba losowa powinna wynosić od 0 do 9. W przeciwnym razie kursor może spróbować przejść obok ostatniego elementu i nic nie zostanie zwrócone.
mat.
4
Dzięki, działało idealnie dla moich celów. @mstearn, twoje komentarze dotyczące zarówno wydajności, jak i warunków wyścigu są ważne, ale w przypadku zbiorów, które nie mają znaczenia (jednorazowy wyciąg partii po stronie serwera w kolekcji, w której rekordy nie są usuwane), jest to znacznie lepsze niż hacky (IMO) rozwiązanie w Mongo Cookbook.
Michael Moussa,
4
co robi ustawienie limitu na -1?
MonkeyBonkey
@MonkeyBonkey docs.mongodb.org/meta-driver/latest/legacy/... "Jeśli liczbaNaZwrócenie wynosi 0, db użyje domyślnego rozmiaru zwracanego. Jeśli liczba jest ujemna, wówczas baza danych zwróci tę liczbę i zamknie kursor. „
ceejayoz
86

Aktualizacja dla MongoDB 3.2

3.2 wprowadził $ sample do potoku agregacji.

Jest też dobry post na blogu dotyczący jego praktycznego zastosowania.

Dla starszych wersji (poprzednia odpowiedź)

To była właściwie prośba o dodanie funkcji: http://jira.mongodb.org/browse/SERVER-533, ale została złożona w polu „Nie naprawię”.

Książka kucharska ma bardzo dobry przepis na wybranie losowego dokumentu z kolekcji: http://cookbook.mongodb.org/patterns/random-attribute/

Aby sparafrazować przepis, przypisujesz losowe liczby do dokumentów:

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

Następnie wybierz losowy dokument:

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

Odpytywanie z oboma $gtei $ltekonieczne jest znalezienie dokumentu z losową liczbę najbliższego rand.

I oczywiście będziesz chciał zaindeksować losowe pole:

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

Jeśli już korzystasz z indeksu, po prostu upuść go, dołącz random: 1do niego i dodaj ponownie.

Michał
źródło
7
Oto prosty sposób dodania losowego pola do każdego dokumentu w kolekcji. function setRandom () {db.topics.find (). forEach (function (obj) {obj.random = Math.random (); db.topics.save (obj);}); } db.eval (setRandom);
Geoffrey
8
Spowoduje to wybranie dokumentu losowo, ale jeśli zrobisz to więcej niż jeden raz, wyszukiwania nie będą niezależne. Bardziej prawdopodobne jest uzyskanie tego samego dokumentu dwa razy z rzędu, niż wynikałoby z losowej szansy.
brakujący
12
Wygląda na złą implementację mieszania cyklicznego. Jest nawet gorzej, niż mówi brakujący: nawet jedno wyszukiwanie jest stronnicze, ponieważ liczby losowe nie są równomiernie rozmieszczone. Aby to zrobić poprawnie, potrzebujesz zestawu, powiedzmy, 10 liczb losowych na dokument. Im więcej liczb losowych używasz na dokument, tym bardziej jednolity staje się rozkład wyjściowy.
Thomas
4
Bilet MongoDB JIRA wciąż żyje: jira.mongodb.org/browse/SERVER-533 Idź skomentuj i głosuj, jeśli chcesz tę funkcję.
David J.
1
Zwróć uwagę na wspomniany rodzaj zastrzeżenia. Nie działa to skutecznie z małą ilością dokumentów. Biorąc pod uwagę dwa elementy z losowym kluczem 3 i 63. Dokument nr 63 będzie wybierany częściej tam, gdzie $gtejest pierwszy. Alternatywne rozwiązanie stackoverflow.com/a/9499484/79201 działałoby lepiej w tym przypadku.
Ryan Schumacher
56

Możesz także użyć funkcji indeksowania geoprzestrzennego MongoDB, aby wybrać dokumenty „najbliższe” losowej liczbie.

Najpierw włącz indeksowanie geoprzestrzenne w kolekcji:

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

Aby utworzyć wiązkę dokumentów z losowymi punktami na osi X:

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

Następnie możesz pobrać losowy dokument z kolekcji w następujący sposób:

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

Lub możesz pobrać kilka dokumentów najbliższych losowemu punktowi:

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

Wymaga to tylko jednego zapytania i bez sprawdzania wartości NULL, a ponadto kod jest czysty, prosty i elastyczny. Możesz nawet użyć osi Y geopoint, aby dodać drugi wymiar losowości do zapytania.

Nico de Poel
źródło
8
Podoba mi się ta odpowiedź: jest to najbardziej wydajna, jaką widziałem, która nie wymaga bałaganu po stronie serwera.
Tony Million
4
Jest to również stronnicze w stosunku do dokumentów, które mają kilka punktów w pobliżu.
Thomas
6
To prawda i istnieją również inne problemy: dokumenty są silnie skorelowane z ich losowymi kluczami, więc jest wysoce przewidywalne, które dokumenty zostaną zwrócone jako grupa, jeśli wybierzesz wiele dokumentów. Również dokumenty bliskie granicom (0 i 1) są mniej prawdopodobne. To ostatnie można rozwiązać za pomocą sferycznego geomapowania, które owija się wokół krawędzi. Jednak powinieneś zobaczyć tę odpowiedź jako ulepszoną wersję przepisu na książkę kucharską, a nie jako doskonały mechanizm losowego wyboru. Jest wystarczająco losowy do większości celów.
Nico de Poel
@NicodePoel, podoba mi się twoja odpowiedź, a także twój komentarz! A ja mam kilka pytań: 1- Skąd wiesz, że rzadziej wybierane są punkty bliskie granicom 0 i 1, czy jest to oparte na matematycznych podstawach? 2- Czy możesz bardziej szczegółowo opracować sferyczną geomapę, jak poprawi losowy wybór i jak to zrobić w MongoDB? ... doceniony!
securecurve
Zatwierdź swój pomysł. Wreszcie mam świetny kod, który jest bardzo przyjazny dla procesora i pamięci RAM! Dziękuję
Qais Bsharat
21

Poniższy przepis jest nieco wolniejszy niż rozwiązanie książki kucharskiej mongo (dodaj losowy klucz na każdym dokumencie), ale zwraca bardziej równomiernie rozmieszczone losowe dokumenty. Jest nieco mniej równomiernie rozłożony niż skip( random )rozwiązanie, ale o wiele szybszy i bardziej bezpieczny w przypadku usunięcia dokumentów.

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;
}

Wymaga to również dodania losowego „losowego” pola do dokumentów, więc nie zapomnij dodać tego podczas ich tworzenia: może być konieczne zainicjowanie kolekcji, jak pokazuje Geoffrey

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

Wyniki testu

Ta metoda jest znacznie szybsza niż skip()metoda (ceejayoz) i generuje bardziej jednolicie losowe dokumenty niż metoda „książki kucharskiej” zgłoszona przez Michaela:

W przypadku kolekcji z 1 000 000 elementów:

  • Ta metoda zajmuje mniej niż milisekundę na moim komputerze

  • skip()sposobie, 180 ms średnio

Metoda książki kucharskiej spowoduje, że duża liczba dokumentów nigdy nie zostanie wybrana, ponieważ ich losowa liczba ich nie sprzyja.

  • Ta metoda zbierze wszystkie elementy równomiernie w czasie.

  • W moim teście było tylko 30% wolniejsze niż metoda książki kucharskiej.

  • losowość nie jest w 100% idealna, ale jest bardzo dobra (w razie potrzeby można ją poprawić)

Ten przepis nie jest idealny - idealne rozwiązanie byłoby wbudowaną funkcją, jak zauważyli inni.
Jednak powinien być dobrym kompromisem dla wielu celów.

spam_eggs
źródło
10

Oto sposób użycia ObjectIdwartości domyślnych _idi odrobiny matematyki i logiki.

// 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];

Jest to ogólna logika reprezentacji powłoki i łatwa do dostosowania.

Więc w punktach:

  • Znajdź minimalną i maksymalną wartość klucza podstawowego w kolekcji

  • Wygeneruj losową liczbę, która przypada między znacznikami czasu tych dokumentów.

  • Dodaj liczbę losową do minimalnej wartości i znajdź pierwszy dokument, który jest większy lub równy tej wartości.

Używa „padding” z wartości znacznika czasu w „hex”, aby utworzyć prawidłową ObjectIdwartość, ponieważ tego właśnie szukamy. Używanie liczb całkowitych jako _idwartości jest zasadniczo prostsze, ale ta sama podstawowa idea w punktach.

Blakes Seven
źródło
Mam kolekcję 300 000 000 linii. To jedyne rozwiązanie, które działa i jest wystarczająco szybkie.
Nikos
8

W Pythonie za pomocą pymongo:

import random

def get_random_doc():
    count = collection.count()
    return collection.find()[random.randrange(count)]
Jabba
źródło
5
Warto zauważyć, że wewnętrznie użyje to przeskakiwania i ograniczania, podobnie jak wiele innych odpowiedzi.
JohnnyHK
Twoja odpowiedź jest poprawna. Jednakże, należy wymienić count()ze estimated_document_count()jak count()jest przestarzałe w Mongdo v4.2.
user3848207
8

Teraz możesz użyć agregatu. Przykład:

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

Zobacz dokument .

dbam
źródło
3
Uwaga: $ sample może otrzymać ten sam dokument więcej niż raz
Saman Shafigh
6

jest to trudne, jeśli nie ma danych, które można by usunąć. jakie są pola _id? czy są to identyfikatory obiektów mongodb? Jeśli tak, możesz uzyskać najwyższe i najniższe wartości:

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

to jeśli założymy, że identyfikatory są rozmieszczone równomiernie (ale tak nie jest, ale przynajmniej to początek):

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.
źródło
1
Wszelkie pomysły, jak to wyglądałoby w PHP? a przynajmniej jakiego języka używałeś powyżej? czy to jest Python?
Marcin
6

Za pomocą Pythona (pymongo) działa również funkcja agregująca.

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

Takie podejście jest znacznie szybsze niż uruchamianie zapytania o liczbę losową (np. Collection.find ([random_int]). Dotyczy to zwłaszcza dużych kolekcji.

Daniel
źródło
5

Możesz wybrać losowy znacznik czasu i wyszukać pierwszy obiekt, który został później utworzony. Będzie skanował tylko jeden dokument, choć niekoniecznie zapewnia jednolitą dystrybucję.

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
źródło
Łatwo byłoby przekrzywić losową datę, aby uwzględnić wzrost superlinearnej bazy danych.
Martin Nowak
jest to najlepsza metoda dla bardzo dużych kolekcji, działa przy O (1), unline skip () lub count () zastosowanych w innych rozwiązaniach tutaj
marmor
4

Moje rozwiązanie na 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;
}
kod_turysta
źródło
3

Aby uzyskać określoną liczbę losowych dokumentów bez duplikatów:

  1. najpierw zdobądź wszystkie identyfikatory
  2. uzyskać rozmiar dokumentów
  3. pętla uzyskuje losowy indeks i pomija duplikaty

    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
źródło
2

Sugerowałbym użycie mapy / zmniejszenia, gdzie używasz funkcji mapy, aby emitować tylko wtedy, gdy losowa wartość przekracza podane prawdopodobieństwo.

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);

Powyższa funkcja zmniejszania działa, ponieważ tylko jeden klawisz („1”) jest emitowany z funkcji mapy.

Wartość „prawdopodobieństwa” jest zdefiniowana w „zasięgu” podczas wywoływania mapRreduce (...)

Korzystanie z mapReduce w ten sposób powinno być również możliwe na dzielonym db.

Jeśli chcesz wybrać dokładnie n spośród m dokumentów z bazy danych, możesz to zrobić w następujący sposób:

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);

Gdzie „countTotal” (m) to liczba dokumentów w bazie danych, a „countSubset” (n) to liczba dokumentów do pobrania.

Takie podejście może powodować pewne problemy w dzielonych bazach danych.

torbenl
źródło
4
Wykonanie pełnego skanowania kolekcji w celu zwrócenia 1 elementu ... musi to być najmniej efektywna technika.
Thomas
1
Sztuka polega na tym, że jest to ogólne rozwiązanie do zwracania dowolnej liczby losowych elementów - w takim przypadku byłoby szybsze niż inne rozwiązania, gdy otrzymano> 2 losowe elementy.
torbenl
2

Możesz wybrać losowy _id i zwrócić odpowiedni obiekt:

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

Tutaj nie musisz tracić miejsca na przechowywanie losowych liczb w kolekcji.

Vijay13
źródło
1

Sugeruję dodanie losowego pola int do każdego obiektu. Następnie możesz po prostu zrobić

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

wybrać losowy dokument. Tylko upewnij się, że masz indeksIndex ({random_field: 1})

mstearn
źródło
2
Jeśli pierwszy rekord w Twojej kolekcji ma stosunkowo wysoką wartość pola_ losowego, czy nie zostanie on zwrócony prawie cały czas?
thehiatus
2
thehaitus jest poprawny, będzie - nie nadaje się do żadnego celu
Heptic
7
To rozwiązanie jest całkowicie błędne, dodanie losowej liczby (wyobraźmy sobie, że między 0 a 2 ^ 32-1) nie gwarantuje dobrej dystrybucji, a użycie $ gte sprawia, że ​​jest jeszcze gorzej, ponieważ twój losowy wybór nie będzie nawet blisko na pseudolosową liczbę. Sugeruję, aby nigdy nie używać tej koncepcji.
Maximiliano Rios,
1

Kiedy miałem do czynienia z podobnym rozwiązaniem, wycofałem się i stwierdziłem, że zlecenie biznesowe dotyczyło stworzenia jakiejś formy rotacji prezentowanych zapasów. W takim przypadku istnieją znacznie lepsze opcje, które mają odpowiedzi z wyszukiwarek takich jak Solr, a nie ze sklepów danych takich jak MongoDB.

Krótko mówiąc, z wymogiem „inteligentnego obracania” treści, powinniśmy zrobić zamiast losowej liczby we wszystkich dokumentach, aby uwzględnić osobisty modyfikator q score. Aby wdrożyć to samodzielnie, zakładając niewielką populację użytkowników, możesz przechowywać dokument na użytkownika, który ma identyfikator produktu, liczbę wyświetleń, liczbę kliknięć, datę ostatniego wyświetlenia i wszelkie inne czynniki, które firma uzna za istotne dla obliczenia wyniku aq modyfikator. Podczas pobierania zestawu do wyświetlenia, zwykle żądasz więcej danych z magazynu danych niż żąda tego użytkownik końcowy, a następnie zastosujesz modyfikator q score, weź liczbę rekordów wymaganych przez użytkownika końcowego, a następnie losowo przejrzysz stronę wyników, niewielki ustaw, więc po prostu posortuj dokumenty w warstwie aplikacji (w pamięci).

Jeśli wszechświat użytkowników jest zbyt duży, możesz podzielić użytkowników na kategorie zachowań i indeksować według grup zachowań, a nie według użytkowników.

Jeśli wszechświat produktów jest wystarczająco mały, możesz utworzyć indeks dla użytkownika.

Uważam, że ta technika jest znacznie bardziej wydajna, ale co ważniejsze, bardziej efektywna w tworzeniu odpowiedniego, wartościowego doświadczenia w korzystaniu z oprogramowania.

paegun
źródło
1

żadne z rozwiązań nie działało dla mnie dobrze. szczególnie, gdy jest wiele luk i zestaw jest mały. to działało bardzo dobrze dla mnie (w php):

$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
Mantas Karanauskas
źródło
Ty określasz język, ale nie używaną bibliotekę?
Benjamin
Do Twojej wiadomości, jeśli dokument zostanie usunięty między pierwszą a trzecią linią, istnieje warunek wyścigu. Również find+ skipjest dość złe, zwracasz wszystkie dokumenty tylko po to, aby wybrać jeden: S.
Martin Konecny
1

Moje sortowanie / zamówienie PHP / MongoDB według rozwiązania RANDOM. Mam nadzieję, że to pomoże każdemu.

Uwaga: W mojej kolekcji MongoDB mam identyfikatory numeryczne, które odnoszą się do rekordu bazy danych MySQL.

Najpierw tworzę tablicę z 10 losowo generowanymi liczbami

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

W mojej agregacji korzystam z operatora potoku $ addField w połączeniu z $ arrayElemAt i $ mod (moduł). Operator modułu da mi liczbę od 0 do 9, której następnie używam do wybrania liczby z tablicy z losowo wygenerowanymi liczbami.

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

Następnie możesz użyć sortowania Pipeline.

    $aggregate[] = [
        '$sort' => [
            'random_sort' => 1
        ]
    ];
feskr
źródło
0

Jeśli masz prosty klucz identyfikatora, możesz przechowywać wszystkie identyfikatory w tablicy, a następnie wybrać losowy identyfikator. (Odpowiedź Ruby):

ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
Pan Demetrius Michael
źródło
0

Używając Map / Reduce, możesz z pewnością uzyskać losowy rekord, ale niekoniecznie bardzo efektywnie, w zależności od wielkości wynikowej filtrowanej kolekcji, z którą ostatecznie pracujesz.

Przetestowałem tę metodę z 50 000 dokumentów (filtr zmniejsza ją do około 30 000) i działa w około 400 ms na procesorze Intel i3 z 16 GB pamięci RAM i dyskiem twardym 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" }
    }
);

Funkcja mapy tworzy po prostu tablicę identyfikatorów wszystkich dokumentów pasujących do zapytania. W moim przypadku przetestowałem to z około 30 000 z 50 000 możliwych dokumentów.

Funkcja Reduce po prostu wybiera losową liczbę całkowitą od 0 do liczby elementów (-1) w tablicy, a następnie zwraca ten _id z tablicy.

400 ms brzmi jak długi czas, a tak naprawdę, jeśli masz pięćdziesiąt milionów płyt zamiast pięćdziesięciu tysięcy, może to zwiększyć obciążenie do tego stopnia, że ​​stanie się bezużyteczne w sytuacjach, w których korzysta wielu użytkowników.

MongoDB ma otwarty problem, aby włączyć tę funkcję do rdzenia ... https://jira.mongodb.org/browse/SERVER-533

Jeśli ta „losowa” selekcja została wbudowana w przegląd indeksu zamiast gromadzenia identyfikatorów w tablicy, a następnie wybierania jednej, pomogłoby to niewiarygodnie. (idź głosuj w górę!)

doublehelix
źródło
0

Działa to dobrze, jest szybkie, działa z wieloma dokumentami i nie wymaga randwypełniania pola, które ostatecznie zapełni się:

  1. dodaj indeks do pola .rand w swojej kolekcji
  2. użyj funkcji znajdź i odśwież, coś takiego:
// 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. Jak znaleźć losowe rekordy w pytaniu mongodb jest oznaczony jako duplikat tego pytania. Różnica polega na tym, że kwestia ta wyraźnie prosi o pojedynczy rekord jako drugi wyraźnie o uzyskanie losowych dokumentu s .

Mirek Rusin
źródło
-2

Jeśli używasz mongoid, otoki dokumentu na obiekt, możesz wykonać następujące czynności w Ruby. (Zakładając, że twój model to Użytkownik)

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

W moim .irbrc mam

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

więc w konsoli szyn mogę zrobić na przykład

rando User
rando Article

aby losowo pobierać dokumenty z dowolnej kolekcji.

Zack Xu
źródło
1
Jest to wyjątkowo nieefektywne, ponieważ wczyta całą kolekcję do tablicy, a następnie wybierze jeden rekord.
JohnnyHK,
Ok, może nieefektywne, ale na pewno wygodne. spróbuj tego, jeśli Twój rozmiar danych nie jest zbyt duży
Zack Xu,
3
Jasne, ale pierwotne pytanie dotyczyło kolekcji zawierającej 100 milionów dokumentów, więc byłoby to bardzo złe rozwiązanie w tej sprawie!
JohnnyHK,
-2

możesz także użyć tablicy losowej po wykonaniu zapytania

var shuffle = wymagany („shuffle-array”);

Accounts.find (qry, funkcja (err, tablica wyników) {newIndexArr = shuffle (tablica wyników);

rabie jegham
źródło
-7

To, co działa wydajnie i niezawodnie, to:

Dodaj pole o nazwie „losowe” do każdego dokumentu i przypisz do niego losową wartość, dodaj indeks do pola losowego i postępuj w następujący sposób:

Załóżmy, że mamy zbiór linków o nazwie „linki” i chcemy z nich losowy link:

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

Aby mieć pewność, że ten sam link nie pojawi się po raz drugi, zaktualizuj jego losowe pole o nową liczbę losową:

db.links.update({random: Math.random()}, link)
wrak pociągu
źródło
2
po co aktualizować bazę danych, skoro można po prostu wybrać inny losowy klucz?
Jason S
Być może nie masz listy kluczy do losowego wyboru.
Mike
Więc za każdym razem musisz sortować całą kolekcję? A co z pechowymi rekordami, które otrzymały duże liczby losowe? Nigdy nie zostaną wybrane.
Fantius
1
Musisz to zrobić, ponieważ inne rozwiązania, szczególnie te sugerowane w książce MongoDB, nie działają. Jeśli pierwsze znalezisko się nie powiedzie, drugie znalezisko zawsze zwraca element o najmniejszej losowej wartości. Jeśli indeksujesz losowo malejąco, pierwsze zapytanie zawsze zwraca element o największej liczbie losowej.
wrak pociągu
Dodanie pola w każdym dokumencie? Myślę, że to nie jest wskazane.
CS_noob