4

膨大な量のテキスト (> 2 Tb、ウィキペディアのフル ダンプ) を調べ、表示されたトークンごとに2 つのカウンターを保持する必要があります (各カウンターは現在のイベントに応じて増加します)。これらのカウンターに必要な唯一の操作は増加です。2 番目のフェーズでは、これらのカウンターに基づいて 2 つの float を計算し、それらを格納する必要があります。

次の手順を実行する必要があります。

  1. 現在のイベントに応じて、大量のテキストを調べ、見つかった単語ごとに2 つのカウンターを増やします。
  2. すべてのトークンを調べ、それらのそれぞれについて、これらのカウンターに基づいて 2 つの追加の float を計算します。
  3. クエリを許可します (任意のトークンの値を取得します)。

要件とその他の詳細:

  • O(10^8) トークンまでスケールアップする必要があります。
  • 最終結果は非常に高速に照会する必要があります。
  • テキストを読んでいる間、2つのカウンターの増加のみが行われます。これは 1 回限りの処理であるため、処理中にクエリは発生しません。値の更新のみ。
  • 動的/更新可能なスキーマは必要ありません。

私は CouchDB と MongoDB を試してきましたが、あまり良い結果は得られませんでした。

この問題に対する最善のアプローチは何だと思いますか?

ありがとうございました!

編集 1:パトリシア トライを試して、すべてのキーがメモリに収まるかどうかをテストするように提案されました(そうではないと思われます)。1 つのステップで各キーの値を増やすための追加の演算子を使用したカスタム パトリシア トライが、可能な解決策になる可能性があります。

EDIT 2:「巨大」の意味を明確にしました: > 2 Tb のテキスト。詳細説明。

編集 3:一意のトークンの見積もり。Mike Dunlavey の提案に従って、一意のトークンを簡単に見積もってみました。データセットの最初の 830Mb では、一意のトークンは 52134 まで直線的に増加します。より多くのデータを処理した後に一意のトークンの数が遅くならない限り (これは可能性が高い)、O(10^8) 個の一意のトークンが存在するはずです。

編集 4: Java および Python ソリューションが推奨されますが、他の言語も問題ありません。

編集 5:通常、トークンには印刷可能な ASCII 文字のみが含まれますが、印刷可能な任意の Unicode 文字を含めることができます。小文字と大文字の両方をそのままにして、同じプロセスを試します。小文字のみ。

4

6 に答える 6

1

高レベルのソリューション:

  1. 入力を解析し、「[token] + X + Y」行を1-of-N出力ファイルに出力します(これらの「シャーディング」出力ファイルはそれぞれ、メモリ内で処理できるほど小さいです)。
  2. [ファイルごとに]メモリに読み込み、「[token] [count1][count2]...」行のソートされたファイルを出力します
  3. クエリ時に、正しいファイルでバイナリ検索を実行します

詳細:ステップ1のPython擬似コードは次のとおりです)

NUM_SHARDS = 1000  # big enough to make each file fit in memory  
output_files = [open("file" + str(n), "w") for n in xrange(NUM_SHARDS)]
for token in input_stream:
   shard_id = hash(token) % NUM_SHARDS
   output_files[shard_id].write(token + " +0 +1\n")
   # TODO: output the correct +X and +Y as needed

これがステップ2のPython擬似コードです)

input_files = [open("file" + str(n)) for n in xrange(NUM_SHARDS)]
for file in input_files:
   counts = {}   # Key: token   Value: { "count1": 0, "count2": 1 }

   # read the file, and populate 'counts'
   for line in file:
      (token, count1, count2) = line.split(" ")
      # make sure we have a value for this token
      counts.setdefault(token, { "count1": 0, "count2": 0 })
      counts[token]["count1"] += int(count1)
      counts[token]["count2"] += int(count2)
      # TODO: compute those floats, and stuff those inside 'counts' also

   # now write 'counts' out to a file (in sorted order)
   output_file = open(file.name + ".index", "w")
   for token, token_counts in sorted(counts.items()):
      output_file.write(token + " " + token_counts["counts1"] + " " + token_counts["counts2"] + "\n")
      # TODO: also write out those floats in the same line

ステップ3)のPythonコードは次のとおりです。

# assume 'token' contains the token you want to find
shard_id = hash(token) % NUM_SHARDS
filename = "file" + str(shard_id) + ".index"
binary_search(token, open(filename), 0, os.path.getsize(filename))

