jsonコンテンツインデックスの一意のIDを生成する

jsonコンテンツインデックスの一意のIDを生成する

bashスクリプトを使用して、次の有効で単純なIDを生成しようとしています。

{"name": "John", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"name": "John1", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"name": "John2", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"name": "John3", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}


{"id": "XXX", "name": "John", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"id": "XXX", "name": "John1", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"id": "XXX", "name": "John2", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}
{"id": "XXX", "name": "John3", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}

約5,000,000件の類似レコードがあり、繰り返し可能で予測可能なIDを生成したいと思います。次のファイルの処理には時間が限られているため、Linuxシステムのsql liteデータベースから20分以内にこれを完了する必要があります。

MD5、SHA1は、AMD Ryzen 1900X CPUの16スレッドでGNU Parallelなどの操作を実行でき、数分で実行できない場合は使用するには高すぎます。

MD5を試してみましたが、1分45秒で28,000個のIDの計算が完了しました。 SHA1を使用すると2分3秒かかりました。

私は非常に簡単にIDを作成しようとしています。

JohnGatesGermany20180
John1GatesGermany20180
John2GatesGermany20180
John3GatesGermany20180

次の要件を満たす必要がある場合は、どの提案をしますか?

  • 強く打つ
  • Linux
  • 5,000,000件のレコードを処理する必要があります。
  • 20分以内
  • 同じjson行の場合、IDは同じでなければなりません。

実行されたテスト:

#!/usr/local/bin/bash

while IFS= read -r line
do
   uuid=$(uuidgen -s --namespace @dns --name "www.example.com" )
done < testfile1.txt

1,000,000行のmd5ハッシュ:

$time bash script.sh 

real    13m6.914s
user    10m24.523s
sys 2m56.095s

cksum は 1,000,000 に対して crc チェックを実行します。

#!/usr/local/bin/bash

while IFS= read -r line
do
#    uuid=$(uuidgen -s --namespace @dns --name "www.example.com" )
    echo "$line $uuid"|cksum >> test3.txt
done < testfile1.txt

$time bash script.sh 

real    12m49.396s
user    12m23.219s
sys 4m1.417s

答え1

スクリプトが長すぎるのは、uuidgen各行で実行しているからです。cksum各プロセスを開始するだけで多くの時間が無駄になります。

