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?
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?
Odpowiedzi:
Począwszy od wersji 3.2 MongoDB, możesz pobrać N losowych dokumentów z kolekcji za pomocą $sample
operatora 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 $match
etap 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ść size
jest większa niż 1, w zwróconej próbce dokumentu mogą występować duplikaty.
Zliczyć wszystkie rekordy, wygenerować losową liczbę od 0 do zliczenia, a następnie:
db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
3.2 wprowadził $ sample do potoku agregacji.
Jest też dobry post na blogu dotyczący jego praktycznego zastosowania.
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 $gte
i $lte
konieczne 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: 1
do niego i dodaj ponownie.
$gte
jest pierwszy. Alternatywne rozwiązanie stackoverflow.com/a/9499484/79201 działałoby lepiej w tym przypadku.
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.
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.
Oto sposób użycia ObjectId
wartości domyślnych _id
i 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ą ObjectId
wartość, ponieważ tego właśnie szukamy. Używanie liczb całkowitych jako _id
wartości jest zasadniczo prostsze, ale ta sama podstawowa idea w punktach.
W Pythonie za pomocą pymongo:
import random
def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]
count()
ze estimated_document_count()
jak count()
jest przestarzałe w Mongdo v4.2.
Teraz możesz użyć agregatu. Przykład:
db.users.aggregate(
[ { $sample: { size: 3 } } ]
)
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);
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)
};
}();
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;
}
Aby uzyskać określoną liczbę losowych dokumentów bez duplikatów:
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)
});
});
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.
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.
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})
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.
ż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();
find
+ skip
jest dość złe, zwracasz wszystkie dokumenty tylko po to, aby wybrać jeden: S.
Jeśli używasz mangusty, możesz użyć mongoose-random mongoose-random
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
]
];
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
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ę!)
Działa to dobrze, jest szybkie, działa z wieloma dokumentami i nie wymaga rand
wypełniania pola, które ostatecznie zapełni się:
// 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 .
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.
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);
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)