À l'aide de Rails 3 et de mongoDB avec l'adaptateur Mongoid, comment puis-je effectuer une recherche par lots sur la base de données Mongo? J'ai besoin de récupérer tous les enregistrements d'une collection particulière de la base de données mongo et de les indexer dans solr (index initial des données pour la recherche).
Le problème que j'ai est que faire Model.all récupère tous les enregistrements et les stocke en mémoire. Ensuite, lorsque je traite dessus et indexe dans solr, ma mémoire est dévorée et le processus meurt.
Ce que j'essaie de faire, c'est de grouper la recherche dans mongo de manière à pouvoir itérer plus de 1 000 enregistrements à la fois, à les passer à la recherche d'index, puis à traiter les 1 000 suivants, etc.
Le code que j'ai actuellement fait ceci:
Model.all.each do |r|
Sunspot.index(r)
end
Pour une collection qui compte environ 1,5 million d'enregistrements, cela consomme plus de 8 Go de mémoire et tue le processus. Dans ActiveRecord, il existe une méthode find_in_batches qui me permet de répartir les requêtes en lots gérables qui empêche la mémoire de devenir incontrôlable. Cependant, je n'arrive pas à trouver quelque chose comme ça pour mongoDB/mongoid.
Je voudrais pouvoir faire quelque chose comme ça:
Model.all.in_batches_of(1000) do |batch|
Sunpot.index(batch)
end
Cela atténuerait mes problèmes de mémoire et mes difficultés d'interrogation en ne résolvant qu'un problème gérable à chaque fois. Cependant, la documentation est rare lorsque vous effectuez des recherches par lots dans mongoDB. Je vois beaucoup de documentation sur les insertions par lots, mais pas les recherches par lots.
Avec Mongoid, vous n'avez pas besoin de mettre manuellement la requête en lot.
Dans Mongoid, Model.all
renvoie une instance Mongoid::Criteria
. Lors de l'appel de #each
sur ces critères, un curseur de pilote Mongo est instancié et utilisé pour parcourir les enregistrements. Ce curseur de pilote Mongo sous-jacent regroupe déjà tous les enregistrements. Par défaut, le batch_size
est 100.
Pour plus d'informations sur ce sujet, lisez ce commentaire de l'auteur et du responsable de Mongoid .
En résumé, vous pouvez simplement faire ceci:
Model.all.each do |r|
Sunspot.index(r)
end
Il est également plus rapide d’envoyer des lots à une tache solaire. Voici comment je procède:
records = []
Model.batch_size(1000).no_timeout.only(:your_text_field, :_id).all.each do |r|
records << r
if records.size > 1000
Sunspot.index! records
records.clear
end
end
Sunspot.index! records
no_timeout
: empêche le curseur de se déconnecter (après 10 min, par défaut)
only
: sélectionne uniquement l'id et les champs qui sont réellement indexés
batch_size
: extraire 1000 entrées au lieu de 100
Si vous parcourez une collection où chaque enregistrement nécessite beaucoup de traitement (par exemple, interroger une API externe pour chaque élément), il est possible que le curseur expire. Dans ce cas, vous devez effectuer plusieurs requêtes pour ne pas laisser le curseur ouvert.
require 'mongoid'
module Mongoid
class Criteria
def in_batches_of(count = 100)
Enumerator.new do |y|
total = 0
loop do
batch = 0
self.limit(count).skip(total).each do |item|
total += 1
batch += 1
y << item
end
break if batch == 0
end
end
end
end
end
Voici une méthode d'assistance que vous pouvez utiliser pour ajouter la fonctionnalité de traitement par lots. Il peut être utilisé comme suit:
Post.all.order_by(:id => 1).in_batches_of(7).each_with_index do |post, index|
# call external slow API
end
Assurez-vous juste d'avoir TOUJOURS un order_by sur votre requête. Sinon, la pagination pourrait ne pas faire ce que vous voulez. Je voudrais aussi coller avec des lots de 100 ou moins. Comme indiqué dans la réponse acceptée, Mongoid interroge par lots de 100 afin de ne jamais laisser le curseur ouvert pendant le traitement.
Je ne suis pas sûr du traitement par lots, mais vous pouvez le faire de cette façon.
current_page = 0
item_count = Model.count
while item_count > 0
Model.all.skip(current_page * 1000).limit(1000).each do |item|
Sunpot.index(item)
end
item_count-=1000
current_page+=1
end
Mais si vous recherchez une solution parfaite à long terme, je ne le recommanderais pas. Laissez-moi vous expliquer comment j'ai géré le même scénario dans mon application. Au lieu de faire des travaux par lots,
j'ai créé un resque job qui met à jour l'index solr
class SolrUpdator
@queue = :solr_updator
def self.perform(item_id)
item = Model.find(item_id)
#i have used RSolr, u can change the below code to handle sunspot
solr = RSolr.connect :url => Rails.application.config.solr_path
js = JSON.parse(item.to_json)
solr.add js
end
fin
Après avoir ajouté l'élément, je viens de mettre une entrée dans la file d'attente Resque
Resque.enqueue(SolrUpdator, item.id.to_s)
Ce qui suit fonctionnera pour vous, essayez-le
Model.all.in_groups_of(1000, false) do |r|
Sunspot.index! r
end