株式会社クリアコード > ククログ

ククログ

クリアコードはプログラミングが好きなソフトウェア開発者を2名募集しています。

クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)Rubyに関連した開発を得意としています。

Ohloh profile for kou RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer
最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|
タグ:

日本Ruby会議2010発表資料: るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索

注: 長いです。

日本Ruby会議2010るりまサーチの作り方について発表しました。

るりまサーチの作り方

[rk10][29S06] るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索

2010-08-30
再生: 45
コメント: 1
マイリスト: 2

[rk10][29S06] るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索 (32:59)
Kouhei Sutou (ClearCode Inc. / COZMIXNG)このトークではるりまサーチについてとるりまサーチの作り方について話します。るりまサーチはRubyリファレンスマニュアル刷新計画の成果物であるRubyのリファレンスマニュアルを高速に検索するWebアプリケーションです。るりまサーチはRubyインタプリタとしてRuby 1.9.1(MRI)、全文検索エンジンとデータストアとしてgroonga、Rubyとgroongaのインターフェイスとしてrroongaを使っています。作り方の説明では、特にこれらの技術の使い方について詳しく説明します。るりまサーチ: http://rurema.clear-code.com/

cache!!

ステージから見た感じだと立ち見の人もいたようでした。セッションに参加してくれたみなさん、会場を担当してくれたりレポートしてくれたスタッフのみなさん、ありがとうございました。

時間の関係で省略したことも含めてまとめておきます。

話すこと

話すこと

資料の中では、まず、るりまサーチについて説明し、その後、全文検索システムとしてのるりまサーチをどう作るのかを説明しています。

るりまサーチとは

るりまサーチとは

るりまサーチはRubyリファレンスマニュアル刷新計画 (通称るりま)の成果物であるRuby本体のリファレンスマニュアルを全文検索するためのWebアプリケーションです。るりまサーチが必要とされていた理由は、既存のリファレンスマニュアル閲覧Webアプリケーションに組み込まれていた検索機能の速度が遅かった*1からです。せっかく有益なリファレンスマニュアルがあっても、目的のエントリにたどりつくのが難しければ、有効に活用することができません。検索機能の面からリファレンスマニュアルの有効活用を支援する全文検索システムがるりまサーチです。

ポイント: ドリルダウン

ポイント: ドリルダウン

るりまサーチはRubyのリファレンスマニュアルに特化した小さな全文検索システムですが、最近の全文検索システムにとって重要なエッセンスが含まれています。全文検索システムを開発する場合はこれらのエッセンスを含めることを検討してみてください。

まず1つ目はドリルダウンと呼ばれる機能です。Solrなど他の全文検索システムによってはファセットと呼ぶこともあります。ドリルダウンとは、通常の検索結果に加えて、別のパラメータでの絞り込み結果も同時に提供する機能です。スライド中では「Rubyのバージョンで絞り込んだ結果、何件ヒットするか」という情報も表示しています。

この機能で嬉しいことは以下の2点です。

  • 検索キーワードを入力しなくてもクリックだけで結果を絞り込んでいける。
  • 絞り込み結果が0件になる条件を除外するので、「絞り込んだ後に0件ヒットになる」無駄な条件を指定せずに済む。

どちらもユーザの使い勝手を向上させるインターフェイスにつながります。ショッピングサイトなどでも使われているインターフェイスですね。

ポイント: URL

ポイント: URL

2つ目はURLのパスに絞り込み条件を含めることです。これは、内部ネットワーク用の全文検索システムではなく、インターネット上に公開する全文検索システム向けです。

最近ではURLにUTF-8でエンコードされたページ情報を含めることは一般的になってきました。WikipediaやAmazonでも行っています。Web検索エンジンはURLからも検索用の情報を抽出しているようなので、SEOになると考えられます。

ポイント: キャッシュ

ポイント: キャッシュ

3つ目はキャッシュです。より快適に検索・絞り込みを行うにはできるだけ速いレスポンスが求められます。レスポンスを高速化するためには、以下のような方法があります。

  • アルゴリズムを改良し、少ない計算量で結果を計算できるようにする。
  • 同じ結果を返す処理の処理結果を保存して、2回目以降の処理で結果を再利用する。

手軽に高速化する場合は後者のキャッシュ機能が便利です。キャッシュをする場合はキャッシュを無効化するタイミングを慎重に検討する必要があります。このタイミングを誤ると、期待した結果が返ってこないという問題が発生します。

キャッシュを無効にするタイミングはアプリケーションに依存します。一般的に、データが変更されるまでは同じキャッシュを利用できます。るりまサーチの場合は1日1回バッチ処理で元データを更新しています。そのため、同じキャッシュを1日使いまわすことができます。これにより高速にレスポンスを返すことができます。

また、キャッシュの効果を高めるためには、処理の内部よりもクライアントに近いところでキャッシュする必要があります。その方がより多くの計算を省略することができるからです。るりまサーチはログインせずに使えるシステムなので、同じ検索リクエストの結果はクライアントに関わらず同一になります。そのため、レスポンスをまるごとキャッシュすることができ、とても高い効果があります。

ログインが必要なシステムの場合は、クライアント毎に変更される部分のみJavaScriptで動的に生成したり、iframeを用いて別HTMLにすることにより、ログインによって変更されない部分ではキャッシュを利用することができます。それが難しい場合はもっと処理の内部でキャッシュをすることになります。この場合はキャッシュの効果が薄くなります。

キャッシュを用いることにより劇的にレスポンス速度を改善することができますが、キャッシュの有効期限とキャッシュする場所についてはよく検討する必要があります。

ポイント

ポイント

るりまサーチに含まれている最近の全文検索システムに重要なエッセンスは以下の3つです。

  • ドリルダウン
  • URLに検索条件を含める
  • キャッシュ

それでは、このようなエッセンスを含む全文検索システムるりまサーチの作り方について説明します。

全文検索システム

全文検索システム

全文検索システムは以下の5つの要素からなります。

  • 検索対象
  • クローラー
  • インデクサー
  • 全文検索エンジン
  • 検索インターフェイス

まず、検索対象からクローラーが検索対象とする文書を収集します。次に、それらからインデクサーがテキストやメタ情報を抽出して全文検索エンジンに登録します。全文検索エンジンに登録したデータからユーザが求めるデータを検索して提示するのが検索インターフェイスです。

るりまサーチの場合

るりまサーチの場合

るりまサーチの場合は以下のようになります。

検索対象
リファレンスマニュアル。
クローラー
リファレンスマニュアルはリポジトリからチェックアウトするので必要なし。
インデクサー
BitClustに含まれる機能を使ってリファレンスマニュアルの情報を全文検索エンジンに登録する。新規開発。
全文検索エンジン
groonga
検索インターフェイス
Ruby 1.9とRackを用いたWebインターフェイス。新規開発。

この中で、るりまサーチの重要な部分である全文検索エンジンgroongaについて説明します。

groonga: 特徴

groonga: 特徴

発表当日に初のメジャーバージョン1.0.0がリリースされたgroongaは、MySQLとの組み合わせで広く利用されているSennaの後継プロジェクトです。Sennaでのよいところを維持しつつ、さらに改良が加えられています。

Sennaは妥協しない転置索引実装と参照ロックしない更新アルゴリズムによるリアルタイム検索の実現が大きな特徴でした。Senna自体はデータストア機能を持たず、MySQLなど外部のデータストアと連携します。MySQLとSennaを連携させるソフトウェアはTritonnと呼ばれ、SQLで高速な全文検索機能を利用できることから広く使われています。しかし、MySQL側のロックモデルのため常に検索可能な状態で更新処理を行うことができません。そのため、せっかくのSennaの参照ロックフリーな更新アルゴリズムの特徴を活かしきれませんでした。

そこで、groongaでは独自のデータストア機能を提供し、外部のシステムによる制限を回避してgroongaの性能を発揮できるようにしました。データストアはドリルダウンを高速に実現できるカラム指向を採用しています。

また、HTTP/memcached/独自プロトコルなどのネットワークプロトコルも実装し、Solrのように検索サーバとして利用することもできるようになっています。

その他にも、より大規模な文書に対してもスケールするような性能改善や、モバイル端末の普及により重要性が増している位置情報データに対応するなど新規機能が含まれています。ただし、これらの改善のためにSennaとの互換性がなくなっています。Sennaの後継としてgroongaと名前を変更した理由はこのためです。

定義例: るりまサーチ

定義例: るりまサーチ

それでは、るりまサーチのケースを例にしてgroongaの使い方を説明します。手順は以下の通りです。

  1. スキーマ定義
  2. データ登録
  3. 検索

RDBと同じようにgroognaでも、まず、スキーマを定義します。

スキーマはRDBと同じように以下の3つの要素から構成されます。

  • テーブル
  • カラム

RDBではさらに索引もでてきますが、groongaでは↑の3つの要素を使って索引を作成するので、RDBより特別な存在ではありません。

スキーマを定義するときは、まず、検索対象がなにかを考えます。そして、その対象がどのくらいの粒度で1エントリになるかを考えます。るりまサーチではリファレンスマニュアルが検索対象で、メソッドやクラスそれぞれが1つのエントリになります。検索対象全体をテーブルとし、エントリをテーブルの各レコードにします。るりまサーチでは検索対象全体を扱う「Entries」テーブルを定義しています。

テーブルには検索結果に表示したい内容と検索時に利用する内容をカラムとして定義します。るりまサーチの場合にはメソッド名やクラス名を格納する「name」カラムやドキュメントを格納する「description」カラムなどを定義しています。

検索対象用のテーブルを定義したら索引を定義します。ここがRDBと異なる部分です。全文検索用の索引では単語と文書を対応させる語彙表が必要になりますが、同じトークナイザー*2を利用している場合は同じ語彙表を共有して省スペース化したり、同じテキストに複数のトークナイザーを適用して検索精度や検索漏れのトレードオフを調整したり、といったRDBよりも細かい制御ができます。

単にヒットしたかどうかではなく、検索結果の重み付けも重要です。有用な検索結果を提供するためには、クエリに適していると思われる結果ほど上位に提示する必要があります。しかし、どのように重み付けをするのが適切かは全文検索システムに大きく依存します。そのため、groongaでは索引毎に重み付けをカスタマイズする機能を提供しています。

るりまサーチではメソッド名やクラス名に完全一致した場合はよりマッチしていると判断するように*3、名前と完全一致だけする語彙表「Names」テーブル*4を定義し、そこに「name」カラムの索引を定義します。検索時にはこの索引にマッチした場合は重み付けを大きくします。

ドキュメント部分(「summary」カラムと「description」カラム)はトークナイザーを設定した全文検索用の語彙表「Terms」テーブルを共有しています。こっちの索引にマッチした場合は重み付けを小さくします。

スキーマはgroongaが提供している組み込みのDDLで定義する方法と、groongaのRubyバインディングであるrroongaが提供するDSLで定義する方法があります。

groongaのDDL:

# 検索対象のテーブル
table_create Entries TABLE_HASH_KEY ShortText
# 全文検索用の語彙表。トークナイザーとしてN-gramを使用。
table_create Terms TABLE_PAT_KEY ShortText --default_tokenizer TokenBigram
# 完全一致検索用の語彙表。トークナイザーはなし。
table_create Names TABLE_HASH_KEY ShortText

# 検索対象のデータ格納場所
column_create Entries name COLUMN_SCALAR Names
column_create Entries summary COLUMN_SCALAR Text
column_create Entries description COLUMN_SCALAR Text

# 全文検索用の索引
column_create Terms Entries_summary COLUMN_INDEX Entries summary
column_create Terms Entries_description COLUMN_INDEX Entries description

# 完全一致検索用の索引
column_create Names Entries_name COLUMN_INDEX Entries name

rroongaのDDL:

Groonga::Schema.define do |schema|
  # 完全一致検索用の語彙表。トークナイザーはなし。
  schema.create_table("Names",
                      :type => :hash,
                      :key_type => "ShortText") do |table|
  end

  # 検索対象のテーブル
  schema.create_table("Entries",
                      :type => :hash,
                      :key_type => "ShortText") do |table|
    table.reference("name", "Names")
    table.text("summary")
    table.text("description")
  end

  # 全文検索用の語彙表。トークナイザーとしてN-gramを使用。
  schema.create_table("Terms",
                      :type => :patricia_trie,
                      :key_type => "ShortText",
                      :default_tokenizer => "TokenBigram",
                      :key_normalize => true) do |table|
   # 全文検索用の索引
    table.index("Entries.summary")
    table.index("Entries.description")
  end

  schema.change_table("Names") do |table|
    # 全文検索用の索引
    table.index("Entries.name")
  end
end

登録例: るりまサーチ

登録例: るりまサーチ

スキーマを定義したらデータを登録します。索引は自動で更新されるため、データ用のカラムにデータを登録するだけで動作します。

データの登録方法はgroongaのloadコマンドを使う方法と、rroongaを使う方法があります。

groongaのloadコマンド:

load --table Entries
[
  ["_key", "name", "summary", "description"],
  ["String#sub", "sub", "置換", "1つ置換"],
  ["String#gsub", "gsub", "置換", "全部置換"]
]

rroonga:

entries = Groonga["Entries"]
entries.add("String#sub",
            name: "sub",
            summary: "置換",
            description: "1つ置換")
entries.add("String#gsub",
            name: "gsub",
            summary: "置換",
            description: "全部置換")

Rubyで登録データの前処理を行う場合はrroongaを使う方がよいでしょう。Ruby以外で処理を行う場合はデータからJSONを生成し、groongaのloadコマンドを使う方がよいでしょう。るりまサーチはRubyで前処理*5をしているのでrroongaでデータを登録しています。

検索例: るりまサーチ

検索例: るりまサーチ

全文検索する場合は検索対象のカラムを指定する方法と、明示的に利用する索引を指定する方法の2通りあります。カラム単位で重み付けをしたい場合はカラムを指定し、索引単位で重み付けをしたい場合は索引を指定します。両方の指定方法を混ぜ合わせることもできます。

データの登録方法はgroongaのselectコマンドを使う方法と、rroongaを使う方法があります。

groongaのselectコマンド:

# 「description」カラムに「1つが」含まれているエントリを検索
select Entries description "1つ"
[[...],
 [[[...],
   [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]],
  [..., "String#sub", "sub", "置換", "1つ置換", ...],
  ...]]
# 「sub」が含まれているエントリを検索。ただし、「name」が
# 「sub」だった場合は重みを大きくする。
select Entries "name * 100 | summary | description" "sub"
[[...],
 [[[...],
   [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]],
  [..., "String#sub", "sub", "置換", "1つ置換", ...],
  ...]]

groongaのselectコマンド(HTTP経由):

# 「description」カラムに「1つが」含まれているエントリを検索
% wget -O - 'http://localhost:10041/d/select?table=Entries&match_columns=description&query=1つ'
[[...],
 [[[...],
   [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]],
  [..., "String#sub", "sub", "置換", "1つ置換", ...],
  ...]]
# 「sub」が含まれているエントリを検索。ただし、「name」が
# 「sub」だった場合は重みを大きくする。
% wget -O - 'http://localhost:10041/d/select?table=Entries&match_columns=name*100|summary|description&query=sub'
[[...],
 [[[...],
   [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]],
  [..., "String#sub", "sub", "置換", "1つ置換", ...],
  ...]]

rroonga:

entries = Groonga["Entries"]
# 「description」カラムに「1つ」が含まれているエントリを検索
result = entries.select do |record|
  record.description =~ "1つ"
end
# 「sub」が含まれているエントリを検索。ただし、「name」が
# 「sub」だった場合は重みを大きくする。
result = entries.select do |record|
  target = record.match_target do |match_record|
    (match_record["name"] * 100) |
      (match_record["summary"]) |
      (match_record["description"])
  end
  target =~ "sub"
end

PHPなどRuby以外の言語から利用する場合はgroongaサーバを立てて、HTTP経由で検索するのがよいでしょう。Rubyから利用する場合は、selectコマンドで十分ならselectコマンドを利用、より複雑なことをしたい場合はrroongaを利用するのがよいでしょう。selectコマンドでもドリルダウンはサポートされて入るので、多くの場合はselectコマンドで十分でしょう。

るりまサーチでは、selectコマンドが提供するクエリ書式を利用したくない、rroongaが提供するページネーション機能を利用したい、などの理由でselectコマンドではなくrroongaを使っています。rroongaを利用してドリルダウンを実現する例にもなっています。

るりまサーチを例にして、groongaを用いて全文検索システムを開発する場合の基本的な流れを説明しました。より詳しいことはGitHubるりまサーチのリポジトリにあるソースコードを見てください。

racknga

racknga

るりまサーチの検索WebインターフェイスはRuby 1.9とRackの上に構築されています*6。るりまサーチを開発した際に、るりまサーチ以外でも使えそうな部分がでてきたので、rackngaという名前でるりまサーチと別パッケージとして公開しています。

rackngaにはRackのミドルウェアとMuninプラグインが含まれています。MuninのプラグインはPassengerの以下の情報を収集します。

  • 処理したリクエスト数
  • 処理中のリクエスト数
  • プロセスの状態
  • プロセスの起動時間

Rackのミドルウェアは1つずつ説明します。

エラー通知

エラー通知

アプリケーション内でエラーが発生した場合にメールでその内容を通知するミドルウェアです。RailsのException NotifierのRack用です。

以下のように利用します。

config.ru:

require 'racknga'

notifier_options = {
  "host" => 127.0.0.1,
  "from" => "rurema@example.com",
  "to" => "developer@example.com",
  "charset" => "iso-2022-jp",
  "subject_label" => "[るりまサーチ] ",
}
notifiers = [Racknga::ExceptionMailNotifier.new(notifier_options)]
use Racknga::Middleware::ExceptionNotifier, :notifiers => notifiers
# ...
run your_application

できるだけ多くのエラーを検出するためになるべく最初の方でuseしてください。

キャッシュ

キャッシュ

主にサーバ1台や2台などで処理できる程度の中規模のPassenger環境で利用することを想定したキャッシュミドルウェアです。ヘッダーやボディを含めHTTPのレスポンス全体をgroongaのデータストアにキャッシュします。Passengerでは複数のインスタンスが別プロセスで起動しますが、groongaは複数プロセス間で同一のデータベースを操作することができるため、別のインスタンスがキャッシュした内容を他のインスタンスから参照することができます。以下のように利用します。

config.ru:

require 'racknga'
require 'racknga/middleware/cache'

# ...
# use Rack::Deflater
# use Rack::ConditionalGet
# ...
base_dir = Pathname.new(__FILE__).dirname.cleanpath.realpath
cache_database_path = base_dir + "var" + "cache" + "db"
use Racknga::Middleware::Cache, :database_path => cache_database_path.to_s
run your_application

他のミドルウェアと組み合わせやすいように、なるべくアプリケーションに近い部分に置くことをよいでしょう。

複数のサーバ間でキャッシュを共有したい場合は別の仕組みを利用することをオススメします。

条件付き圧縮

条件付き圧縮

ネットワーク帯域を節約するためには、レスポンスを圧縮して返すことが有効です。しかし、Internet Explorer 6では問題があることがわかっています。そのため、Internet Explorer 6の場合は常に圧縮しないようにするのがこのミドルウェアです。Rack::Deflaterのラッパーです。以下のように利用します。

config.ru:

require 'racknga'

# ...
use Racknga::Middleware::Deflater
# use Rack::ConditionalGet
# ...
run your_application

JSONP

JSONP

Web APIとしてサービスを提供する場合、JSON形式で結果を返すことが多くなっています。クライアント側でWeb APIにアクセスする場合はJSONPを利用することになります。

このミドルウェアはJSONPに対応しておらず単にJSONデータを返すだけのアプリケーションをJSONPに対応させることができます。また、以下のような配置にすることにより、キャッシュを有効にしたままJSONP対応にすることができます。

config.ru:

require 'racknga'
require 'racknga/middleware/cache'

use Rack::Middleware::JSONP

base_dir = Pathname.new(__FILE__).dirname.cleanpath.realpath
cache_database_path = base_dir + "var" + "cache" + "db"
use Racknga::Middleware::Cache, :database_path => cache_database_path.to_s

run your_application # "Content-Type: application/json"のレスポンスを返す

現在、るりまサーチはWebサービスを提供していませんが、将来の拡張を念頭においてこのミドルウェアがrackngaに含まれています。

まとめ

まとめ

るりまサーチはドリルダウンやキャッシュを利用することにより、快適に目的のドキュメントへ到達できるような工夫をしています。るりまサーチ以外にもリファレンスマニュアルを利用するツールがあるので有効活用しましょう。

ドリルダウンを効果的に利用した高速な全文検索システムにはgroongaが適しています。Rubyとの親和性も高いgroongaで全文検索システムを開発してみてはいかがでしょうか。汎用ユーティリティであるrackngaも一緒に用いることにより開発・運用が改善されるでしょう。

最後にお知らせです。クリアコードではプログラミングが好きな開発者を募集しています。プログラミングが好きな人は検討してみてください。

お知らせ

*1  数十秒以上かかる。

*2  文章から単語を抜き出す処理。

*3  メソッド名で検索することは多いですよね?

*4  実体はトークナイザーなしのハッシュテーブル。

*5  BitClustを使ってメソッド単位にドキュメントを分割するなど。

*6  RailsやSinatraなどは使っていません。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-09-01

プログラミングRuby 1.9

前作プログラミングRubyのRuby 1.9対応版です。

前作:

プログラミングRuby 第2版 ライブラリ編
Dave Thomas/Chad Fowler/Andy Hunt/田和 勝/まつもと ゆきひろ
オーム社
¥ 4,410

このシリーズはページ数からもわかる通り、Rubyについて細かいところまで網羅しているのが特徴です。例えば、Proc.newlambdaの違いはわかりますか?以下のようなクラス定義の書き方による参照解決の違いはわかりますか?

module MyLibrary
  class MyClass
    # ...
  end
end

class MyLibrary::MyClass
  # ...
end

これまでのシリーズと同様、本書でもそのあたりの細かいところにも触れています。もちろん、1.8と1.9で変わった部分についても触れています。このシリーズでRubyを覚えたという人もわりといるようですが、それはこの網羅性が役に立ったのではないでしょうか。

おそらく、1回読んだだけではすべてを覚えることは無理でしょう。読んで、実際にRubyのコードを書いて、気になったところをまた調べ直す、そのサイクルができるのがこのシリーズです。このシリーズでRubyを覚えた人はこのようなサイクルを使ったのではないでしょうか。先日1.9.2がリリースされ、これから1.8から1.9への移行がより進むと考えられます。そのときに、つまずいたところを本書で調べて理解していくというサイクルに使えるでしょう。

Rubyは(わりと)読みやすいコードを(わりと)書きやすい言語仕様になっています*1が、複雑なこともいろいろできる仕様も多く含まれています。ほとんどの場合は複雑なことはしなくても済むはずですし、そのように書いておく方がよい場合の方が圧倒的に多いです。一度、(わからない部分があったとしても)全体を一通り読んでおいて、Rubyの動作はどうなっているかをざっくりと知っておくとよいでしょう。複雑なことをしそうになったときに、別の方法もあった気がする、と気付けるようになるくらいで十分です。気付けたら本書なりるりまなりで詳しく調べることができます。

本書は分量の多さから言語編とライブラリ編の2編構成になっています。言語編に比べてライブラリ編の内容が手薄になっているので気をつけてください。必ずしもそれぞれのライブラリの最新の情報に追従できているわけではありません。本書をきっかけにしてるりまやWeb上での情報などで補正する必要があるでしょう。

試しにRubyを勉強してみたい、という人には敷居が高いでしょう*2。しかし、Rubyを使いこなそうというくらいの気持ちがあるのであれば、助けになってくれることでしょう。

プログラミングRuby 1.9 −ライブラリ編−
Dave Thomas with Chad Fowler and Andy Hunt/まつもとゆきひろ/田和 勝
オーム社
¥ 4,620

*1  読みやすいコードを書こうという気がなければ読みやすいコードにはならないでしょう。

*2  まず、分量が多いですし。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-08-23

名札には名前を大きく書きましょうジェネレータ改: cairoとPangoでPDF生成

早いもので来週末は日本Ruby会議2010です。日本Ruby会議では、たくさんいる(会ったことはないけど名前を知っている)参加者がお互いを認識しやすいように大きな名札をつけることが恒例となっています。

RubyKaigi日記でも名札には名前を大きく書きましょうと呼びかけています。この中で、「あらかじめ太くて大きなフォントで、黒々と印刷してきたものを持参して、名札に貼り付けるのはいかがでしょう。」と提案しています。しかし、自分でデザインするのはわりと面倒なものです。

そこで、kdmsnrさんが名札には名前を大きく書きましょうジェネレータを作りました。これはtwitter IDを指定するだけでRubyKaigi日記で提案されているようなデザインの画像を生成してくれます。

@kdmsnr

でも、印刷するならPDFの方が嬉しいよね、ということでPDFを出力できるように改造したのが名札には名前を大きく書きましょうジェネレータ改です。

@kdmsnr改

PDFも出力できるようにした他に、フォントを選べたり、細かく調整するためにSVGも出力できるようにしています。それでは、どのように実現しているかを説明します。

使っているもの

描画にはcairo、文字の配置にはPango、画像の読み込みにはGdkPixbufを用いています。どれもLinux、*BSD、Mac OS X、Windowsなど多くの環境で動作するライブラリです。ここでは、cairoとPangoだけ説明します。

cairo

cairoは2次元グラフィックを生成するためのライブラリで以下のような特長があります。

  • ベクトルベースのAPI
  • 描画処理のコードを変更せずに出力先を変えることができる

ベクトルベースのAPIとなっているということは品質を落とさずに拡大・縮小ができるということです。ジェネレータ改では、実際のサイズの画像とサムネイル画像を生成しますが、このようなことが以下のように描画処理を変更せずに実現できます。

def render(context)
  # 実際のサイズの描画
end

# 実際のサイズを描画するとき
render(context)

# 1/3サイズのサムネイルを描画するとき
context.scale(1 / 0.3, 1 / 0.3) # 描画処理の前にこれを呼ぶだけでOK
render(context)

描画処理のコードを変更せずに出力先を変えることができると、描画結果はPNGにしてブラウザで確認、印刷するときはPDF、編集する時はSVG、というように用途にあわせたグラフィックのフォーマットを提供することが簡単にできるということです。これは今回のようなWeb上で印刷物を生成する場合はとても便利な機能です。いちいちPDFで確認するのは面倒ですよね。サムネイルで一覧表示する場合もPNG+ブラウザの方が便利です。

cairoの詳しい使い方はRubyist Magazine - cairo: 2 次元画像描画ライブラリを見てください。

Pango

Pangoは多言語に対応したテキストの配置を行うライブラリです。フォントの扱いなどテキストの配置に関することを抽象化してくれるので、TTFやOTFなどフォントフォーマットの違いや、フォントファイルをどこに置くかなどをプログラム側で気にする必要がありません。

ジェネレータ改ではインストールされているフォントを列挙したり、できるだけ大きいテキストサイズを自動検出するためにPangoを利用しています。テキストを中央揃えにするのもPangoの機能を利用しています。

システムにインストールされているフォントの一覧は以下のように取得できます。

font_families = Pango::CairoFontMap.default.families.collect do |family|
  family.name
end
p font_families # => ["Mona", "梅明朝S3", "衡山毛筆フォント草書", ...]

残念ながらPangoを利用するためのまとまった日本語の資料はありません。興味のある人はソースコードを見てください。

まとめ

名札には名前を大きく書きましょうジェネレータ改をネタにしてcairoとPangoを紹介してみました。cairoとPangoはFirefoxやGTK+などデスクトップで使われることが多いライブラリですが、Webアプリケーションのようにサーバサイドでも有用なライブラリです。ジェネレータ改のように用途にあわせて画像のフォーマットを使い分けたい場合は、cairoとPangoを使ってみてはいかがでしょうか。また、日本Ruby会議2010に参加する人はジェネレータで作った名札を印刷して持っていってはいかがでしょうか。

名札には名前を大きく書きましょうジェネレータ改のソースコードはGitHubにあります。

Tags: Ruby | このエントリの Delicious history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-08-18

日本Ruby会議2010で発表します: るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索

毎年開催規模が大きくなっている日本Ruby会議今年も参加します。今年も去年と同じくスポンサーと発表者として参加します。

発表タイトルは「るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索」です。

内容

発表内容はるりまサーチという、Rubyリファレンスマニュアル刷新計画 (通称るりま)の成果物であるRubyのドキュメントを全文検索するWebアプリケーションの関連技術を紹介するというものです。もう少し具体的にいうと以下のような話題になります。

  • 全文検索エンジンgroongaを用いた検索サイトの作り方
    • 「情報を絞り込む」を主体としたユーザインターフェイス
    • 高速に検索するためのデータの持ち方
    • groongaの性能を落とさずによさを活かすには、どのようにRubyと連携すればよいか
  • Ruby 1.9 + Rackで効率よくWebアプリケーションを作る方法
    • 運用時に発生した問題への対応
    • リソースを追加投入する前にやっておくべきスループット改善方法(中規模向け)
    • Webサービス用APIの提供

