2011/01/09

CouchDB: Viewでのkeyの並び順(Order)の確認レシピ

CouchDBでViewを作成して、startkeyendkeyで条件を指定する時に、優先順位がいまいち分かりずらいので検証するための環境を作ってみました。

あらかじめ準備しておくものは次のものです。

  • CouchDB本体 (今回は1.0.1を準備しました)
  • Ruby (今回はjsonライブラリが用意されているRuby1.9を使います。Ruby 1.8を使用する場合にはjsonライブラリを別途御準備ください。curlでの代用も可能ですが、十分な注意が必要です)
  • テスト文書作成用DB (今回は"example"を使用しますが、任意の名前で結構です)

併わせて参考のために本家のCouchDB Wiki - View_collationを確認すると良いでしょう。

とりあえず結論

結果だけを知りたいという方のために、最初に今回の結果を載せておきます。

とりあえず"id"は無視して、keyの右辺の並びを上から順に眺めてください。

/example/_design/order/_view/orderの表示結果

{"id"=>"ordercheck.12", "key"=>nil, "value"=>nil}
{"id"=>"ordercheck.2", "key"=>false, "value"=>nil}
{"id"=>"ordercheck.4", "key"=>true, "value"=>nil}
{"id"=>"ordercheck.14", "key"=>-1, "value"=>nil}
{"id"=>"ordercheck.5", "key"=>1, "value"=>nil}
{"id"=>"ordercheck.3", "key"=>10, "value"=>nil}
{"id"=>"ordercheck.11", "key"=>"", "value"=>nil}
{"id"=>"ordercheck.0", "key"=>"a", "value"=>nil}
{"id"=>"ordercheck.15", "key"=>"bcd", "value"=>nil}
{"id"=>"ordercheck.10", "key"=>"z", "value"=>nil}
{"id"=>"ordercheck.13", "key"=>"\uFFF0", "value"=>nil}
{"id"=>"ordercheck.6", "key"=>[0, 1], "value"=>nil}
{"id"=>"ordercheck.9", "key"=>[0, 3, 2], "value"=>nil}
{"id"=>"ordercheck.7", "key"=>[1], "value"=>nil}
{"id"=>"ordercheck.8", "key"=>[1, nil, ""], "value"=>nil}
{"id"=>"ordercheck.16", "key"=>[1, 2], "value"=>nil}
{"id"=>"ordercheck.1", "key"=>{}, "value"=>nil}

nilからfalse,trueの順に並んでいく様子がわかります。

nil → 真偽値(false→true) → 数値 → 文字列 → 配列 → ハッシュ

配列の場合は基本的に先頭から要素の有無でまずソートされ、その次に要素の値でソートされています。 要素の数は重要ではない事がわかります。

この配列の扱いは個人的にViewの定義を考える時に混乱するところですが、Viewがちゃんとソートされていればlimit, skipを使って部分的な結果を得て、そのままWebページなりエンドユーザに出力することが出来るので便利なはずです。

作業の流れ

今回はCouchDB内に実際に文書とViewを作成します。その結果を表示する事で、どういった順序でソートされるのかを確認します。

作成する文書

文書の構造は次の通りです。

{
  "_id":"check_order.11",
  "_rev":"1-77356980318a930bb8afc1e6193fa981",
  "k":""
}

"k"に真偽値やら数値やらを代入していきます。

Map関数

"k"をキーにしています。Reduce関数は定義していません。

function(doc) {
  if(doc._id.indexOf('check_order.') == 0) {
    emit(doc.k, null);
  }
}
作成したViewの表示

最終的には最初に載せたような結果が得られ、優先順位は次のようになっている事がわかります。

nil → 真偽値(false→true) → 数値 → 文字列 → 配列 → ハッシュ

今回はこういう結果を出力するスクリプトを準備しておいて、どういう並び順になるか確認するための環境を作ります。

スクリプトの準備

流れに従って、文書作成用のスクリプトを作成する前にCouchモジュールに対するWrapperモジュールを作成しておきます。

ディレクトリ・ファイル構造

今回は"test"ディレクトリをトップディレクトリとして、相対的にlib, initdb, viewsディレクトリを作成していきます。libディレクトリ名は固定で、各スクリプトから"../lib"にパスを通します。

"lib"ディレクトリと同じレベルに存在すれば、"initdb", "views"ディレクトリ名は任意の名前に変更できます。

  • test/lib … ライブラリディレクトリ ("lib"ディレクトリ名は変更不可)
  • test/lib/couchdb.rb … CouchDB Wikiに掲載されているCouchモジュール
  • test/lib/util.rb … Couchモジュールにエラー処理を追加したWrapperモジュール
  • test/initdb … 文書作成用ディレクトリ (ディレクトリ名は変更可)
  • test/initdb/init_docs.rb … 文書を作成するスクリプト
  • test/initdb/show_all_docs.rb … 作成されている文書を全て表示するスクリプト
  • test/initdb/remove_docs.rb … 任意の_idを持つ文書を削除するスクリプト
  • test/views … View作成用ディレクトリ (ディレクトリ名は変更可)
  • test/views/_design.views.order.rb … Viewを作成するスクリプト
  • test/views/show_views.rb … 作成したViewを表示するスクリプト
lib/util.rbの作成

require 'couchdb'で呼び出しているライブラリは、Couch Wikiの「Getting started with Ruby」に掲載されているCouchモジュールです。