# print out the line in 'file' whose first token is 'token'
# begin/end always point to the start of a line
def binary_search(token, file, begin, end):
    # If we're close, just do brute force
    if end - begin < 10000:
            file.seek(begin)
            while file.tell() < end:
                    line = file.readline()
                    cur_token = line.strip().split(" ")[0]
                    if cur_token == token:
                            print line
                            return True
            return False  # not found

    # If we're not close, pivot based on a line near the middle
    file.seek((begin + end) / 2)
    partial_line = file.readline()  # ignore the first fractional line
    line = file.readline()

    cur_token = line.strip().split(" ")[0]
    if cur_token == token:
            print line
            return True
    elif cur_token < token:
            return binary_search(token, file, file.tell(), end)
    else:  # cur_token > token
            return binary_search(token, file, begin, file.tell() - len(line))
于 2010-10-15T09:58:33.537 に答える
1

大量のメモリがある場合は、カウンターを保存するために単純なredisを使用できます (それぞれ 2 つのカウンターを持つ 10^8 の一意のトークンは、約 12GB かかると思います)。

それほど多くのメモリがない場合でも、redis を使用できますが、メモリに適合させるために少しハッシュ戦略と vm_enabled を使用します。

トークンを 1 番目と 2 番目の文字 (aa、ab、ac... zz) で分割したハッシュ名、実際の単語 + トークン識別子をハッシュ キーとして、カウントを値として使用できます。次のようになります。

hash ab
- absence_c1 5
- absence_c2 2
- abandon_c1 2
- abandon_c1 10
hash st
- stack_c1 10
- stack_c2 14

しかし、このアプローチでは、redis はハッシュを「インクリメント」できないため、前の値を取得し、値をインクリメントして元に戻します (疑似コード):

var last = redis("hget st stack_c1")
var actual = last + 1
redis("hset st stack_c1 actual")

このハッシュ パターンを使用し、VM を有効にした redis を使用すると、十分な速度を維持しながらメモリ使用量を低く抑えることができます。100MB 未満の RAM とほぼ 4G のディスクを使用して、それぞれ 15 文字の 200 万個のトークンを保存できました。

于 2010-10-14T03:26:07.033 に答える
1

わかりました。MongoDB と CouchDB が機能しない場合、基本的に 1 つの問題があります。十分な電力がありません。

洗濯物のリストを見てみましょう:

O(10^8) トークンまでスケールアップする必要があります。

どのくらいのRAMを持っていますか? あなたは何億ものトークンについて話ているし、7zip ファイルをストリーミングすることについて話している. 「インクリメント」をすばやく発行したい場合は、データ構造全体をメモリに保持できる必要があります。そうしないと、全体の処理が非常に遅くなります。

最終結果は非常に高速に照会する必要があります。

どのくらい速いのか?マイクロ秒、ミリ秒、数百ミリ秒? 8 GB の RAM を搭載したマシンで 5 億件のレコードにクエリを実行したい場合は、かなり大変です。使用しているDBに関係なく、データは収まりません。

データセット > 2Tb

OK、あなたのコンピュータが平均約 50MB/秒の持続的なスループットを維持でき、あなたの proc が実際にそのペースでデータを解凍できると仮定しましょ。そのペースでは、データをストリーミングするためだけに 11 時間以上の処理時間について話していることになります (これを週末に実行したかったですか?)

11 時間で 50MB/s のスループットは小さなものではなく、実際のドライブです。そして、それが起こっている間 (または OS がスワップしている間) にディスクに何かを書き込もうとすると、それは急速に劣化します。

DB の観点から見ると、MongoDBはフロントエンドの更新とバックエンドのクエリの両方を処理できます。しかし、約 1 分ごとにディスクにフラッシュする必要があるため、11 時間の実行時間が大幅に延長されます。

メモリ内の DB 全体とメモリ内のストリーム全体を処理できない限り、合計実行時間はますます悪化します。

私のポイント...

非常に単純です。もっと力が必要です。

この操作を 24GB 以上の RAM で実行していない場合、すべての操作が遅く感じられます。24GB 以上の RAM がない場合、最終的なデータセットは「超高速」にはならず、せいぜい「200 ミリ秒高速」になります。RAMにインデックスを保持できない限り、5億行のインデックスを作成してエントリを見つけることができます。

この操作を素晴らしい HDD で実行していない場合、操作は遅く見えるでしょう。つまり、何時間にもわたる高スループットの持続的な読み取り (およびおそらく書き込み) について話しているのです。

あなたが助けを求めていることは知っています。あなたがこの質問に賞金をかけたことは知っていますが、次の問題を解決するのは本当に難しいです:

私は CouchDB と MongoDB を試してきましたが、あまり良い結果は得られませんでした。