Twitterなどを見てもわかる通り、世界には情報がどんどん増えていきます。そうすると、その中から必要な情報を選ぶことが重要になっていきます。しかし、情報が溢れた世界では人力のみで効率よく情報を選択することは困難です。最近Twitterが提供をはじめた「おすすめユーザー」という機能も、溢れかえった情報の中から必要な情報を見つけることを支援するための機能と言えます。

必要としている人が必要な情報を見つけやすくしたい、そんなアプリケーションを作りたいと考えている人に聞いて欲しい内容です。もしかしたら、groongaが提供する必要な情報を見つけるための機能でそれを実現できるかもしれません。発表日時は最終日8/29(日)の13:30-14:00で、場所は中ホールです。同じ時間帯に、別の場所ではかずひこさんの外国で暮らすRubyistだけど何か質問ある?TermtterKaigiMSWin32版Ruby野良ビルダー養成塾などありますが、こちらは30分なので、こちらの発表の時間だけ抜け出すことも考えてみてください。

まとめ

日本Ruby会議2010で発表する予定の内容を紹介しました。面白そうだと思った方はぜひ参加してみてください。チケットはまだ少し残っているようです。また、チケットを譲りたいという方もいるので、まだチケットを持っていない方は連絡をとってみるのもよいかもしれません。

それでは、日本Ruby会議2010でお会いできることを楽しみにしています。

お知らせ

採用を再開しました。ソフトウェア開発者を2名募集しています。応募条件はプログラミングが好きなことだけです。学歴や年齢などは関係ありません。勤務地は東京都文京区または栃木県小山市になる予定です。詳しくは採用情報を見てください。日本Ruby会議2010の会場にはクリアコードの人が3人はいるはずなので、そのときに声をかけてもらえればその場でも説明します。

Tags: Ruby | このエントリの Delicious history 1 user | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-08-11

Rails 3.0 beta4でDeviseを使ってOpenID認証

とあるRails 3を使っているたいやき用のCMSでDeviseを使ってOpenID認証をするようにしたので、そのやり方を紹介します。RubyはRuby 1.9.2 RC2も出ていますが、今回はRuby 1.9.1を使います。

Deviseとは

DeviseRackベースの認証システムです。バックエンドにWardenを利用しているため、Basic認証やOpenID、OAuthなど認証方法を切り替えることができます。

ただ、以下の説明を読んでみてもらってもわかる通り、動き出すまでにそこそこの作業が必要になります。機能は豊富なので、動き出したらカスタマイズしてアプリケーションの要求に合わせていくことができるでしょう。日本語での情報もあまりありませんが、探せばいくつかはあるので、試してみてはいかがでしょうか。

とはいえ、今回はDeviseのデフォルトの認証方法ではなく、OpenIDでのみ認証することにします。また、未登録のユーザがログインしようとしたときは自動的に新規ユーザを作成することにします。このようにも使えるという例ということで読むとよいかもしれません。

インストール

まず、Rails 3.0 beta4をインストールします。

% sudo gem1.9.1 install rails --pre

サンプル用のアプリケーションを作ります。

% ruby1.9.1 rails new taiyaki
% cd taiyaki

次にDeviseをインストールします。

% sudo gem1.9.1 install devise --version=1.1.rc2

インストールしたDeviseを利用するため、以下のようにGemfileに追記します。

Gemfile:

gem 'devise', "1.1.rc2"

アプリケーションにDeviseが動作するために必要なファイルをインストールします。config/initializers/devise.rbなど主に設定ファイルです。

% ruby1.9.1 script/rails generate devise:install

いくつかは手動で設定する必要があります。それぞれ以下の通りです。

Deviseはパスワードの再設定をする機能もあり、そのときはユーザにメールを送信します。そのような機能を使うときはActionMailerのURL生成オプションを設定する必要があります。例えば、開発時のホスト情報を設定する場合は以下のようになります。

config/environments/development.rb:

config.action_mailer.default_url_options = {:host => 'localhost:3000'}

Deviseはリダイレクト先のURLを生成するときなどにデフォルトではroot_pathを使うので、rootパスへのマッピングを追加します。以下の例ではwelcome#indexを指定しているので、後でWelcomeControllerを作ります。

config/routes.rb:

root :to => "welcome#index"

Deviseはnoticeとalertのflashを設定するので、レイアウトに追加しておくとよいでしょう。例えば、以下のようにyieldの前に追加します。

app/views/layouts/application.html.erb:

<%# ... %>
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

<%= yield %>
<%# ... %>

作成

これでインストールは完了したので、コントローラーやモデルを作成します。

まず、ユーザー用のモデルを作成します。

% ruby1.9.1 script/rails generate devise User

このとき生成されるスキーマは、以下のようにデータベース上にパスワードのダイジェストなどの情報を持ち、それを利用して認証することになります。

db/migrate/XXXX_devise_create_users.rb:

class DeviseCreateUsers < ActiveRecord::Migration
  def self.up
    create_table(:users) do |t|
      t.database_authenticatable :null => false
      t.recoverable
      t.rememberable
      t.trackable

      # t.confirmable
      # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both

      # t.token_authenticatable

      t.timestamps
    end

    add_index :users, :email,                :unique => true
    add_index :users, :reset_password_token, :unique => true
    # add_index :users, :confirmation_token,   :unique => true
    # add_index :users, :unlock_token,         :unique => true
  end

  def self.down
    drop_table :users
  end
end

しかし、今回は自分では認証情報を持たずにOpenIDで認証するので、データベース上に認証情報を持たないようにします。代わりにOpenID用のカラムを追加します。

db/migrate/XXXX_devise_create_users.rb(変更後):

class DeviseCreateUsers < ActiveRecord::Migration
  def self.up
    create_table(:users) do |t|
      t.string :email
      t.string :nickname
      t.string :identity_url
      t.string :fullname
      t.string :birth_date
      t.integer :gender
      t.string :postcode
      t.string :country
      t.string :language
      t.string :timezone

      t.rememberable
      t.trackable

      t.confirmable
      t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both
      t.token_authenticatable

      t.timestamps
    end

    add_index :users, :identity_url,         :unique => true
    add_index :users, :email,                :unique => true
    add_index :users, :confirmation_token,   :unique => true
    add_index :users, :unlock_token,         :unique => true
  end

  def self.down
    drop_table :users
  end
end

変更したらスキーマを反映させます。

% rake1.9.1 db:migrate

モデルのコードにもデータベースで認証するためのコードが入っています。今回は必要のないユーザ登録用の:registerableオプションやパスワードの入力チェックなどをする:validatableオプションなどは外します。password_required?メソッドをオーバーライドしているのは、OpenIDで認証するためパスワードが必要がないからです。

app/models/user.rb:

class User < ActiveRecord::Base
  ...
  # devise :database_authenticatable, :registerable,
  #        :recoverable, :rememberable, :trackable, :validatable
  devise :database_authenticatable, :rememberable, :trackable
  ...

  def password_required?
    false
  end
end

Deviseではログイン画面などデフォルトのビューも提供してくれますが、今回はOpenIDを使った認証にするためビューをカスタマイズします。ビューをカスタマイズする場合は、コントローラーごとカスタマイズする方法と、ビューだけカスタマイズする方法がありますが、今回はコントローラーごとカスタマイズする方法にします。

コントローラーをカスタマイズするにはconfig/routes.rbに追加されたdevise_for:controllersオプションを指定します。以下のように指定するとUsers::SessionsControllerコントローラーを使います。

config/routes.rb:

devise_for(:users,
           :controllers => {:sessions => "users/sessions"})

コントローラーを作成します。

% ruby1.9.1 script/rails generate controller Users::Sessions

コントローラーをカスタマイズする場合は、ApplicationControllerではなくDevise::SessionsControllerを継承します。

app/controllers/users/sessions_controller.rb:

class Users::SessionsController < Devise::SessionsController
end

ログインフォームではOpenID用の識別子を入力してもらうようにします。

app/views/users/sessions/new.html.erb:

<%= form_for(resource,
             :as => resource_name,
             :url => session_path(resource_name)) do |f| %>
  <p>
    <label for="openid_identifier" >OpenID URL:</label>
    <%= text_field_tag :openid_identifier %>
  </p>
  <p><%= f.label :remember_me %> <%= f.check_box :remember_me %></p>
  <p><%= f.submit "Login" %></p>
<% end %>

あとは、トップページを準備すれば画面を確認することができます。

トップページ用のコントローラーを生成します。

% ruby1.9.1 script/rails generate controller welcome index
% rm public/index.html

トップページではログインページに移動できるようにします。ログイン時はログイン中のユーザ情報を表示します。

app/views/welcome/index.html.erb:

<h1>Welcome#index</h1>

<% if user_signed_in? %>
  <p>ようこそ<%= current_user.nickname %>さん</p>
  <%= link_to("ログアウト", destroy_user_session_path) %>
<% else %>
  <%= link_to("ログイン", new_user_session_path) %>
<% end %>

サーバを起動します。

% ruby1.9.1 script/rails server

http://localhost:3000/にアクセスすると以下のような画面になります。

トップページ

ログインページに行くと以下のようなフォームになります。

ログインフォーム

OpenID対応

それではOpenIDに対応します。DeviseからOpenIDを使うために、warden-openidを使います。

% sudo gem1.9.1 install warden-openid

Gemfileにも追記します。

Gemfile:

gem 'warden-openid'

WargenでOpenIDを使うようにします。

config/initializers/devise.rb:

Devise.setup do |config|
  ...
  config.warden do |manager|
    manager.default_strategies(:openid, :scope => :user)
  end
end

OpenIDの設定をします。warden-openidではOpenIDの認証が成功した時にコールバックが実行され、そこで認証情報に対応したアプリケーション用のユーザを返すことになります。今回は、ここで、ユーザが存在しない場合は自動的に新規ユーザを作成することにします。

config/initializers/openid.rb:

Rails.application.config.middleware.insert(Warden::Manager, Rack::OpenID)

Warden::OpenID.configure do |config|
  config.required_fields = User.required_open_id_fields
  config.optional_fields = User.optional_open_id_fields
  config.user_finder do |response|
    user = User.find_by_identity_url(response.identity_url)
    if user.nil?
      user = User.new
      user.extract_open_id_values(response)
      unless user.save
        message = "failed to create user: "
        message << "#{users.errors.full_messages.inspect}: "
        message << user.inspect
        Rails.logger.error(message)
        user = nil
      end
    end
    user
  end
end

OpenIDの情報とアプリケーションのユーザ情報をマッピングする処理はモデルで行います。

app/models/user.rb:

class User < ActiveRecord::Base
  REQUIRED_FIELDS = {
    :nickname => "nickname",
  }

  OPTIONAL_FIELDS = {
    :email => "email",
    :fullname => "fullname",
    :birth_date => "dob",
    :gender => "gender",
    :postcode => "postcode",
    :country => "country",
    :language => "language",
    :timezone => "timezone"
  }

  class << self
    def required_open_id_fields
      REQUIRED_FIELDS.values
    end

    def optional_open_id_fields
      OPTIONAL_FIELDS.values
    end
  end

  def password_required?
    false
  end

  def extract_open_id_values(response)
    profile_data = {}
    [OpenID::SReg::Response, OpenID::AX::FetchResponse].each do |response_class|
      data_response = response_class.from_success_response(response)
      profile_data.merge!(data_response.data) if data_response
    end
    [REQUIRED_FIELDS, OPTIONAL_FIELDS].each do |fields|
      fields.each do |model_key, profile_key|
        unless profile_data[profile_key].blank?
          self.send("#{model_key}=", profile_data[profile_key])
        end
      end
    end
    self.identity_url = response.identity_url
    self.nickname ||= identity_url
  end
end

一応、ニックネーム情報は欲しいとリクエストしますが、もらえなくてもなんとなく動くようになっています。この状態でログインページにOpenID識別子を入力して、認証に成功するとアプリケーションにログインする事ができます。

ログイン成功

ただし、現在リリースされているruby-openidはRuby 1.9のEncodingに対応していないため、認証中にASCII以外の文字列を含むページにアクセスすることになると失敗します。これを修正する方法はRuby 1.9.1 supportで報告済みですが、まだ取り込まれていません。

まとめ

Rails 3.0 beta4でDeviseを使ってOpenID認証する方法を紹介しました。Rails 3で認証まわりはどうしようか、と考えていている人は試してみるとよいかもしれません。ただ、betaやrcのものを使っているので、これから使い方は変わっていく可能性が高いと考えられます。注意してください。

そういえば、トップページにある会社紹介資料PDFを更新しました。エンジニア紹介ページなどが更新されています。

Tags: Ruby | このエントリの Delicious history 10 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-07-13

ActiveLdap 1.2.2 - Rails 2.3.8対応

RubyらしいAPIでLDAPのエントリを操作できるライブラリActiveLdapの新しいバージョンがリリースされました。以下のようにgemでアップデートできます。

% sudo gem install activeldap

ActiveLdapについてはこのあたりを見てください。

今回のリリースではRuby on Railsの最新安定版2.3.8に対応しました。ActiveLdapは国際化対応のためにRuby-GetText-Packageを使っています。そのため、ActiveLdapをRailsで使う場合にlocale_railsと一緒に使っている場合も多いでしょう。しかし、locale_railsの最新版はRails 2.3.8に対応していないので、locale_railsを利用している場合はアップデートするかどうかよく検討してください。(locale_railsのリポジトリ上では2.3.8に対応しているので、locale_railsのリリース版ではなくて未リリースのものを利用するのも対応策の1つです。)

LDAPといえば、日本Ruby会議2010では「Rubyで扱うLDAPのススメ」という企画があります。[ANN]RubyKaigi2010 企画 "Ruby で扱う LDAP のススメ" にご協力頂ける方を募集しています - tashenの日記ということなので、ぜひ、ご協力をお願いします。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-07-05

最後の行から順番に読み込む小さなRubyのクラス

Muninのプラグインを作るときなど、大きなサイズのログファイルを解析する必要がたまにありますよね。そんなとき、ファイルの先頭から処理をしていくとファイルサイズが増加するにしたがって処理時間も増えていってしまいます。Muninのプラグインの場合は最近5分間のデータだけあれば十分なので、ファイルの先頭からではなく、最後から処理する方が効率的です。最後から処理すると、ファイルサイズが大きくなっても処理時間にはほとんど影響がありません。

ということで、ファイルの最後から1行ずつ読み込む小さなRubyのクラスを作りました。groongaのリポジトリに入っているので、groongaと同じライセンスで利用できます。

class ReverseLineReader
  def initialize(io)
    @io = io
    @io.seek(0, IO::SEEK_END)
    @buffer = ""
    @data = ""
  end

  def each
    separator = $/
    separator_length = separator.length
    while read_to_buffer
      loop do
        index = @buffer.rindex(separator, @buffer.length - 1 - separator_length)
        break if index.nil? or index.zero?
        last_line = @buffer.slice!((index + separator_length)..-1)
        yield(last_line)
      end
    end
    yield(@buffer) unless @buffer.empty?
  end

  private
  BYTES_PER_READ = 4096
  def read
    position = @io.pos
    if position < BYTES_PER_READ
      bytes_per_read = position
    else
      bytes_per_read = BYTES_PER_READ
    end

    if bytes_per_read.zero?
      @data.replace("")
    else
      @io.seek(-bytes_per_read, IO::SEEK_CUR)
      @io.read(bytes_per_read, @data)
      @io.seek(-bytes_per_read, IO::SEEK_CUR)
    end

    @data
  end

  def read_to_buffer
    data = read
    if data.empty?
      false
    else
      @buffer.insert(0, data)
      true
    end
  end
end

以下のように使います。

File.open("/var/log/groonga/query.log", "r") do |file|
  ReverseLineReader.new(file).each do |line|
    break if no_more_need?(line)
    # ...
  end
end

ログファイルから直近のログだけを取り出して処理したいときなどに利用してみてはいかがでしょうか。

Tags: Ruby | このエントリの Delicious history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-06-23

Passenger用Muninプラグイン

みなさんはPassengerの管理下にあるRails/Rackプロセスをどのように監視しているでしょうか。

Muninを使った方法もあり、Dan Manges's Blog - Rails Application Visualizationgist: 20319 - munin plugin for passenger- GitHub)で公開されていたりします。このプラグインはpassenger-statusの結果をグラフ化しています。passenger-statusの結果はこんな感じになります。

----------- General information -----------
max      = 6
count    = 3
active   = 0
inactive = 3
Waiting on global queue: 0

----------- Domains -----------
/home/rurema/rurema-search: 
  PID: 17128   Sessions: 0    Processed: 38      Uptime: 4m 54s
  PID: 30722   Sessions: 0    Processed: 543     Uptime: 1h 14m 32s

先述のプラグインではこのうち「General information」の情報だけをグラフ化しています。でも、「Domains」の情報もグラフ化したいですよね。「Processed」を見れば妙にたくさん処理しているプロセスを見つけることができるかもしれませんし、「Uptime」を見れば長生きしすぎているプロセスを見つけることができるかもしれません。

ということで、るりまサーチのリポジトリrackngaのリポジトリに「Domains」もグラフ化するMuninプラグインを入れておきました。

インストール方法

Debian GNU/Linux前提です。

まず、るりまサーチをcloneします。

% git clone git://github.com/kou/rurema-search.git

プラグインを/usr/share/munin/plugins/にコピーします。

% sudo cp rurema-search/munin/plugins/* /usr/share/munin/plugins/

プラグインをroot権限で動かすようにします。

/etc/munin/plugin-conf.d/passenger.conf:

[passenger_*]
  user root

プラグインをインストールします。autoconfに対応しているので、自動検出してくれます。

% sudo -H /usr/sbin/munin-node-configure --shell --remove-also | sudo sh

munin-nodeを再起動します。

% sudo /etc/init.d/munin-node restart

5分もすればグラフに反映されるでしょう。

まとめ

Passenger用のMuninプラグインを紹介しました。

PassengerとMuninを使っている場合は導入してみてはいかがでしょうか。

Tags: Ruby | このエントリの Delicious history 7 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-06-14

クリアコードの公開gitリポジトリ

すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したフリーソフトウェアが入ったgitリポジトリの公開を始めました。

リポジトリ内にはgit用のコミットメール送信スクリプトを含むgit関連ユーティリティ集「git-utils」CPUの使用率を表示するFirefoxアドオン「システムモニター」も含まれています。中には試し作りしただけのものなども含まれています。それぞれのソフトウェアはリポジトリ内に同梱されているライセンスにしたがって自由に利用できます*1

クリアコードは既存のフリーソフトウェアプロジェクトの開発に参加するだけではなく、新たにフリーソフトウェアプロジェクトを立ち上げたりもしてきました。中にはプロジェクトを立ち上げるほどでもないような小さなソフトウェアもあり、それらのソフトウェアはこのようにひっそりと開発していたりします。これらはフリーソフトウェアなので、有用だと思うものがあったのなら、ソースコードにアクセスし、自由に利用してください。

もっと自由にソフトウェアを利用できる世界になるとよいですね。

関連: クリアコードの公開Subversionリポジトリ

*1  設定しているライセンスはGPL/LGPL/MPLあたりです。

Tags: Ruby Mozilla | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-06-08

日本Ruby会議2010スポンサーと発表・企画のお知らせ

今年も夏に日本Ruby会議が開催されますが、昨年に引き続き今年もスポンサーになりました

日本Ruby会議2010のトップページで微妙に公開されていますが、るりまサーチについて発表する予定です。

いくつかの企画にも参加します。今のところ、るびまでActiveLdapの記事などを書いている高瀬さんのLDAPに関する企画とRuby 1.9コミッタQ&Aに参加する予定です*1

日本Ruby会議2010でのクリアコード関連情報のお知らせでした。

Ruby関連といえば、最近、クリアコードもAsakusa.rbデビューしました。

*1  どちらかというと企画側で。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-05-31

るりまサーチ: Rubyでgroonga使ってリファレンスマニュアルを全文検索

先日、るりまの成果物であるRubyのリファレンスマニュアルを検索するWebアプリケーションるりまサーチを公開しました。

るりまサーチ

OpenSearchにも対応しているため、Firefoxの右上の検索窓から検索することもできます。

これまでも、るりまの成果物はBitClustを使ってWebブラウザから見ることができました*1。しかし、BitClustのWebインターフェイスは検索機能が弱く、目的の情報にたどり着くのが難しいと感じたことがあったのではないでしょうか。例えば、全文検索ができなかったり、そもそも検索がとても遅かったりしました。

るりまサーチでは全文検索エンジンとしてgroongaを利用することにより、高速な全文検索機能と使いやすい絞り込み機能を実現しています。それでは、るりまサーチの機能とその実装について簡単に紹介します。

機能

るりまサーチは多くの情報を絞り込んでいきながら目的の情報に到達することを意識したインターフェイスになっています。そのため、できるだけ簡単に絞り込んでいけるような機能を組み込んであります。

ここでは、絞り込みに関する機能を2点だけ紹介します。

ドリルダウン

groongaの得意な機能の1つはドリルダウンと呼ばれる、検索結果の中から特定の値をグループ化し、それぞれのグループのレコード処理を数える処理です。るりまサーチでもこの機能を利用して絞り込みやすいインターフェイスを提供しています。

ドリルダウン

るりまサーチではページ上部にそのときに絞り込める条件を表示します。例えば、トップページではマニュアルの種類によって絞り込めるリンクを表示しています。

このとき、事前に絞り込んだ後のレコード数も表示しています。この時点で絞り込み後のレコード数を数えているので、絞り込んだ後にレコード数がないリンクを表示しないことができます。つまり、「リンクを辿ったけど絞り込んだらマッチするレコードがない!」という状況を防ぐことができます。

便利に絞り込めるリンクを提供し、その一方で、無駄な絞り込みを行わずに済むようになっています。

条件解除

簡単に条件を絞り込めるようにするだけではなく、簡単に条件を解除することもできます。これは様々な絞り込みを行いながら目的の情報に辿りつけるようにするためです。

条件解除

ページ上部にはどのような条件で絞り込んでいったかが表示されるようになっています。それぞれの絞り込み条件は条件の横にあるリンク*2を辿るだけで簡単に解除することができます。

絞り込みすぎてしまったときは、これで条件を解除して違う条件で絞り込んでいくことができます。

実装

るりまサーチはRuby 1.9.1とRackとrroongaを用いて実装されています。rroongaはgroongaをRubyから利用するためのRubyバインディングです。るりまサーチではgroongaをサーバとしてではなく、ライブラリとして利用しています。

rroongaはgroongaの高速な機能を活かしたまま、より使いやすいRubyらしいAPIを提供しています。るりまサーチはそんなrroongaを使って、すっきりとした記述で実現されています。ここでは、rroongaを使ったコードを2つ紹介します。

スキーマ定義

groongaはRDBと同じようにデータの格納場所毎に型を持っています。groongaにデータを格納する前に格納場所を用意する必要があります。

rroongaでは格納場所の定義(スキーマ)をより宣言的に記述するためのAPIを用意しています。以下は検索対象の情報を保存する「Entries」テーブルの定義です。RDBなどのスキーマを見たことがあるなら、この定義からgroongaがどのようなデータを格納できるようになるかを想像できるのではないでしょうか。

Groonga::Schema.define do |schema|
  schema.create_table("Entries",
                      :type => :hash,
                      :key_type => "ShortText") do |table|
    table.short_text("name")
    table.short_text("local_name")
    table.short_text("label")
    table.text("document")
    table.text("signature")
    table.text("description")
    table.reference("type", "Types")
    table.reference("class", "Classes")
    table.reference("module", "Modules")
    table.reference("object", "Objects")
    table.reference("version", "Versions")
    table.reference("visibility", "Visibilities")
  end
end

このように、Rubyでは宣言的に処理を記述することがわりとよく行われます。これは、内部DSLとも呼ばれ、やりすぎる人も出るほどです。例えば、上記のような記述を以下のようにすることもできますが、これは少しやりすぎではないかと感じます。

Groonga::Schema.define do
  create_table("Entries",
               :type => :hash,
               :key_type => "ShortText") do
    short_text :name
    short_text :local_name
    short_text :label
    text       :document
    text       :signature
    text       :description
    reference  :type, :Types
    reference  :class, :Classes
    reference  :module, :Modules
    reference  :object, :Objects
    reference  :version, :Versions
    reference  :visibility, :Visibilities
  end
end
検索条件

rroongaでは検索条件をクエリ文字列ではなく、Rubyの式として記述することができます。

例えば、「name」カラムの値が「Regexp」であるレコードを検索するときは以下のようになります。

entries.select do |record|
  record.name == "Regexp"
end

「name」カラムの値が「Regexp」あるいは「description」カラムに「正規表現」が含まれているレコードを検索するときは以下のようになります。

entries.select do |record|
  (record.name == "Regexp") | (record.description =~ "正規表現")
end

「name」カラムか「description」カラムに「encoding」を含むレコードを検索するときは以下のようになります。ただし、「name」カラムにマッチした場合はスコアをあげて、より上位に表示するようにします。

entries.select do |record|
  target = record.match_target do |match_record|
    (match_record.name * 100) |
    (match_record.description)
  end
  target =~ "encoding"
end

DataMapperSequelなど文字列ではなくRubyの式で条件を指定できるようにするORマッパーはいくつかありますが、最終的にそれらはSQLになります。しかし、rroongaの場合はRubyで書いた式がそのままgroongaのネイティブな条件式になります。カッコいいですね。

また、ORマッパーも少しやりすぎてしまう傾向がある分野ですが、rroongaはやりすぎることなく、Rubyらしさを保ったまま条件式を指定できているのではないでしょうか。少しやりすぎてしまうと、Symbolにメソッドを追加してしまったりします。

まとめ

Rubyのリファレンスマニュアルを検索するWebアプリケーション「るりまサーチ」の機能と実装を簡単に紹介しました。

るりまサーチを使うことでるりまプロジェクトの成果物であるRubyのリファレンスマニュアルをより便利に活用することができます。

また、るりまサーチの実装はgroongaのRubyバインディングであるrroongaのよいサンプルでもあります。groongaをRubyから利用しようと考えていた方はGitHub上にある、るりまサーチのソースコードを読んでみるとよいでしょう。ライセンスはLGPLv3+です。

るりまサーチはSinatraなどのフレームワークを使わずに、直接Rackを使っています。そのような場合にどのようにテスト環境を構築するか、というのもいつか紹介できるとよいですね。今、興味のある人はソースコードを見てください。

このように、るりまサーチにはまだおもしろいところが色々あるのですが、今回はこのへんにしておきます。

*1  自分で設定するのが面倒な場合はokkezさんが公開しているBitClustを利用することもできます。

*2  画像にしたいですね。

Tags: Ruby | このエントリの Delicious history 8 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-04-27

Ruby 1.8.7/1.9.1どちらでも使えるWindows用バイナリ入りgemをDebian GNU/Linux上で作る方法

groongaのRubyバインディングrroonga 0.9.3がリリースされました。rroonga 0.9.3に関することはメーリングリストでのアナウンスを見てください。

rroonga 0.9.3ではWindows用のgemも提供するようにしました。このgemにはgroonga/rroongaのビルド済みのバイナリが含まれているのでビルド環境がないことが多いWindowsでも簡単に使えるようになっています。

さて、このgemですが、1つのgemでRuby 1.8.7にもRuby 1.9.1にも対応しています。そもそも、gemにはWindowsや32bit環境などのプラットフォームを指定することはできますが、Rubyのバージョンは指定することができません。そのため、Rubyのバージョン毎にgemを用意することはできません。用意する場合はgemのパッケージ名を"rroonga187"や"rroonga191"などと変えなければいけません。これはカッコワルイですね。

解決法は、1つのgemの中に1.8用のバイナリと1.9用のバイナリを両方いれ、実行時にどちらを使うかを切り替える、です。

まず、以下のようにバイナリを配置します*1

lib/1.8/groonga.so # <- Ruby 1.8.7のWindows用バイナリ
lib/1.9/groonga.so # <- Ruby 1.9.1のWindows用バイナリ

そして、groonga.soを読み込む部分を以下のようにします。

major, minor, micro, = RUBY_VERSION.split(/\./)
require "#{major}.#{minor}/groonga.so"

これで、適切なバイナリを読み込むことができます。

この他に、rroongaのように依存しているDLL(libgroonga.dll)がある場合はそのDLLがあるフォルダをPATHに入れなければいけない、などといった注意点がありますが、それはまたいつか機会があったら触れるかもしれません。

それでは、1.8.7でも1.9.1でも使えるWindows用バイナリが入ったgem*2をDebian GNU/Linux上のMinGWで作る方法を紹介します。

rake-compiler: Ruby 1.8.7と1.9.1をクロスコンパイル

まず、Windows用のRuby 1.8.7と1.9.1をMinGWでクロスコンパイルします。これにはrake-compilerが便利です。

% sudo gem install rake-compiler

まず、MinGWをインストールします。

% sudo aptitude install -y mingw32

それでは、Ruby 1.8.7-p249をビルドします。

% rake-compiler cross-ruby VERSION=1.8.7-p249 EXTS=--without-extensions

クロスコンパイルしたrubyはextconf.rbを実行してMakefileを作れればいいだけなので、拡張ライブラリなどはいりません。環境変数として「EXTS=--without-extensions」を指定すると拡張ライブラリはビルドされないのですが、もっとカッコイイ方法がありそうな気がします。

同様にRuby 1.9.1-p378もビルドします。

% rake-compiler cross-ruby VERSION=1.9.1-p378 EXTS=--without-extensions

ただ、これは失敗します。失敗したら以下のパッチを当てます*3

diff -ru ruby-1.9.1-p378.orig/win32/win32.c ruby-1.9.1-p378/win32/win32.c
--- ruby-1.9.1-p378.orig/win32/win32.c	2009-12-05 18:40:53.000000000 +0900
+++ ruby-1.9.1-p378/win32/win32.c	2010-04-20 23:10:13.000000000 +0900
@@ -4604,7 +4604,7 @@

     ret += written;
     if (written == len) {
-	(const char *)buf += len;
+	buf = (const char *)buf + len;
 	if (size > 0)
 	    goto retry;
     }

このパッチは以下のように適用できます。

% cd ~/.rake-compiler/sources
% patch -p0 < /tmp/ruby-1.9.1-build-fix.diff

もう一度、同じコマンドでビルドすると成功します。

% rake-compiler cross-ruby VERSION=1.9.1-p378 EXTS=--without-extensions

Rake::ExtensionTask: gem用バイナリをクロスコンパイル

Windows用のRuby 1.8.7とRuby 1.9.1ができたので、これを利用してgem用のバイナリをクロスコンパイルします。これには、rake-compilerが提供するRake::ExtensionTaskが便利です。

Rake::ExtensionTaskの使い方を紹介しますが、ここでは、もうすでにRakefileがあり、その中でGem::Specificationを作っているものとします。

specがGem::Specificationだとすると、Rakefileに以下を追加することでcrossタスクが定義されます。

require 'rake/extensiontask'
Rake::ExtensionTask.new("groonga", spec) do |ext|
  ext.cross_compile = true
  ext.cross_platform = 'x86-mingw32'
end

Rake::ExtensionTask.newに"groonga"を指定していますが、このようなRakefileを使うときは、以下のようなファイル構成になっている必要があります。

./
+-- ext/
|    +-- groonga/
|        +-- extconf.rb
|        +-- rb-grn.c
|        +-- ...
+ Rakefile
...

ext/の下にRake::ExtensionTask.newで指定した名前と同じディレクトリを作り、その下にextconf.rbを置きます。

crossタスクを使って1.8.7用のバイナリと1.9.1用のバイナリをクロスコンパイルするには以下のようにします。

% rake cross compile RUBY_CC_VERSION=1.8.7:1.9.1

うまくいくとlib/1.8/groonga.soとlib/1.9/groonga.soができます。

これらを両方含んだgemを作るには以下のようにします。

% rake cross native gem RUBY_CC_VERSION=1.8.7:1.9.1

これでpkg/rroonga-0.9.3-x86-mingw32.gemができます。あとは、このgemをrubygems.orgにアップロードすれば完了です。

% gem push pkg/rroonga-0.9.3-x86-mingw32.gem

まとめ

Ruby 1.8.7/1.9.1のどちらでも使えるWindows用のgemをDebian GNU/Linux上で作成する方法を紹介しました。もし、拡張ライブラリをWindows上でも簡単に使えるようにしたいのであれば、Ruby 1.8.xと1.9.xの両方をサポートしてみてはいかがでしょうか。

この話とは関係ありませんが、Ruby Summer of Codeの学生の応募の締切りは今週の土曜日だそうです。(参考: [ruby-list:47029] [ANN] Ruby Summer of Code

Rubyとオープンソースに興味のある学生の方は応募してみてはいかがでしょうか。本家のSummer of CodeやRuby Summer of Codeはフリーソフトウェアの開発に関わるよい機会といえます。Rubyベストプラクティスの著者が開発しているPDF生成ライブラリのPrawnなど、いくつものRuby関連のフリーソフトウェアがSummer of Codeのおかげで開発が進んできました。(参考: Summer of Codeと須藤さんとSubversionのRuby bindings - 角谷HTML化計画(2005-10-26)

ぜひ、このような機会を活かして、フリーソフトウェアの開発に積極的に参加してみてください。

*1  Windowsでも拡張子は.soでいいのです。

*2  複数のバージョン向けのバイナリが入ったgemをfat gemというらしいです。

*3  この問題はtrunkではすでに修正されています。

Tags: Ruby | このエントリの Delicious history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-04-21

Ruby 1.9.xでRange#include?を高速に動かす方法

Ruby 1.9.xではRange#include?の実装が変わり、Ruby 1.8.xよりも圧倒的に遅くなるケースがあります。これは、Ruby 1.9.xへ移行したときの有名なハマりポイントの1つでしょう。

例えば、こんなケースです。

require 'time'
require 'benchmark'

Benchmark.bm(10) do |bm|
  march = Time.parse("2010/03/01")...Time.parse("2010/04/01")
  march_15 = Time.parse("2010/03/15")
  bm.report("include?") do
    march.include?(march_15)
  end
end

2010年3月15日が2010年3月に入っているかを調べています。

これをRuby 1.8.7で動かすとこうなります。

% ruby -v /tmp/range-include.rb
ruby 1.8.7 (2010-01-10 patchlevel 249) [x86_64-linux]
                user     system      total        real
include?    0.000000   0.000000   0.000000 (  0.000011)

一瞬ですね。

Ruby 1.9.1で動かすとこうなります。

% ruby1.9.1 -v /tmp/range-include.rb
ruby 1.9.1p378 (2010-01-10 revision 26273) [x86_64-linux]
                user     system      total        real
include?    1.360000   0.030000   1.390000 (  1.766556)

1万倍以上も遅くなります。

原因

どうしてこうなるのかというと、1.8.xのRange#include?と1.9.xのRange#include?では引数の値の見つけ方が異なるからです。

1.8.xでは引数が範囲の最初の値より大きいかつ最後の値より小さい、ことだけを確認していました。1.9.xでは範囲の最初から最後まで順に繰り返し、その中に引数と==な値が含まれるかどうかを確認します。コードで表すとこんな感じです。

def range_include_18(range, value)
  range.begin < value and value < range.end
end

def range_include_19(range, value)
  range_value = range.begin
  loop do
    return true if range_value == value
    range_value = range_value.succ
    break if range_value >= range.end
  end
  false
end

Time#succは1秒先の時間を返します。そのため、一回のRange#include?で最大2678400回ループをまわすことになります(範囲に入っていないとき、つまり、falseを返すとき)。

(Time.parse("2010/03/01")...Time.parse("2010/04/01")).to_a.size # => 2678400

真ん中あたりのTime.parse("2010/03/15")でも1209601番目なので、1.8.xのRange#include?と比べてだいぶ遅くなるというわけです。

(Time.parse("2010/03/01")..Time.parse("2010/03/15")).to_a.size # => 1209601

解決法

1.9.xには1.8.xのRange#include?と同じように比較するメソッドRange#cover?が追加されているのでそれを使うとよいでしょう。

require 'time'
require 'benchmark'

Benchmark.bm(10) do |bm|
  march = Time.parse("2010/03/01")...Time.parse("2010/04/01")
  march_15 = Time.parse("2010/04/01")
  bm.report("cover?") do
    march.cover?(march_15)
  end
end

実行するとたしかに速いです。

% ruby1.9.1 -v /tmp/range-cover.rb
ruby 1.9.1p378 (2010-01-10 revision 26273) [x86_64-linux]
                user     system      total        real
cover?      0.000000   0.000000   0.000000 (  0.000010)

ただし、1.8.xにはRange#cover?はないので、Range#cover?を使うと1.9.x専用のスクリプトになってしまいます。

高速なRange#include?

実は、1.9.xのRange#include?も、数字の範囲または文字の範囲、の場合は高速に動作します。この場合だけ特別扱いされていて1.8.xと同様の比較方法を用いているからです。つまり、どうにかして数字の範囲か文字の範囲に落としこめば1.8.xでも1.9.xでも高速に動作するようなスクリプトを書けるということです。

Timeの場合はTime#to_iで数値にしてしまうのがよいでしょう。

require 'time'
require 'benchmark'

Benchmark.bm(10) do |bm|
  march = Time.parse("2010/03/01")...Time.parse("2010/04/01")
  march_15 = Time.parse("2010/03/15")
  bm.report("Time") do
    march.include?(march_15)
  end

  march_integer = (Time.parse("2010/03/01").to_i)...(Time.parse("2010/04/01").to_i)
  march_15_integer = Time.parse("2010/03/15").to_i
  bm.report("Integer") do
    march_integer.include?(march_15_integer)
  end
end

結果は一目瞭然です。

% ruby1.9.1 -v /tmp/range-include-integer.rb
ruby 1.9.1p378 (2010-01-10 revision 26273) [x86_64-linux]
                user     system      total        real
Time        1.230000   0.030000   1.260000 (  1.504888)
Integer     0.000000   0.000000   0.000000 (  0.000009)

まとめ

徐々にRuby 1.9.xを使う人が増えているかもしれない、ということで、Ruby 1.9.xを使った場合にハマりそうなポイントとその解決法を紹介しました。1.9.xでは変わったことがたくさんあります。1.9.xのことをもっと知って上手に付き合ってみてはいかがでしょうか。

Tags: Ruby | このエントリの Delicious history 9 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-03-25

るびま0029号

るびま0029号がリリースされていますね。おめでとうございます。

せっかくなので少し紹介します。

ActiveLdapを使ってみよう

今回のるびまにはRubyでLDAPを操作するための便利ライブラリActiveLdapの記事の後編ActiveLdap を使ってみよう(後編)が入っています。

後編では今まではあまり文書化されていなかったフィルタのこと関連性のことにも触れています。全てのエントリを扱うクラスは、知る人ぞ知るのノウハウではないでしょうか。(ActiveLdap付属のサンプルアプリケーションでは使われていますが文書化はされていなかったはず。)LDAPサーバの設定やデータを確認する場合にはLDAP のスキーマ情報を ActiveLdap から参照するあたりの情報が役に立ちます。ldapsearchなどではなく、irbでLDAPサーバの情報を確認できるので、環境構築時やデバッグ時にとても便利です。(irbの補完機能を有効にするとより便利です。)

記事を書いている高瀬さんActiveLdapのチュートリアルの翻訳もしている頼もしい方です。ActiveLdapに興味はあるけどまだよく知らないという方は、まず、 ActiveLdap を使ってみよう(前編)を読んでからチュートリアルを読んで、最後に後編を読むのがよいのではないでしょうか。

0029-RubyNewsにもActiveLdap 1.2.1のリリースが載っていますね。

とちぎRuby会議02

発表者として参加したとちぎRuby会議02のレポート記事RegionalRubyKaigi レポート (10) とちぎ Ruby 会議 02もあります。レポートにもありますが、とちぎRuby会議02のお題が他のRegionalRubyKaigiと違ったものだったので、独特な内容になっていましたね。

Rubyベストプラクティス

0029 号 巻頭言で紹介されているRuby 1.9向けに書かれたRubyベストプラクティスという本がそろそろ発売するようです。テストまわりの章だけレビューに参加しました。

少しくせがある印象なので、Ruby初心者の方にはつらいかもしれません。自分で判断できる程度にRubyを知っている方ならいろいろ考えながら読むとおもしろいかもしれません。

まとめ

るびまがリリースされていたので紹介しました。

ところで、みなさんは編集後記は読んでいるのでしょうか。たまにおもしろかったりするので、読んでいない方は読んでみてはいかがでしょうか。短いのでさっと目を通せます。

Tags: Ruby | このエントリの Delicious history 1 user | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-03-16

test-unit 2.0.7とCutter 1.1.1をリリース

先日、Ruby用のxUnit系テスティングフレームワークtest-unit 2.0.7とC・C++用のxUnit系テスティングフレームワークCutter 1.1.1がリリースされました。

どちらも、テストの書きやすさ(テストをキレイに書けるとテストを保守しやすい)だけではなく、テストが失敗した時に「できるだけ素早く問題の原因にたどり着ける」ことも重視しています。

Rails/Rack界隈ではCucumberWebratcapybaraなどを使って、「"ログイン"ボタンをクリックする」とか「click_link("ログイン")」などと、直感的にテストを書けるようになっています。では、テストが失敗したときの結果はどのように表示されるでしょうか。HTML全体やテキスト全体が表示されて、「"ログイン"というボタン(リンク)はなかったよ」と言われたらどうでしょう。あなたのコードはどこが悪かったのでしょう。

そういうときに、失敗結果を見て、すぐに「あぁ、ここが悪いかも!」と作業を進めていけるようなテスティングフレームワークにしたいものです。開発はデバッグの連続なのですから、よりスムーズにデバッグ作業を進める手助けとなるツールを使って開発したいですよね。

test-unitやCutterはWebアプリケーション用に特別なサポート機能は提供していないので上記のようなことをうまい具合に解決できるわけではないのですが、ライブラリのテストでは上記のようなことをうまい具合に解決する機能を提供しています。

Cutterのインストール方法がより簡単に

一応、リリースで変わったことを少し書いておきます。

機能面でもよくなっているのですが、インストールまわりだけにしておきます。

Cutterプロジェクトでは、これまでDebian, Ubuntu用のapt-lineとMacPortsのPortfileを提供していましたが、今回のリリースから、FedoraのYumリポジトリも提供するようにしました。以下でインストールできます。yumの管理下に入るのでアップデートも簡単ですね。

% sudo rpm -Uvh http://cutter.sourceforge.net/fedora/cutter-repository-1.0.0-0.noarch.rpm
% sudo yum install cutter

test-unitの方は変わらずgemをサポートしているので、こちらもインストール・アップデートが簡単ですね。

書きやすさだけではなくデバッグしやすさも重視したテスティングフレームワークに興味のある方は使ってみてはいかがでしょうか。

2010-03-11

LDD '10 Winter: メールフィルタの作り方 - Rubyで作るmilter

先日、LOCAL DEVELOPER DAY '10 WinterでRubyでmilterを作る方法について話してきました。どのタイミングでどのmilterプロトコルのコマンドが発行されるかについても説明しているので、Rubyではなく(libmilterを使って)Cでmilterを実装する場合にも参考になる部分があるはずです。むしろ、Rubyとmilterの組み合わせについて話している部分は薄めです。これは、Rubyそのものとmilterの仕組みを理解していればRubyとmilterを組み合わせることは容易だからです。

メールフィルタの作り方 - Rubyで作るmilter

少しgroongaについてもふれています。

それでは、ダイジェストで資料の内容を紹介します。完全版はリンク先を見てください。資料のPDF・ソースもリンク先にあります。

内容

話題

具体的にRubyでmilterを作る話に入る前に、まず、前提となる知識を確認します。

はじめにメールフィルタ、次にメールフィルタの仕組みの1つであるmilterについて簡単に説明します。その後、一度milterから離れてSMTPについて説明します。これはmilterの動作を理解するためにはSMTPの動作も知っておく必要があるからです。SMTPの動作を確認したらそれをふまえてmilterの具体的な動作を説明します。

ここまできたらRubyでmilterを作るための下準備は整っているはずです。実際に1つRubyでmilterを作ってみます。

ゴール

今日のみなさんのゴール

説明の途中にいくつか確認ポイントがあります。それぞれの技術は他の技術をベースになりたっているので、ベースとなっている技術をおさえていくことが、理解してしっくりくるためのコツです。

それぞれの確認ポイントをゴールとして最終的な「Rubyでmilterを作れる」ようになるゴールまでたどりついてください。

メールフィルタ

メールフィルタ

メールシステムとは外部とユーザ間でメールを配信するシステムです。すべてのメールシステムではそのままメールをやりとりするのではなく、メールを配信するまでのあいだに、メールに対してなんらかの処理を実行します。つまり、すべてのメールシステムにはメールフィルタ機能が備わっています。

MTAのプラグインとする方法

MTAにプラグイン

メールシステムでメールフィルタを実現する方法はいくつかありますが、その1つがMTA(メールサーバ)のプラグインとして実現する方法です。この方法のメリットはMTAを変更せずにメールフィルタ機能を変更できることです。milterはこのタイプで動作するメールフィルタです。

メールフィルタのまとめ

メールフィルタのまとめ

メールフィルタはメールシステムが持っている必須機能の1つです。その実現方法としてMTAのプラグインとして実現する方法があり、milterもその方法で実現されているメールフィルタです。

それでは、milterの概要について説明します。

milterについて

milter?

milterの名前の由来は「mail filter」です。milterは汎用的なメールフィルタの仕組みのため、同じメールフィルタを異なるMTAと一緒に使うことができます。

Sendmailを用いているメールシステムではmilterを利用していることが多く、milterをサポートした商用のメールフィルタも多く存在します。最近ではPostfixのmilterサポートがリリース毎に改善されていっているため、Postfixを用いたメールシステムでもmilterを利用するケースが徐々に増えています。

milterシステム

milter関連用語

「milter」は文脈によって異なるものを指すことがあります。そこで、ここでは混乱を避けるために異なる名前で呼ぶことにします。

まず、メールフィルタそのものを「milter」と呼びます。

メールフィルタとMTAは別プロセスで動作するため、プロセス間通信でフィルタ対象のメールやフィルタ結果などをやりとりする必要があります。そのやりとりのきまりを「milterプロトコル」と呼びます。

そして、「milter」と「milterプロトコル」をサポートしたMTAを含んだメールフィルタの仕組み全体を「milterシステム」と呼びます。

「milter」といった場合は「メールフィルタそのもの(ここでいうmilter)」という意味で使う場合と、「メールフィルタの仕組み(ここでいうmilterシステム)」という意味で使われる場合が多いです。「milter」という単語が使われている場合はどちらの意味かを判断できるようになってください。

milterプロトコルはSMTPと密接に関連したプロトコルです。そのため、milterプロトコルについて説明する前に、SMTPについて確認します。

SMTPの概要

簡単?

SMTPは以下の4つのコマンドが基本となるシンプルなプロトコルです。

  • HELO
  • MAIL FROM
  • RCPT TO
  • DATA

まず、「HELO」で接続したSMTPクライアントの情報を伝えます。以下の例ではSMTPサーバ(MTA)からのメッセージは先頭に「<」をつけて示します。SMTPクライアントのメッセージは先頭に「>」をつけて示します。

% telnet localhost smtp
< 220 note-pc.example.com ESMTP Postfix (Ubuntu)
> HELO localhost.example.com
< 250 note-pc.example.com

挨拶が済んだらSMTPセッションのスタートです。1つのSMTPのセッションで複数のメールを送ることができます。「MAIL FROM」、「RCPT TO」、「DATA」で1つのメールを送ります。

まず、「MAIL FROM」で送信者を伝えます。

> MAIL FROM: <kou@example.com>
< 250 2.1.0 Ok

次に、「RCPT TO」で宛先を伝えます。

> RCPT TO: <info@example.com>
< 250 2.1.5 Ok

同じメールを複数の宛先に送ることもできます。その場合は「RCPT TO」を複数回実行します。

最後に「DATA」でメールの内容を伝えます。メールの最後は「.」だけの行になります。

> DATA
< 354 End data with <CR><LF>.<CR><LF>
> Subject: Hello
> From: <kou@example.com>
> To: <info@example.com>
> 
> This is a test mail!
> .
< 250 2.0.0 Ok: queued as 054C624FB

これで、1通のメールを送信できました。続けてメールを送信する場合はまた「MAIL FROM」から始めます。

SMTPセッションを終了する場合は「QUIT」です。

> QUIT
< 221 2.0.0 Bye

これで1つのSMTPセッションが終了しました。

milterプロトコルはSMTPと密接に関わっています。それでは、milterプロトコルの詳細を説明します。

SMTPとmilterプロトコル

SMTPとmilterプロトコル

milterプロトコルにもSMTPと同じようにコマンドがあります。そして、そのコマンドはSMTPのコマンドと対応したものになっています。まずSMTPのコマンドを説明したのはそのためです。

例えば、SMTPで「HELO」というコマンドが実行された場合、「HELO」に対応する「helo」というmilterプロトコルのコマンドが発行されます。このとき、SMTPクライアントが指定したHELOコマンドの引数がmilterに渡されます。

MTAはmilterにコマンドを送った後、milterからの返答があるまでSMTPクライアントには返答しません。つまり、milterが「helo」でrejectを返すことで、SMTPクライアントの「HELO」コマンドへの返答をrejectとすることができます。これにより、MTAがSMTPレベルでできることとほとんど同じことをmilterで実現できます。

milterプロトコルのコマンドとSMTPのコマンドの対応

コマンド: メタ情報

mitlerプロトコルのコマンドはほとんどSMTPのコマンドに対応していますが、milterプロトコルのコマンドの方がより細かくなっています。例と一緒にコマンドの対応を説明します。

SMTPでの最初のコマンドは「HELO」ですが、milterプロトコルでは「helo」よりも前にコマンドが発行されます。それが、SMTPクライアントがSMTPサーバに接続したときに発行される「connect」コマンドです。

「connect」コマンド以外はSMTPのコマンドとmilterプロトコルのコマンドは1対1で対応します。「envfrom」の「env」は「envelope」の略で、「封筒」という意味です。「envfrom」で「差出人」という意味、「envrcpt」で「宛先」という意味です。「rcpt」は「recipient」の略で「受信者」という意味です。

SMTPでは1つのメールを複数の宛先に送信できます。この場合、複数回「RCPT TO」を指定します。STMPで複数回「RCPT TO」が指定されるので、milterプロトコルでも「envrcpt」コマンドが複数回発行されます。

コマンド: DATA

SMTPの「DATA」コマンドはmilterプロトコルではより細かいコマンドに分解されています。

まず、「DATA」コマンド時にはmilterプロトコルの「data」コマンドがすぐに発行されます*1。その後、SMTPクライアントはメール本体を送信しますが、「header」などのイベントはすぐには発生しません。SMTPクライアントがデータの終了を示す「.」のみの行を入力するまでは何も起きません。「.」のみの行が入力されると、MTA側でメール本文をパースして「header」、「eoh」(end of header: ヘッダーの終わり)、「body」、「eom」(end of message: メッセージの終わり)コマンドを発行します。もちろん、ヘッダーもパースしてあるので、MTAは「ヘッダー名」と「ヘッダー値」と分解した状態で情報を渡します。

このようにmilterプロトコルはSMTPと密接に関わっています。milterプロトコルのコマンドがわかれば、自分が必要な機能を持つmilterを実現するためにはどのコマンドを利用すればよいかを考えることができるでしょう。

milterサンプル: メール検索

扱うもの

説明用のサンプルとしてメール検索を実現するmilterを作成します。今回はSubject、From、Toと本文のみを扱うことにします。

メール検索を実現するために、全文検索エンジンとしてgroongaを、milterライブラリとしてmilter managerのRubyバインディングを使います。

groonga: カラム指向データストア

カラム指向

groongaは全文検索のためのインデックス作成機能だけではなく、データストアの機能も持っています。groongaのデータストアはカラム指向で、リレーショナルデータベースとは違い、レコード(行)毎にデータをまとめて持つのではなく、カラム(列)毎にデータをまとめて持っています。

このようにデータを持つと、同じカラムの複数の値へのアクセスを高速に行うことができます。このため、カラムの値を使った集計処理を高速に実行できます。集計処理とは、例えば、SQLでいうGROUP BYのような処理です。

集計処理を用いると絞り込み検索をしやすいユーザーインターフェイスを提供することができます。例えば、ショッピングサイトで商品に複数のタグがついているとします。このとき、同じタグがついている商品が何項目あるかを表示してリンクにします。1つも商品が属していないタグは表示しないようにすれば、ユーザは無駄な絞り込み操作を行わずにすみます。

全商品(123件)
タグ
  スポーツ(58件)← リンクにする
  映画(45件)    ← リンクにする
  食べ物(36件)  ← リンクにする
  旅行(0件)     ← 表示しない

この状態で「スポーツ」をクリックしたとします。

全商品(123件) > スポーツ(58件)
タグ
  スポーツ      ← 選択済みなので表示しない
  映画(26件)  ← リンクにする
  食べ物(0件) ← 表示しない
  旅行(0件)   ← 表示しない

このように、絞り込んだ後にがっかりするような操作を示さないことにより、絞り込み検索をしやすいユーザインターフェイスを作ることができます。がっかりするような操作かどうかを判断するために、同じ値を持つレコードの個数を数える、といった集計処理をしています。

groonga: バイナリパトリシアトライ

パトリシアトライ

groongaはキー管理のためのデータ構造としてハッシュテーブルとバイナリパトリシアトライを採用しています。バイナリパトリシアトライはパトリシアトライの一種です。

ここにB+木とパトリシアトライの説明を書く予定でしたが、もう、だいぶ長くなっているので省略します。また別の機会があれば紹介します。

パトリシアトライを利用すると効率よく最長一致検索を実現できます。これを試してみるためのサンプルアプリケーションを用意しました。

groongaでキーワード検出

リンク先ではキーワードを変えて試すことができます。

最長一致機能を利用してキーワード検出している部分のソースは以下の通りです。

target_text = "..."
keywords = request["keywords"].split

words = Groonga::PatriciaTrie.create(:key_type => "ShortText",
                                     :key_normalize => true)
keywords.each do |keyword|
  words.add(keyword)
end
tagged_text = words.tag_keys(target_text) do |record, word|
  "<span class='keyword'>#{word}</span>"
end

まず、パトリシアトライを作り、キーワードを登録します。Groonga::PatriciaTrieにはtag_keysという便利メソッドがあり、これを使うと「最長一致検索」→「キーワードにタグ付け」をより簡潔に記述することができます。

全体のソースはリンク先にあるソース一式の中に含まれています。

スキーマ

スキーマ: Messages

groongaのRubyバインディングであるRuby/groongaはスキーマ定義のためのDSLを提供しています。

メールを保存するMessagesテーブルにはsubjectfromtobodyカラムを定義しています。今回は簡単のため、宛先は1つのみ扱うことにしています。

スキーマ: Terms

次に、高速に全文検索を行うために索引を作成します。Termsテーブルのキーに単語(ここではbigramを利用しているので1文字か2文字の文字列)、カラムにその単語が出現するMessagesレコードのID(とN-gramなので単語の出現位置)を保持します。

subjectカラムとbodyカラムでそれぞれに対して索引を作成しています。こうすることにより、「どこかに○○が含まれているメールを検索」といった検索だけではなく、「Subjectに○○が含まれているメールを検索」、「本文に○○が含まれているメールを検索」というような細かい検索ができるようになります。細かい検索が必要ない場合はMessagesテーブルに検索対象をすべて入れたカラムを1つ作り、そのカラムに対して索引を作成してもよいでしょう。

Groonga::Schema.define do |schema|
  schema.create_table("Messages") do |table|
    ...
    table.text("text")
  end

  schema.create_table("Terms",
                      :type => :patricia_trie,
                      :default_tokenizer => "TokenBigram",
                      :key_normalize => true) do |table|
    table.index("text")
  end
end

messages = Groonga["Messages"]
from = "kou@clear-code.com"
to = "info@clear-code.com"
body = "Hello Ruby and milter!"
text = "#{from} #{to} #{body}" # <- textに検索対象をまとめる
messages.add(:from => from
             :to => to,
             :body => body,
             :text => text)

query = "Ruby"
messages.select do |record|
  record["text"].match(query) # <- textカラムで全文検索
end

Rubyでmilterを作る

Rubyでmilter

データの保存・検索の仕組みはできたので、あとは、groongaのデータベースにメールを登録するだけです。

milter managerのRubyバインディングのAPIでは、ユーザがmilterプロトコルのコマンドに対応するメソッドを定義し、ライブラリ側がそのメソッドを呼び出します。今回必要な情報はヘッダーと本文にあります。そのため、今回のmilterは以下のようになります*2

class ArchiveMilter < Milter::ClientSession
  def initialize
    @messages = Groonga["Messages"]
    @values = {}
    @encoding = nil
    @body = ""
  end

  def header(context, name, value)
    case name
    when /\A(Subject|From|To)\z/i
      key = $1.to_s.downcase
      utf8_value = NKF.nkf("-w", value)
      @values[key] = utf8_value
    when /\AContent-Transfer-Encoding\z/i
      @encoding = value
    end
  end

  def body(context, chunk)
    @body << chunk
  end

  def end_of_message(context)
    nkf_option = "-w"
    nkf_option << " -MB" if @encoding == "base64"
    @values["body"] = NKF.nkf(nkf_option, @body)
    @messages.add(@values)
  end
end

このように、Rubyでmilterを作るときは必要な処理の部分だけを記述するだけですみます。つまり、やりたいことを実現するためにどういうデータが必要で、どのタイミングでそのデータを手に入れられるかがわかれば、Rubyでmilterを作ることは簡単だということです。

登録したメールは以下のように検索・表示することができます。

query = "Ruby" # <- 検索キーワード
messages = Groonga["Messages"]
result = messages.select do |record|
  record["subject"].match(query) |
    record["body"].match(query)
end
result.sort([["_score", :desc]]).each do |message|
  puts "-" * 78
  puts "score: #{message.score}"
  puts "Subject: #{message.subject}"
  puts "From: #{message.from}"
  puts "To: #{message.to}"
  puts
  puts message.body
  puts "-" * 78
end

まとめ

Rubyでmilterを作る方法について説明しました。そのために必要な技術として、milterプロトコルの具体的な動作も説明しました。ここで説明されている内容を理解していれば、より詳細なmilter関連情報も理解しやすくなるでしょう。英語ですが、milterに関する情報はmilter.orgにまとまっています。より詳しい情報を知りたい場合はチェックするとよいでしょう。

札幌はやはりやさいい雰囲気に包まれていました。札幌Ruby会議02とは少し違う雰囲気でしたが、似ているとは感じました。

一度、札幌の人たちに会いに行ってみてはいかがでしょうか。

あわせて読みたい

  • 2010-02-14 - iakioの日記 - postgresqlグループ

    「C言語でPostgreSQLを拡張する」というタイトルで石田さんが淡々とライブコーディングされていました。会場とやりとりをしながらコーディングする様子を見ていると、札幌っぽい雰囲気を感じることができるでしょう。

*1  「data」コマンドはmilterプロトコルのバージョン4から追加されたコマンドなのでそれより古い2などを使っている場合は利用できません

*2  milter managerでは「eom」というような省略した名前を「end_of_message」という省略しない自己記述的な名前になっているので注意してください。

2010-02-15

Ruby/groonga 0.9.0, 0.9.1: 高速で使いやすい検索エンジンライブラリへ向けて

今年も肉の日がきましたね。

Ruby/groonga0.9.0がリリースされました。リリース後すぐにgroongaの新しいバージョン0.1.6がリリースされたため、0.1.6に対応した0.9.1がすぐにリリースされました。gemで自動インストールされるgroongaのバージョンが0.1.6になっているだけで、Ruby/groongaの機能は変わっていません。

いつも通り、以下のコマンドでインストールできます。システムにgroongaがインストールされていない場合は自動でダウンロードしてインストールします。

% sudo gem install groonga

全文検索エンジンgroongaの特徴はgroongaのドキュメントを参照してください。

0.9.x

前のリリースでは0.0.7だったバージョン番号が一気に0.9.1まであがっています。バージョン番号から想像できる通り、初のメジャーリリース1.0.0を意識しはじめたということです。

0.9.x系列では1.0.0に向けて以下の2点を重点的に開発していきます。

  1. より使いやすいAPIの提供
  2. 高速化

APIを改良するため、以前のバージョンとは互換性が壊れることがありますが、今後より便利にRuby/groongaを使うために、今のうちに積極的に改良していく予定です。使ってみて、「ここがこうなっていたらもっと使いやすい」、「こういうAPIがあると便利」などという意見があったらぜひお知らせください。

Groonga::Context.default#[]のショートカットとしてGroonga.[]を導入するなど、もうすでに便利なAPIの追加は始まっています。

現在のRuby/groongaにはまだ高速化の余地があります。あまり意識せずにスクリプトを書いてもなるべく高速に動作するようにライブラリ側でできることはなるべくライブラリ側で頑張る方向で開発していく予定です。ユーザが使いやすくなるように処理系が頑張るというのはRuby本体と同じ方向です。

まとめ

今年も年に一度の肉の日がきました。

Ruby/groonga 0.9.1がリリースされています。Rubyで全文検索システムを構築したい場合はRuby/groongaも検討してみてはいかがでしょうか。今のうちに改善案をだしておけばメジャーリリース時にはそれが反映されてより便利に高速な全文検索システムを構築できるようになるかもしれません。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2010-02-09

告知: LOCAL DEVELOPER DAY '10/Winter: メールフィルタの作り方 - Rubyで作るmilter

来月2/13(土)に札幌で開催されるLOCAL DEVELOPER DAY '10/WinterでRubyでメールフィルターを作る方法について話します。

日時
2010/2/13(土) 12:45〜18:35
場所
札幌市産業振興センター セミナールームA
参加費用
無料
参加登録
必要無し(懇親会は登録が必要)

内容

Rubyでメールフィルターを開発する方法について話します。以下、背景などをまじえてもう少し詳しく説明します。

SendmailやPostfixといったよく使われているメールサーバにはmilterというメールフィルターを追加する仕組みが実装されています。milterを使うことにより、メールサーバに迷惑メール対策機能やウィルスチェック機能、メールアーカイブ機能、添付ファイル自動暗号化機能などを追加することができます。つまり、メールサーバ本体を変更せずに組織のポリシーに合わせたメールシステムを構築することができるということです。

milterという仕組みを使ったメールフィルター*1はすでにたくさん開発されているので、既存のものを組み合わせてメールシステムを構築できることも多いです。しかし、組織特有の事情などがある場合は既存のメールフィルターでは対応できないこともあるでしょう。そういった場合、新しくメールフィルターを開発したり既存のメールフィルターを改造して対応できます。

通常、メールフィルターはC言語で開発する必要がありますが、milter maangerが提供する機能を利用することによってRubyを使って素早くメールフィルターを開発することができます。

今回は、milter managerの機能を使ってRubyでメールフィルターを開発する方法やデバッグの方法などを紹介します。milterという仕組みを知らない方でもわかるように、milterという仕組みから順を追って説明します。ただし、Rubyについて詳しく説明しないので、Rubyがまったくわからない方には少し厳しいかもしれません。札幌でRubyについて詳しくなりたい方はRuby札幌に参加することをオススメします。

まとめ

来月開催されるLOCAL DEVELOPER DAY '10/Winterで、Rubyを用いてメールフィルターを作る方法について話すので、それを告知しました。

JavaScript(Ext JS)やWebアプリケーションのテスト(Selenium)、ドキュメント指向データベース(MongoDB)、リレーショナルデータベース(PostgreSQL)の話などもあるようです。参加登録も必要ないので、興味のある方はお気軽に参加してみてはいかがでしょうか。

*1  混乱するかもしれませんが、「milterという仕組みを使ったメールフィルター」もmilterと呼びます。milterといった場合は仕組みよりメールフィルターのことを指すことが多いです。

2010-01-25

Ruby on Rails Technical Night: Railsで作るActive Directoryと連携した社内システム

先日開催された〜Ruby on Rails Technical Night〜 Ruby on RailsセミナーでActive Directoryと連携したRailsアプリケーションの作り方について話しました。

Railsで作るActive Directoryと連携した社内システム

概要

ActiveLdapという社内システムをRailsを使って実現するときに便利なライブラリをデモを交えながら紹介しました。

社内向けのシステムをWebアプリケーションとして実現することは驚くことではなくなりました。Webアプリケーションなので、もちろんRailsを使っても実現することができます。

そのときに避けて通れないのが既存の社内情報との連携です。社内向けのシステムなので、社内情報と密接に連携し、より便利に使えるものであるべきです。多くの組織では社内情報をActive Directoryを用いて一元管理しています。

Railsアプリケーションとして社内システムを実現する場合も、必要に応じてActive Directory内にある情報を利用します。それを助けてくれるライブラリがActiveLdapです。

内容

Active Directoryをあまり知らない方も参加されるかたでもついてこられるように*1、前半でActive DirectoryとLDAPの基本的なところを説明しました。今回は「図での説明 + まとめ」というように、感覚的にわかってもらった後(なんとなくわかってもらった後)に要点を確認するという流れにしました。参加された方に感想を聞けなかったのですが、いかがだったでしょうか。

その後、実際にデモを行いながら、ActiveLdapがActiveRecordと同じようにActive Directory上の情報を操作できることを説明しました。ActiveRecordと同じように操作できるということは、いつもと同じようにコントローラ部分を書けるということです。つまり、今までのRailsアプリケーション開発の知識を活かしながらActive Directoryを操作するRailsアプリケーションを開発することができるということです。

今回は「コードがバンバン出るような内容を」ということで声をかけてもらったので、ここがメインの内容になっています。残念ながら公開されている資料ではデモを表現することができないので、デモで実行したコマンドなどを載せています。

最後に、実際にアプリケーションを開発するときにぶつかることが多い問題点についてふれました。Active Directoryとの接続の仕方、テストの仕方などです。

まとめ

先日開催されたRailsセミナーでの内容を紹介しました。

ActiveLdapに興味をもたれた方はるびまのActiveLdap を使ってみよう(前編)ActiveLdapのチュートリアルも読んでみてください。次号のるびまでは後編が公開される予定です。そちらも楽しみですね。

今回の内容はActive Directoryに特化した内容もありますが、OpenLDAPなどLDAPサーバ一般に通用する内容も多いです。LDAPサーバと連携するRailsアプリケーションを開発している方も参考にしてみてください。

Active Directory関連のことだけではなくて、コードを書くことについても伝えたかったのですが、欲張りすぎました。話の中ではうまく伝えられませんでしたが、プログラマーの方であれば、グラデーションで繋がる世界: 札幌Ruby会議02に行ってみて初心に帰ったもぜひ読んでみてください。

*1  多くの方はご存知のようでしたが。。。

つづき: 2009-12-21
Tags: Ruby | このエントリの Delicious history 5 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-12-15

札幌Ruby会議02: レシピに書かれていないこと

先週末開催された札幌Ruby会議02でライブコーディングしてきました。本州枠の1つで話す機会を作ってくれた実行委員長のしまださん、ありがとうございます。

レシピに書かれていないこと

内容

今回の発表はスライドだけでは伝わらないはずです。いつも、話すことすべてをスライドに盛りこんでいませんが、今回は特にスライドに盛りこまれていないことが多いです*1

仙台では考え方について話しましたが、札幌では技術的なことを話すつもりでした。札幌で何かを伝えることができるのなら、それは技術的なことであって欲しいし、Ruby 逆引きレシピをリリースしているRuby札幌がいる札幌でならそれができるはず、というのが理由です。

そのために、今回はライブコーディングをすることにしました。

自分ができる、一番、技術的なことを伝える方法はプログラムを書くことです。プログラミングに限ったことかもしれませんが、何かを伝えるためには、結果だけではなく、過程も一緒に伝えた方が、より伝わります。ペアプログラミングが成果をあげているという声を聞いたこともあるのではないでしょうか。

ライブコーディングは1対1ではなく、1対nの形になるので、それで本当に伝えることができるのかは不安でしたが、札幌でなら大丈夫だろうということで決行しました。

札幌Ruby会議02の模様はニコニコ動画にアップロードされています。

レシピに書かれていないこと - 須藤 功平

2009-12-22
再生: 104
コメント: 2
マイリスト: 3

レシピに書かれていないこと - 須藤 功平 (20:41)
札幌Ruby会議02 (2009/12/05)mylist: mylist/16614060time table: http://regional.rubykaigi.org/sapporo02

札幌でたいやきってな w

tDiaryの話をした柴田さんも録画してくれました。ありがとうございます。こちらはYouTubeにあります。

FAQ

Q: 練習したのですか?

A: 2, 3回練習しました。

あまり練習すると本番で間違わなくなるのであまり練習しないようにしました。実際、ライブコーディング中にいくつか間違いました。間違ったときにどうやって直していくかという過程も見てほしかったのでよかったです。

実際の開発はデバッグの連続です。間違わずに5分で完成させる動画通りにはいかないものです。

Q: 少し速いですね。

A: 使い慣れたエディタを使ったからです。

自分ではあまり意識して使っていませんが、意識してみると、以下の機能を使っているようです。以下の機能を使うことにより通常より速くコードを書くことができるかもしれません。

  • 動的略語展開: 入力途中の単語を補完します。補完候補は現在開いているすべてのバッファ内にある単語すべてです。デフォルトではM-/にバインドされていますが、補完といえばタブなのでC-c C-iにバインドしています。

    (define-key global-map "\C-c\C-i" 'dabbrev-expand)
  • "end"の挿入: ruby-modeではC-c C-eで"end"を挿入できます。
  • M-f/M-bでの移動: C-f/C-bより大きい単位で移動します。
  • C-hとC-dを同時に使う: C-hはバックスペースでC-dはdeleteですが、行内の箇所を削除する時はC-hとC-dを同時に使って削除しているらしいです。指摘されるまで自分でも気づいていませんでした。
Q: ソースコードは公開されていますか?

A: ラングバプロジェクトのSubversionリポジトリでAGPL v3+で公開しています。

リポジトリのURL
http://groonga.rubyforge.org/svn/examples/message-archiver/

当日利用したリビジョンはr874なので、以下のコマンドで同じソースコードを取得できます。

% svn co -r874 http://groonga.rubyforge.org/svn/examples/message-archiver/

札幌Ruby会議02全体の内容

仙台ともとちぎとも違う雰囲気でした。とてもとても居心地がよかったのが忘れられません。Rubyを使って気分よくプログラムを書いているときと少し似ている気がします。

最初から最後まで最前列で話を聞いていました。文脈は少し違うのですが、ちょうど興味のある話題や、前から考えていたことに関連することが多く、参考にできることがたくさんありました。

田中さんの、ユーザが余計なことを考えなくても簡単に使えるようにしたい、という話、sumimさんや谷口さんの、興味を持って学習するためにはという話、和田さんの、増加するテストに立ち向かう方法の話、高橋さんの、やめる勇気の話、角谷さんの、選んだらそれでハッピーではない、上を見てもキリがないし下は見てもしょうがないんだから自分ができることをやっていく、という話。文脈が違うので自分の中で変換しながら自分に合わせて聞いていたので、発表された方が一番言いたかったこととずれているかもしれません。

ツールでは、しだらさんの紹介していたJekyllも使ってみたくなりました。とちぎではそうでもなかったのですが、今回は使ってみたくなりました。とちぎでは駆け足だったからかもしれません。

Reject Talks

技術的なことは本編とは別のReject Talksが楽しかったです。Reject Talksでは、単に発表者が話すのではなく、観客が随時コメントをしていくという形式に(結果的に)なっていました。その中で、このコードがこう悪い、こうした方がよい、という話をできたことがとても楽しかったです。

話す側と聞く側ではなく、両者が話しあっていることがとても印象的でした。

まとめ

札幌Ruby会議02での発表内容について補足しました。また、1参加者から見た札幌Ruby会議02も簡単にレポートしました。

札幌Ruby会議02がすばらしかったのはスタッフのみなさん、発表者のみなさん、参加者のみなさんなど関係者のみなさんのおかげでしょう。でも、もう少し考えてみると、実行委員長のしまださんがいることが一番大きいのではないかと感じています。みなさんは、司会をしていたしまださんが想いがこもった発表者紹介をしていたのに気づいていましたか。想いのこもった感想を述べていたことに気づいていましたか。しまださんの発表枠はありませんでしたが、しまださんの想いを感じることができたすばらしい札幌Ruby会議02でした。

そんな札幌Ruby会議02に発表者・スポンサーとして参加できてとても光栄です。

今回はActiveLdapには触れられませんでしたが、12/14のRailsセミナーではActiveLdapに触れるので、興味のある方は参加してみてください。

あわせて読みたい

*1  公開用に多少加筆してあります。

Tags: Ruby | このエントリの Delicious history 8 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-12-07

告知: 2009/12/14のRailsセミナーはRailsとActive Directoryについて

来月中旬の12/14(月)に〜Ruby on Rails Technical Night〜Ruby on RailsセミナーでActive Directoryと連携したRailsアプリケーションの開発方法について話します。セミナーの概要は以下の通りです。都合があう方はぜひお越しください。

タイトル
Railsで作るActive Directoryと連携した社内システム
日時
2009年12月14日(月)19時30分〜21時
場所
株式会社オプト会議室
参加方法
atndで申し込み

さも1人で話すように書いていますが、そんなことはなく、株式会社ローハイド.の吉見さんと株式会社万葉の河野さんの3人で、3部構成です。運用のお話や開発時のお話をされるようなので、そちらに興味のある方も参加してみてください。

内容

内容はタイトルの通りで、RailsアプリケーションからActive Directoryの情報を利用する方法、注意しなければいけない点などについて話します。社内システムなど、すでにActive Directoryを導入している環境で動作するRailsアプリケーションを開発する場合、独自にアカウントを管理するのではなく、Active Directory上のアカウント情報を利用する方が、利用者の使い勝手もよくなりますし、運用者の負担も減ります。そういった場合にどのようにActive Directory上のアカウント情報を利用するのがよいか、ということをコード例も示しながら説明します。

Active Directoryとの接続には、ActiveLdap(参考: Rubyist Magazine - ActiveLdap を使ってみよう(前編))を使います。つまり、Active DirectoryをLDAPサーバとして使った場合のRailsアプリケーションの作り方とも言えます。よって、この話の内容はOpenLDAPなどActive Directory以外のLDAPサーバにも応用できます。LDAPサーバと連携したRailsアプリケーションに興味がある方にも楽しんでもらえるのではないでしょうか。

まとめ

来月のRailsセミナーでRailsとActive Directoryについて話すことになったので、それの告知をしました。参加費は無料なので興味のある方はお気軽に参加してみてください。現時点で定員の半分ほど埋まっているようなので、気になる方はお早めにどうぞ。

タイトル
Railsで作るActive Directoryと連携した社内システム
日時
2009年12月14日(月)19時30分〜21時
場所
株式会社オプト会議室
参加方法
atndで申し込み
つづき: 2009-12-07
Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-11-30

とちぎRuby会議02の資料公開

先日開催されたとちぎRuby会議02で社長の一人として話しました。声をかけてくれたtoRubyのみなさん、ありがとうございます。

儲かるRuby - 支えるRuby

システム構成

Debian GNU/Linux sidが動いているMacBookを持っていきました。事前の接続テストでうまくプロジェクターに出力できなかったので、ワイクル株式会社kdmsnrさんのMacBook経由で出力しました。突然のお願いにもかかわらず快く貸してくれました。ありがとうございます。

システム構成

Rabbit本体はDebian上で動いていて、携帯電話からのリモート操作もDebian上のRabbitに対して行っています。しかし、画面表示は別マシンのMac OS X上のX11で行っています。プロジェクターへの出力も別マシンのMac OS Xが行っています。

dRubyなど咳プロダクツを用いたプレゼン環境の1つのパターンのデモとして、上記のような構成を用いました。かっこいいですね。

内容

8割ほどRubyととちぎをまじえた発表者紹介・会社紹介をし、最後にかるく現在の仕事の内容、これから向かおうとしている方向を紹介しました。他の方たちのように具体的な方法は提示していません。私たちも続けられるしくみを模索しているのが現状です。

雰囲気がよい

とちぎRuby会議02はとても楽しく居心地のよい雰囲気に満ちていました。どうしてなのかはわかりません。まだ体験していない人はぜひ一度体験してみることをおすすめします。toRubyに参加すると体験できるでしょう。

まとめ

地域Ruby会議2ndシーズン最初のとちぎRuby会議02に参加しました。

お昼ご飯をゆっくり食べていたおかげで開始時刻が遅れてしまい、すみませんでした。あのよい雰囲気の中でtoRuby勉強会を味わう時間が減ってしまったことが残念です*1

とちぎRuby会議にはRuby札幌の方も参加していましたが、12月には札幌Ruby会議02があります。こちらにも発表者として参加する予定です。とちぎRuby会議02のようにとてもすばらしい地域Ruby会議になりそうな予感がします。都合のあう方は参加してみてはいかがでしょうか。

札幌Ruby会議02までには関西Ruby会議02TokyuRuby会議01もあります。こちらの内容もとてもおもしろそうですね。

*1  お昼ご飯タイムもとてもよい雰囲気でした。

Tags: Ruby | このエントリの Delicious history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-10-25

Ruby/groonga 0.0.7

Ruby/groongaとActiveGroongaの新しいバージョンがリリースされました。

いつも通り、以下のコマンドでインストールできます。

% sudo gem install groonga

0.0.7は最新のgroonga0.1.4に対応しています。

groongaが正式リリース前なので、まだRuby/groongaユーザもあまり多くはありませんが、徐々に使われはじめています。例えば、えにしテックスープカレー好きdaraさんが作ったbuzztterでRuby/groongaが使われています。

daraさんからはRuby/groongaに対するパッチをいくつかもらったりもしたので、Ruby/groongaのコミッタになってもらいました。APIの相談にものってくれる頼もしいCTOです。

今回のリリースでも便利な機能が入っているので、いくつか紹介します。

キーワードリンク

groongaを使ってはてなのようなキーワードリンクをRubyで付与することができます。

グニャラくんのところではSennaを使っていますが、同様の機能をRuby/groongaにも取り込みました。

Ruby/groongaを使うと以下のように書けます。

# -*- coding: utf-8 -*-
require 'groonga'

Groonga::Context.default_options = {:encoding => "utf-8"}
Groonga::Database.create
words = Groonga::PatriciaTrie.create(:key_type => "ShortText",
                                     :key_normalize => true)
words.add('リンク')
words.add('リンクの冒険')
words.add('冒険')
words.add('㍊')
words.add('ガッ')
words.add('MUTEKI')
text = 'muTEkiなリンクの冒険はミリバールでガッ'
tagged_text = words.tag_keys(text) do |record, word|
  "[#{word}(#{record.key})]"
end
puts tagged_text
  # => [muTEki(muteki)]な[リンクの冒険(リンクの冒険)]は
  #    [ミリバール(ミリバール)]で[ガッ(ガッ)]

クエリからスニペット生成

groongaでは独自の構文のクエリから検索条件を指定することができます。buzztterで検索条件を指定するところでも使われています。例えば、以下のような構文があります。

  • 「単語1 単語2」: 単語1と単語2の両方にマッチする条件
  • 「単語1 OR 単語2」: 単語1または単語2にマッチする条件
  • 「単語1 - 単語2」: 単語1にマッチするが単語2にマッチしない条件

もう少し詳しい説明はgroongaのチュートリアルに載っています。

ここまでは前のリリースでもできたところです。今回のリリースからはさらにスニペット(検索語周辺のテキスト)も簡単に生成できるようになりました。

# "description"カラムに「ruby」または「groonga」が入っているレコードを検索
query = "ruby OR groonga"
records = table.select do |record|
  record["description"].match(query)
end

# 「ruby」または「groonga」が含まれる周辺のテキストを表示
tags = [["<", ">"]]
records.each do |record|
  puts record["name"]
  snippet = records.expression.snippet(tags, :normalize => true)
  snippet.execute(record["description"]).each do |text|
    puts "==="
    puts record["description"] # => Rubyでgroonga使って全文検索
    puts "---"
    puts text                  # => <Ruby>で<groonga>使って全文検索
  end
end

どのようになるかはRuby/groongaのサンプルアプリケーションで試してみてください。ここの検索ボックスもクエリ文字列に対応しているので、「OR」や「-」を使ったクエリを使うことができます。

まとめ

Ruby/groonga 0.0.7の新機能を紹介しました。

groongaの機能を手軽に使えるRuby/groongaを試してみてはいかがでしょうか。

つづき: 2010-02-09
Tags: Ruby | このエントリの Delicious history 4 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-10-02

インストールするだけでActiveScaffoldのメニューを日本語化

以前、ActiveScaffoldの地域化の中で、ActiveScaffoldLocalizeを使ってActiveScaffoldのメニューを日本語化する方法を紹介しました。

時は流れて、ActiveScaffoldLocalizeから日本語メニューのリソースが削除(!)されたり、ActiveScaffold本体に各言語のリソースが含まれるようになったりしました。しかし、本体には日本語リソースが含まれていなかったため、Web上には、いまだにActiveScaffold Japanese L10Nを使う、自分で日本語リソースを作成して使う、というような情報がでていました。

本家にフィードバックして取り込んでもらえれば、多くの人がより手軽に日本語化されたActiveScaffoldを使えるだろうに、ということで、先日、本家に日本語リソースを取り込んでもらいました。これからは、ActiveScaffoldLocalizeなどを使わずにActiveScaffold本体のみで日本語メニューを使うことができます。

手順

簡単に日本語メニューのActiveScaffoldを使う手順を説明します。

まず、ActiveScaffoldに依存しない、一般的なI18nまわりを整備します。

% rails shelf
% cd shelf
% script/generate resource book title:string
% rake db:migrate
% gem install amatsuda-i18n_generators -s http://gems.github.com/
% script/generate i18n ja

続いて、ActiveScaffoldまわりを整備します。

% script/plugin install git://github.com/activescaffold/active_scaffold.git

config/routes.rb:

-  map.resources :books
+  map.resources :books, :active_scaffold => true

app/controllers/books_controller.rb:

class BooksController < ApplicationController
  active_scaffold :book
end

app/views/layouts/application.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <title>ActiveScaffold l10n</title>
    <%= javascript_include_tag(:defaults) %>
    <%= active_scaffold_includes %>
  </head>

  <body>
    <h1>ActiveScaffold l10n</h1>
    <%= yield %>
  </body>
</html>

これでメニューが日本語化されます。

日本語メニューのActiveScaffold

日本語関連の手順は最初のI18nまわりのところだけです。ActiveScaffold関連の手順にはまったく日本語関連の手順はありません。通常のインストール手順で日本語メニューが表示されるのは、ActiveScaffold本体に日本語リソースが含まれるようになったおかげです。

データを入れるとこのようになります。

日本語メニューのActiveScaffold(データ入り)

まとめ

ActiveScaffold本体に日本語リソースが含まれて、より簡単に日本語メニューのActiveScaffoldを使えるようになったことを紹介しました。

フリーソフトウェアを改良してよりよくした場合は、手元での変更やブログに書くにとどめずに、本家にフィードバックしてみてはいかがでしょうか。より多くの人が便利に使えるようになるだけではなく、多くの場合、自分のメンテナンスコストも下がります。

Tags: Ruby | このエントリの Delicious history 4 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-09-28

るびま0027号

週末にるびま0027号がリリースされました。5周年だそうです。おめでとうございます。

ささださんのコメント

n周年のときに毎年ささださんがコメントを書いていますが、今年のささださんのコメントが明るめになっているのが印象的です。「脱ささだ体制」が進んでいる影響でしょうか。

URLを集めていて気づいたのですが、4周年のときだけ「ko1-comment」ではなく、「4th-ko1」なんですね。

おすすめ記事

今回のるびまにはActiveLdapの日本語チュートリアルなどで活躍されている高瀬さんActiveLdap を使ってみよう(前編)という記事があります。

前編ということだけあり、基本的な部分から丁寧に解説されています。なかでも、前半のLDAPの説明部分はLDAPを知らない方にもわかりやすくまとまっています。ActiveLdapの入門だけではなく、LDAPの入門としても参照しやすい記事です。

ActiveLdapについても便利に使える感じが伝わる記事で、後編が楽しみです。

RubyでLDAPを操作したい場合はぜひ高瀬さんの記事を参考にしてください。

まとめ

るびま0027号の興味深い記事を2つ紹介しました。

自分でも何かできることはないか、と思っている方は、ただのRubyistであるかずひこさんのRubyist にできることがおすすめです。あるいは、記事の提供や編集でるびまに参加するというのもいかがでしょうか。

まだRubyNewsを読んだことがない方は一度読んでみてはいかがでしょうか。日本Ruby会議2009の間にたくさんのフリーソフトウェアがリリースされたことがわかります。

今回もここでは紹介しきれないくらい盛りだくさんのるびま0027号をぜひ読んでみて下さい。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-09-14

とちぎRuby会議02参加登録受付中

10月24日に開催されるとちぎRuby会議02の参加登録が行われています。懇親会参加の状況から推測するとまだ登録できそうです。

今回は、超優良企業の1つとして声をかけてもらい、出場者として参加できることになりました。ワイクル角さんヴァインカーブやまだあきらさん、タワーズ・クエストの和田さんも出場者として参加されます。とても楽しみですね。

咳さんdRuby本をテキストとしたtoRuby勉強会も開催されるそうです。こちらもとても楽しみですね。

都合があう方は参加してみてはいかがでしょうか。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-09-11

もうすぐRetrospectiva 2.0がリリース

Rubyで実装されたプロジェクト管理システムであるRetrospectivaのバージョン2.0がまもなくリリースされます。cozmixng.orgで最新バージョンが運用されているので、それを触ってみることで最新の機能を確認することができます。見てもらえばわかる通り、日本語表示にも対応しています。

Retrospectivaは一時期開発が停滞していて、その間にRedmineの方が普及しました。しかし、その後、再び開発が活発になり、現在は2.0 RC1がリリースされています。1.xから2.0では多くの改良が行われています。そのいくつかを紹介します。

簡単インストール

Single Step Installerが用意されていて、コマンド一発でインストールできるようになっています。以前より導入の敷居が下がっています。

アジャイル開発支援

AgilePMというアジャイル開発を支援するプラグインが公開され、プロジェクト管理機能がさらに充実しています。tDiaryプロジェクト用のAgilePMがあるので、そこで触ってみることができます。ただし、現時点ではまだ利用されていないのであまり雰囲気がわからないかもしれません。これからのtDiaryプロジェクトの利用に期待しましょう。

git対応

Subversionだけではなく、gitにも対応しました。また、Retrospectiva自体のバージョン管理システムもSubversionからgitに移行しています。

最近はgitを採用するプロジェクトも増えているため、これは嬉しい機能ではないでしょうか。

まとめ

まもなくリリースされるRetrospectiva 2.0を簡単に紹介しました。以前は「ブログがついたTrac」みたいな書かれ方をされていたRetrospectivaですが、実際に使ってみるとその表現が間違っていたことに気付いた人も多かったのではないでしょうか。以前からコミットログで連携する機能などがあり、使っていた人は「便利なプロジェクト管理ツール」という方がしっくりくることに気付いていたはずです。2.0ではより便利で有用な機能がスマートなインターフェイスで追加されています。2.0の紹介のために「ブログがついた〜」と書かれることは減ることでしょう。

Redmineもよいですが、プロジェクト管理ツールとしてRetrospectivaも検討してみてはいかがでしょうか。

もし、使用してみてRetrospectivaの開発に参加したくなった場合はRetrospectivaを使って開発に参加するとよいでしょう。まずは、未翻訳メッセージの翻訳から参加するのが敷居が低いでしょう。kou@clear-code.comまで連絡してもらえれば相談にのります。

Tags: Ruby | このエントリの Delicious history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-08-07

groongaをRackに載せて全文検索

Ruby/groongaのサンプルアプリケーションのデモを用意しました。

RailsなどのWebアプリケーションフレームワークを使うほどのものではないので、ActiveGroongaは使わずに、Ruby/groongaとRackの組み合わせになっています。Rackについてはyharaさんの5分でわかるRackなどを読んでみてください。

デモはPassengerで動かしています。PassengerにRackを設置したことがある人なら10分もかからずにサンプルを動かせるのではないかと思います。

機能

デモを見てもらえばわかる通り、小さなサンプルですが以下のように一通りの機能は備えています。

  • 複数キーワードによる絞り込み
  • スコア順による並べ替え
  • 検索キーワードの正規化(「Ruby」でも「ruby」でも検索可能)
  • キーワード周辺の文章の表示

それぞれ、もう少し詳しく見ていきましょう。

複数キーワードによる絞り込み

通常の検索サイトでは空白で複数のキーワードを区切ることによって検索結果を絞り込むことができます。例えば、「Ruby クリアコード」で検索すると、「Ruby」と「クリアコード」両方にマッチするページがヒットします。いわゆるAND検索です。

まず、1つのキーワードだけを扱う場合のコードを示して、次に複数のキーワードを扱うコードを示します。

1つのキーワードだけを扱う場合はとても単純です。3行です。

# 文書が格納されたテーブルを取得
documents = Groonga::Context.default["documents"]
# 文書テーブルから指定されたキーワードにマッチするレコードを検索
records = documents.select do |record|
  # HTTPの"query"パラメータで指定された単語が
  # "content"カラムにマッチするかチェック
  record["content"] =~ request["query"]
end

全文検索を指示するために「=~」演算子を使うなんてとてもRubyらしい書き方ですね。

複数のキーワードで絞り込みを行う場合はrecord["content"] =~ "keyword"という条件をANDでつなげていきます。イメージは以下の通りです。

records = documents.select do |record|
  (record["content"] =~ keyword1) &
    (record["content"] =~ keyword2) &
    ...
end

サンプルではこのようなコードになっています。

words = request["query"].split
records = documents.select do |record|
  expression = nil
  words.each do |word|
    sub_expression = record["content"] =~ word
    if expression.nil?
      expression = sub_expression
    else
      expression &= sub_expression
    end
  end
  expression
end

ちなみに、injectを使うとこうなります。

records = documents.select do |record|
  words.inject(nil) do |expression, word|
    sub_expression = record["content"] =~ word
    if expression.nil?
      sub_expression
    else
      expression & sub_expression
    end
  end
end

お好みでどうぞ。

スコア順による並び替え

このサンプルでは、「同じ文書中に何回キーワードが出現するか」をスコアとして扱っています。スコアは検索結果のレコードが持っているので、それを使って並び替えます。1行です。

# スコアの大きい順に並び替えて、上位20件だけ使う。
records = records.sort([[".:score", "descending"]], :limit => 20)

groongaでは「:」から始まる特別なアクセス用の名前があります。「:score」もその1つでスコアの値にアクセスするために使います。「:score」の他にはレコードのキーにアクセスする「:key」などがあります。

検索キーワードの正規化

groongaは、全文検索用の索引を作るときにキーワードを正規化することができます。これにより大文字小文字を区別せず「Ruby」でも「ruby」でも同じように検索することができます。

正規化するためにしなければいけないことは、索引用テーブルを作成する時に:key_normalize => trueオプションを指定するだけです。

Ruby/groongaではテーブルやカラムを定義するためのDSLを用意しています。サンプルのためのテーブル・カラム定義は以下のようになっています。少しActiveRecord風です。

# スキーマ定義開始
Groonga::Schema.define do |schema|
  # 文書格納用テーブル作成
  schema.create_table("documents") do |table|
    table.string("title") # 文書のタイトル
    table.text("content") # 文書の内容
    table.string("path")  # 文書の置き場所
    table.time("last-modified") # 文書の最終更新時刻
  end

  # 索引用テーブル作成
  schema.create_table("terms",
                      :type => :patricia_trie,
                      :key_normalize = true, # キーワードを正規化
                      :default_tokenizer => "TokenBigram") do |table|
    table.index("documents.title")   # 文書のタイトルの索引を作成
    table.index("documents.content") # 文書の内容の索引を作成
  end
end

一応コメントを入れましたが、コメントがなくても何をしているのかがわかったのではないでしょうか。

:key_normalize => trueを指定しておくと、あとはgroongaがうまいことやってくれるので、検索時には特に何もする必要はありません。

キーワード周辺の文章の表示

キーワード周辺の文章を表示することにより、その文書が探している文書かどうかを判断しやすくなります。

たとえば、「Ruby」で検索するとRuby-GNOME2 0.18.0リリース*1がヒットしますが、その場合は「...されました。 Ruby-GNOME2はGTK+を含むGNOME関連ライブラリのRubyバインディング...」という文章も一緒に表示されます。これがあれば、文書を全部読まなくてもおおよその内容を想像しやすくなります。

この機能はKWICやスニペットなどと呼ばれていて、groongaではスニペットと呼んでいます。

スニペットの生成は以下のようになります。

# キーワードを囲むタグ
open_tag = "<span class=\"keyword\">"
close_tag = "</span>"
# スニペットオブジェクトの作成
snippet = Groonga::Snippet.new(:width => 100,
                               :default_open_tag => open_tag,
                               :default_close_tag => close_tag,
                               :html_escape => true,
                               :normalize => true) # キーワードを正規化
# 検索キーワードを登録
request["query"].split.each do |word|
  snippet.add_keyword(word)
end

# 本文からスニペットを生成
segments = snippet.execute(record[".content"])
# 整形
separator = "\n<span class='separator'>...</span>\n"
snippet_text = segments.join(separator)
response.write("<p class=\"snippet\">#{snippet_text}</p>")

整形用のタグを入れるコードも一緒になっているので多少長くなっていますが、スニペット作成のための処理は以下の3ステップだということがわかります。

  1. Groonga::Snippet.new
  2. snippet.add_keyword
  3. snippet.execute

簡単ですね。

まとめ

サンプルアプリケーションを例にして、Ruby/groongaを使うと実用的な機能が揃った検索ページを簡単に作成できることを紹介しました。

サンプルアプリケーションはリリースされたばかりのRuby/groonga 0.0.6の中に入っています。

サンプルアプリケーションを参考にしながらgroongaを使った全文検索ページを作ってみてはいかがでしょうか。

最後に、コマンド列でサンプルアプリケーションのセットアップの方法を示します。

% sudo gem install groonga
% cp -r `gem environment gemdir`/gems/groonga-0.0.6/example/ ./
% cd example/search
% ../index-html.rb data/database ~/public_html/ # 最後の引数はHTMLのあるディレクトリ
% rackup config.ru
% firefox http://localhost:9292/

*1  そういえば、先日、Ruby-GNOME2 0.19.1がリリースされました。

つづき: 2009-12-21
Tags: Ruby | このエントリの Delicious history 8 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-07-31

日本Ruby会議2009の資料公開

日本Ruby会議2009で発表した資料を公開しました。

発表を聞いてくれたみなさん、ブースにきてくれたみなさん、スタッフのみなさん、ありがとうございました。

つづき: 2009-12-21
Tags: Ruby | このエントリの Delicious history 5 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-07-19

日本Ruby会議2009でのリリース予定

せっかくの機会なので、日本Ruby会議2009期間中にフリーソフトウェアをいくつかリリースする予定です。

リリース準備ができているフリーソフトウェア

以下のフリーソフトウェアはリリース準備ができているので、会場のネットワーク環境をうまく使えれば期間中にリリースします。

午前11時頃か午後3時頃にスポンサーブース内でリリース作業をする予定です。 日本Ruby会議2009は3日間あるので、リリース作業はそれぞれの日に分散させる予定です。

リリースできる可能性があるフリーソフトウェア

もしかしたら、以下のフリーソフトウェアもリリースできるかもしれません。

1つ実装したい機能が残っているので、それが実装できればリリースします。が、時間的に厳しそうです。

リリースしたかったフリーソフトウェア

以下のフリーソフトウェアもリリースしたかったのですが、準備が十分ではないので、おそらくリリースできないと思います。残念です。

Ruby/Libglade2にGC関連のSEGVバグがあり、それを修正する時間がとれなかったので、日本Ruby会議2009のタイミングではリリースできそうにありません。

また、ソフトウェアではありませんが、GUI関連でRuby GUI調査2008のレポートを日本語に翻訳したものもリリースしたかった*1のですが、間に合いませんでした。Alexさんからは編集可能な形で原文をもらっているので、もし、協力してくれる方がいればkou@clear-code.comまでご連絡ください。

まとめ

日本Ruby会議2009期間中にリリースを予定しているフリーソフトウェアを紹介しました。

RSS Parserrcairoなど、ここに挙げたフリーソフトウェア以外でも構いませんので、なにかコメントなどがあればスポンサーブースで声をかけてください。

*1  永井さんがLightning TalksでRuby/Tkは本当にダメな子なのか?というお話をするようですし。

2009-07-15

日本Ruby会議2009の発表セッション

日本Ruby会議2009のセッション詳細が公開されました。

日本Ruby会議2009は3トラックで3日間の開催のため、たくさんのセッションがありますが、このうち、2セッションで発表します。

ActiveLdap - 2009年07月18日土曜日 14:30-15:30 Lightning Talks

1つ目は2回目のライトニングトークの一番最後です。

ライトニングトークではActiveLdapチュートリアル)について話します。ActiveLdapの開発に関わるようになってから約3年経ちますが、ついに発表する機会に恵まれました。

クリアコードは今月から第四期に入っていますが、ActiveLdapはクリアコード設立初期にLDAPを利用する機会があったことがきっかけで開発に関わるようになったフリーソフトウェアです。当時は多くの問題を抱えていたActiveLdapですが、そのときからコツコツ開発を続けていたため現在では当時よりもかなり便利なライブラリとなりました。その成果を日本Ruby会議で発表できることはとても感慨深いものです。

CとRubyとその間 - 2009年07月18日土曜日 16:00 - 18:30

2つ目は同じ日の同じ会場の次の枠で、CとRubyとその間です。

クリアコードではActiveLdap以外にもたくさんのフリーソフトウェアの開発に関わっていますが、その中でもCとRubyそれぞれのよいところを活かしたフリーソフトウェアについて話します。この発表ではそのようなフリーソフトウェアの例としてmilter managerActiveGroongaを紹介しながら、CとRubyを活かすことのメリットについて話します。

Rubyから使える高速なkey-valueストアとしてはTokyo CabinetLocalmemcacheが有名です。

ActiveGroonga(とその下の層のRuby/groongaは)これらと同様に高速なkey-valueストア機能も備えるgroongaをよりRubyらしく使いやすいAPIで提供します。

milter managerについては最近いろいろなところで話しましたが、ActiveGroongaについて話すことは今回が初めてです。

まとめ

日本Ruby会議2009で発表するセッションを紹介しました。他にも興味深いセッションがたくさんあるので迷うと思いますが、興味があれば上記のセッションにも参加してみてください。

クリアコードはスポンサーとなっているため、スポンサーブースを出します。セッションには参加できなかった方も、ぜひ、足を運んでください。

つづき: 2009-07-19
Tags: Ruby | このエントリの Delicious history 1 user | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-07-06

るびま0026号

先日、るびま0026号がリリースされました。

今回はRegionalRubyKaigi特集のようで、5Kaigiのレポートが載っています。5Kaigiのうち、仙台Ruby会議01に参加しましたが、そのレポートもあります。

仙台Ruby会議01

仙台Ruby会議01のレポートid:monyakataさんが丁寧にすっきりとまとめてくれています。とても読みやすいので参加できなかった人は読んでみてはいかがでしょうか。

レポートによるとまず好きなこと、そしてそれを続けることのセッションが一番参加者が多かったそうです。参加してくれた皆さん、関係者の皆さん、ありがとうございました。

レポート中にある通り、仙台Ruby会議01のサイトには東北・仙台情報が豊富にあります。もし、レポートを読んで仙台を感じたくなったときはサイトにある情報が役に立つことでしょう。お菓子やたいやき情報もあるので、甘いものが好きな人はぜひ参考にしてください。

RubyNews

あまり話題にのぼらない感がある常設コーナーのRubyNewsですが、毎回充実しています。読むと、Ruby界隈では思ったよりいろんなことがあったなぁと感じることができます。今回のRubyNewsはいつも以上に充実しています。読んでみると知らないことも多いかもしれません。

最近は助田さんがruby-talkでリリースアナウンスのあったソフトウェアを紹介していて、海外のRuby関連ソフトウェアに関する情報源としてとても貴重です。実は、RubyNewsにはruby-listでリリースアナウンスのあったソフトウェアが載っているので、日本でのRuby関連ソフトウェアのとても貴重な情報源になっています。

もちろん、肉リリースされたRabbitの情報も載っていますし、RSS Parserのリリースも載っています。ソフトウェアをリリースしたときはruby-listでもリリースアナウンスをしてみてはいかがでしょうか。きっとRubyNewsに載せてもらえます。

まとめ

るびま0026号がリリースされました。るびまは今回のRegionalRubyKaigiレポートのように毎号変わるコーナーにも有用な記事が多くありますが、実は常設コーナーの中にも有用な情報があります。常設コーナーを読み飛ばしている方はもったいないですよ。

高橋編集長の巻頭言はよく読まれていると思いますが、編集後記もわりとおもしろいですよ。

仙台Ruby会議01レポートだけではなく、zundaさん編集のRubyNewsも紹介してみました。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-07-02

オープンソースカンファレンス2009 Hokkaido 北海道情報セキュリティ勉強会枠での資料公開

まっちゃだいふくさんに声をかけてもらったことがきっかけで、オープンソースカンファレンス2009 Hokkaidoのせきゅぽろ枠でmilter managerの話をしてきました。声をかけてくれたまっちゃだいふくさん、参加してくれたみなさん、ありがとうございました。

資料: milter manager

また、Ruby札幌がUstreamで配信してくれたので、動画もあります。

動画: OSC 2009 Hokkaido milter manager

内容

今回はmilterとmilter managerの話をする前に、迷惑メール対策の現状と有効な対策方法についても話しました。これは、第2回静岡ITPro勉強会での佐藤さんの公演内容を参考資料として利用しています。利用を快諾してくれた佐藤さんありがとうございます。札幌のみなさんにも迷惑メール対策の現状と有効な対策方法を伝えられたのではないかと思います。

一般的な迷惑メール対策の話の後にmilterとmilter managerの話をしました。第2回静岡ITPro勉強会の時よりも時間が少ないということもあり、今回はあまり突っ込んだ話をせずに、雰囲気が伝わる程度に抑えました。

話の後、司会をしてくれたまっちゃだいふくさんが今回省略したあたりをフォローしてくれました。ありがとうございます。

まっちゃだいふくさんは勉強会の時間外に、いろいろアドバイスをしてくれます。そのため、発表者として参加したこちらもとても勉強になっています。

そして、まっちゃだいふくさんがもってきてくれたお菓子はとてもおいしかったです。

Ruby札幌

せきゅぽろ枠の時間以外はRuby札幌にお世話になりました。

Ruby札幌とせきゅぷろの枠はセミナーに参加したのですが、それ以外の時間はRuby札幌のブースにおじゃまさせてもらいました。daraさんからbuzztterのバックエンドをgroongaにしたいということを聞いたので、Ruby/groongaで実現するために少し相談しました。Ruby/groonga 0.0.3はbuzztterのバックエンドとして使える機能を提供することになるでしょう。

今回、ActiveLdapを使っている島田さん以外のRuby札幌の人たちとも話すことができたのはよかったです。ActiveLdapやRSS Makerあたりのレビューにも参加することができました。

今回、Ruby札幌の人たちはとても人柄がよいことがわかりました。とてもすばらしいです。また、Ruby札幌がRabbitを応援していることもすばらしいです。

まとめ

オープンソースカンファレンス2009 Hokkaidoのせきゅぷろ枠でmilter managerの話をしてきました。この話は、まっちゃだいふくさんのおかげで実現しました。話の後のフォローなどいろいろありがとうございます。

Ruby札幌はすばらしいです。Ruby会議2009ではニュースがあるようですし、その後には札幌Ruby会議02もあるようです。Ruby札幌から目が離せません。

つづき: 2009-12-21
2009-06-21

ActiveLdap 1.0.9リリース

LDAPのエントリをRubyオブジェクトとして操作するためのライブラリActiveLdapの1.0.9がリリースされました。([ANN] ActiveLdap 1.0.9

ActiveLdapを知らない人はチュートリアルを読んで雰囲気を感じてください。

1.0.9

1.0.9の目玉はRuby 1.9.1とRails 2.3.2の対応です。特に、Rails 2.3.2の対応は待ち望まれていた機能です。trunkではずいぶん前から対応していたにも関わらずリリースされていなかったため、ユーザの方には不便な思いをさせてしまっていました。

1.0.9はそのバージョン番号からもわかるかもしれませんが、近いうちに1.1.0がリリースされるという意味が含まれています。逆に言うと、1.1.0に必要な機能を追加するまでRails 2.3.2に対応したリリースがない状況を解決するために、1.1.0の一部機能が未実装のリリースが1.0.9になります。

とはいえ、1.0.9から1.1.0の間にAPI非互換がでるというわけではないので1.0.9とRails 2.3.2の組み合わせは安心して使えます。1.1.0では1.0.9よりも(非互換なしで)パワーアップするということです。

まとめ

Ruby 1.9.1とRails 2.3.2に対応したActiveLdap 1.0.9がリリースされました。Ruby 1.9.1やRails 2.3.2と一緒にActiveLdap 1.0.9を使ってください。

近いうちに1.1.0もリリースされる予定なので、そちらも楽しみにしていてください。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-06-08

Ruby/groonga 0.0.2リリース

最新のgroongaに対応したRuby/groonga 0.0.2がリリースされました。

Ruby/groonga 0.0.2ではよりAPIが使いやすくなっています。

メソッドの個別化

groongaはgrn_objで抽象化されていて、ハッシュテーブルでも転置インデックスカラムでもgrn_obj_search()で検索できます。Ruby/groongaでもそれを踏襲してGroonga::Object#searchだけを定義して使いまわしていました。しかし、0.0.2ではGroonga::Hash#serachやGroonga::IndexColumn#searchなど、それぞれのオブジェクト毎に定義するようにしました。

こうすることにより以下のような挙動になるため、使いやすいAPIになりました。

  • grn_obj_search()に対応していないオブジェクト(例えばGroonga::Array)に対して#searchしようとするとNoMethodErrorと適切に問題を報告する。
  • 省略可能なオプション引数をより正確に検証して適切なエラーを報告する。
  • それぞれの#search毎にドキュメントを用意することができるので、適切な内容のドキュメントがかかれたAPIになる。

利用できないのであれば、メソッドが定義されていない方がよいAPIだと思います。無駄なものがない方が適切なAPIに誘導しやすくなります。

無駄なものはない方がよいということは、メソッドだけではなく、省略可能なオプション引数にも言えます。1つのメソッドで何でもやろうとすると余計なオプションまで受け付ける必要があります。あるいは、余計なオプションを排除するためのコードが増えてしまいます。こうならないために、適切な粒度で別々のメソッドを定義することが有効です。

例えば、オプション名を検証するコードは以下のように書けます。(エラーメッセージに入力値と問題となった値を両方含めていることにも注意してください。問題を解決するための重要な情報です。)

def search(options={})
  valid_keys = [:name, :path]
  invalid_keys = options.keys - valid_keys
  unless invalid_keys.empty?
    message = "invalid option name(s): #{invalid_keys.inspect}: #{options.inspect}"
    raise ArgumentError, message
  end
end

もし、1つのメソッドでたくさんの状況を考慮しなければいけないとこのようになります。

def search(type, options={})
  case type
  when :fast
    valid_keys = [...]
  when :remote
    valid_keys = [...]
  else
     raise ArgumentError, "invalid type: ..."
  end

  invalid_keys = options.keys - valid_keys
  unless invalid_keys.empty?
    message = "invalid option name(s): #{invalid_keys.inspect}: #{options.inspect}"
    raise ArgumentError, message
  end

  case type
  when :fast
    query = options[:query]
    ...
  when :remote
    remote = DRbObject.new("druby://#{options[:host]}:2929")
    remote.search(options[:query])
  ...
  end
end

これよりは、メソッドを分けた方がすっきりします。

def fast_search(options={})
  valid_keys = [...]
  ...
end

def remote_search(options={})
  valid_keys = [...]
  ...
end

...

あとは、総称的なメソッドを1つ用意すればメソッド分割前と同じように使えます。

def search(type, options={})
  case type
  when :fast
    fast_search(options)
  when :remote
    remote_search(options)
  ...
  end   
end

ここまできたらもう一歩です。オブジェクト指向プログラミングでcase whenやswitch caseで分岐している時はオブジェクトが足りない匂いを感じとってください。このような場合はそれぞれの条件毎にオブジェクトを作り、それぞれのオブジェクトで同じ名前のメソッドを定義します。

class FastSearcher
  def search(options={})
    ...
  end
end

class RemoteSearcher
  def search(options={})
    ...
  end
end

これで、条件分岐がなくなり、総称的なメソッドも定義しなくてもよくなります。それらは言語がやってくれるからです。

searcher = FastSearcher.new
searcher.search(:query => ...)

と、だいぶ遠回りをしましたが、Ruby/groonga 0.0.2では以上のようなAPI設計ポリシーに従って、オブジェクト毎にメソッドを実装するようになりました。これにより使いやすさが向上しています。

また、メソッドが分割されることにより、ドキュメントを書きやすくなります。読む側も読みやすくなります。

まとめ

Ruby/groonga 0.0.2はAPIの使いやすさが向上しています。これは、適切な粒度に実装を分割したからです。使いやすいAPIを検討しているのであれば、実装の粒度を細かくすることを検討してみてください。無駄がなくすっきりして使いやすいAPIになるかもしれません。

Tags: Ruby | このエントリの Delicious history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-06-05

Ruby 1.9.1とREXMLとXML宣言のエンコーディング

Ruby 1.9.1付属のREXMLではXML宣言のエンコーディングの扱いに問題があるためvalidなXMLでもパースできない場合があるという話です。

問題

Ruby 1.9では文字列や正規表現がエンコーディング情報を持つため、REXMLのように正規表現ベースでXMLをパースしている場合は、エンコーディングを適切に設定しないとパースに失敗することがあります。

例えば、tDiaryのseach-yahoo.rbプラグインがこの問題に遭遇しています。

原因

REXMLは内部でUTF-8を用いています。そのため、パース対象のXMLのエンコーディングをUTF-8に変換しながらパースします。この処理はREXML::SourceまたはREXML::IOSourceで行われます。

しかし、REXML::IOSourceに問題があり、UTF-8に変換しないままパースしてしまう場合があります。これは、入力XMLのエンコーディングがUTF-8に設定されていない、かつ、XML宣言のエンコーディングがUTF-8になっている場合です。ちなみに、REXML::Sourceではこの問題は起きません。

tDiaryのsearch-yahoo.rbでは入力XMLのエンコーディングがASCII-8BITでXML宣言のエンコーディングがUTF-8になっていたため問題に遭遇しました。

search-yahoo.rbではopen-uriを使って入力XMLをHTTP経由で取得しています。open-uriはContent-Typeを見て適切なエンコーディングを設定してくれますが、今回はcharsetが指定されていなかったとのことです。このため、open-uriで取得した入力XMLがASCII-8BITになっていました。

xml = open("http://.../xxx.xml") {|f| f.read}
xml.encoding # => ASCII-8BIT
document = REXML::Document.new(xml) # => パースエラー

解決法

この問題に遭遇してしまった場合は、以下のような解決法があります。

  • 入力XMLのエンコーディングをUTF-8に設定する。
  • REXML::IOSourceの代わりにREXML::Sourceを使う。
  • パッチ付きでバグ報告済みなので修正されるのを待つ。

入力XMLのエンコーディングをUTF-8に設定する場合は以下のようになります。

xml = open("http://.../xxx.xml") {|f| f.read}
xml.force_encoding("utf-8")
document = REXML::Document.new(xml)

REXML::Sourceを使う場合は以下のようになります。

xml = open("http://.../xxx.xml") {|f| f.read}
document = REXML::Document.new(REXML::Source.new(xml))

修正されるのを待つ場合は、修正されるまで待ってください。

まとめ

Ruby 1.9で正規表現ベースのコードがうまく動かない場合はマッチ対象の文字列のエンコーディングを確認しましょう。

ちなみに、REXML::IOSource#matchではエンコーディング関係のエラーを握りつぶしているため、実際に発生するREXML::ParseExceptionだけ見てもエンコーディングミスマッチがどこで起こっているかはわかりません。問題が発生したときは問題解決につながるエラーメッセージを提供したいものですね。

Tags: Ruby | このエントリの Delicious history 5 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-05-11

Ruby/groonga 0.0.1リリース

データベース機能も備える全文検索エンジンgroongaをRubyから利用するための拡張ライブラリRuby/groongaがリリースされました。

Ruby/groongaはRubyGemsに対応しているので、以下のようにコマンド一発でインストールできます。(事前にmakeやgccやRubyのヘッダファイルなど拡張ライブラリのビルドに必要なソフトウェアを揃えておいてください。)

% sudo gem install groonga

Ruby/groongaを利用するためには最新のgroonga 0.0.4が必要ですが、もし、システムにインストールされていない場合は自動的にダウンロードし、groongaのRubyGemsディレクトリの中にインストールします。この場合、最適化オプション(gccの-O2オプション)付きでビルドされますが、最適化オプション付きでgroongaをビルドすると、とても時間がかかります。(30分とか。)慌てずにのんびり待ってください。*1

サンプル

Ruby/groongaでは、よりRubyらしい読み書きしやすいAPIでgroongaを利用できるようにすることを目的としています。例えば、groongaのテーブルはRubyのHashのように扱うことができます。また、テーブルやカラムの型に応じてRubyとgroonga上のデータを適切に変換することにより、特別なことを意識せずにgroongaのデータベース機能を使うことができます。

Cで書かれたインデックスを自動更新するプログラムをRubyで書くとこうなります。

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'rubygems'
require 'groonga'

# 初期化
Groonga::Context.default_options = {:encoding => :utf8}
Groonga::Database.create

# テーブル定義
## <bookmarks>テーブルとそのカラムたちを定義
bookmarks = Groonga::Array.create(:name => "<bookmarks>")
bookmarks.define_column("uri", "<shorttext>")
bookmarks.define_column("comment", "<shorttext>")

## <lexicon>テーブルとそのカラムたちを定義
lexicon = Groonga::Hash.create(:name => "<lexicon>",
                               :key_type => "<shorttext>")
## MeCabで検索用単語を切り出す
lexicon.default_tokenizer = "<token:mecab>"
comment_index_column = lexicon.define_column("comment-index", bookmarks,
                                             :type => "index")

# ポイント: インデックス自動更新の設定
comment_index_column.source = "<bookmarks>.comment"

# ブックマークの登録: 3件
def add_bookmark(bookmarks, uri, comment)
  bookmark = bookmarks.add
  bookmark["uri"] = uri
  bookmark["comment"] = comment
end

add_bookmark(bookmarks,
             "http://groonga.org/",
             "an open-source fulltext search engine and column store")
add_bookmark(bookmarks,
             "http://qwik.jp/senna/",
             "an embeddable fulltext search engine")
add_bookmark(bookmarks,
             "http://cutter.sourceforge.net/",
             "a unit testing framework for C")


# 検索: 2回
def search(comment_index_column, word)
  result = comment_index_column.search(word)
  puts("search result: <#{word}>: #{result.size}")
  puts("uri\t\t\t | comment")
  result.each do |record|
    bookmark = record.key
    puts("#{bookmark['uri']}\t | #{bookmark['comment']}")
  end
  puts
end

search(comment_index_column, "search")
# 結果: <search>で検索したら2件ヒット
# search result: <search>: 2
# uri			 | comment
# http://groonga.org/	 | an open-source fulltext search engine and column store
# http://qwik.jp/senna/	 | an embeddable fulltext search engine

search(comment_index_column, "testing")
# 結果: <testing>で検索したら1件ヒット
# search result: <testing>: 1
# uri			 | comment
# http://cutter.sourceforge.net/	 | a unit testing framework for C

注意事項

Ruby/groongaには以下のような既知の問題があります。

  • RubyのGCでgroongaのオブジェクトを開放しすぎてしまう
  • それほど高速化のためのチューニングをしていない
  • リファレンスマニュアル・チュートリアルが半分位しか用意されていない
  • APIがまだ流動的

GCの問題は、groongaのgrn_ctxというメモリ管理機能を提供するオブジェクトとRubyがオブジェクトをどの順番でGCするかはわからないという動作のために起きています。これはgroonga本体とも協調しながら解決する予定です。ちなみに、通常のアプリケーションでこの問題が発生する可能性があるのはプロセスの終了時だけです。そのため、この問題のためにデータが壊れてしまうということはないと考えられます。

今回のリリースではよりRubyらしいAPIの提供の優先度を高くしたため、高速化のためのチューニングはそれほど行われていません。いくつかチューニング案があるので、それらを適用することにより、Rubyの読み書きしやすいAPIを利用しながらより高速な全文検索機能とデータベース機能を利用できるようになるでしょう。

ドキュメントが完備されていないのは、APIがまだ流動的なことやgroongaのすべての機能を網羅していないこととも関係があります。groonga本体もまだAPIが改良され続けています。それに追従したり、より使いやすいAPIを目指してRuby/groongaのAPIはこれから変更されるでしょう。その過程でドキュメントも充実していく予定です。

次のステップ

ラングバプロジェクトではRuby/groongaの開発に参加してくれる人を募集しています。興味のある方は開発者向け情報をご覧ください。

クリアコードではRubyの拡張機能を書けるプログラミングが好きな開発者を募集しています。興味のある方は採用情報をご覧ください。

*1  -O0オプション(非最適化オプション)をつけるとすぐにビルドできます。

つづき: 2009-12-21
Tags: Ruby | このエントリの Delicious history 12 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-04-30

ActiveLdap: ldap_mapping

これまで、ActiveLdapにはまとまった日本語の情報がありませんでしたが、id:tashenさんがActiveLdapのチュートリアルを翻訳してくれています。原文に最新の状況に追従していない部分があるため、いくつか古い情報もあるのですが、現在、最新の状況に追従するように作業が進んでいます。(最初の方はわりと最新の状況に追従しています。)

せっかくなので、ldap_mappingのあたりを簡単に説明します。できるなら、この内容を本家のドキュメントにうまくマージしたいと思っています。

ldap_mapping

ActiveRecordでは何もしなくてもカラムへアクセスすることができるのですが、ActiveLdapではldap_mappingでLDAPとRubyのオブジェクトを対応させる必要があります。ActiveRecordでは1つのテーブルが1つのクラスに対応し、各レコードがインスタンスに対応しますが、LDAPではそのような対応関係を自動的に判断することが難しいためです。どのようなエントリの集合を1つのクラスに対応させるかはアプリケーション毎に異なります。そのため、ActiveLdapでは明示的にユーザに指定してもらう方法をとっています。適切なデフォルト値がない場合はデフォルト値を提供しない方が混乱しないと思います。

ldap_mappingでは以下の3つの情報を使ってクラスに対応するエントリの集合を決めます。

  • objectClass
  • 検索対象のツリー
  • 検索範囲

objectClass

オブジェクトクラスposixGroupに属しているエントリを対象とする場合は以下のようになります。

class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"], ...
end

これで、posixGroupに属するLDAPエントリそれぞれがGroupクラスのインスタンスに対応することになります。

もし、すべてのLDAPエントリを扱いたい場合はオブジェクトクラスtopを指定します。

class Entry < ActiveLdap::Base
  ldap_maaping :classes => ["top"], ...
end

topはすべてのエントリが属しているオブジェクトクラスなので、ldap_mappingにオブジェクトクラスtopを指定することで、すべてのエントリの集合とEntryクラスを対応させることができます。これは、LDAPツリーを表示するアプリケーションを作るときに便利です。

余談ですが、ActiveLdapにはActiveRecordのacts_as_tree相当の機能が標準で組み込まれています。LDAPはツリー構造なので標準で組み込まれていることは自然ですね。Entryクラスのインスタンスもchildrenやparentなどのメソッドが使えるため、簡単にツリー状のビューを作成することもできます。

検索対象のツリー

LDAPのエントリの集合は検索対象のツリーでも絞り込むことができます。以下のように、それぞれのツリーで扱いが異なる場合に役立ちます。

class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"],
               :prefix => "ou=Groups", ...
end

class AdministratorGroup < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"],
               :prefix => "ou=AdministratorGroups", ...
end

Groupは通常ユーザ用のグループで、AdministratorGroupは管理者ユーザ用のグループです。多くの場合、役割毎にLDAPツリーをわけて管理していると思うので、それをRubyの世界でも利用するということです。

このようにクラスをわけることによって、メソッド内で管理者グループかどうかで処理を振り分けなくてもよくなります。

class Group < ActiveLdap::Base
  def users
    # 一般ユーザの配列を返す
  end
end

class AdministratorGroup < ActiveLdap::Base
  def users
    # 管理者ユーザの配列を返す
  end
end

例としてusersメソッドを出しましたが、実は、ActiveLdapにはActiveRecordのhas_manyのように、LDAPエントリ間の関連をRubyで簡単に扱えるようにする機能があり、usersのようなメソッドは自分で定義する必要はありません。別の機会にでも紹介したいと思います。

検索範囲

検索対象のツリーのうち、どのツリーを検索範囲にするのかを指定できます。

class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"],
               :prefix => "ou=Groups",
               :scope => :sub, ...
end

この例では、検索ツリー以下にあるサブツリー全体からエントリを検索します。もし、サブツリーの直下にだけ必要なエントリがある場合は:oneを指定することにより、よけいな検索を避けることができます。アプリケーションにあわせて適切な検索範囲を指定して下さい。

まとめ

ActiveLdapでのLDAPのエントリとRubyのオブジェクトを関連付けるためのメソッドldap_mappingについて簡単に紹介しました。ここでは以下の3つについてだけ触れましたが、他にdn_attributeという重要なオプションがあります。

  • objectClass
  • 検索対象のツリー: LDAPでのbase
  • 検索範囲: LDAPでのscope

より詳しくはActiveLdapの日本語チュートリアルを見てください。

Tags: Ruby | このエントリの Delicious history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-04-14

問題解決につながるエラーメッセージ

プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。

ここでは、Rubyでの例をまじえながら問題解決に有用なエラーメッセージを紹介します。ライブラリなど多くの人が使うようなプログラムを作成する場合は参考になるかもしれません。

問題解決への道

問題に遭遇してから問題を解決するまでには以下の順で作業をする必要があります。

  1. 問題の把握
  2. 問題の原因の調査
  3. 原因の解決方法の検討
  4. 解決方法の実装

役立つエラーメッセージがあると「1. 問題の把握」、「2. 問題の原因の調査」、「3. 原因の解決方法の検討」がはかどります。

問題の値を示す

エラーが発生すれば問題が起こっている事実は把握できます。次にすることは、どのような問題が起こっているかを調査することです。

String#gsubにはいくつかの使い方がありますが、その1つは以下のように正規表現と文字列を引数にする使い方です。

>> "abcde".gsub(/c/, "C")
=> "abCde"

もちろん、違うオブジェクトを渡すとエラーが発生します。

>> "abcde".gsub([:first], [:second])
TypeError: can't convert Array into String
        from (irb):2:in `gsub'
        from (irb):2

配列を文字列に変換できなかったといっています。しかし、ここでは引数に配列を2つ指定しています。このエラーメッセージでは「配列を文字列に変換できなかった」ことはわかりますが、「どの配列を文字列に変換できなかった」かはわかりません。

正規表現のリテラルでも、正規表現の構文が間違っている場合はエラーが発生します。

>> Regexp.new("(")
RegexpError: premature end of regular expression: /(/
        from (irb):3:in `initialize'
        from (irb):3:in `new'
        from (irb):3

この場合は「正規表現に問題がある」というだけではなく、「どの正規表現に問題がある」かも示しています。

このように、問題を起こしたオブジェクトの情報も示すことで「問題を把握」しやすくなります。エラーメッセージには、問題を起こしたオブジェクトの情報も含めるようにしましょう。

どう悪いのかを示す

問題が把握できたら、どうしてその問題が発生したのか、原因を調べます。多くの場合、エラーメッセージに問題の原因は書かれています。しかし、そうではない場合もあります。できるだけ、エラーメッセージには問題の原因も含めるようにしましょう。

Time.iso8601はISO 8601で定められた文字列のフォーマットをパースし、Timeオブジェクトにします。

>> require 'time'
=> true
>> Time.iso8601("2009-04-10T12:02:54+09:00")
=> Fri Apr 10 03:02:54 UTC 2009

不正なフォーマットの場合はエラーが発生します。

>> Time.iso8601("2009-04-10I12:02:54+09:00")
ArgumentError: invalid date: "2009-04-10I12:02:54+09:00"
        from /usr/lib/ruby/1.8/time.rb:376:in `iso8601'
        from (irb):6

この例では真ん中あたりの「T」が「I」になっているためフォーマットに適合していません。

もし、「『I』という不正な文字があります」というようなメッセージが入っていると、問題の原因を簡単に把握できるようになります。

エラーメッセージには大雑把な原因だけではなく、できるだけ詳しく原因を書くようにしましょう。

期待を示す

問題の原因がわかったら、その問題を解決する方法を検討します。期待している値がわかると、解決する方法を検討しやすくなります。

String#deleteは1つ以上の引数をとります。1つも引数を与えない場合はエラーが発生します。

>> "abcde".delete("a")
=> "bcde"
>> "abcde".delete
ArgumentError: wrong number of arguments
        from (irb):2:in `delete'
        from (irb):2

エラーメッセージを見ると「引数の数が違う」ということがわかります。これで「問題の原因」を把握することができます。

しかし、「問題の原因」はわかってもどうすればその問題を解決できるかはわかりません。引数の数を変えればよいということはわかりますが、いくつにすればよいかがわからないのです。

期待している値を示すと、問題を解決しやすくなります。

>> "abcde".gsub
ArgumentError: wrong number of arguments (0 for 2)
        from (irb):3:in `gsub'
        from (irb):3

このエラーメッセージからはString#gsubが2つの引数を期待していることがわかるので、解決案として「引数を2つ渡す」というアイディアが浮かびます。次にすることは「引数に何を2つ渡すか」を考えることです。

エラーメッセージに「期待していること」を含めると、解決案が浮かびやすくなります。できるだけ、期待していることも含めるようにしましょう。

まとめ

Rubyを例にして問題解決に役立つエラーメッセージについて紹介しました。

問題解決に役立つエラーメッセージの特長は、テストの実行結果にもあてはまります。クリアコードが開発に関わっているテスティングフレームワークではテストの実行結果にこだわっています。

  • Cutter: C言語用テスティングフレームワーク
  • UxU: Firefoxアドオン開発用テスティングフレームワーク

あなたが使っているテスティングフレームワークは問題解決に役立つような情報を提供していますか?

2009-04-10

クリアコードの公開リポジトリ

すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したプログラムが入ったSubversionリポジトリリポジトリの更新状況のRSS)の公開を始めました。

クリアコードでは既存のフリーソフトウェアの開発に参加したり、新しくmilter managerなどのフリーソフトウェアを開発したりしていますが、それらの開発成果の公開場所はケースバイケースとなっています。

  • 既存のフリーソフトウェアの開発に参加する場合は、基本的に、開発成果はアップストリームに還元しています。
  • 新しくフリーソフトウェアを開発する場合は、基本的には関連コミュニティで標準的なホスティングサイトを利用しています。例えば、Ruby関連のソフトウェアであればRubyForge、GNOME関連のソフトウェアであればgnome.orgといった具合です。
  • 関連するソフトウェアがGitHubGoogle Codeを利用している場合は、それらのサイトを利用することもあります。
  • 特に標準的なホスティングサイトが無い場合はSourceForgeを利用しています。

このように、クリアコードの開発成果のソースコードは様々なホスティングサイトのリポジトリにて管理、および公開されています。

まもなくクリアコードは設立から3年が経とうとしていますが、その間、プロジェクトを作るまでもないような小規模なソースコードがいくつかたまってきました。この度、そのようなソースコードをSubversionリポジトリで公開することにしました。

このリポジトリには現在、ページの一部を折りたたむfolding.jsや、このククログを生成するためのtDiary関連のスクリプト(日記のデータをSubversionで管理するIOバックエンド日記を静的なHTMLに変換するスクリプトなどの記事で述べた物)、Thunderbird用の各種アドオンのソースコードが入っています。誰でも自由にチェックアウトできますので、注意事項をご了承の上でどうぞご利用ください。

以下、現在入っているプログラムを簡単に紹介します。

注意事項

  • これらのプログラムはすべて無保証です。
  • プログラムは予告なく追加・削除・変更されることがあります。

JavaScript関連

tDiary関連

ククログだけではなく、milter managerのブログでも使っています。使い方はmilter managerのtdiary.confが参考になると思います。

  • /tdiary/subversionio.rb: tDiaryのSubversionバックエンド
  • /tdiary/gitio.rb: ↑のSubverionバックエンドのgitバージョンです。
  • /tdiary/html-archiver.rb: tDiaryのデータをHTML化するで紹介した物ですが、その後、カテゴリに対応するなどいくつかの点で改良されています。
  • /tdiary/patches/customizable-style-path.rb: スタイルファイルのパスをカスタマイズできるようにします。RDスタイルなど、標準では有効になっていないスタイルを使うときに便利です。本家に提案したのですが、rejectされたのでここに入っています。
  • /tdiary/plugin/classed-category-list.rb: class付きでカテゴリリストを生成します。ククログのトップに並んでいる「タグ: ...」の部分です。
  • /tdiary/plugin/date-to-tag.rb: 日付を本文の下に生成します。今思えば名前が悪いですね。後で変えるかもしれません。
  • /tdiary/plugin/link-subtitle.rb: サブタイトルをその記事のリンクにします。通常は日付がリンクになるのですが、このククログでは日付は本文の下に置いてあるので、代わりにサブタイトルをリンクにしています。
  • /tdiary/plugin/multi-icon.rb: ページアイコンとして、favicon.ico(ICO形式の画像)とfavicon.png(PNG形式の画像)を両方指定できるようにします。
  • /tdiary/plugin/title-navi-label.rb: 「前の日記」「次の日記」リンクのラベルをリンク先の日記のタイトルにします。
  • /tdiary/plugin/zz-permalink-without-section-id.rb: section_footerプラグインが生成するpermalinkから「#pXX」を削除します。ククログでは、1記事を1セクションにして同じ日には複数の記事を書かないという方針で運営しており、セクションIDが必要ないため、このプラグインを作成しました。ファイル名の「zz-」は、このプラグインの読み込み順序を最後の方にさせるためのものです。

Thunderbird関連

Thunderbird Add-ons - クリアコードで公開している、Thunderbirdのバグを回避するパッチや挙動の変更を行う拡張機能です。公開ページにも書いてありますが、これらの拡張機能は無保証です。業務上の必要性からの導入をお考えの場合は、Mozilla Firefox & Mozilla Thunderbird保守・サポートサービスのご利用もご検討ください。

おまけ

リポジトリの更新状況を配信しているRSSは、Subversionに標準添付のcommit-email.rbで生成しています。今回、RSSのタイトルや説明を日本語にしたかったので、Subversionのtrunkに--rss-titleと--rss-descriptionオプションを追加しました。また、--repository-uriで指定されたリポジトリのURIをコミットメールのX-SVN-Repositoryヘッダに設定するようにもしています。

Subversionリポジトリの整形表示にはRepos Styleを利用しています。mod_dav_svnにはSVNIndexXSLTというオプションがあって、それを利用しています。

2009-04-06

2009年3月の肉の会

クリアコードでは毎月29日頃に肉の会と呼ばれる社内食事会を行っています。 肉の会というだけあって肉を食べることが多いのですが、今回は肉ではなく、社内でたこやきを食べまくりました。

今回はゲストとしてITproの高橋さんに参加してもらえました。(忙しい中、ありがとうございます!)

高橋さんがおみやげとして発売したばかりの「Ruby技術者認定試験 公式ガイド」を持ってきてくれました。「クリアコードさんへ」とサインももらいました。ありがとうございます!

高橋さんのサイン

この公式ガイドには模擬試験が2回分ついています。Rubyには自信があったのでやってみたところ、90%くらいしか正解できませんでした。普段使わないメソッドや引数が問題になっていると間違えてしまいます。

ひっかけっぽい問題も何題かあり、ひっかけようとしているなぁとニヤニヤしながら解いていたのですが、何題か間違えてしまいました。油断してはいけません。

この模擬試験をやってみて、Hash#invertをはじめて知りました。もうすでにかなりRubyを知っている人も、Ruby技術者認定試験の問題を解いてみると知らないことが見つかるかもしれません。問題を見て、ひっかけっぽいな、とニヤニヤしながら解くのも楽しいと思います。

模擬試験の前にはコンパクトにまとめられたRubyの解説がついているので、まだRubyに詳しくない人のとっかかりにもよさそうな気がします。mapとcollectをきちんと対等に扱っているので、変に偏らなそうなのもよいと思います。*1 Rubyで開発されたデスクトップ・アプリケーションとしてRabbitが挙がっていることも、とてもよいと思います。

*1  私はcollectに偏っています。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-04-02

Rails 2.3.2でActiveLdapを使う

2009/3/24時点でのActiveLdapの最新リリースは1.0.2ですが、ActiveLdap 1.0.2はRails 2.3.2には対応していません。これは、ActiveLdap 1.0.2の方がRails 2.3.2より早くリリースされたからです。基本的に、ActiveLdapはリリース時点での最新のRailsに対応していますが、未来のRailsには対応できていません。

Rails 2.3.2とRuby 1.9.1とActiveLdap

さて、そんなActiveLdapですが、trunkではRails 2.3.2に対応しています。(Issue 18 - ruby-activeldap - [Rails 2.3 Support] :: Running WEBrick Hangs - Google Code

また、Rails 2.3.2だけではなく、Ruby 1.9.1にも対応しています。(Issue 20 - [Ruby 1.9 Support] :: Running Tests - Google Code) 対応の過程でAlexey.Chebotarさんが、Ruby/LDAPをRuby 1.9.1に対応しました。

Ruby 1.9.1対応Ruby/LDAP

Ruby/LDAPはOpenLDAPのRubyバインディングで最終リリースが2006/8/9です。Alexeyさん以外にもRuby 1.9.1対応した成果をフィードバックしている人がいるのですが、まだ対応してもらえていないようです。

Alexeyさんもメンテナの方に連絡をとってみたということですが、まだ対応してもらえていないようです。パッチの形で転がっているよりも、パッチが取り込まれた形の方が利用しやすいということと、AlexeyさんがRuby/LDAPをメンテナンスする意志があるということだったので、ActiveLdapのリポジトリにRuby 1.9.1対応版のRuby/LDAPが入っています。(リポジトリ)Ruby/LDAPをRuby 1.9.1で使いたいという方はこれを使ってみるとよいと思います。

Rails 2.3.2とActiveLdapのtrunk

少し脱線しましたが、ActiveLdapのtrunkをRailsアプリケーションで使う方法がActiveLdapプロジェクトのWikiにあります。(UsingTrunkWithRailsJa) Rails 2.3.2でActiveLdapを使いたい場合はこの方法を参考にしてください。

当初は次のリリースは1.1.0と考えていたのですが、1.1.0の前に、Rails 2.3.2に対応している現時点のものを1.0.3をリリースすることも考えています。いくつか予定されていた項目がまだ実現されていないので1.1.0を出すことはできないのですが、その前にRails 2.3.2対応版を出すことには価値があるかもしれないと思うようになってきました。*1

1.1.0に予定されていた項目の1つにドキュメントの整備というものがあります。この項目もまだ実現できていない項目の1つなのですが、現在、id:tashenさんがチュートリアルの日本語訳を行ってくれています。(ありがとうございます!)

ActiveLdapは活発に開発が行われているライブラリなので、RubyでLDAPを便利に操作したい場合はActiveLdapを使うことを検討してみてください。そして、より使いやすく有用なライブラリにするために、バグ修正やドキュメント作成などでプロジェクトに協力してくれる人を募集しています。お待ちしています。

*1  Rails 2.3.2に対応していないというIssueが2つ登録されたりしたので。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-03-24

Railsで画像アップロード

Railsで画像をアップロードするときはどうやっているんでしょうか。

Fleximageというプラグインがあります。よいAPIだと思うのですが、あまり使っている人がいないようなので紹介します。

ここでは1からサンプルRailsアプリケーションを作成しながらFleximageの使い方を紹介します。順番にコマンドを実行・コードを変更していくと動くように書いてあります。

下準備

まず、sampleというRailsアプリケーションを作成します。

% cd /tmp
% rails sample
% cd sample

次に、Fleximageをインストールし、画像用のテーブルを作成します。

% script/plugin install git://github.com/Squeegy/fleximage.git
% script/generate scaffold photo title:string image_filename:string image_width:integer image_height:integer
% rake db:migrate

以下のカラムは特別なカラムです。Fleximageは以下のカラムが定義されていると、そこに値を設定してくれます。

image_filename
アップロードした画像のファイル名
image_width
画像の幅
image_height
画像の高さ

これらはすべて省略可能です。今回は、せっかくなので、すべて定義しました。

Fleximageを使うため、モデルを以下のように変更します。

app/models/photo.rb:

class Photo < ActiveRecord::Base
  acts_as_fleximage :image_directory => 'public/images/uploaded'
end

これで、アップロードされた画像は"#{RAILS_ROOT}/public/images/uploaded"以下に保存されます。

ここからは、コードを変更し、アプリケーションを動かし、動作を確認しながら進めていきます。そのため、ここでサーバを起動しておきます。

% script/server

アップロード

http://localhost:3000/photos/にアクセスすると以下のような見慣れたscaffoldの画面になるので、「New Photo」リンクから画像アップロードフォームへ進みます。

indexページ

scaffoldのフォームでは画像をアップロードできないので、app/views/photos/new.htmlを以下のように変更します。

app/views/photos/new.html:

<h1>New photo</h1>

<% form_for(@photo, :html => {:multipart => true}) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :title %><br />
    <%= f.text_field :title %>
  </p>
  <p>
    <%= f.label :image_file %><br />
    <%= f.file_field :image_file %>
  </p>
  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

<%= link_to 'Back', photos_path %>

これで、フォームは以下のようになります。

画像アップロードフォーム

タイトルとアップロードする画像のパスを指定して「Create」ボタンを押します。

画像がアップロードされ、ファイル名やサイズが設定されていることが確認できます。

アップロード後

せっかく画像をアップロードしたので、アップロードした画像も表示するようにします。app/views/photos/show.html.erbに以下を追加します。ここでは、タイトルの下に追加しました。

app/views/photos/show.html.erb:

<p>
  <%= image_tag(photo_path(@photo, :format => "png")) %>
</p>

PNGフォーマットを指定しているので、コントローラ側でPNGフォーマットを受けつけるようにします。

app/controllers/photos_controller.rb:

def show
  @photo = Photo.find(params[:id])

  respond_to do |format|
    format.html # show.html.erb
    format.xml  { render :xml => @photo }
    format.png  # <- 追加
  end
end

PNGフォーマットのビューを作ります。ここが、FleximageのAPIのよいところです。

Fleximageを使ったビューは「#{アクション名}.#{フォーマット}.flexi」というファイル名になります。今回の場合だと、「show.png.flexi」になり、以下のような内容になります。

app/views/photos/show.png.flexi:

# -*- ruby -*-
@photo.operate do |image|
end

これで、http://localhost:3000/photos/1.pngで画像を表示することができるようになりました。画像表示ページは以下のようになります。

画像付き表示ページ

サムネイル

画像表示ページではアップロードされた画像をそのまま表示しました。画像一覧ページでは画像のサムネイルを表示することにします。

サムネイル画像はhttp://localhost:3000/photos/1/thumbnail.gifで表示するようにします。そのため、まず、config/routes.rbを変更します。

config/routes.rb:

map.resources :photos, :member => {:thumbnail => :get}

これで、thumbnail_photo_pathが使えるようになるので、index.html.erbを書き換えます。

app/views/photos/index.html.erb:

<h1>Listing photos</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Thumbnail</th>
    <th colspan="3">Action</th>
  </tr>

<% @photos.each do |photo| %>
  <tr>
    <td><%=h photo.title %></td>
    <td><%= image_tag(thumbnail_photo_path(photo, :format => "gif") %></td>
    <td><%= link_to 'Show', photo %></td>
    <td><%= link_to 'Edit', edit_photo_path(photo) %></td>
    <td><%= link_to 'Destroy', photo, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New photo', new_photo_path %>

ここでは意味もなくフォーマットにgifを指定していますが、これはFleximageが画像フォーマットを簡単に変更できることを示すためです。

コントローラにGIFフォーマットに対応したthumbnailアクションを定義します。

app/controllers/photos_controller.rb:

def thumbnail
  @photo = Photo.find(params[:id])

  respond_to do |format|
    format.gif
  end
end

サムネイルのGIF画像用のビューは以下のようになります。とてもすっきり書けています。

app/views/photos/thumbnail.gif.flexi:

# -*- ruby -*-
@photo.operate do |image|
  image.resize('80x60')
end

変更した一覧ページを表示するとこうなります。

サムネイル付き画像一覧ページ

アップロードした画像がサムネイルとして表示されています。

まとめ

すっきりしたAPIのRails用画像アップロードプラグインFleximageを紹介しました。

ここでは触れませんでしたが、Fleximageは機能が豊富でカスタマイズ性にも優れています。例えば、アップロードされた画像を表示するときに影をつけたり、文字を入れたりといろいろ加工することができます。また、ファイルではなく、データベースに画像を保存することもできます。

Fleximageは日本語の情報がほとんどないのが不思議ですね。あまり使われていないのかもしれません。

おまけ

ImageMagickを使って画像に影をつけるシェルスクリプトです。これを使うと、このページのスクリーンショットについているような影をつけることができます。

add-shadow.sh:

#!/bin/sh

if [ $# != 2 ]; then
    echo "Usage: $0 INPUT SHADOWED_OUTPUT"
    exit 1
fi

convert $1  \( +clone -background black -shadow 80x6 \) \
    +swap -background none -layers merge +repage $2

このように使います。

% ./add-shadow.sh image.png shadowed-image.png
つづき: 2009-12-21
Tags: Ruby | このエントリの Delicious history 29 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-03-23

仙台Ruby会議01

先日、仙台Ruby会議01で1コマ話してきました。

まず好きなこと、そしてそれを続けること

東北にいたころにお世話になっている人たちも観にきてくれて、たくさんの方に聞いてもらえました。ありがとうございます。

何人か直接感想を伝えてくれた方もいたので、少しでも言いたかったことが伝わったような気がしています。仙台で話せてよかったです。ただ、いくつか言い損ねたこともありました。

使うこと

前半では、作る側である前に使う側という立場であったから、作る側の考え方が養われたということに触れたのですが、後半の「今日からできること」ではそれに触れませんでした。使うことでよりソフトウェアに愛着がもてるかもしれないので、使うということも大事なことだと思います。

今なら、Ruby 1.9.1を使うというのもエキサイティングでよいかと思います。使うことでバグを踏んで、直す機会に巡りあえるかもしれません。

手を動かしている人は私だけではない

自分の例を使って、東北にいてもプログラミングを続けることはできるし、認めてもらえることもあるという話をしました。そして、プログラミングして直すということがどちらにも有効ではないかと続くのですが、そこで、とてもよい実例をあげそこねてしまいました。

ちょうど1つ前に話していた藤岡さんがまさにそのよい例だと思います。藤岡さんはcgi.rbを直すことで認められ、Rubyのコミッタになっています。

当時はまだ東北にいましたが今は東北にいない自分の例だけよりも、今も東北にいて活躍されている藤岡さんに触れなかったことが悔やまれます。より身近にいる藤岡さんのことに結びつけて聞いてもらえていたら、もっと実感して聞いてもらえたかもしれません。

最近は地方でもすごい

都会に行かないと話せる人がいないという少し昔の話をしましたが、今はその状況は大きく変わっていると思います。それは、仙台Ruby会議01に参加した人たちならわかるはずです。前述の藤岡さんもいますし、仙台Ruby会議01を先頭に立って引っ張ってくれた片平さんもいます。

これは、はっきりと言うべきでした。

感謝

今回、仙台Ruby会議01で話すことができたのは、角谷さんと片平さんのおかげでした。仙台Ruby会議01の話者がすべて決まった後なのに、私も入れてくれました。入ってきた私のために、1コマ増やせるように動いてくれ、実際に1コマ増やしてもらえました。本当にありがとうございました。

東北にいるたくさんのすばらしいRubyistを知ることができた仙台Ruby会議01は、とてもすばらしいRuby会議だったと思います。

Ruby会議2009は、私個人だけではなく、スポンサーとしてクリアコードでも参加したいと思います。

Tags: Ruby | このエントリの Delicious history 1 user | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2009-01-26

tDiaryのデータをHTML化する

tDiaryをローカルなネットワークに配置して、tDiaryが表示する内容を静的なHTMLとして公開したい場合はよくありますよね。ククログもそんなよくある使い方の1つです。

tDiaryには静的なHTMLを生成するためのsqueezeプラグインがありますが、squeezeプラグインが出力するHTMLは以下の点でCGIで表示される内容と異なります。

  • 各日付のページしか生成しない
    • 最新の日記n件ページや月別ページやカテゴリページは生成しない
  • リンクがCGI用のリンクのままで、次の日記のページに移動するリンクが壊れている
  • テーマファイルや画像はコピーしてくれないので、生成したHTMLの入ったディレクトリ以下だけでは完結しない

ただし、これはsqueezeプラグインが検索エンジンへの入力データとしてのHTML生成を目的としているためです。よくある使い方では、生成されたHTMLはCGIで出力されているように表示できることが目的なので、上記のようなミスマッチが発生します。

そこで、ククログではhtml-archiver.rbという静的なHTMLを生成するスクリプトを使っています。html-archiver.rbは最後の方に載せています。

html-archiver.rbの使い方

html-archiver.rbを使うと、CGIで出力されている内容と同じように表示されるHTMLが生成されます。生成例は今見ているこのページです。

使い方はこうなります。

% ruby html-archiver.rb --tdiary tdiayr.rbのあるディレクトリ --conf tdiary.confのあるディレクトリ 出力先ディレクトリ

例えば、以下のような場合を考えます。

  • tdiary.rbは~tdiary/work/ruby/tdiary/core/にある
  • tdiary.confは~tdiary/public_html/にある
  • HTMLは~tdiary/public_html/html/以下に出力する

この場合はこのようなコマンドになります。

% ruby html-archiver.rb --tdiary ~tdiary/work/ruby/tdiary/core/ --conf ~tdiary/public_html/ ~tdiary/public_html/html/

機能

  • 日付ページの生成:
  • 最新n件ページの生成:
  • 月別ページの生成:
  • RSS 1.0の生成:
  • テーマファイルのコピー
  • 画像のコピー

制限

  • ツッコミが生成されるかどうかは試していない
  • カテゴリ一覧ページがきちんと生成されるかは(最近は)試していない
  • タブインデント(tDiary本体のコーディングスタイルに合わせているため)
  • 思ったほど使う場面が少ないかもしれない(もしかしたら、tDiaryが表示する内容を静的なHTMLとして公開することがそんなにないかもしれない)

ライセンス

GPL3あるいは3以降の新しいバージョンのGPL

html-archiver.rb

#!/usr/bin/env ruby
# -*- coding: utf-8; ruby-indent-level: 3; tab-width: 3; indent-tabs-mode: t -*-

require 'uri'
require 'cgi'
require 'fileutils'
require 'pathname'
require 'optparse'
require 'ostruct'
require 'enumerator'
require 'rss'

options = OpenStruct.new
options.tdiary_path = "./"
options.conf_dir = "./"
opts = OptionParser.new do |opts|
	opts.banner += " OUTPUT_DIR"

	opts.on("-t", "--tdiary=TDIARY_DIRECTORY",
			  "a directory that has tdiary.rb") do |path|
		options.tdiary_path = path
	end

	opts.on("-c", "--conf=TDIARY_CONF", "a path of tdiary.conf") do |conf|
		options.conf_dir = conf
	end
end
opts.parse!

output_dir = ARGV.shift

Dir.chdir(options.conf_dir) do
	$LOAD_PATH.unshift(File.expand_path(options.tdiary_path))
	require "tdiary"
end

module HTMLArchiver
	class CGI < ::CGI
		def referer
			nil
		end

		private
		def env_table
			{"REQUEST_METHOD" => "GET", "QUERY_STRING" => ""}
		end
	end

	module Image
		def init_image_dir
			@image_dest_dir = @dest + "images"
		end
	end

	module Base
		include Image

		def initialize(rhtml, dest, conf)
			@ignore_parser_cache = true

			cgi = CGI.new
			setup_cgi(cgi, conf)
			@dest = dest
			init_image_dir
			super(cgi, rhtml, conf)
		end

		def eval_rhtml(*args)
			link_detect_re = /(<(?:a|link)\b.*?\bhref|<img\b.*?\bsrc)="(.*?)"/
			super.gsub(link_detect_re) do |link_attribute|
				prefix = $1
				link = $2
				uri = URI(link)
				if uri.absolute? or link[0] == ?/
					link_attribute
				else
					%Q[#{prefix}="#{relative_path}#{link}"]
				end
			end
		end

		def save
			return unless can_save?
			filename = output_filename
			if !filename.exist? or filename.mtime != last_modified
				filename.open('w') {|f| f.print(eval_rhtml)}
				filename.utime(last_modified, last_modified)
			end
		end

		protected
		def output_component_name
			dir = @dest + output_component_dir
			name = output_component_base
			FileUtils.mkdir_p(dir.to_s, :mode => 0755)
			filename = dir + "#{name}.html"
			[dir, name, filename]
		end

		def mode
			self.class.to_s.split(/::/).last.downcase
		end

		def cookie_name; ''; end
		def cookie_mail; ''; end

		def load_plugins
			result = super
			@plugin.instance_eval(<<-EOS, __FILE__, __LINE__ + 1)
				def anchor( s )
					case s
					when /\\A(\\d+)#?([pct]\\d*)?\\z/
						day = $1
						anchor = $2
						if /\\A(\\d{4})(\\d{2})(\\d{2})?\\z/ =~ day
							day = [$1, $2, $3].compact
							day = day.collect {|component| component.to_i.to_s}
							day = day.join("/")
						end
						if anchor then
							"\#{day}.html#\#{anchor}"
						else
							"\#{day}.html"
						end
					when /\\A(\\d{8})-\\d+\\z/
						@conf['latest.path'][$1]
					else
						""
					end
				end

				def category_anchor(category)
					href = "category/\#{u category}.html"
					if @category_icon[category] and !@conf.mobile_agent?
						%Q|<a href="\#{href}"><img class="category" src="\#{h @category_icon_url}\#{h @category_icon[category]}" alt="\#{h category}"></a>|
					else
						%Q|[<a href="\#{href}">\#{h category}</a>]|
					end
				end

				def navi_admin
					""
				end

				@image_dir = #{@image_dest_dir.to_s.dump}
				@image_url = "#{@conf.base_url}#{@image_dest_dir.basename}"
			EOS
			result
		end

		private
		def setup_cgi(cgi, conf)
		end
	end

	class Day < TDiary::TDiaryDay
		include Base

		def initialize(diary, dest, conf)
			@target_date = diary.date
			@target_diaries = {@target_date.strftime("%Y%m%d") => diary}
			super("day.rhtml", dest, conf)
		end

		def can_save?
			not @diary.nil?
		end

		def output_filename
			dir, name, filename = output_component_name
			filename
		end

		def [](date)
			@target_diaries[date.strftime("%Y%m%d")] or super
		end

		def relative_path
			"../../"
		end

		private
		def output_component_dir
			Pathname(@target_date.strftime("%Y")) + @target_date.month.to_s
		end

		def output_component_base
			@target_date.day.to_s
		end

		def setup_cgi(cgi, conf)
			super
			cgi.params["date"] = [@target_date.strftime("%Y%m%d")]
		end
	end

	class Month < TDiary::TDiaryMonth
		include Base
		def initialize(date, dest, conf)
			@target_date = date
			super("month.rhtml", dest, conf)
		end

		def can_save?
			not @diary.nil?
		end

		def output_filename
			dir, name, filename = output_component_name
			filename
		end

		def relative_path
			"../"
		end

		private
		def output_component_dir
			@target_date.strftime("%Y")
		end

		def output_component_base
			@target_date.month.to_s
		end

		private
		def setup_cgi(cgi, conf)
			super
			cgi.params["date"] = [@target_date.strftime("%Y%m")]
		end
	end

	class Category < TDiary::TDiaryView
		include Base

		def initialize(category, diaries, dest, conf)
			@category = category
			diaries = diaries.reject {|date, diary| !diary.visible?}
			_, diary = diaries.sort_by {|date, diary| diary.last_modified}.last
			@target_date = diary.date
			super("latest.rhtml", dest, conf)
			@diaries = diaries
			@diary = diary
		end

		def can_save?
			not @diary.nil?
		end

		def output_filename
			category_dir = @dest + "category"
			category_dir.mkpath
			category_dir + "#{@category}.html"
		end

		def relative_path
			"../"
		end

		def latest(limit=5)
			@diaries.keys.sort.reverse_each do |date|
				diary = @diaries[date]
				yield(diary)
			end
		end

		protected
		def setup_cgi(cgi, conf)
			super
			cgi.params["date"] = [@target_date.strftime("%Y%m")]
		end
	end

	class Latest < TDiary::TDiaryLatest
		include Base

		def initialize(date, index, dest, conf)
			@target_date = date
			@index = index
			super("latest.rhtml", dest, conf)
		end

		def relative_path
			if @index.zero?
				""
			else
				"../"
			end
		end

		def can_save?
			true
		end

		def output_filename
			if @index.zero?
				@dest + "index.html"
			else
				latest_dir = @dest + "latest"
				FileUtils.mkdir_p(latest_dir.to_s, :mode => 0755)
				latest_dir + "#{@index}.html"
			end
		end

		protected
		def setup_cgi(cgi, conf)
			super
			return if @index.zero?
			date = @target_date.strftime("%Y%m%d") + "-#{conf.latest_limit}"
			cgi.params["date"] = [date]
		end
	end

	class RSS < TDiary::TDiaryLatest
		include Base

		def initialize(dest, conf)
			super("latest.rhtml", dest, conf)
		end

		def mode
			"latest"
		end

		def relative_path
			""
		end

		def can_save?
			true
		end

		def output_filename
			@dest + output_base_name
		end

		def output_base_name
			"index.rdf"
		end

		def do_eval_rhtml(prefix)
			load_plugins
			make_rss
		end

		private
		def make_rss
			base_uri = @conf['html_archiver.base_url'] || @conf.base_url
			rss_uri = base_uri + output_base_name

			@conf.options['apply_plugin'] = true
			feed = ::RSS::Maker.make("1.0") do |maker|
				setup_channel(maker.channel, rss_uri, base_uri)
				setup_image(maker.image, base_uri)

				@diaries.keys.sort.reverse[0, 15].each do |date|
					diary = @diaries[date]

					maker.items.new_item do |item|
						setup_item(item, diary, base_uri)
					end
				end
			end

			feed.to_s
		end

		def setup_channel(channel, rss_uri, base_uri)
			channel.about = rss_uri
			channel.link = base_uri
			channel.title = @conf.html_title
			channel.description = @conf.description
			channel.dc_creator = @conf.author_name
			channel.dc_rights = @conf.copyright
		end

		def setup_image(image, base_uri)
			return if @conf.banner.nil?
			return if @conf.banner.empty?

			if /^http/ =~ @conf.banner
				rdf_image = @conf.banner
			else
				rdf_image = base_uri + @conf.banner
			end

			maker.image.url = rdf_image
			maker.image.title = @conf.html_title
			maker.link = base_uri
		end

		def setup_item(item, diary, base_uri)
			section = nil
			diary.each_section do |_section|
				section = _section
				break if section
			end
			return if section.nil?

			item.link = base_uri + @plugin.anchor(diary.date.strftime("%Y%m%d"))
			item.dc_date = diary.last_modified
			@plugin.instance_variable_set("@makerss_in_feed", true)
			subtitle = section.subtitle_to_html
			body_enter = @plugin.send(:body_enter_proc, diary.date)
			body = @plugin.send(:apply_plugin, section.body_to_html)
			body_leave = @plugin.send(:body_leave_proc, diary.date)
			@plugin.instance_variable_set("@makerss_in_feed", false)

			subtitle = @plugin.send(:apply_plugin, subtitle, true).strip
			subtitle.sub!(/^(\[([^\]]+)\])+ */, '')
			description = @plugin.send(:remove_tag, body).strip
			subtitle = @conf.shorten(description, 20) if subtitle.empty?
			item.title = subtitle
			item.description = description
			item.content_encoded = body
			item.dc_creator = @conf.author_name
			section.categories.each do |category|
				item.dc_subjects.new_subject do |subject|
					subject.content = category
				end
			end
		end
	end

	class Main < TDiary::TDiaryBase
		include Image

		def initialize(cgi, dest, conf, src=nil)
			super(cgi, nil, conf)
			calendar
			@dest = dest
			@src = src || './'
			init_image_dir
		end

		def run
			@date = Time.now
			load_plugins
			copy_images

			all_days = archive_days
			archive_categories
			archive_latest(all_days)

 			make_rss
			copy_theme
		end

		private
		def copy_images
			image_src_dir = @plugin.instance_variable_get("@image_dir")
			image_src_dir = Pathname(image_src_dir)
			unless image_src_dir.absolute?
				image_src_dir = Pathname(@src) + image_src_dir
			end
			@image_dest_dir.rmtree if @image_dest_dir.exist?
			if image_src_dir.exist?
				FileUtils.cp_r(image_src_dir.to_s, @image_dest_dir.to_s)
			end
		end

		def archive_days
			all_days = []
			@years.keys.sort.each do |year|
				@years[year].sort.each do |month|
					month_time = Time.local(year.to_i, month.to_i)
					month = Month.new(month_time, @dest, conf)
 					month.save
					month.send(:each_day) do |diary|
						all_days << diary.date
 						Day.new(diary, @dest, conf).save
					end
				end
			end
			all_days
		end

		def archive_categories
			cache = @plugin.instance_variable_get("@category_cache")
			cache.categorize([], @years).each do |category, diaries|
				categorized_diaries = {}
				diaries.keys.each do |date|
					date_time = Time.local(*date.scan(/^(\d{4})(\d\d)(\d\d)$/)[0])
					@io.transaction(date_time) do |diaries|
						categorized_diaries[date] = diaries[date]
						DIRTY_NONE
					end
				end
 				Category.new(category, categorized_diaries, @dest, conf).save
			end
		end

		def archive_latest(all_days)
			conf["latest.path"] = {}

			latest_days = []
			all_days.reverse.each_slice(conf.latest_limit) do |days|
				latest_days << days
			end

			latest_days.each_with_index do |days, i|
				date = days.first.strftime("%Y%m%d")
				if i.zero?
					latest_path = "./"
				else
					latest_path = "latest/#{i}.html"
				end
				conf["latest.path"][date] = latest_path
			end
			latest_days.each_with_index do |days, i|
				latest = Latest.new(days.first, i, @dest, conf)
				latest.save
				conf["ndays.prev"] = nil
				conf["ndays.next"] = nil
			end
		end

		def make_rss
			RSS.new(@dest, conf).save
		end

		def copy_theme
			theme_dir = @dest + "theme"
			theme_dir.rmtree if theme_dir.exist?
			theme_dir.mkpath
			tdiary_theme_dir = Pathname(File.join(TDiary::PATH, "theme"))
			FileUtils.cp((tdiary_theme_dir + "base.css").to_s, theme_dir.to_s)
			if @conf.theme
				FileUtils.cp_r((tdiary_theme_dir + @conf.theme).to_s,
									(theme_dir + @conf.theme).to_s)
			end
		end
	end
