web-dev-qa-db-ja.com

MongoDBからpython

MongoDBのコレクション内のすべてのドキュメントには、同じフィールドがあります。私の目標は、それらをPython into _pandas.DataFrame_または_dask.DataFrame_にロードすることです。

並列化して読み込み手順をスピードアップしたい。私の計画は、いくつかのプロセスまたはスレッドを生成することです。各プロセスはコレクションのチャンクをロードし、次にこれらのチャンクがマージされます。

MongoDBで正しく行うにはどうすればよいですか?

私はPostgreSQLで同様のアプローチを試しました。私の最初のアイデアは、SQLクエリでSKIPLIMITを使用することでした。各カーソルが特定のクエリごとに開かれ、最初からデータテーブルの読み取りを開始し、指定された行数をスキップしたため、失敗しました。そのため、レコード番号を含む追加の列を作成し、クエリでこれらの番号の範囲を指定する必要がありました。

それどころか、MongoDBは各ドキュメントに一意のObjectIDを割り当てます。ただし、あるObjectIDを別のObjectIDから減算することは不可能であり、それらは順序付け操作(より少ない、より大きい、等しい)とのみ比較できることがわかりました。

また、pymongoは、インデックス作成操作をサポートし、countlimitなど、私のタスクに役立つと思われるいくつかのメソッドを持つカーソルオブジェクトを返します。

SparkのMongoDBコネクタはこのタスクをなんとかして達成します。残念ながら、私はScalaに精通していないため、Scalaがどのように実行するかを知るのは困難です。

では、MongoからPythonにデータを並列ロードする正しい方法は何ですか?

今まで、私は次の解決策に到達しました:

_import pandas as pd
import dask.dataframe as dd
from dask.delayed import delayed

# import other modules.

collection = get_mongo_collection()
cursor = collection.find({ })

def process_document(in_doc):
    out_doc = # process doc keys and values
    return pd.DataFrame(out_doc)

df = dd.from_delayed( (delayed(process_document)(d) for d in cursor) )
_

ただし、_dask.dataframe.from_delayed_は、渡されたジェネレーターから内部的にリストを作成し、すべてのコレクションを単一のスレッドに効果的にロードしているように見えます。

更新docs で、_pymongo.Cursor_のskipメソッドもPostgreSQLのように、コレクションの最初から始まることがわかりました。同じページで、アプリケーションでページネーションロジックを使用することを提案しています。私がこれまでに見つけた解決策は、これにソートされた__id_を使用します。ただし、最後に表示された__id_も格納されます。これは、単一のスレッドでも機能することを意味します。

Update2。公式のMongoDb Sparkコネクタ: https://github.com/mongodb/mongo-spark/blob/7c76ed1821f70ef2259f8822d812b9c53b6f2b98/src/mainでパーティショナーのコードを見つけました/scala/com/mongodb/spark/rdd/partitioner/MongoPaginationPartitioner.scala#L32

最初は、このパーティショナーがコレクション内のすべてのドキュメントからキーフィールドを読み取り、値の範囲を計算しているように見えます。

Update3:私の不完全な解決策。

DaskがCollectionオブジェクトを誤って処理しているように見えるため、機能せず、pymongoから例外を取得します。

_/home/user/.conda/envs/MBA/lib/python2.7/site-packages/dask/delayed.pyc in <genexpr>(***failed resolving arguments***)
     81         return expr, {}
     82     if isinstance(expr, (Iterator, list, Tuple, set)):
---> 83         args, dasks = unzip((to_task_dask(e) for e in expr), 2)
     84         args = list(args)
     85         dsk = sharedict.merge(*dasks)

/home/user/.conda/envs/MBA/lib/python2.7/site-packages/pymongo/collection.pyc in __next__(self)
   2342 
   2343     def __next__(self):
-> 2344         raise TypeError("'Collection' object is not iterable")
   2345 
   2346     next = __next__

TypeError: 'Collection' object is not iterable
_

例外を引き起こすもの:

_def process_document(in_doc, other_arg):
    # custom processing of incoming records
    return out_doc

def compute_id_ranges(collection, query, partition_size=50):
    cur = collection.find(query, {'_id': 1}).sort('_id', pymongo.ASCENDING)
    id_ranges = [cur[0]['_id']]
    count = 1
    for r in cur: 
        count += 1
        if count > partition_size:
            id_ranges.append(r['_id'])
            count = 0
    id_ranges.append(r['_id'])
    return Zip(id_ranges[:len(id_ranges)-1], id_ranges[1: ])    


def load_chunk(id_pair, collection, query={}, projection=None):
    q = query
    q.update( {"_id": {"$gte": id_pair[0], "$lt": id_pair[1]}} )
    cur = collection.find(q, projection)

    return pd.DataFrame([process_document(d, other_arg) for d in cur])


def parallel_load(*args, **kwargs):
    collection = kwargs['collection']
    query = kwargs.get('query', {})
    projection = kwargs.get('projection', None)

    id_ranges = compute_id_ranges(collection, query)

    dfs = [ delayed(load_chunk)(ir, collection, query, projection) for ir in id_ranges ]
    df = dd.from_delayed(dfs)
    return df

collection = connect_to_mongo_and_return_collection_object(credentials)

# df = parallel_load(collection=collection)

id_ranges = compute_id_ranges(collection)
dedf = delayed(load_chunk)(id_ranges[0], collection)
_

_load_chunk_は、直接呼び出されたときに完全に実行されます。ただし、上記の例外を除いて、呼び出しdelayed(load_chunk)( blah-blah-blah )は失敗します。

10
wl2776

私はpymongoの並列化を検討していましたが、これが私にとってうまくいったことです。私の謙虚なゲーミングラップトップが4000万のドキュメントの私のmongodbを処理するのにほぼ100分かかりました。 CPUは100%使用されていました。ACをオンにする必要がありました:)

スキップ関数と制限関数を使用してデータベースを分割し、バッチをプロセスに割り当てました。コードはPython 3:

import multiprocessing
from pymongo import MongoClient

def your_function(something):
    <...>
    return result

def process_cursor(skip_n,limit_n):
    print('Starting process',skip_n//limit_n,'...')
    collection = MongoClient().<db_name>.<collection_name>
    cursor = collection.find({}).skip(skip_n).limit(limit_n)
    for doc in cursor:        
        <do your magic> 
        # for example:
        result = your_function(doc['your_field'] # do some processing on each document
        # update that document by adding the result into a new field
        collection.update_one({'_id': doc['_id']}, {'$set': {'<new_field_eg>': result} })

    print('Completed process',skip_n//limit_n,'...')


if __name__ == '__main__':
    n_cores = 7                # number of splits (logical cores of the CPU-1)
    collection_size = 40126904 # your collection size
    batch_size = round(collection_size/n_cores+0.5)
    skips = range(0, n_cores*batch_size, batch_size)

    processes = [ multiprocessing.Process(target=process_cursor, args=(skip_n,batch_size)) for skip_n in skips]

    for process in processes:
        process.start()

    for process in processes:
        process.join()

最後の分割には残りのドキュメントよりも大きな制限がありますが、エラーは発生しません

6
Ali Abul Hawa

「男を読んでください、彼らはルールです」:)

pymongo.Collectionメソッドがあります parallel_scan カーソルのリストを返します。

[〜#〜]更新[〜#〜]。コレクションがあまり頻繁に変更されず、クエリが常に同じである場合(私の場合)、この関数はジョブを実行できます。クエリ結果をさまざまなコレクションに保存し、並列スキャンを実行するだけで済みます。

0
wl2776