NETSUITE / MAP REDUCE
Map/Reduceって、
なんで処理を分けるんだろう。
Map/Reduce Scriptを触り始めた頃は、
処理がいくつも分かれている理由が
いまいち分かっていませんでした。
データを取って、
mapで1件ずつ処理して、
reduceでまとめて、
最後にsummarize。
「mapで終わるならreduceいらなくない?」
「集約って何のため?」
「keyとvalueって何を入れればいい?」
「summarizeって最後に何してるの?」
実際のCSV処理を追って、
値を渡して、
ログを見ながら、
少しずつ処理の意味をつかんでいきました。
WHY
そもそも、
なんでMap/Reduceを使うの?
自分が触っていた処理の一つに、 CSVの複数行を読み込んで、 NetSuite側で処理するものがありました。
CSVには複数の明細があり、 それぞれの行を確認しながら、 最終的には同じ請求書番号のデータを まとめて扱う必要がありました。
1行ずつ独立して終わる処理ではなく、 最後には同じ単位のデータを 一つにまとめて処理する必要がある。
そこで使われていたのが、 Map/Reduce Scriptでした。
自分の中では、 「データを分けて処理しながら、 必要な単位でまとめ直す仕組み」 と考えると分かりやすかったです。
WHAT
まずは、
全体の流れをつかむ。
Map/Reduceの処理は、 大きく見るとこんな流れでした。
getInputData
最初に処理対象となるデータを取得する。
map
取得したデータを1件ずつ処理する。
mapから
key と
value を渡すと、
同じkeyを持つデータがまとめられます。
reduce
同じkeyごとにまとめられたデータを処理する。
summarize
全体の処理が終わったあとに、
エラーや処理結果を見る。
流れだけ見ると、 そこまで複雑ではありません。
でも実際にコードを書き始めると、 各処理の間で 「何をどう渡すのか」が 一番分かりづらく感じました。
GET INPUT DATA
まず、
処理するデータを取る。
最初に、 CSVや検索結果などから 処理対象となるデータを取得します。
自分が扱っていた処理では、 CSVの各行を読み込み、 後続の処理へ渡していました。
invoiceNo,item,amount
INV-001,A,1000
INV-001,B,2000
INV-002,C,1500
INV-001,D,500
この時点では、 まだ請求書単位にはまとまっていません。
まずは、 後続で処理するためのデータを 取り出すところです。
MAP
mapでは、
1行ずつ処理していく。
mapでは、 取り込んだデータを 1件ずつ処理します。
CSVの内容を確認したり、 必要な値を取り出したり、 後続へ渡すデータを作ったりします。
自分が扱っていた処理では、 請求書番号をkeyにして、 各行の情報をvalueとして渡していました。
function map(context) {
var line =
JSON.parse(context.value);
var invoiceNo =
line.invoiceNo;
context.write({
key: invoiceNo,
value: JSON.stringify(line)
});
}
ここで最初に思ったのが、 「1件ずつ処理できるなら、 mapだけで終われるんじゃない?」 ということでした。
実際、 1件ずつ独立して処理が完結するのであれば、 reduceを使わない形もあります。
でも今回の処理では、 同じ請求書番号の行を 最後に一つへまとめる必要がありました。
REDUCE
reduceって、
何のために集約するの?
reduceの意味が分かってきたのは、 実際の請求書処理とつながったときでした。
map側で同じ請求書番号を 同じkeyとして渡しておく。
するとreduce側では、 同じkeyのデータが 一つのまとまりとして渡されます。
map
INV-001 → item A
INV-001 → item B
INV-002 → item C
INV-001 → item D
reduce
INV-001
item A
item B
item D
INV-002
item C
同じ請求書番号の明細をまとめて、 一つの請求書として処理する。
ここでようやく、 「reduceで集約する」という言葉が 実際の処理とつながりました。
FIRST ATTEMPT
mapの中でまとめようとして、
うまくいかなかった。
最初は、 mapの中で同じ請求書番号のデータを 自分でまとめようとしたこともありました。
例えば、 請求書番号をキーにしたオブジェクトへ 明細をためていくような考え方です。
ただ、 mapは1件ずつ処理されます。
mapの中で毎回データを作っていたため、 前の処理でためたつもりのデータが 思ったようには残りませんでした。
そこで、
mapの中で無理にまとめようとするのではなく、
context.write() を使って
keyとvalueを渡す。
そして、 同じkeyをまとめるところは reduceへ任せる。
その方が、 Map/Reduceの仕組みに合っていました。
KEY / VALUE
次でも使いたい値を詰めていったら、
keyとvalueがどんどん深くなった。
mapからreduceへ データを渡せるようになると、 今度は別のところでハマりました。
reduce側でも使いたい値が、 だんだん増えていったからです。
請求書番号だけではなく、 顧客IDや部門、 明細のアイテム、 金額なども後で使いたい。
最初は、 「必要な値をkeyとvalueにまとめて入れておけば、 次でも使える」 と考えていました。
context.write({
key: JSON.stringify({
invoice: {
number: invoiceNo,
customer: {
id: customerId,
department: {
id: departmentId
}
}
}
}),
value: JSON.stringify({
detail: {
item: {
id: itemId
},
amount: {
value: amount,
currency: currency
}
}
})
});
mapを書いているときは、 必要な情報がまとまっているので むしろ便利に見えました。
でも、 reduce側で値を使い始めると、 一気に追いづらくなりました。
function reduce(context) {
var key =
JSON.parse(context.key);
var invoiceNo =
key.invoice.number;
var customerId =
key.invoice.customer.id;
var departmentId =
key.invoice.customer.department.id;
context.values.forEach(function(value) {
var data =
JSON.parse(value);
var itemId =
data.detail.item.id;
var amount =
data.detail.amount.value;
var currency =
data.detail.amount.currency;
});
}
「顧客IDはどこだっけ」 「金額はkeyとvalueのどっちだっけ」 と、 JSONの構造そのものを 思い出しながらコードを書く状態になりました。
値を一つ取りたいだけなのに、 ネストした構造を 毎回たどる必要があります。
さらに面倒だったのが、 keyは単なるデータの受け渡し用ではないことでした。
reduceでは、 同じkeyを持ったデータが 一つのグループとしてまとめられます。
つまり、 keyを複雑にすると、 「何を基準にまとめたいのか」まで 分かりづらくなっていました。
SOLUTION
keyは集約する単位。
それ以外はvalueへ。
そこで、 keyとvalueの役割を できるだけ単純に考えるようにしました。
今回まとめたい単位は、 「請求書番号」です。
なら、 keyには請求書番号だけを持たせる。
顧客IDや部門、 アイテム、金額など、 後続処理で必要な情報は value側へ持たせる。
context.write({
key: invoiceNo,
value: JSON.stringify({
customerId: customerId,
departmentId: departmentId,
itemId: itemId,
amount: amount,
currency: currency
})
});
こうしておけば、 reduce側でも 「何でまとめられているのか」が 一目で分かります。
function reduce(context) {
var invoiceNo =
context.key;
var lines =
context.values.map(function(value) {
return JSON.parse(value);
});
lines.forEach(function(line) {
var customerId =
line.customerId;
var departmentId =
line.departmentId;
var itemId =
line.itemId;
var amount =
line.amount;
// 請求書の明細処理
});
}
keyを見ると、 「このreduceは請求書番号単位で動いている」 と分かる。
valueを見ると、 「この明細で必要なのはこの情報」 と分かる。
JSONも、 必要以上にネストさせず、 できるだけ浅い構造にする。
結局、 次の処理へたくさん値を渡せることより、 後から見ても意味が分かる形で渡すこと の方が大事でした。
BETWEEN PHASES
一番難しかったのは、
mapとreduceの「間」だった。
mapの中だけを見ていると、 何を持っているのか分かります。
reduceの中だけを見ても、 処理自体は読めます。
でも、 mapで作ったデータが reduceではどういう形で見えるのか。
ここが最初はかなり分かりづらかったです。
function reduce(context) {
var invoiceNo =
context.key;
var lines =
context.values.map(function(value) {
return JSON.parse(value);
});
log.debug({
title: 'invoiceNo',
details: invoiceNo
});
log.debug({
title: 'lines',
details: lines
});
// 同じ請求書番号の明細をまとめて処理
}
mapとreduceの間で どう値が渡っているのかが分かると、 処理全体もかなり見やすくなりました。
Map/Reduceでは、 各処理を個別に見るだけではなく、 「前の処理から何を受け取って、 次へ何を渡しているのか」 を見ることが重要でした。
SUMMARIZE
そして最後に、
summarizeって何よ。
mapとreduceを何とか追えるようになったあと、 最後に出てきたのがsummarizeでした。
最初は、 「reduceで処理終わったんじゃないの?」 と思いました。
summarizeは、 Map/Reduce全体の処理が終わったあとに、 エラーや使用量、 処理結果などを確認するための場所です。
function summarize(summary) {
log.debug({
title: 'usage',
details: summary.usage
});
log.debug({
title: 'seconds',
details: summary.seconds
});
log.debug({
title: 'yields',
details: summary.yields
});
summary.mapSummary.errors
.iterator()
.each(function(key, error) {
log.error({
title: 'map error : ' + key,
details: error
});
return true;
});
summary.reduceSummary.errors
.iterator()
.each(function(key, error) {
log.error({
title: 'reduce error : ' + key,
details: error
});
return true;
});
}
最後に、 「全体としてどう終わったのか」 を見る場所だと考えると、 summarizeの役割も分かりやすくなりました。
ただ、 mapやreduceで使っていた変数を そのままsummarizeまで 持ち回れるわけではありません。
mapとreduceの間で key/valueの受け渡しに苦労したあとだったので、 summarizeまで来た頃には 「今度は何が見えるんだ?」 という状態でした。
ここでも結局、 各フェーズはそれぞれの役割を持っていて、 必要な情報はその役割に合わせて扱う、 という考え方が大事でした。
DEBUG
結局、
実際にログを見るのが早かった。
Map/Reduceでは、 頭の中だけでデータの流れを追うのが 結構大変でした。
mapで何が取れているのか。
keyとvalueに何を渡したのか。
reduceでは どんな形で届いているのか。
summarizeまで 処理が進んでいるのか。
各フェーズでログを出して、 実際に中身を見るようにしていました。
log.debug({
title: 'context.key',
details: context.key
});
log.debug({
title: 'context.values',
details: context.values
});
RESTletと同じで、 Map/Reduceでも 「どこまで処理が通っているか」 を見るのが切り分けの基本でした。
コードを読むだけで分からなければ、 実際に動かす。
値が分からなければ、 ログへ出す。
その繰り返しで、 map、reduce、summarizeのつながりも 少しずつ見えるようになりました。
SUMMARY
Map/Reduceで難しかったのは、
処理そのものより「つながり」だった。
最初は、 map、reduce、summarizeが それぞれ何をしているのかで悩みました。
mapで並行して処理するなら、 reduceはいらないんじゃないか。
reduceの「集約」って、 そもそも何のためにあるのか。
keyとvalueへ 次で使いそうな値を詰めていったら、 今度は自分でも追えなくなる。
summarizeまで来たら、 「最後に何が見えるんだ」 とまた分からなくなる。
でも、 実際に扱っていた処理へ当てはめていくと、 少しずつ意味がつながりました。
CSVから処理対象を取る。
mapで1行ずつ処理する。
請求書番号をkeyとして渡す。
reduceで同じ請求書番号の明細をまとめる。
summarizeで 全体の結果やエラーを見る。
各フェーズの役割と、
その間で何を渡しているのかを見る。
そこがつながってから、 Map/Reduceのコードも かなり追いやすくなりました。