end

cgi = HTMLArchiver::CGI.new
conf = TDiary::Config.new(cgi)
conf.show_comment = true
conf.hide_comment_form = true
def conf.bot?; false; end
output_dir ||= Pathname(conf.data_path) + "cache" + "html"
output_dir = Pathname(output_dir).expand_path
output_dir.mkpath
HTMLArchiver::Main.new(cgi, output_dir, conf, options.conf_dir).run
つづき: 2009-04-06
Tags: Ruby | このエントリの Delicious history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-12-05

tDiaryのSubversionバックエンド

みなさんはtDiaryの日記データをどのようにバックアップしているのでしょうか。cronでアーカイブしていたり、dbi_ioでデータベースに保存して、データベースの内容をアーカイブしていたりしているのでしょうか。

開発者の人なら日記のデータもソースコードと同じようにバックアップしたいですよね。つまり、バージョン管理をして、レポジトリの内容をアーカイブするバックアップです。そんな人はこのようなSubversionIOはいかがでしょうか。保存する毎にSubversionリポジトリに日記データをコミットします。

require 'tdiary/defaultio'

module TDiary
	class SubversionIO < DefaultIO
		def transaction( date, &block )
			dirty = TDiaryBase::DIRTY_NONE
			result = super( date ) do |diaries|
				dirty = block.call( diaries )
				diaries = diaries.reject {|_, diary| /\A\s*\Z/ =~ diary.to_src}
				dirty
			end
			unless (dirty & TDiaryBase::DIRTY_DIARY).zero?
				run( "svn", "add", File.dirname( @dfile ) )
				run( "svn", "add", @dfile )
				Dir.chdir( @data_path ) do
					run( "svn", "ci", "-m", "update #{date.strftime('%Y-%m-%d')}" )
				end
			end
			result
		end

		private
		def run( *command )
			command = command.collect {|arg| escape_arg( arg )}.join(' ')
			result = `#{command} 2>&1`
			unless $?.success?
				raise "Failed to run #{command}: #{result}"
			end
			result
		end

		def escape_arg( arg )
			"'#{arg.gsub( /'/, '\\\'' )}'"
		end
	end
end

# Local Variables:
# ruby-indent-level: 3
# tab-width: 3
# indent-tabs-mode: t
# End:

使い方は以下の2ステップです。

  1. tdiary.confで@io_classに指定する
  2. 日記のデータディレクトリをSubversionのワーキングコピーにする

tdiary.confの設定

まず、上記のソースコードをsubversionio.rbとしてどこかに保存してください。ここでは/home/tdiary/lib/以下に保存したとします。

次にtdiary.confに以下の内容を追記します。

subversion_io_dir = "/home/tdiary/lib" # <- 保存した場所にあわせて変更
require "#{subversion_io_dir}/subversionio"
@io_class = TDiary::SubversionIO

データディレクトリの設定

データディレクトリ(tdiary.conf内の@data_pathで指定したディレクトリ)は/home/tdiary/data/として進めます。また、作業しているユーザはtDiaryのCGIを動かすユーザとします。(ここではtdiaryユーザ)

まず、レポジトリに日記データ用のパスを作ります。既存のSubversionリポジトリを利用する場合は、例えばこのようになります。

[tdiary]% svn mkdir -m 'create tDiary data path' https://.../repos/tdiary-data

ローカルに新しくSubversionのリポジトリを作成して、そこに日記データをコミットするようにする場合はこのようになります。新しく作成するリポジトリは/home/tdiary/repos/に作ることにします。

[tdiary]% svnadmin create /home/tdiary/repos

Subversionリポジトリから日記データ保存用のパスを、tDiaryのデータディレクトリにチェックアウトします。今すでにあるtDiaryのデータディレクトリはどこかによけておきます。

[tdiary]% mv /home/tdiary/data /home/tdiary/data.bak
[tdiary]% svn co file:///home/tdiary/repos /home/tdiary/data

既存のデータをワーキングコピーに移動し、日記データだけをレポジトリにコミットします。

[tdiary]% cd /home/tdiary/data
[tdiary]% cp -rp ../data.bak/* ./
[tdiary]% svn add 200*
[tdiary]% svn add 199* # <- もし2000年より前のデータがあるなら
[tdiary]% svn ci -m 'import'

完了

以上で設定は完了です。日記を保存するとリポジトリにコミットされます。

制限

日記本文しか対応していません。ツッコミや画像には対応していません。

svnコマンドをインストールしている必要があります。

tDiary本体のコーディングスタイルに合わせているためタブインデントになっています。

ライセンス

AGPL3あるいは3以降の新しいバージョンのAGPL

つづき: 2009-04-06
Tags: Ruby | このエントリの Delicious history 4 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-11-13

Test::Unit 2.0.1リリース

Test::Unit 2.0.1が RubyForge上でリリースされました。RubyGemsも提供されているの で以下のようにインストールできます。

% sudo gem install test-unit

経緯

Test::UnitはRuby 1.8.xに標準添付されている単体テストフレーム ワークです。しかし、Ruby 1.9.1からは miniunitが標準 添付され、Test::UnitはRubyForgeで開発が継続されることになりま した。これからもTest::Unitを使うときはRubyGemsでインストール することになるでしょう。

Ruby 1.8.xに標準添付されているTest::Unitは互換性のために、 Test::Unit 1.2.3としてリリースされています。Ruby 1.9.1でも Ruby 1.8.xに標準添付されているTest::Unitと同じTest::Unitを使 用したい場合は以下のようにします。

% sudo gem install test-unit --version '= 1.2.3'

テストファイル内(変更前):

require 'test/unit'
...

テストファイル内(変更後):

require 'rubygems'
gem 'test-unit', '= 1.2.3'
require 'test/unit'
...

余談ですが、Ruby 1.9.1でTest::Unitが標準添付から外れ、 miniunitが標準添付になったのはTest::Unitのソースがメンテナン スしづらくなっていたのが主な理由です。

Test::Unit 2.x

Ruby 1.8.xに標準添付されているTest::Unitは長い間メンテナンス はされていましたが、特に機能拡張などは行われていませんでした。 しかし、その間にもテスト環境を便利にするライブラリが公開され てきました。例えば、RSpecのような BDD用のフレームワークや、 expectations のような軽量の単体テストフレームワーク、 Shoulda/ test/spec/ Mochaのように Test::Unitを拡張するライブラリなどです。Test::Unitは少し時代 遅れになってしまったのです。

最近のテスト用のフレームワークは DSL化の方向に向かっているようにも見えます。 これはRSpecの影響が大きいのでしょう。expectationsやShouldaもテスト用の DSLを提供します。

しかし、Test::UnitはDSLを提供しません。テストを「英語らしく」 ではなく「Rubyプログラムらしく」書きます。好みにもよりますが、 これはTest::Unitのメリットの1つと言えます。

Test::Unitに他のフレームワークやライブラリの機能を、 Test::Unitの「Rubyプログラムらしく」テストを書ける特性を活か したまま追加すれば、Test::Unitはもっと便利で使いやすいテスト フレームワークになるでしょう。Test::Unit 2.x系列はRuby 1.8.x に標準添付されていた頃とは違い、そのような方針の元で活発に開 発されていく系列になります。

例えば、以下のような機能が他のフレームワークやライブラリから 移植されています。

  • 差分表示
  • ネストしたテスト定義
  • 色付け
  • C-cでテスト中断時にもテスト結果を表示
  • 複数のsetup/teardown
  • ...

ここでは「差分表示」と「ネストしたテスト定義」だけ紹介します。 *1

差分表示

RSpecでは比較結果が異なった場合に差分を表示して違いをわかり やすく表示してくれます。

diff_spec.rb:

require 'rubygems'
require 'spec'

describe String do
  it do
    ["I", "am", "a", "boy"].join("\n").should == ["I", "was", "a", "boy"].join("\n")
  end
end

実行結果(差分表示部分のみ):

% ruby diff_spec.rb -D
...
Diff:
@@ -1,5 +1,5 @@
 I
-was
+am
 a
 boy
...

同様の機能がTest::Unit 2.0.1にもあります。

test_diff.rb:

require 'rubygems'
gem 'test-unit'
require 'test/unit'

class TestDiff < Test::Unit::TestCase
  def test_diff
    assert_equal(["I", "am", "a", "boy"].join("\n"),
                 ["I", "was", "a", "boy"].join("\n"))
  end
end

実行結果(差分表示部分のみ):

% ruby test_diff.rb
...
diff:
  I
- am
+ was
  a
  boy
...

この例では、ほとんど同じ差分表示ですが、Test::Unit 2.0.1の 差分表示がRSpecの差分表示よりも便利なこともあります。

今度は"\n"ではなく" "でjoinして1行の文字列として比較します。

test_diff.rb:

require 'rubygems'
gem 'test-unit'
require 'test/unit'

class TestDiff < Test::Unit::TestCase
  def test_diff
    assert_equal(["I", "am", "a", "boy"].join(" "),
                 ["I", "was", "a", "boy"].join(" "))
  end
end

実行結果(差分表示部分のみ):

% ruby test_diff.rb
...
diff:
- I am a boy
?    ^
+ I was a boy
?   + ^
...

Test::Unit 2.0.1では必要なら同じ行のうち、どの列が異なってい るのかも表示します。RSpecでは行単位の差分までで列単位までの 差分は表示しません。

余談ですが、この差分表示形式はPythondifflib ライブラリで使われている形式です。

ネストしたテスト定義

Shouldaではcontextをネストさせることにより、便利にテストを書 くことができます。以下はShouldaのページからの引用です。 *2

class UserTest < Test::Unit::TestCase
  context "A User instance" do
    setup do
      @user = User.find(:first)
    end

    should "return its full name" do
      assert_equal 'John Doe', @user.full_name
    end

    context "with a profile" do
      setup do
        @user.profile = Profile.find(:first)
      end

      should "return true when sent #has_profile?" do
        assert @user.has_profile?
      end
    end
  end
end

ネストされた"with a profile"のcontext内では上位の"A User instance"のcontext内のsetupが実行された後に実行されます。つ まり、以下のような実行順序になります。

  • setup: "A User instance" context
    • should: "return its full name"
  • setup: "A User instance" context
    • setup: "with a profile" context
      • should: "return true when sent #has_profile?"

実行されるフィクスチャ(setup)がネストで自然に表現されていま す。

Test::Unit 2.0.1では以下のように書きます。

class UserTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  def test_full_name
    assert_equal('John Doe', @user.full_name)
  end

  class ProfileTest < UserTest
    def setup
      super
      @user.profile = Profile.find(:first)
    end

    def test_profile
      assert_true(@user.has_profile?)
    end
  end
end

これは以下のように実行されます。

  • UserTest#setup
    • UserTest#test_full_name
  • UserTest#setup
    • UserTest::ProfileTest#setup
      • UserTest::ProfileTest#test_profile

実行されるフィクスチャ(setup)がネストとクラス階層で自然に表現 されています。

まとめ

Test::UnitはRuby 1.9.1からは標準添付ではなくなりましたが、 Test::Unit 2.xとして活発に開発が続けられています。既存の他の フレームワークやライブラリのよいところは積極的に導入している ため、Ruby 1.8.xに標準添付されているTest::Unitよりもはるかに 使いやすくなっています。

今回は紹介しませんでしたが、他のフレームワークやライブラリに はないTest::Unit 2.x独自の便利な機能もあります。Test::Unitの 「Rubyプログラムらしい」テストの書き方が好きな場合はこれから もTest::Unitを使ってみてはいかがでしょうか。

*1  「C-cでテスト中断時にもテスト結果を表示」は開発が進んでい き、たくさんのテストがある場合には地味ですがとても便利な機能 なのです。RSpecにも実装されています。

*2  ただし、Test::UnitはTest::Unit::TestCaseに修正してある。

2008-11-10

Ruby-GetText-Packageとrake gems:installの共存

Ruby-GetText-Packageだけというわけではないですが、app/controllers/application.rbで何かを行う*1gemを使っているとrake gems:installで足りないgemをインストールできません。経験したことがあるけど別に手動でインストールすればいいやということで、おそらく、わりとうやむやにされていることが多い問題ではないでしょうか。

例えば、Ruby-GetText-Packageだと以下のようにapp/controllers/application.rbを変更する必要があります。

class ApplicationController < ActionController::Base
  init_gettext "blog"
end

Ruby-GetText-Packageのgemがない場合は「init_gettext」が定義されていないため「NameError」が発生します。そのため、rake gems:installをしようとすると以下のように失敗してしまいます。

% rake gems:install
(in /tmp/blog)
rake aborted!
undefined method `init_gettext' for ApplicationController:Class

(See full trace by running task with --trace)

これを回避するために「足りないgemがあるときはinit_gettextを使わない」という方法があります。 あまりきれいな方法ではありませんが、紹介します。

class ApplicationController < ActionController::Base
  if Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?
    init_gettext "blog"
  end
end

もし、config/environment.rbでconfig.active_record.observersを指定しているなどして、app/model/以下も読み込まれるのであれば、ダミーのN_を定義しておくとよいでしょう。

class ApplicationController < ActionController::Base
  if Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?
    init_gettext "blog"
  else
    class ActiveRecord::Base
      def self.N_(*args); end
    end
  end
end

これでRuby-GetText-Packageを使っているときでもrake gems:installが動くようになります。

% rake gems:install
(in /tmp/blog)
gem install gettext
Bulk updating Gem source index for: http://gems.rubyforge.org/
Successfully installed gettext-1.93.0
1 gem installed
Installing ri documentation for gettext-1.93.0...
Installing RDoc documentation for gettext-1.93.0...

以下、「足りないgemがあるとき」の判断方法について少し書いてみます。

足りないgemがあるとき

Railsでは「足りないgemがあるかどうか」を示すAPIを提供しているのはRails::Initializerです。ただ、残念ながらconfig/environment.rbの中で作ったRails::Initializerはどこにも保存されていないので、無理やり引っ張り出す必要があります。具体的には以下のようなコードになります。

initializer = nil
ObjectSpace.each_object(Rails::Initializer) do |object|
  initializer = object
  break
end
initializer.gems_dependencies_loaded

しかし、この方法ではRails::InitializerがGCされてしまっていると動きません*2

また、ObjectSpaceはできれば使いたくないものです。そのため、もう少し安全で、何をしているのかがまだわかりそうな方法の方がよさそうです。そのための「足りないgemがあるかどうか」を判断する方法が以下のようになるというわけです。

Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?

ただ、この方法はRails 2.1.2(や2008-10-30でのmaster)では動きますが、もし、Rails内部の「足りないgemがあるかどうか」を判断する方法が変わった場合は動かなくなる可能性もあります。 「あまりきれいな方法ではありませんが」と書いたのはこのためです。

まとめ

Ruby-GetText-Packageを使っている場合でもrake gem:installを利用する方法を紹介しました。

同じような問題は他のライブラリでも起こりうると思うので、そのような場合も同じように問題を回避できると思います。

*1  もう少しいうと、読み込まれた時に実行される場所(例えばクラス定義の中)で何かを行う場合。メソッド定義の中などその場では実行されないものは関係ない。

*2  多くの場合はそんな状況にはならないでしょう

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-10-30

Rabbit 0.5.8リリース

Ruby-GNOME2を使って実装されているプレゼンテーションツールRabbit 0.5.8がリリースされました。

0.5.8では部分的にClutterをサポートしています。

Clutter

Clutterは高速で、視覚的にリッチで、アニメーションするGUIを作成するためのライブラリです。Clutterのこれらの特徴はOpenGLをバックエンドに使う事で実現されています。 ClutterはLinux/Mac OS X/Windowsなどマルチプラットフォームで動作します。さらに、組み込み環境でも動作し(OpenGL ESを利用)、デモ動画も公開されています。

ライブラリを使用する視点で見ると、GStreamer/cairo/Pango/GTK+などGNOME関連のライブラリと親和性が高いこともあり、便利で使いやすいAPIになっています。

ClutterにはRuby/Python/Perl/Valaなど各種言語用のバインディングがあります。リッチなインターフェイスを作成したい場合にClutterを利用してみてはどうでしょうか。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-10-20

Ruby-GNOME2 0.18.0リリース

0.17.0のリリースから1ヶ月も経っていませんが、Ruby-GNOME2バージョン0.18.0がリリースされました。

Ruby-GNOME2はGTK+を含むGNOME関連ライブラリのRubyバインディング集です。

目玉

このリリースの目玉はメモリリークの修正と、新規バインディングの追加です。

メモリリークはRuby/GLibの中にもあり、Ruby-GNOME2関連ライブラリ全体で影響を受ける可能性が高いものでした。0.17.0を利用している場合は0.18.0に更新することをおすすめします。

新規バインディングとして以下の2つが追加されました。ただし、まだどちらも「実験的」マークがついていて、今後APIが変更される可能性があります。

  • Ruby/GtkSourceView2
  • Ruby/GooCanvas
Ruby/GtkSourceView2

Ruby/GtkSourceView2はソースコードハイライトウィジェットであるGtkSourceView 2.x系列をサポートします。以前のリリースにも含まれているRuby/GtkSourceViewはGtkSourceView 1.x系列をサポートしていて、2.x系列はサポートしていませんでした。

今回、別ライブラリになっているのはAPIに非互換性が発生したためです。それぞれのライブラリはrequireが異なります。Ruby/GtkSourceViewからRuby/GtkSourceView2へ移行する場合は以下のように変更する必要があります。

変更前:

require 'gtksourceview'

変更後:

require 'gtksourceview2'

GtkSourceView 1.x系列は開発が終了していて、現在は2.x系列が開発されています。今後のことを考えるとRuby/GtkSourceViewからRuby/GtkSourceView2へ移行を検討した方がよいのではないかと思います。

Ruby/GooCanvas

Ruby/GooCanvasは描画にcairoを用いるキャンバスウィジェットであるGooCanvasのバインディングです。

キャンバスウィジェットとは図形や他のウィジェットなどを自由に配置できるウィジェットです。Inkscapeなどのようなグラフィックツールを思い浮かべるとイメージしやすいかもしれません。Inkscapeでは丸や四角などの図形を好きな場所に配置することができます。キャンバスウィジェットを用いることで、そのような機能を持つアプリケーションを簡単に開発することができます。

現在、GTK+にはキャンバスウィジェットが含まれていませんが、将来のGTK+ではGooCanvasがGTK+のキャンバスウィジェットとして取り込まれるのではないかと予想しています。GTK+ではProjectRidley/CanvasOverview - GNOME Live!で検討しているようです。GooCanvasを含むいくつかのキャンバスウィジェットを比較しています。

ちなみに、Ruby-GNOME2にはRuby/GnomeCanvas2というGnomeCanvasのバインディングがあります。ただし、GnomeCanvasは非推奨ライブラリになっています。そのため、Ruby/GnomeCanvas2も将来的にRuby-GNOME2から削除される可能性があります。

これから新しくキャンバスウィジェットを用いたアプリケーションを開発する場合はRuby/GooCanvasも候補のひとつに入れた方がよいかもしれません。ただし、まだ「実験的」な段階なのでAPIが変更される可能性があることに注意する必要があります。

協力のお願い

アナウンスメールにもありますが、Ruby-GNOME2プロジェクトでは協力してくれる方を募集しています。例えば、バインディングを開発してくれる方、ドキュメントを書いてくれる方、英語のドキュメントを日本語化してくれる方、リリース作業をしてくれる方などを募集しています。

興味のある方はruby-gnome2-devel-ja MLまでお願いします。

0.17.0がリリースされてから何人かの方がドキュメント関連作業で協力してくれています。ありがとうございます!

つづき: 2009-07-31
Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-10-01

rcairo 1.8.0リリース

2008/09/26にマルチプラットフォームで動作するベクトルベースの グラフィックライブラリであるcairo 1.8.0がリリースされました。また、同日のうちにcairoを Rubyから利用するためのライブラリ rcairo 1.8.0が リリースされました。([ruby-list45520] [ANN] rcairo 1.8.0 )rcairo 1.8.0はcairo 1.8.0に対応しています。rcairoの基本的 な使い方はるびまの記事「cairo: 2 次元画像描画ライブラリ」 にまとまっ ています。

ちなみに、誰も気づいていないかもしれませんが、cairoの最新API への対応は各種言語バインディングの中ではrcairoが最速です。

cairo 1.8.0

cairo 1.8.0ではテキストの扱いが改良されています。例えば、テキ ストを検索、選択、コピペできるようなPDFを出力するようになって います。また、「ユーザフォント」という機能が導入されています。

「ユーザフォント」はその名の通り、ユーザ(cairoを使う開発者) 独自のフォントを定義・利用できる機能です。利用例として、SVGフォ ントやFlashフォントなど標準化されていないフォーマットのフォン トの実装があげられています。通常は利用することはないと思いま すが、cairoがより広く利用される機会を増やす機能になるかもし れません。

もちろん、rcairoでは「ユーザフォント」もサポートしています。

rcairoの今後

rcairoには、cairoのバインディングだけではなく、rcairo独自の cairoをもっと便利に使うための機能が追加されています。例えば、 Cairo::Color もそのひとつです。

rcairoをもっと便利に使えるようにするためには、レンダリングエ ンジンのような機能が必要だと考えています。例えば、以下のよう にすれば表を描画できるというような機能を考えています。

table = Cairo::Table.new(:width => 400)
table << Cairo::Table::Header.new("column1", "column2", "column3")
table << Cairo::Table::Row.new("1x1 value", "1x2 value", "1x3 value")
table << Cairo::Table::Row.new("2x1 value", "2x2 value", "2x3 value")
context.show_table(table)

現在考えているサポートしたい描画対象は以下の通りです。もし興 味があったらぜひ手伝ってください。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-09-27

ActiveScaffoldの地域化

現在、Railsに対応した国際化の仕組みがいくつかあります。しかし、それぞれが 独自の方法で実現しているため、それらを組み合わせて使うと混沌 とした状態に陥ることも少なくありません。

ここでは、モデルから動的にきれいな画面とコントローラ部分を生 成するActiveScaffoldを用 いた場合の国際化(i18n)と地域化(l10n)の実現方法のひとつを 紹介します。この方法では、 ActiveScaffoldLocalizeRuby-GetText-Package を組み合わせます。混沌とする部分はそれなりになじませます。

国際化の仕組み

Railsで使用できる国際化の仕組みの比較はRails Wiki (英語)が詳しいです。

Ruby-GetText-Package には、以下のような地域化対象のメンテナン スのことを考慮した機能があるので、地域化対象メッセージが増加 したり更新される場合には有力な候補になるでしょう。

  • 地域化対象のメッセージを抽出する機能
    • テーブルにカラムを追加した場合、画面に表示するメッセージを追加・更新した場合などに利用
  • 抽出したメッセージを既存の翻訳済みメッセージにマージする機能
    • 翻訳者が新しい地域化対象のメッセージを翻訳する場合に利用
  • wikipedia:gettext用の翻訳支援ツールを利用可能(.po のフォーマットがGNU gettextと互換性があるため)
    • Emacs用のpo-modeや.po専用のエディタ

Railsやプラグインなどが提供しているメッセージだけを地域化した いなど、地域対象メッセージが変化しない場合はその他の仕組みも 有力な候補になるでしょう。例えば、ActiveScaffold用の ActiveScaffoldLocalizeがその場合です。

ActiveScaffoldLocalize

ActiveScaffoldは、国際化の仕組みとしてObject#as_を提供してい ます。その仕組みを利用して国際化・地域化を実現しているのが ActiveScaffoldLocalizeです。

ActiveScaffoldLocalizeには日本語用のメッセージも含まれている ので、以下のようにすればActiveScaffoldのメッセージを日本語に することができます。

% rails shelf
% cd shelf
% script/generate resource book title:string
% rake db:migrate
% script/plugin install git://github.com/activescaffold/active_scaffold.git
% script/plugin install git://github.com/edwinmoss/active_scaffold_localize.git

config/routes.rb:

-  map.resources :books
+  map.resources :books, :active_scaffold => true

app/controllers/books_controller.rb:

class BooksController < ApplicationController
  active_scaffold :book
end

app/views/layouts/application.html.erb:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
    <title>ActiveScaffold l10n</title>
    <%= javascript_include_tag(:defaults) %>
    <%= active_scaffold_includes %>
  </head>

  <body>
    <h1>ActiveScaffold l10n</h1>
    <%= yield %>
  </body>
</html>

app/controllers/applications.rb:

class ApplicationController < ActionController::Base
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    ActiveScaffold::Localization.lang = "ja-jp"
    true
  end
end

サーバを起動してhttp://localhost:3000/books/にアクセスします。

% script/server
% firefox http://localhost:3000/books/

ActiveScaffold + ActiveScaffoldLocalize

見ての通り、「検索」などのメニューは日本語になりますが、テー ブル名からきている「Books」やカラム名の「Title」などは日本語 になりません。

ActiveScaffoldLocalizeの方針では、これらを日本語にするために以下のような内容の config/initializers/lang/ja-jp.rb*1を作成します。

config/initializers/lang/ja-jp.rb:

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

ActiveScaffold::Localization.define('ja-jp') do |lang|
  lang["Books"] = "本一覧"
  lang["Book"] = "本"
  lang["Title"] = "タイトル"
end

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + ActiveScaffoldLocalize + モデルの地域化

(「本を作成」ではなく「本一覧を作成」になっているのはこ のパッチ で直ります。)

ActiveScaffoldLocalizeのこのやり方は手軽ですが、地域化対象の メッセージが変更になった場合(例: 「Title」から「Name」に変更) や、地域化対象のメッセージをtypoした場合(例: 「Title」ではな く「title」としていた)に気づきにくいという問題があります。 このような問題に対してはRuby-GetText-Packageが有効です。

ということで、ActiveScaffoldのメッセージは ActiveScaffoldLocalizeで地域化し、それ以外は Ruby-GetText-Packageで地域化するようにします。

Ruby-GetText-Package

ActiveScaffoldLocalizeとRuby-GetText-Packageのすみわけは上述 の通りですが、エラーメッセージの地域化はRuby-GetText-Package ではなく、ActiveScaffldLocalizeに任せます。これは、 ActiveScaffoldがエラーメッセージ部分を上書きしているため、 Ruby-GetText-Packageが提供するエラーメッセージ国際化処理とな じまないためです。

また、Ruby-GetText-Packageが取得したロケール情報を使って ActiveScaffoldLocalizeのlangを設定していることもコツのひとつ です。

config/environment.rb:

# ...
Rails::Initializer.run do |config|
  # ...
  config.gem "gettext", :lib => "gettext/rails"
  # ...
end

lib/active_scaffold_gettext.rb:

module ActiveScaffoldGetText
  include GetText::Rails

  bindtextdomain(GETTEXT_DOMAIN)
end

class Object
  def as__with_gettext(message, *args)
    return nil if message.nil?
    localized_message = ActiveScaffoldGetText.send(:sgettext, message)
    if localized_message == message
      as__without_gettext(message, *args)
    else
      localized_message % args
    end
  end
  alias_method_chain :as_, :gettext
end

module ActiveScaffold::DataStructures
  class Column
    def initialize_with_gettext(name, active_record_class)
      initialize_without_gettext(name, active_record_class)
      self.label = "#{active_record_class.name.demodulize}|#{@label.humanize}"
    end
    alias_method_chain :initialize, :gettext
  end
end

config/initializers/gettext.rb:

GETTEXT_DOMAIN = "your-rails-application"
require 'active_scaffold_gettext'

class ActiveRecord::Errors
  # restore default error messages overridden by Ruby-GetText-Package.
  @@default_error_messages = {
    :inclusion => "is not included in the list",
    :exclusion => "is reserved",
    :invalid => "is invalid",
    :confirmation => "doesn't match confirmation",
    :accepted  => "must be accepted",
    :empty => "can't be empty",
    :blank => "can't be blank",
    :too_long => "is too long (maximum is %d characters)",
    :too_short => "is too short (minimum is %d characters)",
    :wrong_length => "is the wrong length (should be %d characters)",
    :taken => "has already been taken",
    :not_a_number => "is not a number",
    :greater_than => "must be greater than %d",
    :greater_than_or_equal_to => "must be greater than or equal to %d",
    :equal_to => "must be equal to %d",
    :less_than => "must be less than %d",
    :less_than_or_equal_to => "must be less than or equal to %d",
    :odd => "must be odd",
    :even => "must be even"
  }

  alias_method :on, :on_without_gettext
  alias_method :[], :on
end

lib/tasks/gettext.rb:

namespace :gettext do
  namespace :po do
    desc "Update pot/po files."
    task :update => :environment do
      require 'gettext/utils'

      module GetText::ActiveRecordParser
        class << self
          alias_method :add_target_original, :add_target
          def add_target(targets, file, msgid)
            if /\|/ !~ msgid
              add_target_original(targets, file, msgid.classify)
              add_target_original(targets, file, msgid.classify.pluralize)
            end
            add_target_original(targets, file, msgid)
          end
        end
      end

      targets = Dir.glob("{app,config,components,lib}/**/*.{rb,erb,rjs}")
      GetText.update_pofiles(GETTEXT_DOMAIN, targets, "#{GETTEXT_DOMAIN} 0.0.1")
    end
  end

  namespace :mo do
    desc "Create mo-files"
    task :create do
      require 'gettext/utils'
      GetText.create_mofiles(true, "po", "locale")
    end
  end
end

app/controllers/application.rb:

class ApplicationController < ActionController::Base
  init_gettext GETTEXT_DOMAIN
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    posix_locale = GetText.locale.to_posix
    posix_locale = "#{posix_locale}-#{posix_locale}" if /_/ !~ posix_locale
    lang = posix_locale.gsub(/_/, '-').downcase
    ActiveScaffold::Localization.lang = lang
    true
  end
end

翻訳メッセージのファイルpoを作って翻訳します。

% rake gettext:po:update
% mkdir po/ja
% msginit -i po/your-rails-application.pot -o po/ja/your-rails-application.po -l ja_JP
# 途中でメールアドレスを聞かれるので入力する

po/ja/your-rails-application.po:

# ...
#: app/models/book.rb:-
msgid "Book"
msgstr "本"

#: app/models/book.rb:-
msgid "Books"
msgstr "本一覧"
# ...
#: app/models/book.rb:-
msgid "Book|Title"
msgstr "タイトル"
# ...

翻訳メッセージをmoにコンパイルしてアクセスするとテーブル名や カラム名などが日本語になります。

% rake gettext:mo:create

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + Ruby-GetText-Package

まとめ

ActiveScaffoldLocalizeとRuby-GetText-Packageを使って、 ActiveScaffoldを用いたアプリケーションの国際化・地域化を実現する方法 のひとつを紹介しました。

