web-dev-qa-db-fra.com

Recherche d’enregistrements mongoDB par lots (avec l’adaptateur mongoid Ruby)

À 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.

36
Dan L

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
83
Ryan McGeary

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

5
Mic92

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.

4
HaxElit

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)
    
  • C'est tout, commencez la resque et il s'occupera de tout
2
RameshVel

Ce qui suit fonctionnera pour vous, essayez-le

Model.all.in_groups_of(1000, false) do |r|
  Sunspot.index! r
end
0
ratnakar