先頭にあるDBNameには文書を作成するために使用する、作成済みDB名を'/'から始めて書いてください。

次にYaCouch::getCouchの中を適宜変更して、Couch::Serverクラスのインスタンスをcouchに代入できるようにオプションを適宜変更します。

util.rbファイル全体

# -*- coding: utf-8 -*-

require 'json'
require 'uri'
require 'couchdb'

module YaCouch
  DBname = '/example'
  def YaCouch::getCouch
    ## couch = Couch::Server.new('user'=>'admin', 'password'=>'')
    couch = YaCouch::Main::getCouchAsAdmin
    return YaCouch::Main.new(couch)
  end
  class Main
    require 'json'
    require 'uri'
    def initialize(couch = nil, debug = false)
      @couch = couch
      @debug = debug
    end
    def get(uri)
      json = Hash.new
      begin
        res = @couch.get(URI.escape(uri))
        json = JSON.parse(res.body)
      rescue
        p $! if @debug
      end
      json = Hash.new if json.has_key?("error")
      json
    end
    def put(uri, json)
      res = nil
      begin
        res = @couch.put(URI.escape(uri), json.to_json)
      rescue
        p $! if @debug
      end
      res
    end
    def post(uri, json)
      res = nil
      begin
        res = @couch.post(URI.escape(uri), json.to_json)
      rescue
        p $! if @debug
      end
      res
    end
    def delete(uri)
      res = nil
      begin
        res = @couch.delete(URI.escape(uri))
      rescue
        p $! if @debug
      end
      res
    end
  end
end
initdb/init_docs.rbの作成

文書名(_id)は、「"check_order." + 数字」にしていますが、何でも構いません。

init_docs.rbファイル全体

#!/usr/bin/env ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'

@couch = YaCouch::getCouch
@num = 0
def up(json_value)
  uri = YaCouch::DBname + '/check_order.' + @num.to_s
  json = @couch.get(uri)
  json["k"] = json_value
  res = @couch.put(uri, json)
  @num += 1
end

## prepare documents
up("a")
up(Hash.new)
up(false)
up(10)
up(true)
up(1)
up([0,1])
up([1])
up([1,nil,""])
up([0,3,2])
up("z")
up("")
up(nil)
up("\ufff0")
up(-1)
up([0,4])
up("bcd")
up([1,2])
views/_design.views.order.rbの作成

Viewを作成するポイントは "/example/_design/order" です。

_design.views.order.rbファイル全体

#!/usr/bin/env ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'

@couch = YaCouch::getCouch
uri = YaCouch::DBname + "/_design/order"
json = @couch.get(uri)
json['language'] = 'javascript'
json['views'] = Hash.new
json['views']['order'] = Hash.new
json['views']['order']['map'] = <<-MAP
function(doc) {
  if(doc._id.indexOf('check_order.') == 0) {
    emit(doc.k,null);
  }
}
MAP
res = @couch.put(uri,json)
p res.body
views/show_views.rb

show_views.rbファイル全体

#!/usr/local/bin/ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'
@couch = YaCouch::getCouch

uri = YaCouch::DBname + "/_design/order/_view/order"
json = @couch.get(uri)
json['rows'].each do |row|
  p row
end

このスクリプトの実行結果は、最初に掲載したようなdoc.kをキーとしてソートされた文書のリストになります。

まとめ

タイトルを「〜レシピ」にしたので、その体裁で書こうと思ったものの、挫折しました。

それはさておき、Rubyで使えるライブラリはいろいろありますが、手元の環境ではStunnel4を使い、CouchDBサーバはSSLクライアント認証を有効にしているため、接続部分をカスタマイズする必要があります。

テストのためにApacheのmod_proxyを使ってDigest認証での接続も出きるようにしていますが、いずれにしてもデフォルトの接続処理のセキュリティに満足していないので、低レベルなCouchモジュールに手を入れて使っています。

そんな事をしていないのであれば他のライブラリに慣れるのが良さそうですが、その場合でもこのスクリプトを大きく変更する必要はないと思います。

Appendix. 追加スクリプト

処理の本筋ではない、initdb/show_all_docs.rb と initdb/remove_docs.rb スクリプトを掲載しておきます。

initdb/show_all_docs.rb

show_all_docs.rbファイル全体

#!/usr/bin/env ruby

$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'
@couch = YaCouch::getCouch
uri = YaCouch::DBname + '/_all_docs?include_docs=true'
json = @couch.get(uri)
json['rows'].each do |row|
  p row['doc']
end
initdb/remove_docs.rb

引数無しに実行すると、内部でdelete_doc_prefix変数に設定されている"_id"名が"check_order."で始まる文書が削除されます。

View定義を削除する時には引数に "_design"を指定してください。

remove_docs.rbファイル全体

#!/usr/bin/env ruby
$:.unshift File.join([File.dirname($0), "..", "lib"])
require 'util'

delete_doc_prefix ='check_order.'
delete_doc_prefix = ARGV[0] if ARGV.length == 1

@couch = YaCouch::getCouch
uri = YaCouch::DBname + '/_all_docs?include_docs=true'
json = @couch.get(uri)
json['rows'].each do |row|
  d = row['doc']
  if d['_id'] =~ /^#{delete_doc_prefix}/
    uri = format("%s/%s?rev=%s", YaCouch::DBname, d['_id'], d['_rev'])
    res = @couch.delete(uri)
    p res.body
  end
end

0 件のコメント: