クリアコードはプログラミングが好きなソフトウェア開発者を2名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyに関連した開発を得意としています。
注: 長いです。
日本Ruby会議2010でるりまサーチの作り方について発表しました。
|
2010-08-30 |
[rk10][29S06] るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索 (32:59) cache!! |
ステージから見た感じだと立ち見の人もいたようでした。セッションに参加してくれたみなさん、会場を担当してくれたりレポートしてくれたスタッフのみなさん、ありがとうございました。
時間の関係で省略したことも含めてまとめておきます。
資料の中では、まず、るりまサーチについて説明し、その後、全文検索システムとしてのるりまサーチをどう作るのかを説明しています。
るりまサーチはRubyリファレンスマニュアル刷新計画 (通称るりま)の成果物であるRuby本体のリファレンスマニュアルを全文検索するためのWebアプリケーションです。るりまサーチが必要とされていた理由は、既存のリファレンスマニュアル閲覧Webアプリケーションに組み込まれていた検索機能の速度が遅かった*1からです。せっかく有益なリファレンスマニュアルがあっても、目的のエントリにたどりつくのが難しければ、有効に活用することができません。検索機能の面からリファレンスマニュアルの有効活用を支援する全文検索システムがるりまサーチです。
るりまサーチはRubyのリファレンスマニュアルに特化した小さな全文検索システムですが、最近の全文検索システムにとって重要なエッセンスが含まれています。全文検索システムを開発する場合はこれらのエッセンスを含めることを検討してみてください。
まず1つ目はドリルダウンと呼ばれる機能です。Solrなど他の全文検索システムによってはファセットと呼ぶこともあります。ドリルダウンとは、通常の検索結果に加えて、別のパラメータでの絞り込み結果も同時に提供する機能です。スライド中では「Rubyのバージョンで絞り込んだ結果、何件ヒットするか」という情報も表示しています。
この機能で嬉しいことは以下の2点です。
どちらもユーザの使い勝手を向上させるインターフェイスにつながります。ショッピングサイトなどでも使われているインターフェイスですね。
2つ目はURLのパスに絞り込み条件を含めることです。これは、内部ネットワーク用の全文検索システムではなく、インターネット上に公開する全文検索システム向けです。
最近ではURLにUTF-8でエンコードされたページ情報を含めることは一般的になってきました。WikipediaやAmazonでも行っています。Web検索エンジンはURLからも検索用の情報を抽出しているようなので、SEOになると考えられます。
3つ目はキャッシュです。より快適に検索・絞り込みを行うにはできるだけ速いレスポンスが求められます。レスポンスを高速化するためには、以下のような方法があります。
手軽に高速化する場合は後者のキャッシュ機能が便利です。キャッシュをする場合はキャッシュを無効化するタイミングを慎重に検討する必要があります。このタイミングを誤ると、期待した結果が返ってこないという問題が発生します。
キャッシュを無効にするタイミングはアプリケーションに依存します。一般的に、データが変更されるまでは同じキャッシュを利用できます。るりまサーチの場合は1日1回バッチ処理で元データを更新しています。そのため、同じキャッシュを1日使いまわすことができます。これにより高速にレスポンスを返すことができます。
また、キャッシュの効果を高めるためには、処理の内部よりもクライアントに近いところでキャッシュする必要があります。その方がより多くの計算を省略することができるからです。るりまサーチはログインせずに使えるシステムなので、同じ検索リクエストの結果はクライアントに関わらず同一になります。そのため、レスポンスをまるごとキャッシュすることができ、とても高い効果があります。
ログインが必要なシステムの場合は、クライアント毎に変更される部分のみJavaScriptで動的に生成したり、iframeを用いて別HTMLにすることにより、ログインによって変更されない部分ではキャッシュを利用することができます。それが難しい場合はもっと処理の内部でキャッシュをすることになります。この場合はキャッシュの効果が薄くなります。
キャッシュを用いることにより劇的にレスポンス速度を改善することができますが、キャッシュの有効期限とキャッシュする場所についてはよく検討する必要があります。
るりまサーチに含まれている最近の全文検索システムに重要なエッセンスは以下の3つです。
それでは、このようなエッセンスを含む全文検索システムるりまサーチの作り方について説明します。
全文検索システムは以下の5つの要素からなります。
まず、検索対象からクローラーが検索対象とする文書を収集します。次に、それらからインデクサーがテキストやメタ情報を抽出して全文検索エンジンに登録します。全文検索エンジンに登録したデータからユーザが求めるデータを検索して提示するのが検索インターフェイスです。
るりまサーチの場合は以下のようになります。
この中で、るりまサーチの重要な部分である全文検索エンジン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の使い方を説明します。手順は以下の通りです。
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るりまサーチのリポジトリにあるソースコードを見てください。
るりまサーチの検索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
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も一緒に用いることにより開発・運用が改善されるでしょう。
最後にお知らせです。クリアコードではプログラミングが好きな開発者を募集しています。プログラミングが好きな人は検討してみてください。
前作プログラミングRubyのRuby 1.9対応版です。
前作:
オーム社
¥ 3,990
オーム社
¥ 4,410
このシリーズはページ数からもわかる通り、Rubyについて細かいところまで網羅しているのが特徴です。例えば、Proc.newとlambdaの違いはわかりますか?以下のようなクラス定義の書き方による参照解決の違いはわかりますか?
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を使いこなそうというくらいの気持ちがあるのであれば、助けになってくれることでしょう。
オーム社
¥ 3,990
オーム社
¥ 4,620
早いもので来週末は日本Ruby会議2010です。日本Ruby会議では、たくさんいる(会ったことはないけど名前を知っている)参加者がお互いを認識しやすいように大きな名札をつけることが恒例となっています。
RubyKaigi日記でも名札には名前を大きく書きましょうと呼びかけています。この中で、「あらかじめ太くて大きなフォントで、黒々と印刷してきたものを持参して、名札に貼り付けるのはいかがでしょう。」と提案しています。しかし、自分でデザインするのはわりと面倒なものです。
そこで、kdmsnrさんが名札には名前を大きく書きましょうジェネレータを作りました。これはtwitter IDを指定するだけでRubyKaigi日記で提案されているようなデザインの画像を生成してくれます。
でも、印刷するならPDFの方が嬉しいよね、ということでPDFを出力できるように改造したのが名札には名前を大きく書きましょうジェネレータ改です。
PDFも出力できるようにした他に、フォントを選べたり、細かく調整するためにSVGも出力できるようにしています。それでは、どのように実現しているかを説明します。
描画にはcairo、文字の配置にはPango、画像の読み込みにはGdkPixbufを用いています。どれもLinux、*BSD、Mac OS X、Windowsなど多くの環境で動作するライブラリです。ここでは、cairoとPangoだけ説明します。
cairoは2次元グラフィックを生成するためのライブラリで以下のような特長があります。
ベクトルベースの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は多言語に対応したテキストの配置を行うライブラリです。フォントの扱いなどテキストの配置に関することを抽象化してくれるので、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にあります。
毎年開催規模が大きくなっている日本Ruby会議に今年も参加します。今年も去年と同じくスポンサーと発表者として参加します。
発表タイトルは「るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索」です。
発表内容はるりまサーチという、Rubyリファレンスマニュアル刷新計画 (通称るりま)の成果物であるRubyのドキュメントを全文検索するWebアプリケーションの関連技術を紹介するというものです。もう少し具体的にいうと以下のような話題になります。
Twitterなどを見てもわかる通り、世界には情報がどんどん増えていきます。そうすると、その中から必要な情報を選ぶことが重要になっていきます。しかし、情報が溢れた世界では人力のみで効率よく情報を選択することは困難です。最近Twitterが提供をはじめた「おすすめユーザー」という機能も、溢れかえった情報の中から必要な情報を見つけることを支援するための機能と言えます。
必要としている人が必要な情報を見つけやすくしたい、そんなアプリケーションを作りたいと考えている人に聞いて欲しい内容です。もしかしたら、groongaが提供する必要な情報を見つけるための機能でそれを実現できるかもしれません。発表日時は最終日8/29(日)の13:30-14:00で、場所は中ホールです。同じ時間帯に、別の場所ではかずひこさんの外国で暮らすRubyistだけど何か質問ある?、TermtterKaigi、MSWin32版Ruby野良ビルダー養成塾などありますが、こちらは30分なので、こちらの発表の時間だけ抜け出すことも考えてみてください。
日本Ruby会議2010で発表する予定の内容を紹介しました。面白そうだと思った方はぜひ参加してみてください。チケットはまだ少し残っているようです。また、チケットを譲りたいという方もいるので、まだチケットを持っていない方は連絡をとってみるのもよいかもしれません。
それでは、日本Ruby会議2010でお会いできることを楽しみにしています。
採用を再開しました。ソフトウェア開発者を2名募集しています。応募条件はプログラミングが好きなことだけです。学歴や年齢などは関係ありません。勤務地は東京都文京区または栃木県小山市になる予定です。詳しくは採用情報を見てください。日本Ruby会議2010の会場にはクリアコードの人が3人はいるはずなので、そのときに声をかけてもらえればその場でも説明します。
とあるRails 3を使っているたいやき用のCMSでDeviseを使ってOpenID認証をするようにしたので、そのやり方を紹介します。RubyはRuby 1.9.2 RC2も出ていますが、今回はRuby 1.9.1を使います。
DeviseはRackベースの認証システムです。バックエンドに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に対応します。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のものを使っているので、これから使い方は変わっていく可能性が高いと考えられます。注意してください。
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の日記ということなので、ぜひ、ご協力をお願いします。
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
ログファイルから直近のログだけを取り出して処理したいときなどに利用してみてはいかがでしょうか。
みなさんはPassengerの管理下にあるRails/Rackプロセスをどのように監視しているでしょうか。
Muninを使った方法もあり、Dan Manges's Blog - Rails Application Visualization(gist: 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を使っている場合は導入してみてはいかがでしょうか。
すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したフリーソフトウェアが入ったgitリポジトリの公開を始めました。
リポジトリ内にはgit用のコミットメール送信スクリプトを含むgit関連ユーティリティ集「git-utils」やCPUの使用率を表示するFirefoxアドオン「システムモニター」も含まれています。中には試し作りしただけのものなども含まれています。それぞれのソフトウェアはリポジトリ内に同梱されているライセンスにしたがって自由に利用できます*1。
クリアコードは既存のフリーソフトウェアプロジェクトの開発に参加するだけではなく、新たにフリーソフトウェアプロジェクトを立ち上げたりもしてきました。中にはプロジェクトを立ち上げるほどでもないような小さなソフトウェアもあり、それらのソフトウェアはこのようにひっそりと開発していたりします。これらはフリーソフトウェアなので、有用だと思うものがあったのなら、ソースコードにアクセスし、自由に利用してください。
もっと自由にソフトウェアを利用できる世界になるとよいですね。
*1 設定しているライセンスはGPL/LGPL/MPLあたりです。
今年も夏に日本Ruby会議が開催されますが、昨年に引き続き今年もスポンサーになりました。
日本Ruby会議2010のトップページで微妙に公開されていますが、るりまサーチについて発表する予定です。
いくつかの企画にも参加します。今のところ、るびまでActiveLdapの記事などを書いている高瀬さんのLDAPに関する企画とRuby 1.9コミッタQ&Aに参加する予定です*1。
日本Ruby会議2010でのクリアコード関連情報のお知らせでした。
Ruby関連といえば、最近、クリアコードもAsakusa.rbデビューしました。
*1 どちらかというと企画側で。
先日、るりまの成果物である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
DataMapperやSequelなど文字列ではなく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 画像にしたいですね。
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で作る方法を紹介します。
まず、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
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))
ぜひ、このような機会を活かして、フリーソフトウェアの開発に積極的に参加してみてください。
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専用のスクリプトになってしまいます。
実は、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のことをもっと知って上手に付き合ってみてはいかがでしょうか。
るびま0029号がリリースされていますね。おめでとうございます。
せっかくなので少し紹介します。
今回のるびまにはRubyでLDAPを操作するための便利ライブラリActiveLdapの記事の後編ActiveLdap を使ってみよう(後編)が入っています。
後編では今まではあまり文書化されていなかったフィルタのことや関連性のことにも触れています。全てのエントリを扱うクラスは、知る人ぞ知るのノウハウではないでしょうか。(ActiveLdap付属のサンプルアプリケーションでは使われていますが文書化はされていなかったはず。)LDAPサーバの設定やデータを確認する場合にはLDAP のスキーマ情報を ActiveLdap から参照するあたりの情報が役に立ちます。ldapsearchなどではなく、irbでLDAPサーバの情報を確認できるので、環境構築時やデバッグ時にとても便利です。(irbの補完機能を有効にするとより便利です。)
記事を書いている高瀬さんはActiveLdapのチュートリアルの翻訳もしている頼もしい方です。ActiveLdapに興味はあるけどまだよく知らないという方は、まず、 ActiveLdap を使ってみよう(前編)を読んでからチュートリアルを読んで、最後に後編を読むのがよいのではないでしょうか。
0029-RubyNewsにもActiveLdap 1.2.1のリリースが載っていますね。
発表者として参加したとちぎRuby会議02のレポート記事RegionalRubyKaigi レポート (10) とちぎ Ruby 会議 02もあります。レポートにもありますが、とちぎRuby会議02のお題が他のRegionalRubyKaigiと違ったものだったので、独特な内容になっていましたね。
0029 号 巻頭言で紹介されているRuby 1.9向けに書かれたRubyベストプラクティスという本がそろそろ発売するようです。テストまわりの章だけレビューに参加しました。
少しくせがある印象なので、Ruby初心者の方にはつらいかもしれません。自分で判断できる程度にRubyを知っている方ならいろいろ考えながら読むとおもしろいかもしれません。
るびまがリリースされていたので紹介しました。
ところで、みなさんは編集後記は読んでいるのでしょうか。たまにおもしろかったりするので、読んでいない方は読んでみてはいかがでしょうか。短いのでさっと目を通せます。
先日、Ruby用のxUnit系テスティングフレームワークtest-unit 2.0.7とC・C++用のxUnit系テスティングフレームワークCutter 1.1.1がリリースされました。
どちらも、テストの書きやすさ(テストをキレイに書けるとテストを保守しやすい)だけではなく、テストが失敗した時に「できるだけ素早く問題の原因にたどり着ける」ことも重視しています。
Rails/Rack界隈ではCucumberやWebrat、capybaraなどを使って、「"ログイン"ボタンをクリックする」とか「click_link("ログイン")」などと、直感的にテストを書けるようになっています。では、テストが失敗したときの結果はどのように表示されるでしょうか。HTML全体やテキスト全体が表示されて、「"ログイン"というボタン(リンク)はなかったよ」と言われたらどうでしょう。あなたのコードはどこが悪かったのでしょう。
そういうときに、失敗結果を見て、すぐに「あぁ、ここが悪いかも!」と作業を進めていけるようなテスティングフレームワークにしたいものです。開発はデバッグの連続なのですから、よりスムーズにデバッグ作業を進める手助けとなるツールを使って開発したいですよね。
test-unitやCutterはWebアプリケーション用に特別なサポート機能は提供していないので上記のようなことをうまい具合に解決できるわけではないのですが、ライブラリのテストでは上記のようなことをうまい具合に解決する機能を提供しています。
一応、リリースで変わったことを少し書いておきます。
機能面でもよくなっているのですが、インストールまわりだけにしておきます。
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をサポートしているので、こちらもインストール・アップデートが簡単ですね。
書きやすさだけではなくデバッグしやすさも重視したテスティングフレームワークに興味のある方は使ってみてはいかがでしょうか。
先日、LOCAL DEVELOPER DAY '10 WinterでRubyでmilterを作る方法について話してきました。どのタイミングでどのmilterプロトコルのコマンドが発行されるかについても説明しているので、Rubyではなく(libmilterを使って)Cで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を作れる」ようになるゴールまでたどりついてください。
メールシステムとは外部とユーザ間でメールを配信するシステムです。すべてのメールシステムではそのままメールをやりとりするのではなく、メールを配信するまでのあいだに、メールに対してなんらかの処理を実行します。つまり、すべてのメールシステムにはメールフィルタ機能が備わっています。
メールシステムでメールフィルタを実現する方法はいくつかありますが、その1つがMTA(メールサーバ)のプラグインとして実現する方法です。この方法のメリットはMTAを変更せずにメールフィルタ機能を変更できることです。milterはこのタイプで動作するメールフィルタです。
メールフィルタはメールシステムが持っている必須機能の1つです。その実現方法としてMTAのプラグインとして実現する方法があり、milterもその方法で実現されているメールフィルタです。
それでは、milterの概要について説明します。
milterの名前の由来は「mail filter」です。milterは汎用的なメールフィルタの仕組みのため、同じメールフィルタを異なるMTAと一緒に使うことができます。
Sendmailを用いているメールシステムではmilterを利用していることが多く、milterをサポートした商用のメールフィルタも多く存在します。最近ではPostfixのmilterサポートがリリース毎に改善されていっているため、Postfixを用いたメールシステムでもmilterを利用するケースが徐々に増えています。
「milter」は文脈によって異なるものを指すことがあります。そこで、ここでは混乱を避けるために異なる名前で呼ぶことにします。
まず、メールフィルタそのものを「milter」と呼びます。
メールフィルタとMTAは別プロセスで動作するため、プロセス間通信でフィルタ対象のメールやフィルタ結果などをやりとりする必要があります。そのやりとりのきまりを「milterプロトコル」と呼びます。
そして、「milter」と「milterプロトコル」をサポートしたMTAを含んだメールフィルタの仕組み全体を「milterシステム」と呼びます。
「milter」といった場合は「メールフィルタそのもの(ここでいうmilter)」という意味で使う場合と、「メールフィルタの仕組み(ここでいうmilterシステム)」という意味で使われる場合が多いです。「milter」という単語が使われている場合はどちらの意味かを判断できるようになってください。
milterプロトコルはSMTPと密接に関連したプロトコルです。そのため、milterプロトコルについて説明する前に、SMTPについて確認します。
SMTPは以下の4つのコマンドが基本となるシンプルなプロトコルです。
まず、「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プロトコルの詳細を説明します。
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で実現できます。
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」コマンドが複数回発行されます。
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を作成します。今回はSubject、From、Toと本文のみを扱うことにします。
メール検索を実現するために、全文検索エンジンとしてgroongaを、milterライブラリとしてmilter managerのRubyバインディングを使います。
groongaは全文検索のためのインデックス作成機能だけではなく、データストアの機能も持っています。groongaのデータストアはカラム指向で、リレーショナルデータベースとは違い、レコード(行)毎にデータをまとめて持つのではなく、カラム(列)毎にデータをまとめて持っています。
このようにデータを持つと、同じカラムの複数の値へのアクセスを高速に行うことができます。このため、カラムの値を使った集計処理を高速に実行できます。集計処理とは、例えば、SQLでいうGROUP BYのような処理です。
集計処理を用いると絞り込み検索をしやすいユーザーインターフェイスを提供することができます。例えば、ショッピングサイトで商品に複数のタグがついているとします。このとき、同じタグがついている商品が何項目あるかを表示してリンクにします。1つも商品が属していないタグは表示しないようにすれば、ユーザは無駄な絞り込み操作を行わずにすみます。
全商品(123件) タグ スポーツ(58件)← リンクにする 映画(45件) ← リンクにする 食べ物(36件) ← リンクにする 旅行(0件) ← 表示しない
この状態で「スポーツ」をクリックしたとします。
全商品(123件) > スポーツ(58件) タグ スポーツ ← 選択済みなので表示しない 映画(26件) ← リンクにする 食べ物(0件) ← 表示しない 旅行(0件) ← 表示しない
このように、絞り込んだ後にがっかりするような操作を示さないことにより、絞り込み検索をしやすいユーザインターフェイスを作ることができます。がっかりするような操作かどうかを判断するために、同じ値を持つレコードの個数を数える、といった集計処理をしています。
groongaはキー管理のためのデータ構造としてハッシュテーブルとバイナリパトリシアトライを採用しています。バイナリパトリシアトライはパトリシアトライの一種です。
ここにB+木とパトリシアトライの説明を書く予定でしたが、もう、だいぶ長くなっているので省略します。また別の機会があれば紹介します。
パトリシアトライを利用すると効率よく最長一致検索を実現できます。これを試してみるためのサンプルアプリケーションを用意しました。
リンク先ではキーワードを変えて試すことができます。
最長一致機能を利用してキーワード検出している部分のソースは以下の通りです。
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という便利メソッドがあり、これを使うと「最長一致検索」→「キーワードにタグ付け」をより簡潔に記述することができます。
全体のソースはリンク先にあるソース一式の中に含まれています。
groongaのRubyバインディングであるRuby/groongaはスキーマ定義のためのDSLを提供しています。
メールを保存するMessagesテーブルにはsubject、from、to、bodyカラムを定義しています。今回は簡単のため、宛先は1つのみ扱うことにしています。
次に、高速に全文検索を行うために索引を作成します。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
データの保存・検索の仕組みはできたので、あとは、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を拡張する」というタイトルで石田さんが淡々とライブコーディングされていました。会場とやりとりをしながらコーディングする様子を見ていると、札幌っぽい雰囲気を感じることができるでしょう。
今年も肉の日がきましたね。
Ruby/groongaの0.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.0.7だったバージョン番号が一気に0.9.1まであがっています。バージョン番号から想像できる通り、初のメジャーリリース1.0.0を意識しはじめたということです。
0.9.x系列では1.0.0に向けて以下の2点を重点的に開発していきます。
APIを改良するため、以前のバージョンとは互換性が壊れることがありますが、今後より便利にRuby/groongaを使うために、今のうちに積極的に改良していく予定です。使ってみて、「ここがこうなっていたらもっと使いやすい」、「こういうAPIがあると便利」などという意見があったらぜひお知らせください。
Groonga::Context.default#[]のショートカットとしてGroonga.[]を導入するなど、もうすでに便利なAPIの追加は始まっています。
現在のRuby/groongaにはまだ高速化の余地があります。あまり意識せずにスクリプトを書いてもなるべく高速に動作するようにライブラリ側でできることはなるべくライブラリ側で頑張る方向で開発していく予定です。ユーザが使いやすくなるように処理系が頑張るというのはRuby本体と同じ方向です。
今年も年に一度の肉の日がきました。
Ruby/groonga 0.9.1がリリースされています。Rubyで全文検索システムを構築したい場合はRuby/groongaも検討してみてはいかがでしょうか。今のうちに改善案をだしておけばメジャーリリース時にはそれが反映されてより便利に高速な全文検索システムを構築できるようになるかもしれません。
来月2/13(土)に札幌で開催されるLOCAL DEVELOPER DAY '10/WinterでRubyでメールフィルターを作る方法について話します。
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といった場合は仕組みよりメールフィルターのことを指すことが多いです。
先日開催された〜Ruby on Rails Technical Night〜 Ruby on RailsセミナーでActive Directoryと連携したRailsアプリケーションの作り方について話しました。
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 多くの方はご存知のようでしたが。。。
先週末開催された札幌Ruby会議02でライブコーディングしてきました。本州枠の1つで話す機会を作ってくれた実行委員長のしまださん、ありがとうございます。
今回の発表はスライドだけでは伝わらないはずです。いつも、話すことすべてをスライドに盛りこんでいませんが、今回は特にスライドに盛りこまれていないことが多いです*1。
仙台では考え方について話しましたが、札幌では技術的なことを話すつもりでした。札幌で何かを伝えることができるのなら、それは技術的なことであって欲しいし、Ruby 逆引きレシピをリリースしているRuby札幌がいる札幌でならそれができるはず、というのが理由です。
そのために、今回はライブコーディングをすることにしました。
自分ができる、一番、技術的なことを伝える方法はプログラムを書くことです。プログラミングに限ったことかもしれませんが、何かを伝えるためには、結果だけではなく、過程も一緒に伝えた方が、より伝わります。ペアプログラミングが成果をあげているという声を聞いたこともあるのではないでしょうか。
ライブコーディングは1対1ではなく、1対nの形になるので、それで本当に伝えることができるのかは不安でしたが、札幌でなら大丈夫だろうということで決行しました。
札幌Ruby会議02の模様はニコニコ動画にアップロードされています。
|
2009-12-22 |
レシピに書かれていないこと - 須藤 功平 (20:41) 札幌でたいやきってな w |
tDiaryの話をした柴田さんも録画してくれました。ありがとうございます。こちらはYouTubeにあります。
A: 2, 3回練習しました。
あまり練習すると本番で間違わなくなるのであまり練習しないようにしました。実際、ライブコーディング中にいくつか間違いました。間違ったときにどうやって直していくかという過程も見てほしかったのでよかったです。
実際の開発はデバッグの連続です。間違わずに5分で完成させる動画通りにはいかないものです。
A: 使い慣れたエディタを使ったからです。
自分ではあまり意識して使っていませんが、意識してみると、以下の機能を使っているようです。以下の機能を使うことにより通常より速くコードを書くことができるかもしれません。
動的略語展開: 入力途中の単語を補完します。補完候補は現在開いているすべてのバッファ内にある単語すべてです。デフォルトではM-/にバインドされていますが、補完といえばタブなのでC-c C-iにバインドしています。
(define-key global-map "\C-c\C-i" 'dabbrev-expand)
A: ラングバプロジェクトのSubversionリポジトリでAGPL v3+で公開しています。
当日利用したリビジョンはr874なので、以下のコマンドで同じソースコードを取得できます。
% svn co -r874 http://groonga.rubyforge.org/svn/examples/message-archiver/
仙台ともとちぎとも違う雰囲気でした。とてもとても居心地がよかったのが忘れられません。Rubyを使って気分よくプログラムを書いているときと少し似ている気がします。
最初から最後まで最前列で話を聞いていました。文脈は少し違うのですが、ちょうど興味のある話題や、前から考えていたことに関連することが多く、参考にできることがたくさんありました。
田中さんの、ユーザが余計なことを考えなくても簡単に使えるようにしたい、という話、sumimさんや谷口さんの、興味を持って学習するためにはという話、和田さんの、増加するテストに立ち向かう方法の話、高橋さんの、やめる勇気の話、角谷さんの、選んだらそれでハッピーではない、上を見てもキリがないし下は見てもしょうがないんだから自分ができることをやっていく、という話。文脈が違うので自分の中で変換しながら自分に合わせて聞いていたので、発表された方が一番言いたかったこととずれているかもしれません。
ツールでは、しだらさんの紹介していたJekyllも使ってみたくなりました。とちぎではそうでもなかったのですが、今回は使ってみたくなりました。とちぎでは駆け足だったからかもしれません。
技術的なことは本編とは別のReject Talksが楽しかったです。Reject Talksでは、単に発表者が話すのではなく、観客が随時コメントをしていくという形式に(結果的に)なっていました。その中で、このコードがこう悪い、こうした方がよい、という話をできたことがとても楽しかったです。
話す側と聞く側ではなく、両者が話しあっていることがとても印象的でした。
札幌Ruby会議02での発表内容について補足しました。また、1参加者から見た札幌Ruby会議02も簡単にレポートしました。
札幌Ruby会議02がすばらしかったのはスタッフのみなさん、発表者のみなさん、参加者のみなさんなど関係者のみなさんのおかげでしょう。でも、もう少し考えてみると、実行委員長のしまださんがいることが一番大きいのではないかと感じています。みなさんは、司会をしていたしまださんが想いがこもった発表者紹介をしていたのに気づいていましたか。想いのこもった感想を述べていたことに気づいていましたか。しまださんの発表枠はありませんでしたが、しまださんの想いを感じることができたすばらしい札幌Ruby会議02でした。
そんな札幌Ruby会議02に発表者・スポンサーとして参加できてとても光栄です。
今回はActiveLdapには触れられませんでしたが、12/14のRailsセミナーではActiveLdapに触れるので、興味のある方は参加してみてください。
*1 公開用に多少加筆してあります。
来月中旬の12/14(月)に〜Ruby on Rails Technical Night〜Ruby on RailsセミナーでActive Directoryと連携したRailsアプリケーションの開発方法について話します。セミナーの概要は以下の通りです。都合があう方はぜひお越しください。
さも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について話すことになったので、それの告知をしました。参加費は無料なので興味のある方はお気軽に参加してみてください。現時点で定員の半分ほど埋まっているようなので、気になる方はお早めにどうぞ。
先日開催されたとちぎRuby会議02で社長の一人として話しました。声をかけてくれたtoRubyのみなさん、ありがとうございます。
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会議02とTokyuRuby会議01もあります。こちらの内容もとてもおもしろそうですね。
*1 お昼ご飯タイムもとてもよい雰囲気でした。
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で検索条件を指定するところでも使われています。例えば、以下のような構文があります。
もう少し詳しい説明は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を試してみてはいかがでしょうか。
以前、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>
これでメニューが日本語化されます。
日本語関連の手順は最初のI18nまわりのところだけです。ActiveScaffold関連の手順にはまったく日本語関連の手順はありません。通常のインストール手順で日本語メニューが表示されるのは、ActiveScaffold本体に日本語リソースが含まれるようになったおかげです。
データを入れるとこのようになります。
ActiveScaffold本体に日本語リソースが含まれて、より簡単に日本語メニューのActiveScaffoldを使えるようになったことを紹介しました。
フリーソフトウェアを改良してよりよくした場合は、手元での変更やブログに書くにとどめずに、本家にフィードバックしてみてはいかがでしょうか。より多くの人が便利に使えるようになるだけではなく、多くの場合、自分のメンテナンスコストも下がります。
週末にるびま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号をぜひ読んでみて下さい。
10月24日に開催されるとちぎRuby会議02の参加登録が行われています。懇親会参加の状況から推測するとまだ登録できそうです。
今回は、超優良企業の1つとして声をかけてもらい、出場者として参加できることになりました。ワイクルの角さん、ヴァインカーブのやまだあきらさん、タワーズ・クエストの和田さんも出場者として参加されます。とても楽しみですね。
咳さんのdRuby本をテキストとしたtoRuby勉強会も開催されるそうです。こちらもとても楽しみですね。
都合があう方は参加してみてはいかがでしょうか。
Rubyで実装されたプロジェクト管理システムであるRetrospectivaのバージョン2.0がまもなくリリースされます。cozmixng.orgで最新バージョンが運用されているので、それを触ってみることで最新の機能を確認することができます。見てもらえばわかる通り、日本語表示にも対応しています。
Retrospectivaは一時期開発が停滞していて、その間にRedmineの方が普及しました。しかし、その後、再び開発が活発になり、現在は2.0 RC1がリリースされています。1.xから2.0では多くの改良が行われています。そのいくつかを紹介します。
Single Step Installerが用意されていて、コマンド一発でインストールできるようになっています。以前より導入の敷居が下がっています。
AgilePMというアジャイル開発を支援するプラグインが公開され、プロジェクト管理機能がさらに充実しています。tDiaryプロジェクト用のAgilePMがあるので、そこで触ってみることができます。ただし、現時点ではまだ利用されていないのであまり雰囲気がわからないかもしれません。これからのtDiaryプロジェクトの利用に期待しましょう。
Subversionだけではなく、gitにも対応しました。また、Retrospectiva自体のバージョン管理システムもSubversionからgitに移行しています。
最近はgitを採用するプロジェクトも増えているため、これは嬉しい機能ではないでしょうか。
まもなくリリースされるRetrospectiva 2.0を簡単に紹介しました。以前は「ブログがついたTrac」みたいな書かれ方をされていたRetrospectivaですが、実際に使ってみるとその表現が間違っていたことに気付いた人も多かったのではないでしょうか。以前からコミットログで連携する機能などがあり、使っていた人は「便利なプロジェクト管理ツール」という方がしっくりくることに気付いていたはずです。2.0ではより便利で有用な機能がスマートなインターフェイスで追加されています。2.0の紹介のために「ブログがついた〜」と書かれることは減ることでしょう。
Redmineもよいですが、プロジェクト管理ツールとしてRetrospectivaも検討してみてはいかがでしょうか。
もし、使用してみてRetrospectivaの開発に参加したくなった場合はRetrospectivaを使って開発に参加するとよいでしょう。まずは、未翻訳メッセージの翻訳から参加するのが敷居が低いでしょう。kou@clear-code.comまで連絡してもらえれば相談にのります。
Ruby/groongaのサンプルアプリケーションのデモを用意しました。
RailsなどのWebアプリケーションフレームワークを使うほどのものではないので、ActiveGroongaは使わずに、Ruby/groongaとRackの組み合わせになっています。Rackについてはyharaさんの5分でわかるRackなどを読んでみてください。
デモはPassengerで動かしています。PassengerにRackを設置したことがある人なら10分もかからずにサンプルを動かせるのではないかと思います。
デモを見てもらえばわかる通り、小さなサンプルですが以下のように一通りの機能は備えています。
それぞれ、もう少し詳しく見ていきましょう。
通常の検索サイトでは空白で複数のキーワードを区切ることによって検索結果を絞り込むことができます。例えば、「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ステップだということがわかります。
簡単ですね。
サンプルアプリケーションを例にして、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がリリースされました。
日本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 Parserやrcairoなど、ここに挙げたフリーソフトウェア以外でも構いませんので、なにかコメントなどがあればスポンサーブースで声をかけてください。
*1 永井さんがLightning TalksでRuby/Tkは本当にダメな子なのか?というお話をするようですし。
日本Ruby会議2009のセッション詳細が公開されました。
日本Ruby会議2009は3トラックで3日間の開催のため、たくさんのセッションがありますが、このうち、2セッションで発表します。
1つ目は2回目のライトニングトークの一番最後です。
ライトニングトークではActiveLdap(チュートリアル)について話します。ActiveLdapの開発に関わるようになってから約3年経ちますが、ついに発表する機会に恵まれました。
クリアコードは今月から第四期に入っていますが、ActiveLdapはクリアコード設立初期にLDAPを利用する機会があったことがきっかけで開発に関わるようになったフリーソフトウェアです。当時は多くの問題を抱えていたActiveLdapですが、そのときからコツコツ開発を続けていたため現在では当時よりもかなり便利なライブラリとなりました。その成果を日本Ruby会議で発表できることはとても感慨深いものです。
2つ目は同じ日の同じ会場の次の枠で、CとRubyとその間です。
クリアコードではActiveLdap以外にもたくさんのフリーソフトウェアの開発に関わっていますが、その中でもCとRubyそれぞれのよいところを活かしたフリーソフトウェアについて話します。この発表ではそのようなフリーソフトウェアの例としてmilter managerとActiveGroongaを紹介しながら、CとRubyを活かすことのメリットについて話します。
Rubyから使える高速なkey-valueストアとしてはTokyo Cabinetや Localmemcacheが有名です。
ActiveGroonga(とその下の層のRuby/groongaは)これらと同様に高速なkey-valueストア機能も備えるgroongaをよりRubyらしく使いやすいAPIで提供します。
milter managerについては最近いろいろなところで話しましたが、ActiveGroongaについて話すことは今回が初めてです。
日本Ruby会議2009で発表するセッションを紹介しました。他にも興味深いセッションがたくさんあるので迷うと思いますが、興味があれば上記のセッションにも参加してみてください。
クリアコードはスポンサーとなっているため、スポンサーブースを出します。セッションには参加できなかった方も、ぜひ、足を運んでください。
先日、るびま0026号がリリースされました。
今回はRegionalRubyKaigi特集のようで、5Kaigiのレポートが載っています。5Kaigiのうち、仙台Ruby会議01に参加しましたが、そのレポートもあります。
仙台Ruby会議01のレポートはid:monyakataさんが丁寧にすっきりとまとめてくれています。とても読みやすいので参加できなかった人は読んでみてはいかがでしょうか。
レポートによるとまず好きなこと、そしてそれを続けることのセッションが一番参加者が多かったそうです。参加してくれた皆さん、関係者の皆さん、ありがとうございました。
レポート中にある通り、仙台Ruby会議01のサイトには東北・仙台情報が豊富にあります。もし、レポートを読んで仙台を感じたくなったときはサイトにある情報が役に立つことでしょう。お菓子やたいやき情報もあるので、甘いものが好きな人はぜひ参考にしてください。
あまり話題にのぼらない感がある常設コーナーのRubyNewsですが、毎回充実しています。読むと、Ruby界隈では思ったよりいろんなことがあったなぁと感じることができます。今回のRubyNewsはいつも以上に充実しています。読んでみると知らないことも多いかもしれません。
最近は助田さんがruby-talkでリリースアナウンスのあったソフトウェアを紹介していて、海外のRuby関連ソフトウェアに関する情報源としてとても貴重です。実は、RubyNewsにはruby-listでリリースアナウンスのあったソフトウェアが載っているので、日本でのRuby関連ソフトウェアのとても貴重な情報源になっています。
もちろん、肉リリースされたRabbitの情報も載っていますし、RSS Parserのリリースも載っています。ソフトウェアをリリースしたときはruby-listでもリリースアナウンスをしてみてはいかがでしょうか。きっとRubyNewsに載せてもらえます。
るびま0026号がリリースされました。るびまは今回のRegionalRubyKaigiレポートのように毎号変わるコーナーにも有用な記事が多くありますが、実は常設コーナーの中にも有用な情報があります。常設コーナーを読み飛ばしている方はもったいないですよ。
高橋編集長の巻頭言はよく読まれていると思いますが、編集後記もわりとおもしろいですよ。
仙台Ruby会議01レポートだけではなく、zundaさん編集のRubyNewsも紹介してみました。
まっちゃだいふくさんに声をかけてもらったことがきっかけで、オープンソースカンファレンス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札幌のブースにおじゃまさせてもらいました。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札幌から目が離せません。
LDAPのエントリをRubyオブジェクトとして操作するためのライブラリActiveLdapの1.0.9がリリースされました。([ANN] ActiveLdap 1.0.9)
ActiveLdapを知らない人はチュートリアルを読んで雰囲気を感じてください。
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もリリースされる予定なので、そちらも楽しみにしていてください。
最新の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になりました。
利用できないのであれば、メソッドが定義されていない方がよい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になるかもしれません。
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に設定する場合は以下のようになります。
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だけ見てもエンコーディングミスマッチがどこで起こっているかはわかりません。問題が発生したときは問題解決につながるエラーメッセージを提供したいものですね。
データベース機能も備える全文検索エンジン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には以下のような既知の問題があります。
GCの問題は、groongaのgrn_ctxというメモリ管理機能を提供するオブジェクトとRubyがオブジェクトをどの順番でGCするかはわからないという動作のために起きています。これはgroonga本体とも協調しながら解決する予定です。ちなみに、通常のアプリケーションでこの問題が発生する可能性があるのはプロセスの終了時だけです。そのため、この問題のためにデータが壊れてしまうということはないと考えられます。
今回のリリースではよりRubyらしいAPIの提供の優先度を高くしたため、高速化のためのチューニングはそれほど行われていません。いくつかチューニング案があるので、それらを適用することにより、Rubyの読み書きしやすいAPIを利用しながらより高速な全文検索機能とデータベース機能を利用できるようになるでしょう。
ドキュメントが完備されていないのは、APIがまだ流動的なことやgroongaのすべての機能を網羅していないこととも関係があります。groonga本体もまだAPIが改良され続けています。それに追従したり、より使いやすいAPIを目指してRuby/groongaのAPIはこれから変更されるでしょう。その過程でドキュメントも充実していく予定です。
ラングバプロジェクトではRuby/groongaの開発に参加してくれる人を募集しています。興味のある方は開発者向け情報をご覧ください。
クリアコードではRubyの拡張機能を書けるプログラミングが好きな開発者を募集しています。興味のある方は採用情報をご覧ください。
*1 -O0オプション(非最適化オプション)をつけるとすぐにビルドできます。
これまで、ActiveLdapにはまとまった日本語の情報がありませんでしたが、id:tashenさんがActiveLdapのチュートリアルを翻訳してくれています。原文に最新の状況に追従していない部分があるため、いくつか古い情報もあるのですが、現在、最新の状況に追従するように作業が進んでいます。(最初の方はわりと最新の状況に追従しています。)
せっかくなので、ldap_mappingのあたりを簡単に説明します。できるなら、この内容を本家のドキュメントにうまくマージしたいと思っています。
ActiveRecordでは何もしなくてもカラムへアクセスすることができるのですが、ActiveLdapではldap_mappingでLDAPとRubyのオブジェクトを対応させる必要があります。ActiveRecordでは1つのテーブルが1つのクラスに対応し、各レコードがインスタンスに対応しますが、LDAPではそのような対応関係を自動的に判断することが難しいためです。どのようなエントリの集合を1つのクラスに対応させるかはアプリケーション毎に異なります。そのため、ActiveLdapでは明示的にユーザに指定してもらう方法をとっています。適切なデフォルト値がない場合はデフォルト値を提供しない方が混乱しないと思います。
ldap_mappingでは以下の3つの情報を使ってクラスに対応するエントリの集合を決めます。
オブジェクトクラス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という重要なオプションがあります。
より詳しくはActiveLdapの日本語チュートリアルを見てください。
プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。
ここでは、Rubyでの例をまじえながら問題解決に有用なエラーメッセージを紹介します。ライブラリなど多くの人が使うようなプログラムを作成する場合は参考になるかもしれません。
問題に遭遇してから問題を解決するまでには以下の順で作業をする必要があります。
役立つエラーメッセージがあると「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を例にして問題解決に役立つエラーメッセージについて紹介しました。
問題解決に役立つエラーメッセージの特長は、テストの実行結果にもあてはまります。クリアコードが開発に関わっているテスティングフレームワークではテストの実行結果にこだわっています。
あなたが使っているテスティングフレームワークは問題解決に役立つような情報を提供していますか?
すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したプログラムが入ったSubversionリポジトリ(リポジトリの更新状況のRSS)の公開を始めました。
クリアコードでは既存のフリーソフトウェアの開発に参加したり、新しくmilter managerなどのフリーソフトウェアを開発したりしていますが、それらの開発成果の公開場所はケースバイケースとなっています。
このように、クリアコードの開発成果のソースコードは様々なホスティングサイトのリポジトリにて管理、および公開されています。
まもなくクリアコードは設立から3年が経とうとしていますが、その間、プロジェクトを作るまでもないような小規模なソースコードがいくつかたまってきました。この度、そのようなソースコードをSubversionリポジトリで公開することにしました。
このリポジトリには現在、ページの一部を折りたたむfolding.jsや、このククログを生成するためのtDiary関連のスクリプト(日記のデータをSubversionで管理するIOバックエンドや日記を静的なHTMLに変換するスクリプトなどの記事で述べた物)、Thunderbird用の各種アドオンのソースコードが入っています。誰でも自由にチェックアウトできますので、注意事項をご了承の上でどうぞご利用ください。
以下、現在入っているプログラムを簡単に紹介します。
ククログだけではなく、milter managerのブログでも使っています。使い方はmilter managerのtdiary.confが参考になると思います。
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というオプションがあって、それを利用しています。
クリアコードでは毎月29日頃に肉の会と呼ばれる社内食事会を行っています。 肉の会というだけあって肉を食べることが多いのですが、今回は肉ではなく、社内でたこやきを食べまくりました。
今回はゲストとしてITproの高橋さんに参加してもらえました。(忙しい中、ありがとうございます!)
高橋さんがおみやげとして発売したばかりの「Ruby技術者認定試験 公式ガイド」を持ってきてくれました。「クリアコードさんへ」とサインももらいました。ありがとうございます!

この公式ガイドには模擬試験が2回分ついています。Rubyには自信があったのでやってみたところ、90%くらいしか正解できませんでした。普段使わないメソッドや引数が問題になっていると間違えてしまいます。
ひっかけっぽい問題も何題かあり、ひっかけようとしているなぁとニヤニヤしながら解いていたのですが、何題か間違えてしまいました。油断してはいけません。
この模擬試験をやってみて、Hash#invertをはじめて知りました。もうすでにかなりRubyを知っている人も、Ruby技術者認定試験の問題を解いてみると知らないことが見つかるかもしれません。問題を見て、ひっかけっぽいな、とニヤニヤしながら解くのも楽しいと思います。
模擬試験の前にはコンパクトにまとめられたRubyの解説がついているので、まだRubyに詳しくない人のとっかかりにもよさそうな気がします。mapとcollectをきちんと対等に扱っているので、変に偏らなそうなのもよいと思います。*1 Rubyで開発されたデスクトップ・アプリケーションとしてRabbitが挙がっていることも、とてもよいと思います。
日経BP社
¥ 2,100
*1 私はcollectに偏っています。
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には対応できていません。
さて、そんな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/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で使いたいという方はこれを使ってみるとよいと思います。
少し脱線しましたが、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つ登録されたりしたので。
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は以下のカラムが定義されていると、そこに値を設定してくれます。
これらはすべて省略可能です。今回は、せっかくなので、すべて定義しました。
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」リンクから画像アップロードフォームへ進みます。

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
先日、仙台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は、私個人だけではなく、スポンサーとしてクリアコードでも参加したいと思います。
tDiaryをローカルなネットワークに配置して、tDiaryが表示する内容を静的なHTMLとして公開したい場合はよくありますよね。ククログもそんなよくある使い方の1つです。
tDiaryには静的なHTMLを生成するためのsqueezeプラグインがありますが、squeezeプラグインが出力するHTMLは以下の点でCGIで表示される内容と異なります。
ただし、これはsqueezeプラグインが検索エンジンへの入力データとしてのHTML生成を目的としているためです。よくある使い方では、生成されたHTMLはCGIで出力されているように表示できることが目的なので、上記のようなミスマッチが発生します。
そこで、ククログではhtml-archiver.rbという静的なHTMLを生成するスクリプトを使っています。html-archiver.rbは最後の方に載せています。
html-archiver.rbを使うと、CGIで出力されている内容と同じように表示されるHTMLが生成されます。生成例は今見ているこのページです。
使い方はこうなります。
% ruby html-archiver.rb --tdiary tdiayr.rbのあるディレクトリ --conf tdiary.confのあるディレクトリ 出力先ディレクトリ
例えば、以下のような場合を考えます。
この場合はこのようなコマンドになります。
% ruby html-archiver.rb --tdiary ~tdiary/work/ruby/tdiary/core/ --conf ~tdiary/public_html/ ~tdiary/public_html/html/
GPL3あるいは3以降の新しいバージョンのGPL
#!/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
みなさんは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ステップです。
まず、上記のソースコードを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
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のソースがメンテナン スしづらくなっていたのが主な理由です。
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 に標準添付されていた頃とは違い、そのような方針の元で活発に開 発されていく系列になります。
例えば、以下のような機能が他のフレームワークやライブラリから 移植されています。
ここでは「差分表示」と「ネストしたテスト定義」だけ紹介します。 *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では行単位の差分までで列単位までの 差分は表示しません。
余談ですが、この差分表示形式はPython の difflib ライブラリで使われている形式です。
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)がネストで自然に表現されていま す。
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
これは以下のように実行されます。
実行されるフィクスチャ(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を使ってみてはいかがでしょうか。
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があるとき」の判断方法について少し書いてみます。
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を利用する方法を紹介しました。
同じような問題は他のライブラリでも起こりうると思うので、そのような場合も同じように問題を回避できると思います。
Ruby-GNOME2を使って実装されているプレゼンテーションツールRabbit 0.5.8がリリースされました。
0.5.8では部分的に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を利用してみてはどうでしょうか。
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はソースコードハイライトウィジェットである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は描画に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がリリースされてから何人かの方がドキュメント関連作業で協力してくれています。ありがとうございます!
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ではテキストの扱いが改良されています。例えば、テキ ストを検索、選択、コピペできるようなPDFを出力するようになって います。また、「ユーザフォント」という機能が導入されています。
「ユーザフォント」はその名の通り、ユーザ(cairoを使う開発者) 独自のフォントを定義・利用できる機能です。利用例として、SVGフォ ントやFlashフォントなど標準化されていないフォーマットのフォン トの実装があげられています。通常は利用することはないと思いま すが、cairoがより広く利用される機会を増やす機能になるかもし れません。
もちろん、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)
現在考えているサポートしたい描画対象は以下の通りです。もし興 味があったらぜひ手伝ってください。
現在、Railsに対応した国際化の仕組みがいくつかあります。しかし、それぞれが 独自の方法で実現しているため、それらを組み合わせて使うと混沌 とした状態に陥ることも少なくありません。
ここでは、モデルから動的にきれいな画面とコントローラ部分を生 成するActiveScaffoldを用 いた場合の国際化(i18n)と地域化(l10n)の実現方法のひとつを 紹介します。この方法では、 ActiveScaffoldLocalize と Ruby-GetText-Package を組み合わせます。混沌とする部分はそれなりになじませます。
Railsで使用できる国際化の仕組みの比較はRails Wiki (英語)が詳しいです。
Ruby-GetText-Package には、以下のような地域化対象のメンテナン スのことを考慮した機能があるので、地域化対象メッセージが増加 したり更新される場合には有力な候補になるでしょう。
Railsやプラグインなどが提供しているメッセージだけを地域化した いなど、地域対象メッセージが変化しない場合はその他の仕組みも 有力な候補になるでしょう。例えば、ActiveScaffold用の 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/
見ての通り、「検索」などのメニューは日本語になりますが、テー ブル名からきている「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/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。
(「本を作成」ではなく「本一覧を作成」になっているのはこ のパッチ で直ります。)
ActiveScaffoldLocalizeのこのやり方は手軽ですが、地域化対象の メッセージが変更になった場合(例: 「Title」から「Name」に変更) や、地域化対象のメッセージをtypoした場合(例: 「Title」ではな く「title」としていた)に気づきにくいという問題があります。 このような問題に対してはRuby-GetText-Packageが有効です。
ということで、ActiveScaffoldのメッセージは ActiveScaffoldLocalizeで地域化し、それ以外は 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/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。
ActiveScaffoldLocalizeとRuby-GetText-Packageを使って、 ActiveScaffoldを用いたアプリケーションの国際化・地域化を実現する方法 のひとつを紹介しました。
基本的に複数の国際化のしくみを同時に使うと問題が起きますが、 今回は以下のようにそれぞれの長所を活かすようにすみわけて、問 題を回避しています。
*1 config/initializers/lang/以下にファイルを作るというのはActiveScaffoldLocalizeの方針ではありません。ファイルの場所は特に方針はないようです。
昨日、 RD (るびまの記事 )でスラ イドが書けるプレゼンテーションツール Rabbit がリリースされました。
Rabbitではスライドを画像 やHTML(+画像) 、 PDF (一覧表示 )などで出力することができます。
発表した後に資料を公開する場合や配布資料を作成する場合に利用 するとよいでしょう。
RDで書けることがウリというくらいなので、テキストエディタでス ライドを作成したい人が対象になります。おそらく、そのような人 はプログラマであることが多いと思うので、Rabbitはプログラマ向 けのプレゼンテーションツールといえるかもしれません。
スライドをテキストで作成すると以下のような利点があります。
一方、GUIの編集インターフェイスを備えたプレゼンテーションツー ル(wikipedia:PowerPointや wikipedia:Keynoteなど)と比較すると、以下のような欠 点があります。
RabbitはRDで書かれたテキストだけではなく、PDFを入力としても 受け付けます。つまり、PDFビューアにもなります。
そこで、上記のような編集時の欠点を解決するために、別途PDF出 力ができるソフトウェアでスライドを作成し、Rabbitで表示すると いうことができます。RabbitをPDFビューアとして使うことにより、 Rabbitのユニークで実用的なユーザインターフェイスを使うことが できます。Rabbitの使い勝手に興味がある場合はこの方法を試して みるとよいかもしれません。
Rabbitのユーザインターフェイスに関してはまた別の機会にしてお きます。
Rabbitの外面だけを紹介しました。難易度が高いと言われているイ ンストール方法や特徴的なユーザインターフェイスなどについては 触れませんでした。
LDAPのエントリを ActiveRecord風のAPIでア クセスするためのライブラリ、 ActiveLdap 1.0.1がリリースされました。
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は以下のライブラリをバックエンドとして利用できます。
以下は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...
各項目はそれぞれ以下の通りです。
上記の結果からは以下のことが言えます。
多くの場合、1度に100エントリを処理することは少ないでしょう。 そのため、通常はActiveLdapで各エントリをオブジェクト化しても 問題は少ないといえます。
もし、1度に多くのエントリを扱う場合で、読み込み専用ならば、 オブジェクト化しない方法で利用することでパフォーマンスを改善 することができます。
ActiveLdapを利用することでLDAPのエントリをオブジェクト指向的 なAPIで自然に処理することができます。
ActiveLdapは複数のLDAPバックエンドに対応しており、Rubyがイン ストールされている環境さえあれば動かすこともできます。 (Net::LDAPバックエンド使用時。ただしそんなに速くない)また、 JRubyでもほとんどの機能が動きます。
もし、Ruby/LDAPを利用できる環境であれば、Net::LDAPを直接利用 するよりも、ActiveLdap + Ruby/LDAPバックエンドを利用した方が よりオブジェクト指向らしいAPIでLDAPのエントリを操作できます。 また、速度が要求される場合であれば、オブジェクト化を行わない (オブジェクト指向らしいAPIを利用しない)ことにより、より高 速にLDAPのエントリを読み込むことができます。
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にも微妙に対応している