基本的に複数の国際化のしくみを同時に使うと問題が起きますが、 今回は以下のようにそれぞれの長所を活かすようにすみわけて、問 題を回避しています。

  • ActiveScaffoldが利用する固定のメッセージは ActiveScaffoldLocalizeで地域化
  • モデル関連や追加・更新が行われるメッセージについては Ruby-GetText-Packageで地域化

*1  config/initializers/lang/以下にファイルを作るというのはActiveScaffoldLocalizeの方針ではありません。ファイルの場所は特に方針はないようです。

つづき: 2009-09-28
Tags: Ruby | このエントリの Delicious history 5 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-08-12

Rabbit 0.5.7リリース

昨日、 RDるびまの記事 )でスラ イドが書けるプレゼンテーションツール Rabbit がリリースされました。

サンプル

Rabbitではスライドを画像 HTML(+画像) PDF一覧表示 )などで出力することができます。

発表した後に資料を公開する場合や配布資料を作成する場合に利用 するとよいでしょう。

対象ユーザ

RDで書けることがウリというくらいなので、テキストエディタでス ライドを作成したい人が対象になります。おそらく、そのような人 はプログラマであることが多いと思うので、Rabbitはプログラマ向 けのプレゼンテーションツールといえるかもしれません。

スライドをテキストで作成すると以下のような利点があります。

  • バージョンコントロールシステムとの親和性が高い (diffの表示など)
  • 使い慣れたテキストエディタで編集できるため、編集作業の効 率がよい
  • 単なるテキストなので、専用のスライド表示ソフトウェアを用 いなくても内容を確認できる

一方、GUIの編集インターフェイスを備えたプレゼンテーションツー ル(wikipedia:PowerPointwikipedia:Keynoteなど)と比較すると、以下のような欠 点があります。

  • 見た目を微調整しずらい
  • 簡単な図を挿入することが面倒
    • 画像作成ソフトを起動して図を作成し、スライドに挿入

RabbitはRDで書かれたテキストだけではなく、PDFを入力としても 受け付けます。つまり、PDFビューアにもなります。

そこで、上記のような編集時の欠点を解決するために、別途PDF出 力ができるソフトウェアでスライドを作成し、Rabbitで表示すると いうことができます。RabbitをPDFビューアとして使うことにより、 Rabbitのユニークで実用的なユーザインターフェイスを使うことが できます。Rabbitの使い勝手に興味がある場合はこの方法を試して みるとよいかもしれません。

Rabbitのユーザインターフェイスに関してはまた別の機会にしてお きます。

まとめ

Rabbitの外面だけを紹介しました。難易度が高いと言われているイ ンストール方法や特徴的なユーザインターフェイスなどについては 触れませんでした。

Tags: Ruby | このエントリの Delicious history | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-08-01

ActiveLdap 1.0.1リリース

LDAPのエントリを ActiveRecord風のAPIでア クセスするためのライブラリ、 ActiveLdap 1.0.1がリリースされました。

ActiveLdapとは

ActiveRecord風のAPIとは1エントリを1オブジェクトとして扱える ということです。例えば、ユーザの説明を変更する場合は以下のよ うになります。

alice = User.find("alice")
alice.description = "New user"
alice.save!

ActiveRecordと同じように、各クラス間の関係を設定して便利にア クセスすることもできます。

class User < ActiveLdap::Base
  belongs_to :groups, :many => "memberUid"
end

class Group < ActiveLdap::Base
  has_many :users, :wrap => "memberUid"
end

alice = User.find("alice")
alice.groups # => [Group("friend"), Group("office"), ...]
alice.groups << Group.find("home")
alice.groups # => [Group("friend"), Group("office"), Group("home"), ...]

friend = Group.find("friend")
friend.users # => [User("alice"), User("bob"), ...]

ActiveRecordと同じように、Ruby on Railsと使用することもでき ます。

% script/plugin install http://ruby-activeldap.googlecode.com/svn/tags/r1.0.1/rails/plugin/active_ldap
% script/generate scaffold_active_ldap
% vim config/ldap.yml

ActiveLdapは以下のライブラリをバックエンドとして利用できます。

  • Ruby/LDAP
    • 拡張ライブラリ(速い、インストールが大変かもしれない)
  • Net::LDAP
    • Rubyのみで実装(遅い、インストールは簡単)
    • 2008/06/17時点の最新版0.0.4では動かない。 Subversion 版を利用する必要がある。
  • JNDIのLDAPサービスプロバイダ (実験的)
    • JRubyでのみ利用可能。

ベンチマーク

以下はActiveLdapに付属するベンチマークの結果です。ベンチマー クでは100エントリを検索しています。「Rehearsal(リハーサル)」 を行って、それぞれ2回ずつ実行しているのは、以前はキャッシュ などで2回目以降の結果がよくなることなどがあったためです。現 在はあまり意味がありませんが、歴史的に残っています。

% ruby benchmark/bench-al.rb --config benchmark/config.yaml
Populating...
Rehearsal -------------------------------------------------------
  1x: AL              0.080000   0.010000   0.090000 (  0.098738)
  1x: AL(No Obj)      0.010000   0.000000   0.010000 (  0.016623)
  1x: LDAP            0.000000   0.000000   0.000000 (  0.008674)
  1x: Net::LDAP       0.030000   0.000000   0.030000 (  0.045199)
---------------------------------------------- total: 0.130000sec

                          user     system      total        real
  1x: AL              0.080000   0.020000   0.100000 (  0.100959)
  1x: AL(No Obj)      0.010000   0.010000   0.020000 (  0.020697)
  1x: LDAP            0.000000   0.000000   0.000000 (  0.010129)
  1x: Net::LDAP       0.030000   0.000000   0.030000 (  0.042075)
Entries processed by Ruby/ActiveLdap: 100
Entries processed by Ruby/ActiveLdap (without object creation): 100
Entries processed by Ruby/LDAP: 100
Entries processed by Net::LDAP: 100
Cleaning...

各項目はそれぞれ以下の通りです。

  • AL: Ruby/LDAPバックエンドのActiveLdapで検索を行い、各エ ントリをオブジェクト化する(ActiveRecord風のAPIを利用す る場合)
  • AL(No Obj): Ruby/LDAPバックエンドのActiveLdapで検索を行 い、各エントリの結果をオブジェクト化しない(エントリを配 列やハッシュなどを使って表現)
  • LDAP: Ruby/LDAPで検索を行う
  • Net::LDAP: Net::LDAPで検索を行う

上記の結果からは以下のことが言えます。

  • 本当に速度が重要な場合にはRuby/LDAPを直接利用する方がよ い。
  • 利用できるならば、Net::LDAPよりもRuby/LDAPバックエンドを 利用した方がよい。
  • Net::LDAPを直接利用するよりも、オブジェクト化しない ActiveLdap + Ruby/LDAPバックエンドの方が速い。

多くの場合、1度に100エントリを処理することは少ないでしょう。 そのため、通常はActiveLdapで各エントリをオブジェクト化しても 問題は少ないといえます。

もし、1度に多くのエントリを扱う場合で、読み込み専用ならば、 オブジェクト化しない方法で利用することでパフォーマンスを改善 することができます。

まとめ

ActiveLdapを利用することでLDAPのエントリをオブジェクト指向的 なAPIで自然に処理することができます。

ActiveLdapは複数のLDAPバックエンドに対応しており、Rubyがイン ストールされている環境さえあれば動かすこともできます。 (Net::LDAPバックエンド使用時。ただしそんなに速くない)また、 JRubyでもほとんどの機能が動きます。

もし、Ruby/LDAPを利用できる環境であれば、Net::LDAPを直接利用 するよりも、ActiveLdap + Ruby/LDAPバックエンドを利用した方が よりオブジェクト指向らしいAPIでLDAPのエントリを操作できます。 また、速度が要求される場合であれば、オブジェクト化を行わない (オブジェクト指向らしいAPIを利用しない)ことにより、より高 速にLDAPのエントリを読み込むことができます。

つづき: 2009-02-25
Tags: Ruby | このエントリの Delicious history 3 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-06-15

コミットログでRetrospectivaと連携

Rubyで実装されたSubversion*1リポジトリブラウザ(兼ITS(Issue Tracking System)/BTS(Bug Tracking System))としてRetrospectivaがあります。

Retrospectivaにはコミットログを解析してより便利にRetrospectivaを使うための機能がいくつかあります。Retrospectivaを採用しているプロジェクトであれば、コミットログの書き方をRetrospectivaの解釈できる書き方にすることにより、さらにRetrospectivaを便利に使うことができます。

チケットへのリンク

RetrospectivaはチケットベースのITS機能を備えています。コミットログには「XXX番のチケットの問題を修正した」というようなログを書くことも多いでしょう。その時、以下のフォーマットでチケットの番号を書くことにより、そのコミットログをRetrospectiva上で見ると該当するチケットにリンクが張られます。(

[#XXX]

この機能はブラウザ上から変更履歴を見ているときにとても便利です。

チケットの更新

RetrospectivaにはExtensionという拡張機能の仕組みがあります。この仕組みを利用したSCM Ticket Updateという拡張機能を導入することにより、コミットログでチケットの状態を更新することができます。

SCM Ticket Updateの導入方法は以下の通りです。(現時点(2008-05-23)のtrunkを利用している場合)

% RAILS_ENV=production ruby script/rxm checkout http://retrospectiva.googlecode.com/svn/extensions/1-1/scm_ticket_update
% RAILS_ENV=production ruby script/rxm install scm_ticket_update
% # Retrospectivaを再起動

拡張機能をインストールした場合はRetrospectivaを再起動することを忘れないでください。

この拡張機能を入れた後は以下のような書式でコミットログを書くことにより、コミットと一緒にチケットも更新することができます。

チケット#123の状態を修正済みに変更:

クラッシュバグを修正 [#123] (status:fixed)

チケット#29の割り当てユーザをaliceに変更:

[#29] テストを追加 (assigned:alice)

他にも以下のような書式が使えます。

[#N] (NAME1:VALUE1 NAME:VALUE2 ...) ログ

また、もし変更後の値に空白が入っている場合は「"..."」とダブルクォートで囲みます。

[#2929] (status:fixed milestone:"2.9 (バラ)") fix a trivial bug.

この機能を使うと、コミットした後にブラウザからチケットを変更する作業がなくなるのでチケットのクローズし忘れも減るかもしれません。また、コミットメールで(コミットログから)チケットがクローズされたことが分かるのも便利な点です。

まとめ

このようにコミットログの書き方を少しRetrospectivaよりにするだけでもっと便利にRetrospectivaが使えるようになります。

Retrospectivaが要求している書き方を使ってもコミットログが見づらくなるわけではないので、少し意識して使ってみてはいかがでしょうか。

*1  Gitにも微妙に対応している

つづき: 2009-08-07
Tags: Ruby | このエントリの Delicious history 2 users | このエントリを含む Yahoo!ブックマーク | このエントリを含むはてなブックマーク | このエントリを含む livedoor クリップ | このエントリを含む FC2ブックマーク | このエントリを含む Buzzurl | このエントリをTweetする | | Permalink
2008-05-23

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|