5Mライン形式を{"name": "John%d", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"}tmpfsファイルシステム上のファイルに配置するには、次のPythonスクリプトが数秒で完了します。

#! /usr/bin/env python3

import hashlib
import sys
for line in sys.stdin:
    print(hashlib.md5(line.rstrip('\n').encode('utf-8')).hexdigest())

実装する:

$ time ./foo.py < input > output
./foo.py < input > output  6.00s user 0.13s system 99% cpu 6.135 total
% wc -l input output
  5000000 input
  5000000 output
 10000000 total

Pythonなので、行をJSONにデコードして各行にIDを挿入することもできます。次のような非効率的なコードもあります。

#! /usr/bin/env python3

import hashlib
import json
import sys
for line in sys.stdin:
    l = line.rstrip('\n').encode('utf-8')
    o = json.loads(line)
    o["id"] = hashlib.md5(l).hexdigest()
    print(json.dumps(o))

1分で完了:

% time ./foo.py < input > output
./foo.py < input > output  42.11s user 0.42s system 99% cpu 42.600 total

% head output 
{"name": "John1", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "2dc573ccb15679f58abfc44ec8169e52"}
{"name": "John2", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "ee0583acaf8ad0e502bf5abd29f37edb"}
{"name": "John3", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "a7352ebb79db8c8fc2cc8758eadd9ea3"}
{"name": "John4", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "2062ad1b67ccdce55663bfd523ce1dfb"}
{"name": "John5", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "5f81325c104c01c3e82abd2190f14bcf"}
{"name": "John6", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "493e0c9656f74ec3616e60886ee38e6a"}
{"name": "John7", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "19af9ef2e20466d0fb0efcf03f56d3f6"}
{"name": "John8", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "2348bd47b20ac6445213254c6a8aa80b"}
{"name": "John9", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "090a521b4a858705dc69bf9c8dca6c19"}
{"name": "John10", "surname": "Gates", "country": "Germany", "age": "20", "height": "180", "id": "fc3c699323cbe399e210e4a191f04003"}

私の仕様:

  • インテル®Core™i7-8700 CPU @ 3.20GHz×12
  • 2666MHz DDR4メモリ

uuidgen私はあなたのスクリプトに基づいて4分で500,000行をかろうじて管理しました。出力を保存するように変更します。

#!/usr/bin/bash

while IFS= read -r line
do
   uuidgen -s --namespace @dns --name "$line"
done < input > uuid

実装する:

% timeout 240 ./foo.sh
% wc -l uuid
522160 uuid

答え2

JSON行が指示したとおりであると仮定し、awkで単純なIDアイデアを実装します。すべて1行にあります。

awk -F'"' 'BEGIN{OFS=FS} {$1=$1"\"id\": \""$4$8$12$16$20"\", "; }1' < input

私はあなたに似たシステムを持っていないので、時間が許されることを確認する必要があります。

答え3

事故実験として、私はこの種の問題を解決するためにCLIツールをどれだけ活用できるかを確認したかったのです。このために、高速ハッシュCLIツールを試してみたいと思います。xx ハッシュ値タスクを実行します。

xxHash は、RAM の制限に近い動作をする非常に高速な非暗号化ハッシュアルゴリズムです。 32ビットと64ビットの2つのバージョンがあります。

xxhsumすべてのプログラミング言語で動作しますが、この実験ではCLIバージョン、特に32ビットモードを使用しているためxxhsum -H0

あなたが見つけて他の人が言ったように、ハッシュ関数CLIツールまたはツールを繰り返し呼び出すことは、通常、このタイプのアプローチが失敗した場合です。これを5M回呼び出すことは、xxhsumそれを使用するための次善策になります。利点はファイルI / Oですが、5M行を5Mファイルに変換するとどうなりますか?

これはLinuxで本当に簡単です。次のsplitコマンドを使用します。

split -l 1 afile

各ファイルに1行ずつこれらのファイル(1Mなど)をハッシュするのはどのくらい高速ですか?

例1 ラインファイル
$ cat datadir/xzeyw
{"name": "John4000", "surname": "Gates", "country": "Germany", "age": "20", "height": "180"} 
1Mファイルを含むディレクトリ
$ ls -l datadir | wc -l
1000002
ハッシュする時間
$ { time xxhsum -H0 * > ../nfile 2>&1 ;} |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'
real: 0m6.998s  user: 0m5.007s  sys: 0m1.569s

はい、そうです。約7秒ほどかかりました!これはとても印象的だと思います。この方法では、xxhsum1回だけ実行するコストしか発生せず、1Mファイルを繰り返すことができます。

この方法の欠点

もちろん、その欠点の1つは、splitあなたが想像できるように、これが最も高価な作業になるということです。これは、X行を含む単一のファイルをインポートして、単一の行を含むXファイルとしてHDDに分割する必要があるためです。

以下はいくつかのデータです。

./hashy.bash

make data
---------
real: 0m17.492s user: 0m12.434s sys: 0m4.788s

split data
----------
real: 2m15.180s user: 0m0.700s  sys: 2m4.443s

hash data
---------
real: 0m6.487s  user: 0m5.798s  sys: 0m0.459s

splitここで作業が約2分かかったことがわかります。メモ:この出力の最初の行は、100万行のJSONを含むファイルをビルドするのにかかる時間を示しています。

もう1つの欠点は、コマンドラインによって処理されるファイルの数です。一部の場所で使用されているため、*1Mまたは5Mファイル名に拡張され、これは危険と見なされる可能性があります。ファイル数を増やすと、コマンドラインパラメータに割り当てられたスペースを超える危険があることに注意してください。

コマンドラインの長さの詳細については、次のリンクを参照してください。

結論として

想像できるように、1Mまたは5Mファイルでこれらの問題を解決するのはほとんど面白そうです。私も同意する必要があります。しかし、CLIツールを正しい方法で活用すると、優れたパフォーマンスが得られることが示されているため、まだ興味深い実験です。

hashy.bashコード

誰もがコードに興味がある場合:

$ cat hashy.bash
#!/bin/bash

echo ""
echo "make data"
echo "---------"
rm -f afile
{ time for i in {0..1000000};do echo "{\"name\": \"John${i}\", \"surname\": \"Gates\", \"country\": \"Germany\", \"age\": \"20\", \"height\": \"180\"}">> afile ;done ;} \
  |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'
echo ""
echo ""

rm -fr datadir && mkdir datadir && cd datadir

echo "split data"
echo "----------"
{ time split -l 1 ../afile ;} |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'
echo ""
echo ""

echo "hash data"
echo "---------"
{ time xxhsum -H0 * > ../nfile 2>&1 ;} |& awk '/real|user|sys/ {print $1": "$2"\t"}' | tr -d '\n'

cd - > /dev/null 2>&1
echo ""
echo ""

引用する

答え4

まず、データをSQLiteデータベースにインポートします。ここでは、Miller(mlr)を使用して提供されたJSONLデータをCSVに変換し、data新しいデータベースのテーブルに読み込みます。

mlr --l2c cat file.json | sqlite3 database.db '.import --csv /dev/stdin data'

完了したら、UPDATEステートメントを使用して、提案されたスキームを使用して識別子を生成できます。

sqlite> .mode box
sqlite> SELECT * FROM data;
┌───────┬─────────┬─────────┬─────┬────────┐
│ name  │ surname │ country │ age │ height │
├───────┼─────────┼─────────┼─────┼────────┤
│ John  │ Gates   │ Germany │ 20  │ 180    │
│ John1 │ Gates   │ Germany │ 20  │ 180    │
│ John2 │ Gates   │ Germany │ 20  │ 180    │
│ John3 │ Gates   │ Germany │ 20  │ 180    │
└───────┴─────────┴─────────┴─────┴────────┘
sqlite> ALTER TABLE data ADD COLUMN id TEXT;
sqlite> UPDATE data SET id = concat(name,surname,country,age,height);
sqlite> SELECT * FROM data;
┌───────┬─────────┬─────────┬─────┬────────┬────────────────────────┐
│ name  │ surname │ country │ age │ height │           id           │
├───────┼─────────┼─────────┼─────┼────────┼────────────────────────┤
│ John  │ Gates   │ Germany │ 20  │ 180    │ JohnGatesGermany20180  │
│ John1 │ Gates   │ Germany │ 20  │ 180    │ John1GatesGermany20180 │
│ John2 │ Gates   │ Germany │ 20  │ 180    │ John2GatesGermany20180 │
│ John3 │ Gates   │ Germany │ 20  │ 180    │ John3GatesGermany20180 │
└───────┴─────────┴─────────┴─────┴────────┴────────────────────────┘

id明らかに、Millerに即座に列を作成するように依頼することができます。以下は、スペースで区切られた各レコードフィールドのMD5ハッシュを使用します。

mlr --l2c put '$id = md5(joinv($*," "))' file | sqlite3 database.db '.import --csv /dev/stdin data'
sqlite> .mode box
sqlite> SELECT * FROM data;
┌───────┬─────────┬─────────┬─────┬────────┬──────────────────────────────────┐
│ name  │ surname │ country │ age │ height │                id                │
├───────┼─────────┼─────────┼─────┼────────┼──────────────────────────────────┤
│ John  │ Gates   │ Germany │ 20  │ 180    │ 150c35e2efb7093e1c30a46a0226f82c │
│ John1 │ Gates   │ Germany │ 20  │ 180    │ c58a8be627dc1d6c9da36dd6de9fa62d │
│ John2 │ Gates   │ Germany │ 20  │ 180    │ e41b62a821f51c13eea2191ebcbb5837 │
│ John3 │ Gates   │ Germany │ 20  │ 180    │ 8e1012a599356fee66727107b750ba1a │
└───────┴─────────┴─────────┴─────┴────────┴──────────────────────────────────┘

最近(2020年)、MacBook Air(M1)でこれをテストするためにMillerを使用してMD5ハッシュを計算し、500万レコードをデータベースにインポートするのに約42秒かかりました。

関連情報