問題を解決するための適切なギアを実際にまとめていないように思われる場合。

于 2010-10-19T05:14:54.877 に答える
1

解決策ではなく戦略。

1 つのプロセスによる入力データのリードスルーを回避することはできません。つまり、ファイルが並列 I/O システム上にない限り、初期操作を並列化する方法がわかりません。それでも、7z に取り組むのは難しいと思います。並行してファイルします。

ただし、試すことができるのは、入力データを読み取り、ファイルシステム全体にそのチャンクを書き込むプロセスを実装することです。できれば、次に開始するプロセスが同じ読み取り/ヘッドを書きます。

最初のチャンクが書き込まれるとすぐに、そのチャンクの消化を開始するために、別のコアでプロセスを開始します (マルチコアを使用しているのではないでしょうか? ワークステーションのクラスターまたはネットワークでさえありますか?)。このプロセスは、部分的な結果をファイルに書き込みます。

2番目のチャンクが書き込まれるとすぐに、別のコアでプロセスを開始します...

...あなたは写真を手に入れます

入力全体が処理されたら、各チャンクを処理するタスクの出力からの結果をマージするタスクを考案します。これはある種のカスケードで行います (たとえば、32 個のチャンクと 16 個のプロセッサがある場合、それぞれ 2 つのチャンクをマージし、そのうちの 8 つがマージされたチャンクの 2 つをマージする、など)。

私の最善の推測では、これにはフラットファイルで問題ないはずですが、DB の追加のパワーが追加のコストに見合うかどうかはわかりません (パフォーマンスとプログラミングの複雑さの点で)。クエリをサポートするために、最終結果をデータベースに書き込みたいと思うかもしれません。

編集:すべてのクエリが「トークン XXX のカウンターを取得してください」という形式である場合、単一の並べ替えられたテキスト ファイルを介してバイナリ検索を実行できます。そうするべきだと言っているわけではありませんが、解決策の方向性を示してくれるかもしれません。とりあえず、トークンが任意の文字で始まる可能性があることを忘れると (これは単なるアルファベットの問題です)、A で始まるトークン用に 1 つ、B で始まるトークン用に 1 つ、というように 26 個のファイルを持つことができます。

または、A (ファイルの先頭からのオフセット 0) B (先頭からのオフセット 12456) などのエントリを使用して、マスター ファイルにインデックスを作成することもできます。

個人的には、実用的な解決策が得られるまで、最初の文字ごとに 1 つの並べ替えられたテキスト ファイルのアプローチを少し試してから、それが十分に高速かどうかを判断します。しかし、大量のディスクと大量の RAM を備えた大規模なクラスターにアクセスできるため、プラットフォームによっては、おそらくより洗練された別のアプローチが必要になる場合があります。

于 2010-10-12T10:24:15.317 に答える
1

私が理解したように、トークンを数えたいだけです。最初の解決策は、メモリ内のハッシュ マップを使用することです。52-100k トークン (英語の単語の利点の長さは約 5.1 です) + カウントを保持するための各トークンの 4 バイトは、それほど多くのデータではありません。開発者マシンのメモリにマップを簡単に保存できます。

2 番目の解決策は、新しいトークンを格納するために apache lucene を使用することです。100 万のエントリがない場合を除き、インデックスを分割する必要はありません。また、sqllite などのデータベースに格納するカウンター値 (更新lucene インデックスは最良のアイデアではありません)。

両方のソリューションでプロセスを高速化するには、データセットを k*100 データセットに分割し、それらを異なるマシンで個別に (または並行して) 実行し、それらの結果をマージします。数えた結果、問題なく合計できます。

あなたのユース ケースは apache hadoop チュートリアルの古典的な例ですが、それを展開するのはやり過ぎだと思います。

于 2010-10-13T16:39:28.683 に答える
0

テキスト ファイルを読み取るのではなく、DB を使用する必要がありますか?

単純な C タイプのコンパイル済み言語は、ファイルの読み取りにかかる時間の何分の 1 かで単純なパーサーを実行できるため、基本的に「I/O バウンド」である必要があります。wcunix 、 word-count に似たプログラムになります。

数学は些細なことのように聞こえ、目立たないはずです。

編集:OK、一意のトークンの辞書を作成して、それぞれを数えたいと思っていることを理解できませんでした。その場合、トライまたはハッシュベースの辞書で十分です。そのストレージ サイズは、トークンの一般的な長さと異なるトークンの数によって異なります。sort | uniqこれは、UNIX のイディオムに似ている可能性があります。

于 2010-10-11T15:37:02.823 に答える