CouchDB のビューについての簡単な紹介
コンセプト
ビューは、CouchDB のドキュメントに対してクエリーやレポート作成を行うときに使われる主要ツールです。ビューには、永続的なビューと一時的なビューの 2 種類があります。
永続的なビューは、デザイン・ドキュメントと呼ばれる特別なドキュメントに格納され、/{dbname}/{docid}/{viewname} という URI に対して HTTP GET リクエストを発行することでアクセスできます。ここで、{docid} にはプレフィクス _view/ が付くので、CouchDB は該当するドキュメントをデザイン・ドキュメントとして認識します。
一時的なビューはデータベース内には格納されず、オンデマンドで実行されます。一時的なビューを実行するには、/{dbname}/_temp_view という URI に対して HTTP POST リクエストを発行します。このとき、リクエストのボディにはビュー関数のコードを含め、Content-Type ヘッダーには application/json を設定します。
注: 一時的なビューの使用が適しているのは開発中だけです。一時的なビューは、呼び出されるたびに計算を行うので非常に高価であり、データベース内のデータが増えるに従って処理速度も遅くなるため、最終的なコードでは一時的なビューの使用を避けるべきです。アドホックなビューでは解決できるのに永続的なビューでは解決できない問題があるときは、設計を考え直した方がよいでしょう。(TODO: 典型的な例とその解決方法の追加)
永続的なビューと一時的なビューのどちらの場合も、ビューは、ビューのキーに値をマップする JavaScript の関数で定義します(ただし、サードパーティ製のビュー・サーバーをプラグインすれば、JavaScript 以外の言語を使うことも可能です)。
デフォルトでは、ビューはドキュメントが保存されるときではなく、ドキュメントがアクセスされたときに作成、更新される点に注意してください。したがって、初めてドキュメントにアクセスするときは、対象となるデータのサイズによって、CouchDB がビューを作成するまで若干の時間がかかる可能性があります。必要なら、更新が行われたときにビューを呼び出す外部スクリプトを使うことで、ドキュメントの保存時にビューも更新されるようにすることができます。例は、「更新時のビューの再作成」にあります。
なお、一つのデザイン・ドキュメントに含まれるビューのいずれかに対してクエリーが実行されると、そのデザイン・ドキュメントに含まれるすべてのビューが更新される点に注意してください。
また、JavaScript API が変更されている点に注意してください。2008 年 5 月 20 日(火)以前 (Subversion リビジョン r658405 以前) では、マップ・インデックスに行を出力する関数は "map" と呼ばれていましたが、現在では "emit" に名前が変わっています。
基本操作
Map 関数
次に示すのは、最もシンプルな map 関数の例です。
function(doc) {
emit(null, doc);
}
この関数は、特別なキーなしに、CouchDB データベースのすべてのドキュメントを含むテーブルを定義します。
ビュー関数は引数を一つだけ取る必要があります。引数の内容はドキュメント・オブジェクトです。結果を引き出すには、暗黙に利用可能になっている emit(key, value) 関数を呼び出す必要があります。この関数が呼び出されるたびに、結果の行がビューに追加されます (key と value の両方が定義されていない場合、行は追加されません)。ドキュメントが追加、編集、または削除されると、計算済みのテーブル内の行は自動的に更新されます。
もう少し複雑な例として、顧客 (customer) ドキュメントから取得した値に対してビューを定義する関数を示しましょう。
function(doc) {
if (doc.Type == "customer") {
emit(null, {LastName: doc.LastName, FirstName: doc.FirstName, Address: doc.Address});
}
}
データベース内のドキュメントのうち、Type フィールドの値が customer の各ドキュメントに対して、ビューに行が一つ作成されます。ビューの value 列には、各ドキュメントの LastName、FirstName、および Address の各フィールドが収められます。この例では、すべてのドキュメントに対するキーは null です。
ドキュメントの何らかのプロパティによって、ビューをフィルタリングしたりソートしたりできるようにするには、目的のプロパティをキーとして使います。たとえば、次のビューでは、顧客ドキュメントを LastName フィールドまたは FirstName フィールドで参照することができます。
function(doc) {
if (doc.Type == "customer") {
emit(doc.LastName, {FirstName: doc.FirstName, Address: doc.Address});
emit(doc.FirstName, {LastName: doc.LastName, Address: doc.Address});
}
}
ビューの結果は、たとえば次のようになります。
{
"total_rows":4,
"offset":0,
"rows":
[
{
"id":"64ACF01B05F53ACFEC48C062A5D01D89",
"key":"Katz",
"value":{"FirstName":"Damien", "Address":"2407 Sawyer drive, Charlotte NC"}
},
{
"id":"64ACF01B05F53ACFEC48C062A5D01D89",
"key":"Damien",
"value":{"LastName":"Katz", "Address":"2407 Sawyer drive, Charlotte NC"}
},
{
"id":"5D01D8964ACF01B05F53ACFEC48C062A",
"key":"Kerr",
"value":{"FirstName":"Wayne", "Address":"123 Fake st., such and such"}
},
{
"id":"5D01D8964ACF01B05F53ACFEC48C062A",
"key":"Wayne",
"value":{"LastName":"Kerr", "Address":"123 Fake st., such and such"}
},
]
}
上の出力例は、読みやすいように整形しています。
emit はキーと値のペアを 1 つの配列に格納し、同一の _design ドキュメント内のすべてのビューが計算されたら、すべての結果を一度にまとめて返す点に注意してください。したがって、何らかの計算をするために 1 つのオブジェクトを使用し、同一のドキュメントに対して複数の emit を実行するような場合には、コピーを作成して、同一のオブジェクトに対して複数回 emit を実行することがないようにしなければなりません。たとえば、次のようにします。
function(doc) {
if (doc.Type == "measurement") {
var timestamp = new Date(doc.timestamp)
emit(eval(uneval(timestamp)), doc.lastTemp);
timestamp.setSeconds(timestamp.getSeconds - 30);
emit(eval(uneval(timestamp)), doc.temp30secsAgo);
timestamp.setSeconds(timestamp.getSeconds - 30);
emit(eval(uneval(timestamp)), doc.temp1minAgo);
}
}
Reduce 関数
ビューに reduce 関数がある場合、そのビューの集約結果を引き出すために reduce 関数が使われます。reduce 関数には中間結果の値の集合が渡され、reduce 関数ではこれをまとめて単一の値を返します。reduce 関数では、対応する map 関数によって引き出された結果に加え、reduce 関数自身が返す結果も、入力として受け取れるようにしなけばなりません。後者のケースは、rereduce と呼ばれます。
次に示すのは、reduce 関数の例です。
function (key, values, rereduce) {
return sum(values);
}
reduce 関数は、順に key、values、rereduce の 3 つの引数を取ります。
reduce 関数は、次の 2 つのケースを処理しなければなりません。
1. rereduce が false の場合。
key は配列で、その要素は [key,id] 形式の配列です。ここで、key は map 関数によって emit されたキーで、id はそのキーが生成されたドキュメントの id です。
values は、keys のそれぞれの要素に対して emit された値の配列になります。
すなわち、次のようになります。reduce([ [key1,id1], [key2,id2], [key3,id3] ], [value1,value2,value3], false)
2. rereduce が true の場合。
key は null になります。
values は、reduce 関数に対するこれまでの呼び出しで返された値の配列になります。
すなわち、次のようになります。reduce(null, [intermediate1,intermediate2,intermediate3], true)
reduce 関数は、最終的なビューの value フィールドと、reduce 関数に渡される values の配列のメンバのどちらにとっても適切なように、単一の値を返す必要があります。
多くの場合、reduce 関数は、上に示した合計を求める関数のように、特別なコードを追加することなく rereduce の呼び出しを処理できるよう記述することができます。その場合には rereduce 引数は無視することができ、JavaScript では、関数の定義からまったく省いてしまっても構いません。
reduce と rereduce
大規模なデータベースでは、reduce の対象となるオブジェクトは、バッチ単位で reduce 関数に送られます。これらのバッチは B ツリーの境界で分割されますが、分割は任意の位置で行われる可能性があります。
たとえば、 次のような key->value ペアを emit するビューがあるとします。
[X, Y, 0] -> Object_A [X, Y, 1] -> Object_B1 [X, Y, 1] -> Object_B1 [X, Y, 1] -> Object_B1 [Z, Q, 0] ....
reduce 関数では、次の内容を受け取る可能性があります。
[Object_A, Object_B1]
そして、続く呼び出しでは次のような内容を受け取る可能性があります。
[Object_B1, Object_B1]
これら 2 つの reduce 関数の出力は、最終結果を得るために rereduce=true として再度 reduce 関数に渡されます。つまり最初の reduce 関数に、4 つの行すべてが渡されると仮定することはできません。
さらに、reduce の最適化が原因で、reduce するべきブロックの一部しか受け取れない可能性もあります。たとえば、次のような 3 つの B ツリー・ノードがあるとします。
[a b c d e f g] [h i j k l m n] [o p q r s t u]
R1 R2 R3
各 B ツリー内のすべてのアイテムの reduce 値は、各ノード内に格納されます。たとえば、[a b c d e f g] は R1 に reduce されます。ここで、あるキー・レンジにまたがって reduce 値を求めたとしましょう。
key range
<----------------------------->
[a b c d e f g] [h i j k l m n] [o p q r s t u]
CouchDB では、定義された reduce 関数を呼び出して [e f g] と [o p q r] の値を計算しますが、間のブロックについては既存の、または計算済みの R2 の値を使用することになります。
したがって、複数の呼び出しにまたがって reduce 関数の中で何らかの状態を保持しようとするのは不適切です。さらに、B ツリー・ノードの境界は任意の位置に現れる可能性があるので、隣接するドキュメントを相互参照するような操作も不適切です。相互参照は reduce 関数の中ではなく、クライアント側で行う必要があります。
アクセスするときのヒント
実際に大量の情報を抽出するをことを意図していないクエリーでは、多くの場合、reduce 関数なしで済ませることができます。この場合、選択したいデータを key の部分に入れ、次に、結果に対して startkey と endkey を使うのが一般的な方法です。
参照ビュー
emit() 関数の 2 番目の引数に NULL を指定することもできます。この場合、CouchDB では指定されたキーをビューに格納するだけです。キーにドキュメント ID を指定すれば、ビューをコンパクトな参照メカニズムとして利用しすることができ、必要なら以後のリクエストで、該当するドキュメントの詳細を取得することができます。
複合キー
キーは必ずしも単一の値である必要はありません。任意の JSON 値を使って、ソート処理に反映させることができます。照合の規則については、「ビューの照合」を参照してください。
キーに配列を指定した場合、ビューの結果をキーのサブセクションによってグループ化することができます。たとえば、キーが [year, month, day] という形式だったとすると、結果を単一の値に reduce したり、年、月、または日によってグループ化したりすることができます。詳細については、「HTTP ビュー API」を参照してください。
実際のビューの使い方
ビューの使い方については、「HTTP ビュー API」を参照してください。「ビューのコード例」にもいくつか例があります。
グループ化
group=false (HTTP 経由ではデフォルト) の場合の基本的な reduce の処理は、単一の値に reduce することです。しかし、startkey と endkey を使うと、任意のキー範囲を対象にサマリー値を取得することができます。
group=true (Futon ではデフォルト) の場合、map 内の一意のキーごとに独立した reduce の値を取得することができます。すなわち、同一のキーを持つすべての値は一緒にまとめてグループ化され、単一の値に reduce されます。
group_level=N のクエリーは実際にはマクロで、レベルに指定された範囲の集合上にある個々の範囲を対象に、通常の (すなわち group=false の) reduce クエリーが 1 つ、自動的に実行されます。
たとえば、group_level=1 でキーが次のようになっているとします。
["a",1,1] ["a",3,4] ["a",3,8] ["b",2,6] ["b",2,6] ["c",1,5] ["c",4,2]
この場合、CouchDB は内部的に 3 つの reduce クエリーを実行します。1 つは、キーの最初の要素が "a" であるすべての行を reduce するクエリーで、同様に "b" と "c" についてもそれぞれ 1 つずつクエリーが実行されます。
group_level=2 を指定してクエリーを実行した場合には、次に示すように一意(最初の 2 つの要素が一意)のキーの集合のそれぞれに対して、reduce クエリーが実行されます。
["a",1], ["a",3], ["b",2"], ["c",1], ["c",4]
group=true はコンセプト上 group_level=exact と等価なので、CouchDB は map 行セット内の一意のキーそれぞれについて reduce を実行します。
注:map と reduce の結果はあらかじめ計算されて B ツリーに格納されます。ただし、中間結果の reduce 値は、クエリーのパラメータには従わずに B ツリーの構造に従って、キャッシュされます。したがって、指定した範囲が、ある与えられた内部ノードの下にあるキーと正確に一致する場合を除いて、reduce クエリーごとに少なくとも 1 つの JavaScript reduce 関数が実行されることになります。group=true を指定したクエリーは、実質的に複数の reduce クエリーを実行することになるので、処理速度が遅いと感じるかもしれません。
これらの点についてさらに詳しく書かれた
ブログ記事があります(日本語訳はこちら)。見出しが「クエリー処理 (Query Processing)」になっている部分を参照してください。
map 関数と reduce 関数に関する制約
map 関数については、これらの関数が参照透明 (referentially transparent) でなければならないという制約があります。すなわち、同一の入力ドキュメントに対しては、map 関数は常に同じキー/値のペアを返します。このような特質のおかげで、CouchDB では、最後に行われたインデックスの更新以後に変更されたドキュメントだけを対象にインデックスの作成をやり直し、ビューをインクリメンタルに更新することができるようになっています。
インクリメンタルな Map/Reduce を可能にするため、reduce 関数は参照透明でなければならないだけでなく、自身の出力に対して reduce を実行して同一の結果が得られるよう、配列値の入力に対して可換的かつ結合的 (commutative and associative) でなければならないという要件があります。これを式で示すと、次のようになります。
f(Key, Values) == f(Key, [ f(Key, Values) ] )
reduce 関数にこのような要件があることによって、CouchDB では中間結果の reduce 値を B ツリーの内部ノードに直接格納することができ、ビュー・インデックスの更新と取得は対数コストを持つことになります。さらに、この要件によって、インデックスが複数のマシンにまたがることが可能になり、クエリー時に対数コストでインデックスを reduce することができます。
詳細については、
このブログ記事を参照してください。
もう一つ付け加えると、reduce と re-reduce の段階で複数のドキュメントがどのように分割されるかについては、制御することはできません。つまり、「隣接する」ドキュメントすべてが一度にまとめて reduce 関数に渡されることを前提にすることはできない、ということです。1 つのサブセットが R1 に reduce され、別のサブセットは R2 に reduce されて、その後最終的な reduce 値を得るために、R1 と R2 が一緒に re-reduce される、ということは大いにありえます。極端な場合、各ドキュメントが個別に reduce され、その結果がまとめて re-reduce されることもあります(この re-reduce の処理は 1 つの段階で行われることもあれば、複数の段階で行われることもあります)。
したがって、reduce/re-reduce 関数は、次の式が成り立つように設計する必要があります。
f(Key, Values) == f(Key, [ f(Key, Value0), f(Key, Value1), f(Key, Value2), ... ] )
reduce された値のサイズ
CouchDB はビュー・インデックスを計算するときに、対応する reduce 値も計算し、この値を B ツリー・ノードのポインターのそれぞれの内部にキャッシュします。CouchDB ではこの仕組みにより、B ツリーを更新する時に、reduce された値を再利用できます。ただし、このような仕組みを上手に利用するためには、reduce 関数から返されるデータの量に注意する必要があります。
原則として、reduce 関数が返すデータは「小ぶりな」ものにとどめ、データが log(num_rows_processed) よりも速く増えることがないようにする必要があります。この要件を守らなくてもエラーは発生しませんが、B ツリーのパフォーマンスは劇的に低下します。小さなデータ・セットが対象の場合にはまったく問題なくビューが動作するのに、データが増えるに従って、コンピュータが停止したように思われるほど処理が遅くなる場合には、上の要件を満たしていないことが原因と考えられます。
対話的に学べる CouchDB チュートリアル
map/reduceをはじめ、ビューの照合、CouchDB で RESTful にクエリーを実行する方法などについて説明した対話的に学べるチュートリアル(JavaScript で記述されたエミュレータ)が
このブログ記事にあります。
実装
以下のブログ記事には、map/reduce の動作の仕組みに加えて、reduce 値がどのように B ツリー・ノードに保持されるかについての記述があります。