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

ククログ

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|
タグ:

モーショノロジー2012 #1: rroongaによる検索サービスの実装

注意: 長いです。

簡単まとめ: 検索サービスを作るにはrroongaが便利です。groongaサポートサービスをはじめます。

CROOZ株式会社が主催する「モーショノロジー2012 #1 全文検索&検索を利用したサービスの使命、利用プロダクト、事例紹介」が開催されました。今回のテーマは検索ということでgroonga開発チームに声をかけてもらいました。groonga関連の枠がいくつかあったのですが、ここではRubyとgroongaを使った検索サービスの作り方についての枠の内容を紹介します。

rroongaによる検索サービスの実装

以下、多少省略しながらスライドの内容を紹介します。

概要

話すこと

紹介する内容はrroongaを使った場合のメリット・デメリットと入力補完についてです。メリットは事例も交えながら紹介します。入力補完は「Ruby + groongaだからできる」という機能ではなくgroonga単体でも利用できる機能なのですが、最近の検索サービスでは当たり前になっている大事な機能なので、あわせて紹介します。

rroonga?

rroonga?

Rubyとgroongaを一緒に使う方法には以下の2つの方法があります。

  • groongaサーバーを起動し、HTTPで通信してgroongaの機能を使う方法
  • groongaをライブラリとして使用し、API経由でgroongaの機能を使う方法

全文検索システムはgroonga以外にもたくさんありますが、その中にSolrという全文検索システムがあります。Solrは全文検索エンジンとしてLuceneを利用したシステムです。groongaはLuceneと同じ全文検索エンジン機能もSolrと同じサーバー機能も備えています。groongaサーバーを起動する使い方はSolrのように使う使い方で、ライブラリとして使う使い方はLuceneのように使う使い方になります。

今回は後者のgroongaをライブラリとして使用する方法でのメリット・デメリットを事例を交えながら紹介します。

Rubyからgroongaをライブラリとして使うためにはrroongaというRubyのライブラリを使います。この方法ではアプリケーションがデータベースを持つことになります。

システム構成

システム構成

groongaをライブラリとして使う方法ではアプリケーションサーバーがデータベースを持つことになります。これは、よくあるアプリケーションサーバーとデータベースサーバーが分離している構成とは異なります。このあたりがメリット・デメリットにつながってきます。

メリット

メリット

groongaをライブラリとして使う場合、以下のようなメリットがあります。

  • 通信コストがないため、小さいコストで細かくデータを読み書きできる
  • 低レベルのAPIから高レベルのAPIまで使えるため柔軟に演算を組み合わせた検索ができる

小さいコストで細かいデータの読み書きができるメリットを活かした例は後で紹介します。まずは、柔軟に演算を組み合わせられるメリットを活かした例を紹介します。

組み合わせ例: 多段ドリルダウン

組み合わせの例の1つが「多段ドリルダウン」です*1。「ドリルダウン」は「ファセット」と呼ばれることの方が多いのですが*2、ECサイトなどでよく使われている機能です。

amazon.co.jpなどで絞り込める条件がリンクになっているインターフェイスを見たことがないでしょうか。あれがドリルダウン(ファセット)です。

ドリルダウン=ファセット

このスクリーンショットは「本」で検索した状態です。「本」カテゴリーのうち、さらに絞り込める項目(「コンピュータ・IT」、「ビジネス・経済」など)がリンクとしてリストされています。ここをクリックすると検索語などを入力せずに簡単に絞り込んでいくことができます。

また、各項目の横に「(5,248)」などヒット件数も表示されていることに気づいたでしょうか。これは「コンピュータ・IT」、「ビジネス・経済」などサブカテゴリーで絞り込んだ結果ヒットする件数を示しています。事前に検索システム側で検索して、0件ヒットする項目はそもそもこのリストに入らないようになっています。そのため、「絞り込んだけど0件ヒット」という無駄な検索を避けることができます。

このような点でより効率的に検索できるような機能なので、より広く使われるようになりました。

ドリルダウン

さて、多段ドリルダウンはどう違うかというと最終的な検索結果を求める途中でもドリルダウンをするという点が違います。

多段ドリルダウン

高レベルな検索機能しかない場合は、多段ドリルダウンを実現するために「全データ→途中結果」までの検索と「全データ→最終結果」までの検索を2回実施し、それぞれの検索結果に対してドリルダウンする必要があります。これは、高レベルな検索機能では検索の途中結果を保存しておいて再利用するような機能がないためです。

低レベルな検索機能も使えると「全データ→途中結果→ドリルダウン」をしてから「途中結果→最終結果→ドリルダウン」というように効率のよい処理を実現できます。

では、多段ドリルダウンが有用なケースはどのようなケースでしょうか。

用途

多段ドリルダウンは、絞り込み後に値を変更することが多い条件に有用です。amazon.co.jpのカテゴリーの例でいえば『「コンピュータ・IT」で絞り込んだ後に、やっぱり「ビジネス・経済」に変更しよう』ということが多いかどうかということになります。多段ドリルダウンを実施しておけば「絞り込み→解除→再絞り込み」という操作ではなく「絞り込み→再絞り込み」という操作を実現でき、少ない手順で検索できます。

例えば、「価格帯」が再絞り込みをしたくなるような条件です。最初は安めの価格帯で絞り込んでいたけど、よいのがなかったからもう少し高めのものも見てみよう、ということはよくありますよね。

デメリット

デメリット

デメリットはスケールアウトする標準的な仕組みがないことです。そのため、レプリケーションの仕組みを自分で作り込む必要があります。

開発事例

開発事例

Rubyとgroongaでテレビ番組を検索するWeb APIを開発しています。別のシステムから提供される番組情報をrroongaを使ってgroongaのデータベースへ取り込み、番組情報を検索するためのHTTP + JSONベースのWeb APIを提供するシステムです。このシステムが直接視聴者から利用されることはなく、番組検索APIを利用した連携アプリケーションがユーザー用のインターフェイスを提供します。

このシステムは外部からの更新がないとてもシンプルな構成のため、番組情報ソースを各アプリケーションサーバーにコピーし、各サーバーでそのソースを元にデータベースを構築することで冗長化・スケールアウトを実現しています。

このAPIを利用したアプリケーションの1つがテレコ!です。地上波・BS放送・CS放送の番組をメディア横断で検索できるテレビ番組情報サービスです。ぜひ利用してみてください。

メタデータの抽出

メタデータ抽出

番組検索Web APIではデータロードのときにもrroongaが活躍しています。その1つがメタデータの抽出処理です。

メタデータはドリルダウン条件として使えるため検索しやすいシステムを構築するためには重要な情報になります。番組情報の場合は出演者やカテゴリなどがメタデータとなります。メタデータは重要な情報なのですが、用意するにはそれなりの手間がかかるため、なかなか充実させることができません。そのため、ある程度機械的に抽出することで補うことが有効です。番組検索Web APIでは以下のようなコードで番組説明から出演者情報を抽出しています。

1
2
3
4
5
6
7
8
9
10
11
names = []
# 番組説明
description = "出演者: ビートたけし・所ジョージ"
# 人物テーブル
people = Groonga["People"]
# people.records -> ["ビートたけし", "明石家さんま"]
people.scan(description) do |record,|
  # 番組説明内に人物テーブル内の人物名があったら抽出
  names << record.key # "ビートたけし"
end
p names # -> ["ビートたけし"]

やっていることは「はてなダイアリーでのキーワード自動リンク」と同じことです。テキスト(番組説明)中から事前に用意した語(人物名)を抽出しています。なお、この処理のことを「multiple string matching」と呼ぶそうです。

これはgroongaの低レベルのAPIを使って実現します。ただ、この処理のときはデータベース内のデータを頻繁に参照することになるので、データロードするアプリケーションがデータベースを持っていないと時間がかかって実用的にはならないでしょう。

入力補完

最後に入力補完の実現方法について説明します。

入力補完

groonga本体にはサジェスト機能があり、この機能は以下の機能を提供します。

  • 補完: 一部分を入力するだけで完全な検索語を入力できるようにする機能(Googleの検索ボックスにもある機能)
  • 補正: 検索語の一部が間違っていても修正して正しい検索語にする機能(Googleでいえば「もしかして」機能)
  • 提案: 検索結果が多いときに絞り込み用の追加の検索語を提示する機能(Googleでいえば「他のキーワード」機能)
  • ユーザーの入力から統計的に学習し↑の3つを強化する機能

このうち補完機能を使った入力補完の実現方法について説明します。

補完例

groongaの補完機能は補完候補そのものの字面(例えば「万葉集」が補完候補なら「万」など)でなくても、ローマ字やひらがな・カタカナで入力しても補完候補を提示できます。これはIMEがOFFの状態でも利用できて日本語を利用した検索システムではとても便利です。Googleでも同様のことをできますが、amazon.co.jpではできないようです。

補完方法

補完方法には大きく分けて以下の2つの方法があります。

  • コンテンツベースの方法
  • 統計情報ベースの方法

コンテンツベースの方法ではすでにデータベース内にある既知の情報を補完候補とする方法です。番組検索Web APIの場合は番組名や人物名などが適切な補完候補になります。この方法ではデータベース内にある正しい補完候補のみを利用するので、間違った候補を出すことがないというメリットがあります。一方、候補数が少なくて思ったより補完してくれないということもありえます。例えば、「コナ」では「名探偵コナン」を補完してくれなくて「名探偵」と入力しないといけない、といったことがあります。

統計情報ベースの方法ではユーザーの検索履歴をアクセスログなどから収集・解析し、多くのユーザーが検索した語などを補完候補とします。この方法では「コナ」で「コナン」を補完候補とできる可能性があります。一方、ある程度の統計情報がないと適切な補完候補を抽出できなかったり、補完候補の精度が低くなってしまう可能性もあり、調整が必要になります。例えば、特定のキーワードをわざと多く検索してくるようなアクセスがあった場合はそのようなアクセスを無視するといったことが必要になるかもしれません。

まとめ

おさらい

rroongaを使った検索システムの構成とその構成ならではのメリットとデメリットを紹介しました。メリットはメタデータの抽出などgroongaの低レベルのAPIも利用しながら検索システムを構築できることです。デメリットは標準的なレプリケーションの仕組みがないため冗長化やスケールアウトの仕組みを自作する必要があることです。

また、rroongaを使った検索システムの構成とは関係ないのですが、入力補完の実現方法についても紹介しました。

お知らせ

お知らせ

groongaの開発元である有限会社未来検索ブラジルとMySQLからgroongaを使うためのソフトウェアmroongaの開発に参加している斯波さんとクリアコードでgroongaのサポートサービスを提供することにしました。サポート開始は2/29の予定ですがすでにお問い合わせは受け付けていますので、groongaサポートサービスに興味のある方はぜひお問い合わせフォームからご連絡ください。

*1  「多段ドリルダウン」は一般的な用語ではないので注意してください。名前がなかったので今回名前をつけただけです。

*2  Solrでもファセットと呼んでいます。

タグ: groonga | Ruby
2012-01-26

デバッグしやすいHTMLのテストの書き方

注意: 長いです。

一言まとめ: withinとtest-unit-capybaraを使ってHTMLのテストを書くと問題を見つけやすくなる。あわせて読みたい: デバッグしやすいassert_equalの書き方

HTMLに対するテストに限らず、開発を進めていく中でテストが失敗する状況になることは日常的にあることです。HTMLの場合は、入力フォームのラベルを変更したり、項目を追加したら既存のテストが失敗するようになるでしょう。そのとき、どのようにテストを書いていれば原因を素早く見つけられるのかを説明します。ポイントは「注目しているノードを明示すること」です。

HTMLテストのライブラリ

さて、Rubyで処理結果のHTMLをテストするときにはどんなライブラリを使っていますか?The Ruby ToolboxにあるBrowser testingカテゴリを見てみると、Capybaraが最も使われていて、次にWebratが広く使われているようです。どちらも同様の機能が揃っているため、特別な使い方をしないならどちらを使っても困ることはないでしょう。もし、今から使いはじめるならCapybaraの方がよいでしょう。これは、Capybaraの方がより活発に開発されているためです。

ここでは、CapybaraでHTMLのテストを書いたときを例にして、どのようにテストを書けば失敗した時に原因をすぐに見つけられるようになるかを説明します。考え方は他のツールでも応用できますし、test-unitやRSpecなどのテスティングフレームワークにも依存しません。そのため、ここでは、もっともRubyらしく書けるテスティングフレームワークであるtest-unit 2を使います。

よくある書き方

まずは一般的なCapybaraでのHTMLのテストがどのように書けるのかを確認します。

まず、以下のようなRackアプリケーションがあるとします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <p>No navigation.</p>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end

はじめにCapybaraでテストを書ける状態にします。include Capybara::DSLCapybara.app=がポイントです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyRackApplication
  # ...
end

gem "test-unit"
require "test/unit"
require "capybara/dsl"

class TestMyRackApplication < Test::Unit::TestCase
  include Capybara::DSL

  def setup
    Capybara.app = MyRackApplication.new
  end
end

それでは、まずは期待した見出しが返ってきているかを確認してみましょう。

1
2
3
4
5
6
7
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_heading
    visit("/")
    assert_equal("Welcome!", find("h1").text)
  end
end

実行するとテストがパスするので正しい値が返ってきていることを確認できます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
.

Finished in 0.427469388 seconds.

1 tests, 1 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

2.34 tests/s, 2.34 assertions/s

ヘッダー部分も確認しましょう。テスト対象のHTMLの一部も再掲します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <!-- ... -->
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <p>No navigation.</p>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end
# ...
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    assert_equal("No navigation.", find(".header p").text)
  end
end

これもパスします。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
..

Finished in 0.419134417 seconds.

2 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

4.77 tests/s, 4.77 assertions/s

雰囲気はわかりましたね。

HTMLを変更

それでは、HTMLを少し変更してナビゲーション用のリンクをいれましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <!-- ... -->
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end

HTMLの構造が変わったのでテストは失敗します。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
E
===============================================================================
Error:
test_header(TestMyRackApplication):
Capybara::ElementNotFound: Unable to find css ".header p"
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:154:in `raise_find_error'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:27:in `block in find'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/base.rb:46:in `wait_until'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:27:in `find'
    (eval):2:in `find'
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/dsl.rb:161:in `find'
    test-capybara.rb:40:in `test_header'
===============================================================================
.

Finished in 0.431328598 seconds.

2 tests, 1 assertions, 0 failures, 1 errors, 0 pendings, 0 omissions, 0 notifications
50% passed

4.64 tests/s, 2.32 assertions/s

さて、ここでどうしますか?よく使われる方法は以下のような方法ではないでしょうか。

  • puts sourceを埋め込んでHTMLを確認する。
  • assert_equalのメッセージにsourceを指定する。
  • save_and_open_browserでブラウザで確認する。

それでは、それぞれの方法についてみていきましょう。

puts sourceを埋め込む

include Capybara::DSLするとsourceメソッドが追加され、アプリケーションが返した生のHTMLを確認することができます。

1
2
3
4
5
6
7
8
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    puts source
    assert_equal("No navigation.", find(".header p").text)
  end
end

テストを実行するとHTMLが出力されます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
E
===============================================================================
Error:
test_header(TestMyRackApplication):
Capybara::ElementNotFound: Unable to find css ".header p"
...

出力されたHTMLと「Unable to find css ".header p"」というエラーメッセージと目CSSセレクタを活用して、どうしてCSSセレクタ".header p"がマッチしなくなったのかを考えます。

この方法はテスト自体を書き換える必要があるため、テストが失敗したらテストを修正してもう一度実行し直さなければいけません。テストがパスするようになったら変更を元に戻すことも忘れてはいけません。

assert_equalのメッセージにsourceを指定

assert_equalに限らずアサーションには失敗時に表示する追加のメッセージを指定できます。この方法では、失敗したときのみHTMLが表示されるため「失敗したときだけテストを変更する」といったことをする必要はありません。

1
2
3
4
5
6
7
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    assert_equal("No navigation.", find(".header p").text, source)
  end
end

実行してみます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
E
===============================================================================
Error:
test_header(TestMyRackApplication):
Capybara::ElementNotFound: Unable to find css ".header p"
    /var/lib/gems/1.9.1/gems/capybara-1.1.2/lib/capybara/node/finders.rb:154:in `raise_find_error'
    ...
===============================================================================
...

おや、エラーメッセージの中にHTMLが出力されていませんね。もう一度assert_equalを確認してみましょう。

1
assert_equal("No navigation.", find(".header p").text, source)

問題なさそうに見えますが、どうして出力されていないのでしょうか。

それは、assert_equalが呼び出される前にfindの中で例外が発生しているからです。Capybaraでは要素が見つけられなかったときはCapybara::ElementNotFound例外を投げるのでassert_equalは呼び出されなかったのです。

これを回避するためには以下のようにfindではなくhas_selector?で事前にCSSセレクタがマッチするかを確認する必要があります。

1
2
3
4
5
6
7
8
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    assert_true(has_selector?(".header p"), source)
    assert_equal("No navigation.", find(".header p").text)
  end
end

これを実行するとHTMLが出力されます。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
F
===============================================================================
Failure:
test_header(TestMyRackApplication) [test-capybara.rb:40]:
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
.
<true> expected but was
<false>
===============================================================================
...

しかし、今度は指定したCSSセレクタが出力されません。

そもそも、findする前に事前に存在を確認するようなテストの書き方になってしまうのでは、せっかくのCapybaraのすっきりした記法を活かせていないと言えるでしょう。

ちなみに、RSpecでは以下のように書くことになります。こちらもHTMLとCSSセレクタを同時に出力してくれないのであまりうれしくありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
require "capybara/rspec"

class MyRackApplication
  def call(env)
    html = <<-HTML
<html>
  <head>
    <title>Welcome! - my site</title>
  </head>
  <body>
    <h1>Welcome!</h1>
    <div class="header">
      <ul>
        <li><a href="/">Top</a></li>
      </ul>
    </div>
  </body>
</html>
HTML
   [200, {"Content-Type" => "text/html"}, [html]]
  end
end

Capybara.app = MyRackApplication.new

describe MyRackApplication, :type => :request do
  it "should have header content" do
    visit("/")
    page.should have_selector(".header p")
    find(".header p").text.should == "No navigation."
  end
end

実行すると以下のようにCSSセレクタのみが出力されます。

% rspec capybara_spec.rb
F

Failures:

  1) MyRackApplication should have header content
     Failure/Error: page.should have_selector(".header p")
       expected css ".header p" to return something
     # ./capybara_spec.rb:29:in `block (2 levels) in <top (required)>'

Finished in 0.60582 seconds
1 example, 1 failure

Failed examples:

rspec ./capybara_spec.rb:27 # MyRackApplication should have header content

ということで、この方法は「失敗したときだけ必要な情報を出力して原因を素早く見つけたい」という期待する結果を実現できません。メッセージの中にsourceとCSSセレクタを指定すれば実現できなくもありませんが、テストが書きづらくなってしまうため割にあいません。

save_and_open_browserでブラウザで確認

include Capybara::DSLするとsave_and_open_browserメソッドが追加され、アプリケーションが返した生のHTMLをブラウザで開いて確認することができます*1

1
2
3
4
5
6
7
8
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    save_and_open_page
    assert_equal("No navigation.", find(".header p").text)
  end
end

ブラウザでHTMLを確認することによりFirebugなどHTMLの構造を視覚的に確認することができます。確認してどうしてCSSセレクタ".header p"がマッチしなくなったのかを考えます。

この方法もテスト自体を書き換える必要があるため、テストが失敗したらテストを修正してもう一度実行し直さなければいけません。テストがパスするようになったら変更を元に戻すことも忘れてはいけません。元に戻さないままコミットしてしまうと、他の開発者の環境でもテストを実行するたびにブラウザが起動してしまいます。

どの方法がよいか

ブラウザで確認する方法が視覚的で一番わかりやすいですが、ページの内容が増えてくるとページ全体から問題の箇所を素早く探すのは大変です。一方、コンソールにHTMLを出力する方法は視覚的ではありませんが、HTMLをテキストとして検索することができるところは便利です。しかし、やはりページ全体から問題の箇所を素早く探すのは大変です。

小規模のWebアプリケーションでもそこそこのHTMLになり、どの方法でもページ全体から問題の箇所を素早く探すのは大変です。ということで、どの方法も今一歩と言えます。

ページ全体ではなくする

どうして問題の箇所を素早く探すのが大変かというと、探索範囲が広いからです。探索範囲が狭くなれば素早く見つけやすくなります。ついでに言うと、テストの書きやすさを損なわずにできるだけ自然に狭くしたいという希望もあります。

ところで、Capybaraにはwithinというメソッドがあることを知っていますか?これを使うと検索範囲を限定できます。例えば、以下は同じ意味になります。

1
2
3
4
5
6
# これまでの書き方
find(".header p").text
# withinを使った書き方
within(".header") do
  find("p").text
end

withinを使ってテストを書きなおしてみましょう。

1
2
3
4
5
6
7
8
9
class TestMyRackApplication < Test::Unit::TestCase
  # ...
  def test_header
    visit("/")
    within(".header") do
      assert_equal("No navigation.", find("p").text)
    end
  end
end

テストを実行するとfindのところで失敗するのですが、この時点では".header"はマッチすることがわかっています。つまり、findはHTML全体ではなく、以下のHTML断片内で"p"がマッチすることを期待しています。

1
2
3
4
5
<div class="header">
  <ul>
    <li><a href="/">Top</a></li>
  </ul>
</div>

このくらいの量であればコンソールに出力されても解析しやすいでしょう。少なくとも「<p>はないが<ul>はある」ということはすぐにわかります。このヒントがあれば原因を特定するのにだいぶ役立つはずです。

では、withinのブロック内でマッチしなかった場合に現在マッチしたノードのHTMLを出力するにはどうしたらよいでしょうか。これを実現できると、失敗した時だけ必要最低限の情報を得られて素早く原因を見つけられそうです。しかもCapybaraの自然な使い方です。

test-unit-capybara

test-unit 2でCapybaraを便利に使うためのtest-unit-capybaraというライブラリがあります。これを使えば、これまで通りCapybaraの作法で書くだけで必要な情報を過不足なく表示してくれます。以下のようにrequire "test/unit/capybara"とするだけでtest-unit-capybaraを使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gem "test-unit"
require "test/unit/capybara"
# require "test/unit"
# require "capybara/dsl"
# ...
class TestMyRackApplication < Test::Unit::TestCase
  include Capybara::DSL

  def setup
    Capybara.app = MyRackApplication.new
  end

  # ...

  def test_header
    visit("/")
    within(".header") do
      assert_equal("No navigation.", find("p").text)
    end
  end
end

実行すると以下のようになります。

% ruby test-capybara.rb
Loaded suite test-capybara
Started
F
===============================================================================
Failure:
test_header(TestMyRackApplication)
    [/var/lib/gems/1.9.1/gems/test-unit-capybara-1.0.1/lib/test/unit/capybara.rb:77:in `raise_find_error_for_test_unit'
...
     test-capybara.rb:40:in `test_header']:
<"p">(:css) expected to find a element in
<<div class="header">
      <ul>
<li><a href="/">Top</a></li>
      </ul>
</div>>
===============================================================================
...

探索範囲であるHTML断片<div class="header">...</div>とCSSセレクタ"p"が表示されています。これがわかれば素早く原因を見つけられますね。

最近のCapybaraはfindメソッドでノードを見つけられなかったときの挙動をカスタマイズできます*2。test-unit-capybaraはその機能を使って必要な情報を収集して表示しています。同様のことは他のテスティングフレームワークでも実現できるでしょう。

まとめ

現状のテストの書き方・ツールではHTMLに対するテストが失敗したときの原因を素早く見つけることが困難であることを示しました。また、解決方法としてwithinを使って「注目しているノードを明示」し、テストツールとしてtest-unit-capybaraを使う方法を紹介しました。この方法は他のテスティングフレームワークでも実現できる一般的な方法です。

インターフェイスの改良や文言の変更などでHTMLのテストが失敗することはよくあることです。そんなときもすぐにどこが変わったかに気付けるようなテストだと変更を嫌がらずによいものを作ることに専念できますよね

あわせて読みたい: デバッグしやすいassert_equalの書き方

おまけ: assert_match問題

ここで取り上げた問題は「問題があるだろう範囲が広すぎて問題を発見することが困難になる」ことが原因です。同様の問題はtest-unitのassert_match、RSpecのshould matchにもあります。

以下はUser-Agentが任意のバージョンのbingbotであることをテストしています。このテストは失敗するのですが、どうして失敗するかすぐにわかるでしょうか?

1
2
3
4
5
6
7
def assert_bingbot(user_agent)
  assert_match(/;\s+bingbot\/[\d]+;/, user_agent)
end

def test_bot
  assert_bingbot("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)")
end

正規表現は少し間違えて書いてしまうとどこが間違えているのか見つけることが大変です。これを防ぐために、assert_matchではなく、以下のようにマッチ対象を正規化してからassert_equalで比較することをオススメします。

1
2
3
4
5
6
7
8
def assert_bingbot(user_agent)
  assert_equal("Mozilla/5.0 (compatible; bingbot/XXX; +http://www.bing.com/bingbot.htm)",
               user_agent.gsub(/bingbot\/[\d]+/, "bingbot/XXX"))
end

def test_bot
  assert_bingbot("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)")
end

assert_equalだと以下のようにdiffがでるので何が悪かったのかがすぐにわかります。

===============================================================================
Failure:
test_bot(TestMyRackApplication)
    [test-capybara.rb:50:in `assert_bingbot'
     test-capybara.rb:55:in `test_bot']:
<"Mozilla/5.0 (compatible; bingbot/XXX; +http://www.bing.com/bingbot.htm)"> expected but was
<"Mozilla/5.0 (compatible; bingbot/XXX.0; +http://www.bing.com/bingbot.htm)">

diff:
? Mozilla/5.0 (compatible; bingbot/XXX.0; +http://www.bing.com/bingbot.htm)
===============================================================================

バージョン番号を検出しようとしていた[\d]+が「.」を考慮していないことが原因ですね。

*1  別途launchy gemが必要なことに注意。

*2  raise_find_errorメソッドをオーバーライドする

タグ: Ruby | テスト
2012-01-18

Ruby関連のコンテストっぽいものへの応募例

Rubyに関する取り組みは無理のない範囲で応援しています。コンテストっぽいものもそのうちの1つで、ちょうど募集している時期にそれっぽいものがあれば応募しています*1。これは、応募者が少しでも増えると、次回の開催時には少しは盛り上がるのではないかという期待からです。「応募したくなる人が増えるには」という方向で考えた応援の仕方です。

逆に、応募する敷居を下げる方法もあるでしょう。その1つに「どのように応募すればよいかのヒントを提供する」ことがあるのではないでしょうか*2。ということで、Rubyアソシエーション:2011年度助成金公募への応募内容と第3回フクオカRuby大賞への応募内容を紹介します。Rubyアソシエーションの方は落ちた応募で、第3回フクオカRuby大賞は受かった応募なので参考にするときは注意してください。なお、どちらも現在(2011-12-12)は募集していませんので、気になった方は次回の募集時に応募してみてください。

Rubyアソシエーションの2011年度助成金への応募内容

目的が似ていそうな原さんの世界の誰でも読めるRubyリファレンスマニュアル(に向けて)が採択されています。その観点から、「この応募内容では何が足りないのか」を考えると参考にしやすいのではないでしょうか。なお、2011年12月18日(日) 13:00から秋葉原でRubyの次世代リファレンスマニュアルを構想する会あるそうなので、興味のある方は参加してみてはいかがでしょうか。

----
* 応募者名(個人名又は団体名)

株式会社クリアコード

* 担当者名

須藤功平

* 連絡先電子メールアドレス

kou@clear-code.com

* 略歴

  * 2006年7月25日: 設立
  * 2008年8月5日: 代表取締役を須藤に交代
  * 2009年6月: 日本Ruby会議2009にスポンサー・発表者として参加
  * 2009年10月: とちぎRuby会議02にスポンサー・発表者として参加
  * 2009年12月: 札幌Ruby会議02にスポンサー・発表者として参加
  * 2010年8月: 日本Ruby会議2010にスポンサー・発表者として参加
  * 2011年3月: 第3回フクオカRuby大賞でコミュニティ特別賞を受賞
  * 2011年6月: 本社移転
  * 2011年6月: 日本Ruby会議2011にスポンサー・発表者として参加

* プロジェクト名

Ruby用ドキュメントツールの国際化対応

* プロジェクトの詳細

以前のRubyは他のプログラミング言語と比較してドキュメントが弱
いと言われていたが、最近ではるりまプロジェクト(*1)や"Ruby
1.9.3 Documentation Challenge"(*2)などによりRubyのドキュメン
トが改善されている。

(*1) http://redmine.ruby-lang.org/projects/rurema/wiki
(*2) http://blog.segment7.net/2011/05/09/ruby-1-9-3-documentation-challenge

しかし、上記の2つのプロジェクトはそれぞれ独立に動いており、片
方の成果をもう一方が取り込む、といったことが難しい状況となっ
ている。ドキュメントを改善する作業はあまり人々の関心を得られ
ず、慢性的にリソースが足りない状況である。それにも関わらず
リソースが分散してしまっており、日本語のRubyのドキュメントも
英語のRubyのドキュメントもRuby全体を網羅したものにはなってい
ないのが現状である。

このような状態になってしまっているのは、ドキュメントツールが
国際化に対応していないことが問題であると考える。そこで、この
プロジェクトではRuby用のドキュメントツールであるYARD(*3)に国
際化機能を実装し、貴重なドキュメント改善作業のリソースを分散
せずに協力して作業できる仕組みを提供する。

(*3) http://yardoc.org/

なお、すでにこの作業は開始しており、YARDのメーリングリストで
国際化対応について議論を行なった(*4)。国際化対応作業の方向に
ついてYARDの作者から合意を得ており、実際にいくつかの機能は実
装済み(*5)である。これより、以下のことが言えるため本プロジェ
クトの実現可能性は高いと考える。

  * YARD開発チームとの関係が良好である
  * すでに方向性が決まっている
  * すでに動き出している

(*4) http://groups.google.com/group/yardoc/browse_thread/thread/9aecb2fe3c6c9d5
(*5) https://github.com/kou/yard

Ruby標準添付のRDocではなくYARDに対して国際化対応を行う理由は
以下のとおりである。

  * 国際化機能を実装するにあたり、RDocでは他のライブラリを利
    用しづらいため。
    (他のライブラリもRubyに標準添付しなければいけないため。)
  * YARDはRDocの上位互換であり、RDocで書かれたドキュメントも
    YARDで扱えるので、YARDの方が国際化機能を適用できる範囲が
    広いため。
  * YARDの方が拡張を意識した作りとなっており、実装しやすいた
    め。

YARDを国際化対応した結果、使い勝手がよく継続的に利用できるも
のであると判断できた場合はRDocへのポーティングも検討すること
を考えているが、本プロジェクトの範囲とはしない。

* プロジェクトの成果物

以下を成果物とする。

  * 国際化機能付きのYARD
  * 国際化されたYARDのドキュメント(日本語のドキュメント)

ただし、YARDのリリースはYARDの開発者が行うため、期間内に上記
のYARDがリリースされるとは限らない。

実装した国際化機能を用いて実際にYARD自体のドキュメントを国際
化する。これにより国際化機能が利用しやすいか・継続可能な仕組
みになっているかを評価する。Ruby本体や多くのライブラリは、一
度作ったら完成するものではなく、日々改良が続けられていくもの
である。そのため、ドキュメントも一度作成したら完成ではなく、
改良に追従していく必要がある。国際化機能そのものも重要である
が、ドキュメントの改良が無理なく続けられる仕組みであることも
重要であるため、その観点でも評価する。
----

第3回フクオカRuby大賞への応募内容

第3回フクオカRuby大賞は予備審査と本審査があり、予備審査は応募内容を使って審査するそうです。この応募内容で予備審査を通過しました。1年ほど前のものなので内容が古くなっていたりしますね。

----
○ 応募日

平成22年11月13日

○ ソフトウェア、取組等の名称

るりまサーチ

○ 応募者区分

法人・団体として応募

○ 応募者

○○ 応募者名

須藤功平

○○ 応募者名ふりがな

すとうこうへい

○○ 法人・団体 代表者名

須藤功平

○○ 法人・団体 代表者役職

代表取締役

○ 所在地

○○ <都道府県>

東京都

○○ <市区町村名、番地>

文京区本郷1-25-4

○○ <ビル名等>

ベルスクエア本郷5F

○ URL

http://www.clear-code.com/

○ 連絡担当者

○○ 担当者 氏名

須藤功平

○○ 担当者 氏名ふりがな

すとうこうへい

○○ 担当者 所属部署

所属なし

○○ 担当者 役職

代表取締役

○○ 連絡先TEL

03-6231-7270

○○ 連絡先FAX

03-6231-7271

○○ 連絡先e-mail

kou@clear-code.com

○ 1.ソフトウェア、取組等の概要について

○○ (機能・特徴を簡潔に説明してください。)

るりまサーチはRuby本体のリファレンスマニュアルを高速に検索す
るWebアプリケーションです。説明文などテキスト情報から全文検索
して目的のマニュアルを検索する機能はもちろん、マニュアルの記
述対象の種類(クラスについて記述している、インスタンスメソッ
ドについて記述しているなど)など付加的な情報からも絞り込んで
いける機能を提供しています。この多角的に絞り込んでいく機能に
より、少ない労力で目的のマニュアルを見つけ出せることが特徴で
す。

○○ (ソフトウェア、取組等について、参考となるURLを記載してください。)

URL: http://rurema.clear-code.com/

○ 2.ソフトウェア、取組等の目的、ねらいについて

○○ (開発の目的や取組の動機、ターゲット等を記述してください。)

開発の目的:

目的は「Rubyのドキュメントが十分実用的であると評価されるよう
にすること」です。開発に用いる言語を選択する場合の指標のひと
つにドキュメントの充実度が挙げられることがよくあります。その
時、PHPやPythonなどは充実しているが、Rubyはそれほどでもない、
と評価されます。これは、せっかく充実してきたリファレンスマニュ
アルを十分に活用するためのインターフェイスがないことが原因だ
と考えています。

従来のリファレンスマニュアル閲覧Webアプリケーションにも検索機
能がありましたが、全文検索に数十秒かかる、全文検索以外の検索
方法がない、などの理由により、せっかく充実してきたリファレン
スマニュアルを十分に活用することができませんでした。るりまサー
チを開発することにより、検索機能を充実させ、リファレンスマニュ
アルを活用できる環境を提供することで目的の達成に近づくと考え
ています。

ターゲット: Rubyを用いて開発している開発者すべて

○ 3.Rubyを用いた理由、またはRubyに注目した理由について

Ruby本体のリファレンスマニュアルを検索するシステムにはRubyを
用いることが必然だからです。

○ 4.ソフトウェア、取組等の特徴について

○○ 1)優位性、セールスポイントについて、具体的に記載してください。

まず高速であることです。すぐに結果が返ってこないと格段に使い
勝手が悪くなります。処理に何秒もかかった上に「0件ヒット」と
なるようなシステムは使われなくなります。

次にリンクを辿るだけで絞り込んでいけることです。

キーワードがわかっている場合はそのキーワードを入力するだけで
目的のマニュアルをすぐに見つけることができますが、そうでない
場合の方がほとんどです。その場合は絞り込み条件を増やすことに
より、検索結果を絞り込んでいき目的のマニュアルを見つけていき
ます。この操作を簡単にできる仕組みを提供しています。

検索結果内にあらかじめ絞り込み条件をリンクとして挿入しておき
ます。例えば、エンコーディング関連を調べているとします。まず
「enc」というキーワードで絞り込みます。

  http://rurema.clear-code.com/query:enc/

このとき、ページ左のサイドバーには「インスタンスメソッド」や
「定数」など、種類での絞り込み条件を表示します。リンクの右側
に表示されている「(...)」は絞り込み後のヒット数です。アプリ
ケーション側は絞り込みリクエストを受け取る前にすでにヒット数
を知っているので、絞り込み後にヒット数が0になる条件はここに表
示しません。つまり、「絞り込んだ後にヒット数が0になる」という
無駄な操作をユーザがしなくても済むようになっています。

また、ページ本体の検索結果表示部分にも「Rubyのバージョンによ
る絞り込みリンク」、「インスタンスメソッドなどの種類」、「所
属するクラスなど関連する付加情報」などによる絞り込みリンク表
示しています。これにより、リンクを辿るだけでどんどん絞り込ん
でいけるため、少ない労力で目的のマニュアルを見つけることがで
きます。

上記のように高速である、リンクを辿るだけで絞り込んでいけると
いう機能が優位性、セールスポイントになります。

○○ 2)新規性、革新性について、具体的に記載してください。

上記の優位性、セールスポイントは先進的なECサイトや情報提供サ
イトではすでに導入されています。(例えば、amazon.co.jpやぐる
なびなどでも導入されています。)このような機能は今後の検索シ
ステムでは標準的な機能になっていくと考えています。

るりまサーチはそのような先進的な検索機能をRubyで実現できるこ
とを示した、実際に動くオープンソースソフトウェアであることに
新規性があります。先進的なECサイトなどの実装がオープンソース
ソフトウェアとして公開されることはないため、同様の機能を実現
するためには1から自分で調べる必要があります。しかし、るりま
サーチはオープンソースソフトウェアとして公開されているため、
どのように機能を実現しているかを参考にしたり、改変して利用す
ることなどができます。

○○ 3)社会的効果、インパクトについて、具体的に記載してください。

上述の内容と重複しますが、以下の2点が社会的効果となります。

  * Rubyのドキュメント環境が改善されることによりRubyの普及に
    つながる。
  * オープンソースソフトウェアとして公開されているため、同様
    の検索機能を実装する場合に有用である。

○ 5.ソフトウェア、取組等の実績について

○○ (具体的な実績、市場等からの評価があれば記載してください。)

Rubyのリファレンスマニュアルの公式検索サービスに採用されまし
た。(現在はメンテナンス中のようです。)

  http://doc.ruby-lang.org/ja/search/

るりまサーチをもっと便利に利用するためのソフトウェアを開発し
てくれた方がいました。

  http://codnote.net/2010/09/20/rurema-instant/
  https://github.com/sorah/rurema_instant

○ 6.今後の展開について

○○ (今後の目標や事業展開について記載してください。)

るりまサーチはRubyのリファレンスマニュアルを便利に検索する機
能を提供するという目的の他に、バックエンドで利用している全文
検索エンジンgroonga(*)のデモという目的もあります。

(*) groonga: http://gronoga.org/

クリアコードではgroongaを利用した検索システムの開発・開発支援
も行っています。groongaを利用した場合にどのような検索システム
を作ることができるか、ということを実際に動くるりまサーチとい
うアプリケーションでデモできます。

るりまサーチ自体はこれまで通り、groongaの機能を活かした使い
やすい機能を追加していきます。るりまサーチはデモとして使い、
新しい開発案件などにつなげていきます。るりまサーチ自体は今後
もオープンソースソフトウェアのままです。

○ 7.添付資料について(添付資料は返却しません。)

○○ (概要説明図やこれまでの発表資料、新聞・雑誌の記事等)

るりまサーチ: Rubyでgroonga使ってリファレンスマニュアルを全
文検索 - ククログ(2010-04-27):
  http://www.clear-code.com/blog/2010/4/27.html

日本Ruby会議2010発表資料: るりまサーチの作り方 - Ruby 1.9で
groonga使って全文検索 - ククログ(2010-09-01):
  http://www.clear-code.com/blog/2010/9/1.html

○ 8.ソフトウェアの動作環境について(ソフトウェアのみ)

○○ (OS、ソフトウェア、ハードウェア構成等について記載してください。)

OS: Linux 64bit
    (rurema.clear-code.comではDebian GNU/Linux lenny 64bitを利用)
ソフトウェア:
  * Ruby 1.9.x
  * groonga
  * Rack
  * rroonga
  * racknga
ハードウェア構成:
  * CPU: 64bit対応のもの
  * メモリ: 512MB以上
----

まとめ

Rubyアソシエーション:2011年度助成金公募への応募内容(落選)と第3回フクオカRuby大賞への応募内容(書類審査通過)を紹介しました。このようなコンテストっぽいものに応募しようとしている人の参考になり、応募者が増えたら、紹介した甲斐があるというものです。

*1  それっぽいものがないときは無理して用意しません。

*2  実際、「第3回フクオカRuby大賞」はどのような内容で応募したかを聞かれたことがあるため、少しは必要とする人がいるはず。

タグ: Ruby
2011-12-12

いらないキャッシュを消すとRubyスクリプトが速くなる

いらないキャッシュを消すことでRubyスクリプトが倍速で動作するようになった話です。

先日、るりまサーチRackspaceのクラウドサーバー(メモリ1GB)からさくらのVPS 512(メモリ512MB)に移行しました。理由はRackspaceのサーバーが遠くにあるのでレスポンスがもっさりするからです。最速検索がウリなのにこれでは遅いのでないかと誤解されてしまいます。さくらのVPSにしたら近くにあるためサクサクとレスポンスが返ってくるようになりました。しかも、リソース(主にメモリ量)はスケールダウンしているのにです。

るりまサーチはバックエンドの全文検索エンジンgroongaが高速に動作するのとそれほどアクセスがない(!)ため、CPUやI/Oがネックになることはありません。それよりも、ほとんどメモリを搭載していないマシンで動かしているため、ボトルネックになるとすればメモリです。実メモリ以上にメモリを使おうとしてスワップを使い始めると遅くなります。

るりまサーチに一番負荷がかかるのは1日1回のるりまデータの更新です。もう少し言うとBitClustでHTMLを生成する処理が一番負荷が高いのです。このとき、BitClustのHTML生成プロセスはCPUを100%使い*1、500MB程度のメモリを使用します*2。Rackspaceのときのように1GBのメモリが載っているサーバーであれば耐えられますが、さくらのVPS 512のように512MBしかメモリが載っていないサーバーでは致命的です。スワップを使いはじめてI/O待ちが多発し、システムの応答性が著しく悪化します。

ということで、BitClustがHTMLを生成するときのメモリ使用量を改善したのですが、そのときに2倍くらい高速に動作するようになりました。その高速化の方法を一言でいうと「いらないキャッシュを消す」です。具体的な変更点でいうとr4873(4行)とr4874(1行)です。

いらないキャッシュを消して高速化

キャッシュはメモリを通常より使って処理時間を短くする方法なので、キャッシュを消すことでメモリ使用量が減るのは納得できます。しかし、処理時間も短くなるのは意外ですよね。

なお、メモリ使用量と実行時間は以下のようになりました。Ruby 1.8.7の場合も1.9.3の場合もメモリ使用量は半分以下、実行時間はほぼ半分になっています。

RSSVSZ実行時間
変更前(Ruby 1.9.3)187MB237MB55秒
変更後(Ruby 1.9.3)84MB134MB38秒
変更前(Ruby 1.8.7)525MB558MB1分52秒
変更後(Ruby 1.8.7)132MB165MB59秒

メモリ使用量が減って速くなった場合はだいたいGCに関係していると相場が決まっています。では、こんなときはどうやって確認したらよいでしょうか。Ruby 1.9.2以降にはGC::Profilerという便利なモジュールが追加されているのでこれを使います。

GCがどんな感じで実行されているかを調べたい処理の前でGC::Profiler.enableをし、処理を実行した後にGC::Profiler.reportで結果を表示します。

1
2
3
GC::Profiler.enable
# ... GCについて調べたい処理 ...
GC::Profiler.report

GC::Profiler.reportは以下のようにGCの統計結果を表示してくれます。

GC 445 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
    1               0.092               964640              2257680                56442         0.00000000000000000000
...

まず、いらないキャッシュを消さない場合の結果を見てみましょう。

GC 341 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
    1               0.080               964600              2257680                56442         0.00000000000000000000
...
   27               0.348              1893800              4057280               101432         4.00100000000003230838
...
   50               0.764              3283520              7296560               182414         8.00100000000003674927
...
   73               1.588              6627960             13120720               328018        12.00099999999992839150
...
  106               3.720             11035000             23607480               590187        32.00199999999986744115
...
  124               5.532             19510200             42486920              1062173        56.00399999999972067144
...
  151               9.741             36667720             44008400              1100210        96.00600000000092393293
...
  187              16.541             50259800             62249800              1556245       128.00800000000123191057
...
  237              28.470             61845360             72409360              1810234       176.01100000000258205546
...
  328              53.351             63548120             80262160              2006554       148.00900000000183354132

オブジェクト数が増える毎にGCにかかる時間が増えています。

次に、いらないキャッシュを消した場合の結果を見てみましょう。

GC 445 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
    1               0.092               964640              2257680                56442         0.00000000000000000000
...
   27               0.376              1893800              4057280               101432         8.00099999999997990585
...
   50               0.824              3283520              7296560               182414        12.00100000000004030198
...
   73               1.644              6627400             13120720               328018        12.00099999999992839150
...
  106               3.928              6736400             13120720               328018        20.00100000000015754154
...
  256              14.061             10642920             23607480               590187        32.00199999999853162080
...
  302              18.561             18979640             42486920              1062173        52.00299999999913325155
...
  432              36.330             25030040             42486920              1062173        48.00300000000135014488

トータルのGC回数は増えていますが、オブジェクト数の最大値が増えていかないため、1回のGCにかかる時間が短くなっています。この差が全体の実行時間に効いてきているんですね。

まとめ

速くするためにやみくもにキャッシュしていると、生きているオブジェクト数が多くなるためにGC時間が長くなってしまい、逆に遅くなることがあります。キャッシュしたのに思ったより速くならなかった場合は必要なくなったキャッシュを持ち続けていないか確認し、いらないキャッシュを消すことを試してみましょう。ただし、ある程度のオブジェクト数をキャッシュしていない場合はあまり関係がないでしょう。

*1  2コアあるので1コアがフル稼働してしまってもシステム全体としては問題になりません。

*2  Ruby 1.8.7使用時

タグ: Ruby
2011-11-24

ApacheとPhusion PassengerでデプロイしたWebアプリケーションが不調になる問題と解決

ApacheとPhusion PassengerでWebアプリケーションをデプロイしている際に、たまにシステムが不安定になることがありました。調査したところ、原因はレスポンスの取得が遅いクライアントであるということが分かりました。この問題を発見し、原因を特定し、Phusion Passengerを修正するまでについて、紹介します。

発見の経緯

るりまサーチではApacheとPhusion Passengerを使っているのですが、たまに動作が不安定になることがありました。その不安定の原因を調査することにしました。

原因の特定

調査した結果、遅いクライアントがるりまサーチに接続するのが不安定になるトリガーになっていることが分かりました。

以下に、詳しく説明します。

Phusion Passengerは、遅いクライアントが接続していると、他のリクエストの処理を行いません。その間に、他のクライアントからのリクエストが処理待ちとして大量に溜まります。そして遅いクライアントがレスポンスを受け取り終わると、Phusion Passengerは、一斉に溜まった処理待ちのリクエストを処理し始めるため、システムのリソースを急激に消費します。そして、システムが不安定になります。

解決の方法

この問題の原因を解決するためには、遅いクライアントのリクエストを処理している際にも、他のリクエストの処理ができればいいということになります。

遅いクライアントの時の状況を詳しく説明します。遅いクライアントがレスポンスを受け取るのを待っている間は、サーバー側ではレスポンスの作成の処理は既に完了しているということになります。それにも関わらず他のリクエストの処理を行わないのです。つまり、その間、サーバーは実質的には何も処理を行っていません!

この時間は、無駄な時間と言えます。

なので、この無駄な時間をなくすようにする必要があります。具体的には、Webアプリケーションからの作成されたレスポンスを一旦Phusion Passengerがバッファに保存し、次のリクエストの処理に移ります。遅いクライアントには、そこのバッファから、ゆっくりレスポンスを返せばいいのです。

実は、このような挙動はNginxとPhusion Passengerの構成でデプロイされた場合には実現されています。ですが、ApacheとPhusion Passengerの構成の場合にはそうでは無いのです。

情報の収集

解決の方法が分かったので調べたところ、Phusion Passengerのバグトラッキングシステムにこの問題はすでに登録されていました。

さらにそこには、この問題の解決方法が記載されていました。コメント#3の情報によるとソースコードを書き換えればいいということが分かりました。ソースコードを書き換えることで、Nginxと同じように、Apacheでもレスポンスをバッファに保存する挙動に変更できます。

実際に、るりまサーチで検証したところ、問題は解決されました。遅いクライアントが接続してきた際にも、処理が止まらず、別のリクエストを処理するようになりました。

Phusion Passengerの修正

問題は解決されましたが、この問題を解決するためには、ソースコードを書き換えなければいけません。端的に言って、これは不便です。上のバグレポート内に書いてあるように設定ファイルからレスポンスをバッファに保存するかどうかを切り替えられると便利です。

なので、設定できるようにPhusion Passengerを修正したパッチを作成しました。そしてそのパッチをPhusion Passengerに取り込んでもらうように依頼をしました。

(追記: Fri Nov 25 03:05:28 2011 JSTにPhusion Passengerに無事に取り込んでもらえました。使えるようになるのは、まだリリースされていませんが次のリリースバージョンである、3.0.10からの予定です)

(追記: November 28th, 2011にこの修正が取り込まれたPhusion Passengerの3.0.11がリリースされました(3.0.10はバグがあったためにスキップされました)。

この修正によって、レスポンスをバッファに保存するかどうかを次のように設定することができるようになります。

<VirtualHost *:80>
  ServerName www.yourhost.com
  DocumentRoot /somewhere/public
  PassengerBufferResponse on
  # PassengerBufferResponse off # レスポンスのバッファへの保存をしたくない場合。
  <Directory /somewhere/public>
    AllowOverride all
    Options -MultiViews
  </Directory>
</VirtualHost>

レスポンスをバッファに保存する副作用

遅いクライアント問題はレスポンスをバッファに保存することで解決できます。半面、レスポンスをバッファに保存すると、次の副作用が発生するので注意が必要です。

  • レスポンスをストリーミングで返せない。
  • レスポンスが非常に大きいとメモリを逼迫する。

どちらも、Phusion Passengerがレスポンスをメモリ上のバッファに一旦保存してからクライアントに返すようになるためです。

まとめ

ApacheとPhusion PassengerでデプロイされたWebアプリケーションが不安定になるという問題を調査し、Phusion Passengerを修正しました。

同じような問題に遭遇している場合には、ぜひ試してみてください。

タグ: Ruby
2011-11-17

tDiaryのRDスタイルにCodeRayを使ったシンタックスハイライト機能を追加

タイトルにキーワードを埋め込んでみました。ククログはtDiaryのRDスタイルで書いています。コードを貼り付けることも多々あるため、シンタックスハイライトもできるようにしてあります。今まではGNU Source-highlightでシンタックスハイライトをしていたのですが、CodeRayでシンタックスハイライトをするように変更しました。

設定方法

~tdiary/clear-code-tdiary/にクリアコードのtDiary関連Subversionリポジトリをチェックアウトしているとします。この状態でtdiary.confに以下のように記述すると、RDスタイルでシンタックスハイライトできるようになります。

1
2
3
clear_code_tdiary_base = "/home/tdiary/clear-code-tdiary"
require "#{clear_code_tdiary_base}/patches/after-load-styles-hook"
require "#{clear_code_tdiary_base}/patches/rd-syntax-highlight"

事前にgemでCodeRayをインストールしておくことを忘れないでください。

使い方

以下のようにverbatimブロックに「# source: ruby(言語名)」というようなヘッダーを入れます。

tdiary.confの設定:(←ここはパラグラフ。)

  # source: ruby (←ここは表示されない。)
  @style = "RD" (←ここがRubyプログラムとしてシンタックスハイライトされる。)

乗り換え理由

GNU Source-highlightからCodeRayに乗り換えたのは以下の理由からです。

  • Rubyのライブラリとして使える。
  • Rabbitで使ってみていい感じだった。

まとめ

ほとんど需要がないはずですが、tDiaryのRDスタイルにシンタックスハイライト機能を追加したので紹介しました。

タグ: Ruby
2011-08-11

伝えることを伝えること

しばらく人前で話す予定がないので、講演者が本当に話したかったスピーチを残しておきます。

実は、日本Ruby会議2011で話したこと札幌Ruby会議02で話したこと札幌Ruby会議03で話しかけたことの続きでした。話の流れも札幌Ruby会議02と同じ流れにしました。一番伝えたいことは話しの真ん中に持っていき、その後に実例を伝えるという流れです。

札幌Ruby会議のころは「伝えること」について考えていました。

私がプログラミングを始めたのは大学のころ*1で、世間で活躍しているプログラマーよりだいぶ遅いです。でも、プログラミングが好きでたくさんプログラムを書いてきました。今では、プログラミングをはじめて10年くらい経ち、だいぶ上手くなってきました。でも、このままだとよくないなぁと思うようになりました。

私は独学でプログラミングを学んできました。本を読んだりコードを書いたり、他の人のよいところを盗んだり。でも、他の人も同じようにやる必要はないなぁと思うようになりました。自分がよいとわかったことを厳選して伝えられて、その分、他の人がもっと別のことをできるなら、そっちの方がいいなぁと思ったのです*2

そのため、札幌では自分がプログラミングで大事だと思っていることを伝えようとしました。

それから、普段でもいろいろ伝えてみました*3。 そしたら、伝えるだけでも足りないと思うようになりました。「魚をあげるんじゃなくて、魚の捕り方を教えなよ」では足りないのです。これでは、魚を捕れる人は増えるかもしれませんが、「魚の捕り方を教えなよ」という人は増えないのではないかと思うようになりました。「伝えること」だけではなく「伝えることを伝えること」をしなければいけないのではないか。

「伝えることを伝えること」は日本Ruby会議2011では落とした話題ですが、拾ってくれた人がいて嬉しかったです*4。いつかリベンジしたい話題です。

まだ、「伝えること」も伝えきれていないので、「伝えることを伝えること」はもう少し先の話になると思いますが、実現できたらいいなぁと思っています。まずは、クリアコード内で「伝えること」をまとめて、ここで公開しようとしているところです*5。公開したものが誰かの役にたつといいなぁ。

日本Ruby会議2011のことはもう書いたので、ここに書くことは技術的な話に戻そう*6と思っていました。でも、日本Ruby会議2011関連のWeb日記を読んでいたら、「書いておかないと」と思ったので、書いてしまいました*7

*1  今、ざっと読みなおしてみたら札幌で話したこととか練馬で話したことと同じことを話していました。変わっていないですね。

*2  ソフトウェアに関してはプログラミングをはじめたころからそんな風に思っていた気がします。自分がよいと思ったソフトウェアがあったら他の人にも「これを使いなよ」と渡せるとよいなぁと思います。だから、自由に使えるソフトウェアが好きなんだと思います。

*3  そして、いろいろうまくいきませんでした。

*4  参考文献を教えてもらいました。手元に用意したので、そっちも少し調べてみようと思います。

*5  自由に利用できるようにする予定です。

*6  RDocとYARDとI18Nの話とか。

*7  ここでは「思いました。」という風には書かないようにしているのですが、Ruby会議関連の話題のときだけは勝手に「特別に使ってよい」ことにしています。

つづき: 2011-12-26
タグ: Ruby
2011-07-25

日本Ruby会議2011: テスティングフレームワークの作り方

日本Ruby会議2011で「テスティングフレームワークの作り方」について話してきました。前に人前で話したのが2月のフクオカRuby大賞だったので半年くらい人前に出ていなかったのですね。

テスティングフレームワークの作り方

発表内容のこととRuby会議のことについて書いておきます。

発表内容

数年前なら自分がよいと思っている技術的なことを「これかっこいいでしょ!」みたいな感じで話したかったのですが、最近は自分がよいと思っている開発スタイルをまだそれを知らない人に「こういうスタイルもあるんだよー」と伝えたくなってきました*1。そのため、今回の発表内容は「自分が開発しているときに無意識のうちに考えていること」になりました。

発表内容についてざっくりまとめるとこんな感じになります。

  • ライブラリやツールを作るときは、頭の中で考えていることをそのまま書けるような使い方にするといいよ。
  • そのためには「だれが」「なんのために」「頭の中で考える」かを意識しよう。

キーワードは「判断基準」にしました。

「判断基準」はここ数年で意識することになったキーワードで、今まで無意識でやっていたことを改めて考えるきっかけになっていました。今のところ、1回の話で伝えられる程には自分の中で整理できているわけではなく、何度も繰り返し伝えていくことでしか伝えきれないと感じています。今回は1回だけの機会になるので伝えきれる気がしなかったのですが、せめてきっかけくらいになればいいなぁと思い、伝えようとしてみました。

本当は一緒に開発をしながら伝えていきたいことです。現状だとクリアコードに就職するしか方法がないのですが、別の方法でもなにか機会があるといいなぁと思っています。

Ruby会議のこと

Ruby会議ではセッションを聞かずにロビーなどをぶらぶらしながら誰かと立ち話をするのが好きでした。ふだんは直接会えない人と初めて会ったり久しぶりに会ったりできるのがRuby会議でした。会ったことがない人同士を引き合わせるのも好きでした。

今回のRuby会議ではクリアコード関係者が4人も壇上で話していました。クリアコードができた頃はこんなことになるとは思ってもいませんでした。とても嬉しいことです。

Ruby会議じゃない場所でもまた会えるといいなぁと思います。

*1  でも、まだ、「これかっこいいでしょ!」という内容を話すときの方が楽しいです。

タグ: Ruby
2011-07-19

RSpecとtest-unit 2での抽象化したテストの書き方の違い

日本Ruby会議2011の3日目の「テスティングフレームワークの作り方」の準備をしていますが、30分だと詰め込み過ぎになってしまうので、話さないことを事前に書いておきます。それは、テストを抽象化するためのAPIの違いです。

RSpecとtest-unit 2でのAPIの違いというと、class UserTest < Test::Unit::TestCasedescribe Userassertshouldの違いの方が目に付きますが、抽象化するためのAPIにもツールの特徴が出ています。抽象化するためのAPIはテストの量が増えてくると必要になる大事な機能です。ここでは、その中でも「テストを共有するAPI」について考えます。

まず、ツールの考え方について確認し、その後、それぞれのツールでどのようなAPIになっているかをみます。

ツールの考え方

まず、それぞれのツールの考え方について確認しましょう。

RSpecの考え方

RSpecでの書き方を見る前に、RSpecがどういうことを実現するためのツールとして開発されているかを確認しましょう。

BDD is an approach to software development that combines Test-Driven Development, Domain Driven Design, and Acceptance Test-Driven Planning. RSpec helps you do the TDD part of that equation, focusing on the documentation and design aspects of TDD.

[RSpec Documentationより引用]

ざっくり訳すと、

BDDはテスト駆動開発とドメイン駆動設計と受け入れテスト駆動計画づくりとATDPを合わせたソフトウェア開発の方法で、RSpecはそのうちのテスト駆動開発の部分だけをお手伝いしますよ。もう少し言うと、テスト駆動開発の特徴であるドキュメントと設計の部分を特に重視しています。

となります*1

ドキュメントと設計(とRSpecというツール名)を合わせて考えると、仕様をテストとして実行できる形にしながら開発を進めたいのではないかという解釈ができます。そうすると、仕様とテストを一緒に書きやすいAPIを目指しているはずです。

test-unit 2の考え方

test-unit 2はxUnit系のテスティングフレームワークです。Rubyで書かれたコードのテストをRubyで書けることを重視しています*2。そのため、Rubyとしてテストを書きやすいAPIを目指しています。

書き方

それでは、このような考え方を持つツールはどのようなAPIを提供するかを見てみましょう。

RSpecでの書き方

RSpecでテストを共有する場合はit_behaves_likeを使います。以下は、shared examples - Example Groups - RSpec Coreにあるコードです。「別途記述した動作通りに動くこと」と読めるAPIになっていますね。期待した動作を取り込むのではなく、参照しているように読めるところがポイントです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
require "set"

shared_examples "a collection" do
  let(:collection) { described_class.new([7, 2, 4]) }

  context "initialized with 3 items" do
    it "says it has three items" do
      collection.size.should eq(3)
    end
  end

  describe "#include?" do
    context "with an an item that is in the collection" do
      it "returns true" do
        collection.include?(7).should be_true
      end
    end

    context "with an an item that is not in the collection" do
      it "returns false" do
        collection.include?(9).should be_false
      end
    end
  end
end

describe Array do
  it_behaves_like "a collection"
end

describe Set do
  it_behaves_like "a collection"
end
test-unit 2での書き方

test-unit 2でテストを共有する場合は共有したいテストを書いたModuleincludeします。こちらは「テストの実装を共有する」と読めるAPIになっていますね。RubyではModuleは実装を共有する手段として提供されているため、それをそのまま「テストを共有」するために使っていることがポイントです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require "set"

gem "test-unit"
require "test/unit"

module CollectionTests
  def collection
    @collection ||= collection_class.new([7, 2, 4])
  end

  def test_initilize
    assert_equal(3, collection.size)
  end

  def test_include_true
    assert_true(collection.include?(7))
  end

  def test_include_false
    assert_false(collection.include?(9))
  end
end

class ArrayTest < Test::Unit::TestCase
  include CollectionTests

  def collection_class
    Array
  end
end

class SetTest < Test::Unit::TestCase
  include CollectionTests

  def collection_class
    Set
  end
end

まとめ

テスティングフレームワークの作り方からもれた話題のひとつである「 RSpecとtest-unit 2の考え方の違いとそれが『テストを共有するAPI』にどう現れているか」をみてみました。

考え方としてRSpecとtest-unit 2のどちらがよいかではなく、自分がやろうとしている作業にはどちらが合っているかを考えるべきです。仕様としても使えるテストとRubyとして書けるテストのどちらが必要か・重要かを考えます。

例えば、すでにある仕様書や要望リストを実現するために作業している場合はRSpecの方が作業に合っているかもしれません。仕様っぽいAPIである程度Rubyと切り離して作業することにより、仕様を意識しながら作業を進めることができます。

そうではなく、内部で使うためのもので外部とのインターフェイスとなっていない部分であれば、test-unit 2の方が合っているかもしれません。RubyのそのままのAPIを使ってテストを書くため、仕様としてどうかということよりも、Rubyのプログラムとしてどのように動くのがよいかという部分に集中できます。

*1  テスト駆動開発がドキュメントを重視していたかどうかを覚えていないので、訳し間違いかもしれません。

*2  それとテストを自動化できること。

タグ: Ruby
2011-07-12

日本Ruby会議2011でLDAPの話をしませんか?

日本Ruby会議2011Lightning Talkの締め切りが残り数日になっていますが、みなさんいかがお過ごしでしょうか。

去年開催された日本Ruby2010では、はRuby で扱う LDAP のススメ - 選択肢とその事例という企画があり、RubyとLDAPに関する情報を共有する場になりました。(企画を主催した高瀬さんのまとめ

日本では「RubyでLDAPを使う」というケースを情報共有する場が無く、この企画はとても意義のあるものでした。今年も同じような場が設けられると有意義でしょう。ということで、RubyKaigi2011の講演以外の企画についてを参考に、日本 Ruby会議2011でそういう場を設けるための方法案をまとめてみました。興味がでてきた人はぜひ実現させてください。

Lightning Talk

自分が知っていることなどを発表して情報を共有できるでしょう。ただし、持ち時間5分のなかで質疑応答などをすることはできないので、別の機会で行うことを考慮した方がよいでしょう。

会期中は会場3Fの和室をハッカソンなどのために開放します

日本Ruby会議2010でやった企画のように、「数人の発表者 + 質疑応答」というスタイルを実現できるかもしれません。あるいは、その場で一緒にコードを書いたりすることもできるかもしれません。

または、Lightning Talkと組み合わせて質疑応答をこちらで行うということも考えられます。

会期中にアンカンファンレス、!RubyKaigi(NotRubyKaigi)2011を開催しておきます

Lightning Talkより自由度が高いので、この時間だけで「発表 + 質疑応答」ができるかもしれません。

RubyKaigi Advent Calendar 2011

日本Ruby会議 2011の会期中ではなく、それより前に場を設けるという方法です。他の方法より準備が大変そうですが、一番自由度は高くなります。いろいろやりたいことがある場合はこの方法がよいかもしれません。

まとめ

日本Ruby会議2011でRubyとLDAPの話をする場を設ける方法案を紹介してみました。興味が出てきた方は[activeldap:88] 今年も日本Ruby会議2011に参加しませんか?へどうぞ。

(どの方法案もLDAPに関係ないような気がします。)

タグ: Ruby
2011-06-16

日本Ruby会議2011でるりまとテスティングフレームワークについて話します

日本Ruby会議2011のスケジュールが発表されました。クリアコードのメンバーはるりまに関することとテスティングフレームワークに関することを話します。面白そうだなと思ったら聞きに来てください。

タグ: Ruby
2011-06-07

日本Ruby会議2011にGoldスポンサーとして参加

昨年に引き続き、クリアコードは今年も日本Ruby会議2011のGoldスポンサーになりました。

まだでていませんが、発表などでも参加する予定なので、会場で見かけたら声でもかけてください。

開催は2ヶ月後で、まだ少し時間がありますが、RubyKaigi2011の講演以外の企画についてRubyKaigi Advent Calendar 2011など関連情報がいくつかでています。Online.kaigi.rbなど日本Ruby会議2011以前に行われるイベントもあるようなので、興味のある方はそちらにも参加してみてはいかがでしょうか。

タグ: Ruby
2011-05-19

64bit版Windows用のRubyInstallerの作り方

WindowsにRubyをインストールする場合、どうやってインストールしますか?現在のところ、以下のようにいくつも選択肢があります。

それぞれ特徴があるので自分の使い方にあったものを選ぶ必要がありますが、今回の話の趣旨は「これがオススメです!」というものを伝えることではないので、簡単に紹介するだけにしておきます。ピンときたものがあったら、それについてもう少し詳しく調べてみることをおすすめします。

  • Ruby-mswin32: インストーラではなく、バイナリをzip形式のアーカイブで配布。展開すればそのまま使える。32bit版と64bit版の両方あり。1.8と1.9の両方あり。Visual Studioでビルド。
  • ActiveScriptRuby: インストーラ形式。32bit版のみ。1.8と1.9の両方あり。HTML Application内でRubyを使うことができる。Visual Studioでビルド。
  • RubyInstaller for Windows: インストーラ形式。ただし、メッセージは英語。32bit版のみ。1.8と1.9の両方あり。MinGWでビルド。
  • Rumix: インストーラ形式。32bit版のみ。1.8と1.9の両方あり。バイナリはRuby-mswin32のものを利用。
  • 能楽堂: インストーラ形式。64bit版のみ。1.9.3(2011/05/15時点では未リリース)のみ。Visual Studioでビルド。Rails実行環境入り。

今回の話ではRubyInstaller for Windowsを使います。では、どうしてRubyInstaller for Windowsを使うのでしょうか。

RubyInstaller for Windowsの特長

今回挙げたビルド済みRubyのパッケージの中で、RubyInstaller for WindowsのみがMinGWでRubyをビルドしています。これが、今回RubyInstaller for Windowsを選んだ理由です。RubyがMinGWでビルドされているとDebian GNU/Linux上で拡張ライブラリをクロスコンパイルすることができます*1

GNU/Linuxなどでは64bit版Rubyが普通に使われていますが、Windowsではまだそうでもないようです。しかし、今後はWindowsでも64bit版Rubyの利用が進んでいくでしょう。そうなった場合、拡張ライブラリは64bit版Ruby用のバイナリ入りで配布することが求められます*2

普段からWindowsで開発している拡張ライブラリであれば簡単に64bit版Ruby用バイナリを作成できるでしょうが、メインの開発環境がGNU/Linux*3という場合は難しいです。GNU/Linuxで開発している場合は、GNU/Linux上でクロスコンパイルして64bit版Ruby用バイナリを作成できるととても便利です。そして、MinGW-w64を利用すれば64bit版Windows用のDLLをクロスコンパイルできるのです。

しかし、RubyInstallerはまだ64bit版Rubyには対応していません。つまり、64bit版Ruby用の拡張ライブラリをクロスコンパイルしてバイナリを作っても、それを使うための64bit版Rubyの入手が困難な状況ということです。

64bit版Ruby対応RubyInstaller

RubyInstallerが64bit版Rubyに対応していないため、クロスコンパイルした拡張ライブラリを使える64bit版Rubyを簡単にインストールできない問題をどのように解決すればよいでしょうか?簡単ですね。RubyInstallerを64bit版Rubyに対応させればいいのです。

ということで、対応させたものがGitHub上のkou/rubyinstallerのmingw-w64ブランチにあります。これを使って作成した64bit版Ruby*4用のRubyInstallerと、Debian GNU/Linux上でクロスコンパイルしたrroongaのgemを以下に置いておきます*5

rroongaのチュートリアルで、実際にきちんと動くことを確認できます。

というように、なんとなく動くようになっていますが、まだいくつかやらなければいけないことがあります。

ということで、Ruby 1.9.3がリリースされるまでにみなさんがこれらの修正できるように64bit版Ruby用RubyInstallerの作り方を紹介します。タイトルからは想像できなかった展開ですね。

64bit版Ruby用RubyInstallerの作り方

まず、初期設定の方法を紹介し、それからRubyをビルド・テストする方法とRubyInstallerを作成する方法を紹介します。

初期設定

必要なものは以下の通りです。

  • Ruby 1.8.7の実行ファイル
  • Inno Setup
  • Cygwin
  • Ruby trunkのソースコード
  • RubyInstall for Windowsのソースコード

Ruby 1.8.7はすでにあるRubyInstaller for Windowsを使ってインストールします。Ruby 1.9.2では動作しないので注意してください。

Inno SetupはWindowsインストーラを作成するフリーソフトウェアです。ダウンロードページからisetup-5.4.2.exeをダウンロードしてインストールします。

本当はCygwinはなくてもよいのですが、初期設定が楽なのでCygwinを使います。まず、setup.exeをダウンロードしてCygwinをインストールします。追加でインストールするソフトウェアは以下の通りです。

  • subversion
  • autoconf
  • git

必須ではありませんが、mingw64-x86_64-binutilsもあると便利でしょう。

CygwinをインストールしたらRubyをチェックアウトします。trunkのRubyを修正しないと取り込んでもらえないからです。また、作業は~/work/ruby/*6以下で行うことにします。

% mkdir -p ~/work/ruby
% cd ~/work/ruby
% svn co http://svn.ruby-lang.org/repos/ruby/trunk ruby
% cd ruby
% autoconf

RubyInstaller for Windowsのソースコードもチェックアウトします。mingw-w64ブランチの成果はオフィシャルリポジトリに取り込まれそうな雰囲気はあるのですが、まだ取り込まれていないのでmingw-w64ブランチを使います。

% cd ~/work/ruby
% git clone https://github.com/kou/rubyinstaller.git
% cd rubyinstaller
% git checkout mingw-w64

以上が初期設定です。ここまでの作業は最初に1回行うだけです。今後は~/work/ruby/rubyinstaller/で作業を行います。

ビルド

ここからはcmd.exe上で実行します。ただし、cmd.exe上で長いコマンドを打つのは大変なので、バッチファイルを作成します。

build.bat:

c:\Ruby187\bin\ruby.exe c:\Ruby187\bin\rake ruby19 LOCAL='C:\Cygwin\home\kou\work\ruby\ruby' ProgramFiles='c:\Program Files (x86)' dkver=mingw64-64-4.5.4 --trace > build.log 2>&1

これをC:\cygwin\home\kou\work\ruby\rubyinstaller\で実行します。

C:\cygwin\home\kou\work\ruby\rubyinstaller>build.bat

小一時間ほど待つとsandbox\ruby19_mingw\以下に64bit用Rubyが作成されているはずです。

C:\cygwin\home\kou\work\ruby\rubyinstaller>sandbox\ruby19_mingw\bin\ruby.exe --version
ruby 1.9.3dev (2011-05-14) [x64-mingw32]

ビルド時のログはbuild.logに保存されているので、問題が発生した場合はログを見て問題を解決し、RubyやRubyInstallerの開発チームに報告して解決内容を取り込んでもらいましょう。開発チームにコンタクトをとる方法など、関連リソースについては一番最後に書いてあるので参考にしてください。

パッケージの作成

パッケージの作成もコマンドが長いのでバッチファイルを作成します。

package.bat:

c:\Ruby187\bin\ruby.exe c:\Ruby187\bin\rake ruby19:package LOCAL='C:\cygwin\home\kou\work\ruby\ruby' ProgramFiles='c:\Program Files (x86)' dkver=mingw64-64-4.5.4 --trace > package.log 2>&1

これをC:\cygwin\home\kou\work\ruby\rubyinstaller\で実行します。

C:\cygwin\home\kou\work\ruby\rubyinstaller>package.bat

10分ほど待つとpkg\以下にRubyInstallerが作成されているはずです。このRubyInstallerにはsandbox\ruby19_mingw\以下にビルドされたRuby用が含まれています。RubyInstallerのファイル名はpkg\rubyinstaller-1.9.2-p180.exeになっていて、直さなければいけないものの1つです。ただ、このファイル名は後で直すことにして、まずは、試しにインストールしてみましょう。インストーラをダブルクリックするとインストールできます*7

ここまでくればパッケージの修正作業はできますね。現在のところ、以下のような問題があります。

テスト

テストの実行もコマンドが長いのでバッチファイルとシェルスクリプトを作成します。テストはMSYS上で実行するため、まずはcmd.exeからMSYSのbashに入るためのバッチファイルが必要です。

shell.bat:

c:\Ruby187\bin\ruby.exe c:\Ruby187\bin\rake devkit:sh LOCAL='C:\Cygwin\home\kou\work\ruby\ruby' ProgramFiles='c:\Program Files (x86)' dkver=mingw64-64-4.5.4

次に、bash上でテストを実行するためのシェルスクリプトです。

run-test.sh:

1
2
3
4
5
6
#!/bin/sh

export PATH=$PWD/sandbox/ruby19_mingw/bin:$PATH
ruby --version
cd sandbox/ruby19_build
make test-all

以下のように使います。

C:\cygwin\home\kou\work\ruby\rubyinstaller>shell.bat
...
sh-3.1#$ ./run-test.sh > test.log 2>&1

テスト結果はtest.logに出力されます。この結果ですべてのテストがパスするようにしましょう。なお、r31560では「9486 tests,1877705 assertions, 29 failures, 8 errors, 82 skips」となりました。結果の詳細は以下にあります。

やりがいがありそうですね。

まとめ

MinGW-w64で64bit版Rubyをビルド・テストする方法と、ビルドした64bit版RubyのRubyInstallerを作成する方法を紹介しました。まだ、いくつか問題があるのでピンときた人はRuby 1.9.3がでるまでに修正してみてはいかがでしょうか?

関連リソース:

*1  補足すると、Ruby-mswin32とActiveScruptRubyで配布されている32bit版Ruby用の拡張ライブラリもクロスコンパイルできます。しかし、64bit版Ruby用の拡張ライブラリをクロスコンパイルできません。理由はVisual Studioでビルドした64bit版Rubyではmsvcrt.dllではなくmsvcr80.dllやmsvcr100.dllが使われているためです。MinGWでクロスコンパイルするとmsvcrt.dllを使うようになるため、異なるCランタイムライブラリを使うことになってしまい、うまく動かないのです。参考: C ランタイム ライブラリの「アプリケーションで msvcrt.dll と msvcr100.dll の両方を使用した場合に発生する問題」のところなど。

*2  Windows上でも需要がある拡張ライブラリであれば。

*3  rroongaやrcairoのケース。

*4  ruby --versionは"ruby 1.9.3dev (2011-05-14) [x64-mingw32]"。

*5  一時的に作ったものを置いているだけなので、ある日突然削除されているかもしれません。

*6  WindowsのパスではC:\cygwin\home\kou\work\ruby\。

*7  右クリックで「管理者として実行」を選ばないといけないかもしれません。

つづき: 2011-12-26
タグ: Ruby
2011-05-15

RDocとYARDの比較

リファレンスマニュアルの記述方法を検討し、埋め込み方式のドキュメントツールを採用したとします。Rubyで埋め込み方式のドキュメントツールを使うとしたらRDocかYARDになります*1

RDocからYARDへの移行方法につなげたいのでYARDを使う方向で話を進めたいわけですが、その前にRDocとYARDの背景や機能の違いを確認しておきましょう。

RDocの背景

RDocはRuby 1.8.1からRuby本体に標準添付されています。Ruby1.8.1は2003年のクリスマスにリリースされているので、もう7年くらい前になります。Ruby本体や標準添付されているライブラリもRDoc用にドキュメントが書かれていますし、Rubyで標準的なドキュメントツールといえばRDocという存在です。

RDocは2004年くらいまではRuby本体のリポジトリ上で活発に開発されていましたが、それから数年は開発が停滞していました。その後、2008年頃より開発リポジトリをRuby本体のリポジトリからRubyForgeのリポジトリに移動して*2、再び開発が活発になりはじめます*3。RDoc 2.Xがはじまったのもこの頃です。2年後の2010年の12月にRDoc 3.Xがはじまるなど、今でも開発のペースは衰えていません。

と、こう書くとRDocでいいんじゃないかと思うところです。しかし、Ruby 1.9を使っていて日本語も含むドキュメントを扱っていた人はそんなことはないということに気づいているはずです。Ruby 1.9の大きな変更の1つがEncodingの導入ですが、RDoc 2はEncodingの対応が不十分です。RDoc 2でHTMLを生成しようとして、Encoding関連の例外が発生した人もいるのではないでしょうか。

そして、ハマリポイントなのが、Ruby 1.9.2に標準添付されているRDocは RDoc 3ではなくRDoc 2だということです。Encoding関連で問題が発生している人はgemでRDoc 3をインストールして、そっちを試してみてください。

RDocの機能

RDocではドキュメントのマークアップ言語として独自のマークアップ言語を作成しています。MarkdownTextileなどといったマークアップ言語よりもシンプルで、機能が足りないと感じることが多いかもしれません。特に、Ruby用のドキュメントシステムとして開発されたのにも関わらず、コード用の専用マークアップがないことには驚くかもしれません*4

クラスやメソッド用のメタ情報記述方法ではRuby用ドキュメントツールらしい記述がサポートされています。Rubyは動的な言語なので「メソッドを定義するメソッド」*5も定義できます。そのようなメソッドのドキュメントも記述できるようになっています。

例えば、以下のようにprotected_attr_readerという独自の「メソッドを定義するメソッド」を定義したとします。このとき、protected_attr_readerで定義したメソッドもattr_readerと同じように読み込み専用属性としてドキュメント化して欲しいですよね。RDocでは以下のように記述することによって、独自の「メソッドを定義するメソッド」で定義したメソッドにも適切にドキュメントを記述することができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CoveredBook
  class << self
    def protected_attr_reader(name)
      attr_reader name
      protected name
    end
  end

  ##
  # :attr_reader:
  # タイトルを返す。ただし、内部からのみから利用可能。
  protected_attr_reader :title

  def initialize(title)
    @title = title
  end
end

と、こう書くとRDocでいいんじゃないかと思うところです。しかし、DoxygenGTK-Docなど他のドキュメントツールにはあるような専用マークアップがありません。例えば、メソッドの引数*6や戻り値*7のドキュメントを記述するための専用マークアップがありません。これらはメソッドのドキュメントを読みやすく整形するときに便利です。また、「バージョンいくつから追加された機能」*8や「非推奨のAPI」*9を記述する専用マークアップもありません。

現在も開発が継続しており、RDoc 3もリリースされ、継続的に改良されている*10のですが、まだ細かいところのサポートが不足しているという印象です。

YARDの背景

YARDは2007年から開発が始まっています。RDocの開発が再び活発になりはじめたのが2008年頃なので、あまり改良されていなかった当時のRDocに不満を持っていたのかもしれません。

YARDが重視していることは拡張のしやすさです。YARDのトップページにはアピールポイントとして「Preview As You Document」、「Easily Customize Templates」、「Support Your Own DSL」、「Extend, Extend, Extend!」の4つが挙げられています。このうち3つは拡張性に関することです。Rubyは動的な言語なので使われ方も様々です。様々な使われ方をしているときでもドキュメントを記述できるように拡張性を高くしているものと考えられます。

また、RDocとの互換性も重視しています。RDoc用に書かれたドキュメントを変更なしで処理できるため、RDocからの移行も容易です。

YARDの機能

YARDは複数のマークアップ言語をサポートしています。組み込みではRDoc由来のマークアップ言語だけのサポートですが、gemをインストールすることでMarkdownやTextileも利用することができます。チュートリアルや設計に関する文書など、ある程度長めの文書を書くときはRDocのマークアップではなく、より汎用的なマークアップ言語の方を使うのがよいでしょう。

YARDはメソッドなどのメタ情報の記述方法はRDocとは異なる書式を採用しています。YARDはDoxygenやGTK-Docなどと同様に@タグ名という記述方法を採用しています*11。YARDではこれをタグと呼んでいます。組み込みで利用できるタグにはRDocにはなかった、引数記述用タグ、戻り値記述用タグ、初出バージョン記述用タグ、非推奨API記述用タグなどライブラリのドキュメントを記述するために必要そうなタグが一通り揃っています。また、タグを追加することもできます*12

また、ドキュメントとして抽出したテキスト情報やメタ情報を再利用しやすい形で提供しています。これを利用することでるりまサーチのようなドキュメント検索システムを作りやすくなりそうですが、これについてはまた別の機会にします。

今後はRDocも記述力が高まったりメタ情報の扱いが改良されていくのかもしれませんが、現時点ではYARDの方がより実践的な機能を持っているといえるでしょう。ただし、ドキュメント生成速度はRDoc 3の方が圧倒的に速いです*13

まとめ

Ruby用のドキュメントツールであるRDocとYARDについて比較しました*14。YARDの方がよさそうに思えるように書いているので、YARDを使いたくなったかもしれません。次こそはYARDの使い方を紹介できるかもしれません。

*1  他にもsdocとかあったりします。

*2  今はGitHubのリポジトリに移動しています。

*3  つまり、RDocがRuby本体のリポジトリと独自リポジトリの複数のリポジトリに存在するようになったのもこの頃です。RDocは今でも標準添付ライブラリなので、RDocのリポジトリを更新するだけではなく、Ruby本体のリポジトリのRDocも更新する必要があります。これは、新しいバージョンをリリースしたタイミングなどでごっそりRuby本体のリポジトリのRDocを上書きするという方法で行われています。これに関してはいろいろ意見がある人もいるので、このあたりに興味がある人は、Rubyの開発を見ていたり参加したりしているまわりの人に聞いてみましょう。

*4  整形済みテキスト用のマークアップはあります。

*5  RDocではメタメソッドと呼んでいるようです。

*6  Doxygenなら\param、GTK-Docなら@引数名

*7  Doxygenなら\return、GTK-DocならReturns:

*8  Doxygenなら\since、GTK-Docなら@since

*9  Doxygenなら\deprecated、GTK-Docなら#ifdef ... #endifから自動抽出。

*10  ドキュメントの生成速度はだいぶ速くなっているという印象です。

*11  Doxygenは\sinceでも@sinceでもどちらでも書けます。

*12  RDocでもできるようです。

*13  ここで「YARDに速度改善パッチを送れるなぁ」と考えられる人はいいセンスを持っていますよ。:-)

*14  偏った視点が含まれているので、自分が必要な機能に関する部分は念のため自分で本家の情報を確認することをオススメします。

タグ: Ruby
2011-05-11

groonga関連リリース週間

今週は全文検索エンジンgroongaとその関連ソフトウェアがいろいろリリースされました。

リリースされたソフトウェア

以下がリリースされたソフトウェアとそのバージョンです。

groonga 1.2.0
全文検索エンジン。一部非互換があるためマイナーバージョンがあがっています。詳細はgroonga-dev MLでのアナウンスを参照してください。
groonga storage engine 0.5.0
ストレージエンジンとしてgroongaを利用するMySQLのプラグイン。まだベータ版です。このリリースにはMySQL 5.5への対応強化などが含まれています。
rroonga 1.2.0
groongaをRubyから使えるようにするためのRubyの拡張ライブラリ。groonga 1.2.0対応だけではなく、新機能もあります。詳細はgroonga-dev MLでのアナウンスを参照してください。
ActiveGroonga 1.0.4
Rails 3でrroongaを利用するためのRubyライブラリ。Rails 3用のページネーションライブラリKaminari(のViewの部分のみ)に対応しています。これも詳細はgroonga-dev MLでのアナウンスを参照してください。
ActiveGroonga Fabrication 1.0.0 [NEW!]

オブジェクト生成ライブラリFabricationにActiveGroongaサポートを追加するライブラリ。gemでインストールできます。

% sudo gem install activegroonga-fabrication

使うための準備はこれだけです。

1
require 'active_groonga_fabrication'

後は、通常のFabricationの使い方と同じです。

まとめ

今週リリースされたgroonga関連のソフトウェアを紹介しました。

そういえば、先日、groongaを使ったアプリケーションであるるりまサーチで受賞した第3回フクオカRuby大賞のコミュニティ特別賞の賞状が届きました。

第3回フクオカRuby大賞のコミュニティ特別賞の賞状

つづき: 2011-12-26
タグ: Ruby
2011-04-01

ソーシャルウィジェットtDiaryプラグイン

kdmsnrさんが設定していたFacebookコメントプラグインがうらやましかったので、ククログにもつけてみました。ククログはtDiaryを使っていますが、CGIやRackで動かすのではなく、オフラインで静的なHTMLに変換してからWebサーバーにアップロードしています。そのため、tDiaryが持っているツッコミ機能は使っていません*1

追加でfacebook_comments.rbを使おうとしたのですが、section_footer2.rbと衝突するのでやめました。また、section_footer2.rbに以下のような不満がありました。

  • 名前に「2」と入っていて名前付けとして不適切。
  • (インラインCSSでスタイルを設定しているからなのか)一列に並んだウィジェットがガタガタになっている。

そこで、section_footer2.rbが提供している機能のうち必要なものだけと、facebook_comments.rbの機能を合わせたsection-footer-social-widgets.rbプラグインを作りました。上記の不満や衝突問題は解決しているのですが、設定画面には対応していないので、簡単には使えないでしょう。同じようなことをしたかったら参考になるかもしれません、という程度の扱いのものです。

興味があったら見てみてください。

*1  tDiaryなのに。

タグ: Ruby
2011-03-15

るりまサーチが第3回フクオカRuby大賞のコミュニティ特別賞を受賞

先日、第3回フクオカRuby大賞の本審査に行ってきましたが、その結果が発表されました。

応募していたるりまサーチはコミュニティ特別賞に入っていました。認めてもらえてよかったです。

るりまサーチでるりまが便利だと感じた人はるりまプロジェクトにも参加してみてはいかがでしょうか?また、何か最近Rubyで作ったシステムがあれば、来年のフクオカRuby大賞に応募してみてはいかがでしょうか?

タグ: Ruby
2011-03-08

デバッグしやすいassert_equalの書き方

デバッグしやすいassert_equalの書き方とデバッグしにくいassert_equalの書き方があるのは知っていますか?*1

デバッグしやすいassert_equalの書き方を2パターン紹介します。

まとめたassert_equal

まず、1つ目のよくみるデバッグしにくいassert_equalの書き方です。

1
2
3
4
5
def test_parse
  assert_equal(29, parse_integer("29"))   # (1)
  assert_equal(29, parse_integer("+29"))  # (2)
  assert_equal(-29, parse_integer("-29")) # (3)
end

これがデバッグしにくいのは、(1)が失敗したら(2)、(3)が実行されないからです。すべてのassert_equalが実行されて、どのassert_equalが失敗したかを確認することでバグの傾向を見つけやすくなります。

例えば…(1)だけが失敗するなら符号が無い場合の処理が怪しいでしょう。(3)だけが失敗するなら、マイナス記号に対応していないのかもしれません。全部失敗するなら根本的におかしいのでしょう。

このように、どのassert_equalが失敗したかがデバッグのヒントになります。よって、↑のような書き方はデバッグがしにくいassert_equalの書き方といえます。

デバッグしやすく書くと、例えば、こうなります。

1
2
3
4
5
6
7
8
9
10
11
def test_parse_no_sign
  assert_equal(29, parse_integer("29"))
end

def test_parse_plus_sign
  assert_equal(29, parse_integer("+29"))
end

def test_parse_mius_sign
  assert_equal(-29, parse_integer("-29"))
end

まとめないassert_equal

続いて、1つ目のよくみるデバッグしにくいassert_equalの書き方です。

1
2
3
4
5
def test_parse
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal("bob", bob["name"])
  assert_equal(29, bob["age"])
end

1つ目の書き方のように複数のassert_equalが書いてあります。1つ目の書き方と同じように直すならこうなります。

1
2
3
4
5
6
7
8
9
def test_parse_string_value
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal("bob", bob["name"])
end

def test_parse_numeric_value
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal(29, bob["age"])
end

でも、本当にこれでよいでしょうか。この書き方では値の型だけを注目しています。値の型に注目するならば以下のように書いた方がよいでしょう。こうした方が余計なものがなくなり、より注目できています。

1
2
3
4
5
6
7
8
9
def test_parse_string_value
  bob = MyJSONParser.parse('{"name": "bob"}')
  assert_equal("bob", bob["name"])
end

def test_parse_numeric_value
  anonymous = MyJSONParser.parse('{"age": 29}')
  assert_equal(29, anonymous["age"])
end

もし、最初のコードが「複数のキーと値をパースできること」を確認したい場合はassert_equalを複数のテストに分割するのではなく、複数のassert_equalを1つのassert_equalにまとめるべきです。

1
2
3
4
5
def test_parse
  bob = MyJSONParser.parse('{"name": "bob", "age": 29}')
  assert_equal({"name" => "bob", "age" => 29},
               bob)
end

1つのassert_equalにまとめると、失敗する場合でも、一部の比較だけが実行されるのではなく、すべての比較が実行されます。そのため、"name"はパースに成功しているけど"age"は失敗している、というように、失敗の傾向がわかります。失敗の傾向がわかるとデバッグしやすくなるというのは前述の通りです。

おまけ: Hashのassert_equal結果が見づらい

ところで、Hashのassert_equalの結果は見づらいですよね。これは、RSpecでも一緒です。

1
{"age" => 30, "name" => "bob"}.should == {"name" => "bob", "age" => 29}

この結果は以下のようになります。

expected: {"name"=>"bob", "age"=>29}
     got: {"age"=>30, "name"=>"bob"} (using ==)
Diff:
@@ -1,2 +1,2 @@
-{"name"=>"bob", "age"=>29}
+{"age"=>30, "name"=>"bob"}

一見しただけではどこが違うのかわかりません。

しかし、最新のtest-unit 2は一味違います。

1
2
assert_equal({"name" => "bob", "age" => 29},
             {"age" => 30, "name" => "bob"})

この結果は以下のようになります。

<{"age"=>29, "name"=>"bob"}> expected but was
<{"age"=>30, "name"=>"bob"}>

diff:
? {"age"=>29, "name"=>"bob"}
?         30

違いがわかりやすいですね。

まとめ

デバッグしやすいassert_equalの書き方を2パターン紹介しました。今度から、assert_equalを書くときは、単に数を増やしたりするのではなく、どうすれば有用なassert_equalになるかを考えてみてください。assert_equalが失敗したときにいかにデバッグしやすいかがヒントになるはずです。

*1  assert_equalではなくて、should ==と読み替えてもよい。

つづき: 2012-01-18
タグ: Ruby | テスト
2011-02-28

TDDきのたん

今日は年に一度の肉の日だからか、いろいろなソフトウェアがリリースされていますね。

このうち、Cutter 1.1.6について紹介します。

Cutterとは

CutterはC/C++用の単体テストフレームワークです。スクリプト言語の単体テストフレームワークのように簡単にテストを書けること、テストが失敗した時にデバッグしやすいことを重視しています。どちらも「テストが苦痛」にならないために大事なことです。

Cutter 1.1.6ではテストをより頻繁に実行しやすくするための機能を強化しました。それがTDDきのたんのサポートです。

TDDきのたんとは

TDDきのたんとはMayu & Co.さんが描いた色違いのかさのきのこたちです。Cutter 1.1.6を使うとテストを実行するたびに愛らしいTDDきのたんに会うことができます。頻繁にテストを実行したくなりますね。

TDDきのたんのスクリーンショット

テストとグリーンとレッドとフィードバック

TDD(テスト駆動開発)ではテストの結果を色で表すことが多いです。グリーンならパスで、レッドなら失敗です。グリーンになったら気持ちいいよね、レッドは落ち着かないね、早くグリーンにしたいね、そんな風に色を使っています。色を使うことで、テストをすべてパスしている状態をキープしたくなる力を推進しています。

RSpecやCutterなど最近も開発が続いている単体テストフレームワークは結果を表示する時にグリーンやレッドなどの色を使っています。でも、それだけで十分でしょうか。

テストを頻繁に実行するようになると、開発の中で自然にテストを実行するようになります。それはもうlsコマンドを実行するように*1テストを実行します。そうなると、テストの実行中に違う作業をして、合間にテスト結果を見るようになります。そのため、テストの失敗に気付くのが遅れる場合があります。

それを解決するためには、これまで通り端末に色付きの結果を表示させているだけでは不十分ではないでしょうか。

通知

デスクトップ環境では多くのアプリケーションが動いていて、常にすべてのアプリケーションを見ていることはできません。そこで必要なときにアプリケーションから「通知」してきます*2。Linuxや*BSDではDesktop Notification、Mac OS XではGrowlなどを利用します。

テスト結果も「通知」した方がよいのではないでしょうか。

そういうわけで、Cutter 1.1.6では「notify-send」コマンドを使ってTDDきのたん付きの通知を行うようになりました。test-unit 2では2ヶ月くらい前からTDDきのたん付きの通知をサポートしています。test-unit 2で利用する場合はtest-unit-notifyを使います。こちらもTDDきのたんを使っています。

まわりくどかったですね。

まとめ

まわりくどくCutter 1.1.6の新機能である「テスト結果通知」を紹介しました。単体テストフレームワークに限らず、うるさくない程度に「通知」に対応するとより使いやすくなるかもしれません。

「通知」を使う場合は画像も入れることをおすすめします。画像を入れるとよりピンときます。例えば、Mayu & Co.さんのTDDきのたんは愛らしくて何度も会いたくなりますよね。それでは、テストを実行して会いにいきましょう。

*1  慣用句です。実際にlsコマンドを実行しまくっている人はあまりシェルを使いこなしていないのでしょう。

*2  デスクトップだけではなくスマートフォンなどの携帯端末でも「通知」してきます。

つづき: 2011-12-26
タグ: Ruby | テスト | Cutter
2011-02-09

もっと知られてもいい人たち

るりまサーチという最近の検索技術を使ってRubyのリファレンスマニュアルを検索するWebアプリケーションがあります。表向きの存在理由は「手早く簡単にドキュメントを検索できるシステムを提供することで、Rubyユーザが楽しくプログラミングすることを妨げないようにする」ですが、実はもう一つ理由があります。それは、「Rubyのリファレンスマニュアルをよいものにしている人たちがいることに気づいてもらう」というものです。

ここを読んでいる人の中に、他の人が実装したプログラミング言語やライブラリのドキュメントを書いて、メンテナンスしている(アップデートに追従するなど)人がどれだけいるのかわかりませんが、この作業はとても大変で根気のいる作業です。しかも、その成果をなかなか実感してもらえません。Ruby本体に新しい機能(例えば、「バイト長」ではなく「文字長」を数える機能)が入ったら、プログラムが簡潔になるなど、すぐに便利さを感じることができます。しかし、ドキュメントの方は「いやぁ、今読んだドキュメントはいいドキュメントだったなぁ!」と感じることはあまりないのではないでしょうか。

るりまサーチでRubyのリファレンスマニュアルを利用する人が増え、何度もその恩恵を受ければ徐々に便利さを感じてくるのではないでしょうか。そして、そう感じた人たちがリファレンスマニュアルをよくするプロジェクトに参加して、さらによくしていくことができたらよい循環になるのではないでしょうか。「Rubyを開発している人たちだけではなく、リファレンスマニュアルをよくしている人たちがいる」、るりまサーチがそのことを知ってもらうきっかけになれば作った甲斐があるというものです。

以下は第3回フクオカRuby大賞の本審査用に作った資料です。

るりまサーチ

タグ: Ruby
2011-02-03

プログラミング言語のドキュメント検索をもっとリッチに

昔から「Rubyはドキュメントが弱い」と言われてきました。「PythonやPHPはあんなにドキュメントが揃っているのに、それに比べてRubyは。。。」というわけです。でも、待ってください。ドキュメントは記述が充実しているだけで十分ですか?簡単に目的のドキュメントにたどりつけますか?

GoogleやBingなどの検索エンジンや、Amazonを筆頭とするECサイトは「探しものを見つけやすくする」ことにどんどん磨きをかけています。なぜ、ドキュメントの検索機能はそれほど進歩しないのでしょうか?

ドキュメントの検索機能が、検索エンジンやECサイトでも使われているような最新の検索パターンを取り入れ、もっとリッチになればプログラミングがもっと楽しくなるはずです。それをRubyのドキュメントで実現しようとしているのがるりまサーチです。

最近、るりまサーチがよりパワーアップしたので、どう変わったかを紹介します。↓は、第3回フクオカRuby大賞用に作ったるりまサーチの資料です。

第3回フクオカRuby大賞用のるりまサーチの資料

よくある検索機能

PythonのドキュメントページSphinxで生成されています*1。SphinxにはJavaScriptで実装された検索機能がついていて、キーワードによる検索のみが可能です。シンプルで検索サーバーを用意しなくてもよい手軽さはうれしいですが、よいキーワードを思いつかないと目的のドキュメントにたどり着くのは難しいです。

PHPのドキュメントページの検索機能は検索対象のカテゴリ化とYahoo!の検索機能を利用しています。Yahoo!の検索機能を使った部分ではHTMLのページだけではなく、Atomフィードのファイルもヒットしてしまっています。通常のWebサイト検索ではAtomフィードもヒットしてよいかもしれませんが、ドキュメント検索ではAtomフィードはノイズです。既存の検索機能を利用することもできますが、より精度をよくしたい場合はチューニングが必要だということです。

プログラミング言語のドキュメント検索は通常のWeb検索や文書検索などの自然言語の検索と傾向が違います。例えば、記号が含まれていたり、省略語が使われていたり、メソッド名・クラス名などキーワード(スコアを上げた方がよい語)となる語がわかりやすかったりといった具合です。このあたりをふまえた上でチューニングするとより探しやすいドキュメント検索機能ができあがります。

では、チューニングした結果は、どのようにユーザーに見せるとよいでしょうか。それには先進的な検索機能を提供するWebサイトが参考になります。よい検索機能がデザインパターンとしてまとめられています。

検索のデザインパターン

検索と発見のためのデザイン ―エクスペリエンスの未来へ
Peter Morville/Jeffery Callender/浅野 紀予
オライリージャパン
¥ 2,520

昨年末、「検索と発見のためのデザイン」という本がオライリージャパンから出版されました*2。この本の中で、実際のWebサイトのスクリーンショットと一緒に、「検索のデザインパターン」としてよく使われるよい検索機能をパターン化しています*3。パターンの数は少ないのですが、このように検索機能をデザインパターンとしてまとめているのがよかったです。検索に興味のある方はこの部分だけでも眺めてみるとよいでしょう。

パワーアップしたるりまサーチにもこの本で作っている検索のデザインパターンにでてくる機能が実装されています。るりまサーチが、ドキュメント検索にチューニングした結果をどのように見せているかを2つ紹介します。

入力補完(オートコンプリート)

検索のデザインパターンの1つとして入力補完(オートコンプリート)機能が紹介されています。デスクトップでもWebでもかなり広まっているので見かける機会が多いのではないでしょうか。

Web検索の場合はクロールした文書の中によく含まれている単語や、ユーザが多く入力する単語などを元データとして、ユーザーの入力を補完することが多いでしょう。しかし、ドキュメント検索の場合、クラス名やメソッド名など、補完対象とすべきキーワードがわかりやすく含まれているので、それを使います。これにより、ノイズが少ない補完候補を出すことができます。

Yahoo!など外部の検索エンジンを使っている場合はこのようなチューニングは難しいのではないでしょうか。

ちなみに、るりまサーチの入力補完機能はgroonga本体に組み込まれている「サジェスト機能」を利用しています。groongaのサジェスト機能はまだ仕様が固まっていないため、APIが変更される可能性が大きくあります*4。使用する場合はそのことに注意してください。

なお、るりまサーチの入力補完のUI部分はjQuery UIのAutocompleteを利用しています。

ファセット型ナビゲーション

groongaでは「ドリルダウン」と呼んでいる機能で、絞り込み候補を表示する機能です。るりまサーチでは「クラス名」や「ドキュメントの種類」(インスタンスメソッドのドキュメントや定数のドキュメントなど)など、いくつかのグループを作って、それぞれのグループ毎に絞り込み候補を表示しています。

ファセット型ナビゲーション

この機能のよいところは、キーボードで入力しなくてもマウスクリックだけで絞り込んでいけることです。一昔前の検索では「よいキーワード」を自分で考えて絞り込んでいく必要がありましたが、この機能を使うことにより「提示されたキーワードから選ぶ」だけでよくなります。便利で探しやすくなりましたね。

まとめ

プログラミング言語のドキュメント検索システムはもっと便利になって、プログラマを助けるべきという話でした。そのためにるりまサーチが提供している今風の検索機能を2つ紹介しました。どちらも、ユーザーが探したいものを見つけるためのキーワードを完全に知らなくても見つけることができるようにするための機能です。今後は、これらの機能のように、ユーザーがそれほど頑張らなくても見つけたいものにたどりつける機能がより一般的になってくることでしょう。

みなさんもWebサイトに検索機能をつけるときは、テキスト入力フォームを1つ置くだけではなく、さらに別の方法も加えてより簡単に目的のものを見つけやすくできないかを考えてみてください。

*1  Sphinxはgroongaのドキュメントでも使っていますね。

*2  翻訳本です。

*3  著者は建設業界やXP界隈でもよく名前のでるアレグザンダーに大きく影響を受けているようです。

*4  なのでまだドキュメント化されていない。

つづき: 2011-12-26
タグ: Ruby
2011-01-26

milter manager 1.6.4リリース

先日、milter managerのスケーラビリティ向上計画を紹介しましたが、そこに書いた内容を実装したmilter manager 1.6.4をリリースしました。(メーリングリストでのアナウンス

スケーラビリティ向上のための主な追加機能は以下の3点です。

  1. マルチプロセス対応
  2. libev対応
  3. writeの非同期化

このうち、「マルチプロセス対応」と「libev対応」は「実験的」な扱いということで、デフォルトでは無効になっています。「writeの非同期化」は有効になっています。このため、Postfixや子milterのreadが遅い場面では劇的にスループットが向上するでしょう*1

また、スケーラビリティ向上に加えてRuby対応を強化しています。代表的な変更はRuby 1.9.2に対応したことです。Rubyでmilterを書くのがより便利になりますね。

まとめ

milter manager 1.6.4をリリースしたので代表的な変更内容を紹介しました。

*1  例えば大量の宛先を削除するmilterを使っている場合。この場合、Postfixのcleanupのreadが遅い。

タグ: milter manager | Ruby
2011-01-20

るりまサーチで第3回フクオカRuby大賞の本審査

Rubyのリファレンスマニュアルを高速に全文検索するるりまサーチ第3回フクオカRuby大賞に応募したら本審査に呼んでもらえたので、2/3に福岡に行ってるりまサーチを紹介することにしました。

(おそらく、)一般の人は審査会場(?)には入れないので、ここで紹介して「興味があったら見にきてください」とは言えないのが残念です。(おそらく、)2/3は時間にたいへん余裕があるので、福岡の人たちと交流できると嬉しいですね。

タグ: Ruby
2011-01-14

地獄のジェネレータ: Ruby Advent Calendar jp: 2010の24日目

これはRuby Advent Calendar jp: 2010の参加エントリです。前日はysakakiさんでした。

今年のAdvent Calender界隈では惚れさせを集めたり惚れさせを作ったり忙しい人でも惚れさせを見れるようにしたりしているので、もう一歩先を行くことにしました。

地獄の名札ジェネレータ

名札には名前を大きく書きましょうジェネレータに「地獄モード」が追加されました。

地獄の名札ジェネレータ

これを使うと札幌Ruby会議03のプロの無職を楽しむことができます。

戦力外(フォントはIPA Pゴシック)

ちゃんとした会社員

地獄のコマンドラインジェネレータ

地獄の名札ジェネレータはTwitterのプロフィール画像や説明文から惚れさせ名札を生成します。フォームに自分のTwitter IDを(カチャカチャカチャ…)と入力して(ッターン!)と「生成!」できるのでお手軽ですが、もっと惚れさせるために試行錯誤したい場合は少し不便ですね。

エンターキー

ということで、ささたつさんが作ったmisawa gem画像生成に対応させました。

% sudo gem install gtk2
% wget https://github.com/kou/misawa/raw/master/lib/misawa.rb
% wget -O misawa_background.jpg http://a2.twimg.com/profile_images/461389564/aaa.jpg
% ruby -r misawa -e 'misawa("俺ってそんなに\n唐揚げとたいやき食べてる\nイメージあるー?\n\nそれどこ情報?どこ情報よー?")'
% display misawa.png

どこ情報?

縦書き

名言はPangoとcairoで描画しています。この2つのフリーソフトウェアを組み合わせることにより日本語の縦書きも描画できます。「ー」など単純に回転させるだけだと問題が起こる文字も、フォントが対応していれば*1きちんと描画できます。

ポイントはPangoGravitycairo_rotate()です。

Pangoにはpango-viewというPangoのオプションを簡単に試せるツールがついています。これを使ってオプションの効果を確認します。

まず、そのまま描画した場合です。横書きになっていますね。

% pango-view --output normal.png --font "MotoyaLMaru 48" --text "つれーわー"

実質(そのまま)

次に、PangoGravityにPANGO_GRAVITY_EASTを指定した場合です。横に転がっていますが縦書きになっています。

% pango-view --output east.png --font "MotoyaLMaru 48" --text "つれーわー" --gravity east

実質(PANGO_GRAVITY_EAST)

ここまでくればあとは実質回転させるだけだからつれーわー。最後に、-90度回転させれば縦書きが完成です。

% pango-view --output east-rotate.png --font "MotoyaLMaru 48" --text "つれーわー" --gravity east --rotate -90

実質(PANGO_GRAVITY_EASTと-90度回転)

この縦書き機能はPango 1.16からサポートされています。Pango 1.16は2007年にリリースされているので、4年くらい前に見て、一番先に飽きた人もいるのではないでしょうか。

もちろん、Ruby/PangoでもPangoGravityを使うことができます。具体的な使い方は名札ジェネレータmisawa gemを見てください。misawa gemの方がシンプルでわかりやすいでしょう。

名言を描画するところでは、文字を縦書きで描画するだけではなく、縦書きの文字に白い影もつけています。そのあたりのやり方もソースを見ればわかるでしょう。

まとめ

惚れさせるために必要なRubyで縦書きを描画する方法を紹介しました。みなさんもRuby/Pangoとrcairoを使って縦書きなクリスマスカードや年賀状をRubyで生成してみてはいかがでしょうか。

明日のRuby Advent Calendar jp: 2010tmaedaさんです。最後なのでとてもすごいのがくるはずですよ!

*1  たぶん。フォントによって問題が起きたり起きなかったりするから。

つづき: 2010-12-29
タグ: Ruby
2010-12-24

デバッグ力 - 札幌Ruby会議03

12月4日に開催された札幌Ruby会議03に参加し、ライトニングトークで少し話しました。話さなかったことと感じたことを残しておきます。

文書検索ラングバ

文書検索ラングバの話をするつもりでしたが、もっと話したいことができたのでそっちを話すことにしました*1。それが「デバッグ力」についてです。

デバッグ力?

「デバッグ力」とは一般的な用語ではありません。実際、検索してもほとんど使っている人はいません。札幌で伝えたいことを表す言葉に一番しっくりくるのが「デバッグ力」だったので、そう呼ぶことにしました*2

コンピュータの世界では、「バグ」は「不具合」とか「問題」といった意味で使われます。デバッグとはバグを取り除くこと、つまり、問題を解決する作業です。プログラマは問題を解決していくことで、徐々に目的を達成するプログラムに近づけていきます。「デバッグ力」には、目的のものへ近づくための力、前へ進む力という意味をこめています。

デバッグ力を発揮するために大事なことはひとつずつ問題を解決していくことです。便利なツールやtipsなどはいろいろありますが、一番大事なことはこの姿勢です。ひとつずつ、問題と向き合って、事実から目を逸らさずに、ていねいに、順番に、解決していきます。この姿勢で問題と向かい合えるようになったら、あとは、テクニックやツールなどを覚えながら少しずつうまくなっていけばよいのです。ひとつずつ、ひとつずつ、ひとつずつ。順番に解決していきます。簡単そうに聞こえますが、けっこう難しいことです。わりとすぐに忘れてしまいます。大変なことなのでなかなか勧めづらいくらいです。しかし、正しい方向に進んでいるかの判断基準になるくらい覚えておきたいことです。

このデバッグ力の基本はプログラムに限定されません。もっと広く応用できるものです。デバッグ力を使って、前へ進んで欲しい、誰かが前へ進むことを助けてほしい、誰かが前へ進むことを助けようとしている人を助けてほしい、そういうことはステージでは話しませんでした。まだ、自分ができていないというのもありますし、みんなの前で話すには少し恥ずかし過ぎました。もう少し自信がでてきたら、個別にもっとていねいに伝えたいと思っています。

札幌Ruby会議03

札幌Ruby会議03では、しまださんがよく使う「感極まった」を、多くの参加者も使っていました。冗談の意味合いも多くあるでしょうが、これは、参加者がしまださんと同じ視点に立ったからではないかと思っています。しまださんの想いが伝わったのだと思います。しまださんの発表者紹介はとても想いがこもっていますが、参加者も同じくらい想いをこめて誰かを紹介できるようになっているのではないでしょうか。だとしたら、これはとてもステキなことです。

ところで、札幌由来の人たちは頑張りすぎではないでしょうか。彼らを助けることができるようになりたいと思います。

先輩芸人より。

*1  リリースされていないことを言い訳にしたけど、別にリリースされていなくても紹介できるのです。

*2  もっとそのままの名前もあったのですが、「デバッグ力」の方がキャッチーだったのでそうしました。

タグ: Ruby
2010-12-08

「全文検索エンジンgroongaを囲む夕べ #1」のRuby枠の資料公開

先月の29日に、全文検索エンジンgroongaを囲む夕べ #1が開催されました。内容はgroonga本体について、groongaとRubyについて、groongaとMySQLについて、groongaとPostgreSQLについて、とgroonga三昧の内容でした。

groongaとRubyについての資料は以降で紹介します。groongaとPostgreSQLについてはすでに資料が公開されています(textsearch groonga v0.1)。参加できなかった方は参考にしてください。

それでは、groongaとRubyについての資料を簡単な解説付きで紹介します。

Ruby loves groonga

Ustreamで配信したものの録画もあります。Ruby枠は49分くらいからです。

リリース情報

開催日当日の29日、groongaの新しいバージョン1.0.4がリリースされました。もちろん、この夕べに合わせたものです。

祝!groonga 1.0.4リリース!

さらに、groongaをRubyから使うためのライブラリrroongaも新しいバージョンがリリースされました。もちろん、これもこの夕べに合わせたものです。

祝!rroonga 1.0.4リリース!

興味がある方はぜひ最新版を利用してください。

話すこと

話すこと

Rubyからgroongaを使う、ということが実に手になじむということを重点的に話します。これはgroongaをRubyらしく使えるライブラリが提供されているためです。そして、そのライブラリを提供しているのがラングバプロジェクトです。

ラングバプロジェクト

ラングバプロジェクト

groongaをRubyらしく使えるライブラリや周辺ライブラリ・ツールなど、groongaとRubyに関連するソフトウェアなどを提供しているのがラングバプロジェクトです。WebサイトはRubyForgeにホスティングしてもらっていて、ソースコードはGitHubにホスティングしてもらっています。以降で紹介するライブラリ・ツールはすべてGitHub上にあります。

ラングバプロジェクト

ラングバプロジェクトの目標は「使いやすい検索システムを手早く、簡単に」実現できるようにすることです。

提供物

そのために、ラングバプロジェクトは全文検索システムを提供します。しかし、1つの全文検索システムですべてのケースをカバーすることはできません。そのため、全文検索システムを作るために必要な部品も再利用できる形で提供します。部品を組み合わせることで環境に合わせた全文検索システムを構築することができます。

このようにして「使いやすい検索システムを手早く、簡単に」という目標の実現を目指します。

提供物一覧

まずは、ラングバプロジェクトがどのような名前のライブラリ・ツールを提供しているかを紹介します。それぞれのライブラリ・ツールが全文検索システムのどの部分の機能を提供するかは図で示します。

全文検索システム

まず、全文検索システムにはどのような機能があるかを確認しておきましょう。

全文検索システムは、まず、「検索対象」から文書を収集します。これが「クローラー」と呼ばれる機能です。次に、収集してきた文書からテキスト情報やタイトル・更新時刻などといったメタ情報を抽出し、全文検索エンジンに登録します。この文書を登録する機能を「インデクサー」と呼びます。最後に、全文検索エンジンに登録されている文書を簡単・便利にユーザが検索できるインターフェイスが必要です。これが「検索インターフェイス」です。最近はWebアプリケーションとして「検索インターフェイス」を提供することが多いです。

それでは、ラングバプロジェクトが提供する機能の紹介です。

rroonga

全文検索エンジンgroonga(ぐるんが)をRubyから利用するためのライブラリであるrroonga(るるんが)を提供します。

ChupaText

インデクサーで必要になる、文書からテキストとメタ情報を抽出するライブラリ・ツールがChupaText(ちゅぱてきすと)です。

ChupaRuby

ChupaTextをRubyのライブラリとして使えるようにするのがChupaRuby(ちゅぱるびー)です。

ActiveGroonga

rroongaをRuby on Rails 3から簡単に使えるようにするのがActiveGroonga(あくてぃぶぐるんが)です。

racknga

Rails 3も含むRackアプリケーションで使えるユーティリティを集めたのがracknga(らくんが)です。

文書検索ラングバ

これらのライブラリ・ツールを利用した全文検索システムが文書検索ラングバです。プロジェクトが提供するライブラリ・ツールを集結したものなので、プロジェクトと同じ名前が付いています。

それでは、それぞれのツールについて紹介します。

rroonga

roonga

rronngaはRubyからgroongaの全文検索エンジンの機能を使えるようにするためのライブラリです。groongaの機能をRubyらしい書き方で書けることを重視したAPIになっていることが特徴です。

スキーマ定義

例えば、groongaのスキーマを言語内DSLとして書けるようになっています。groonga自体もスキーマを定義するための言語を持っていますが、rroongaではこのように通常のRubyの式としてスキーマを書けるようにしています。Rubyでスキーマを書けるため、新しくgroongaのスキーマ定義用の文法を覚える必要がありません。

クエリ

検索条件も通常のRubyの式で書けます。Rubyでは「=~」は「マッチさせる」場合に使われる演算子ですが、rroongaでも「マッチ(キーワードを全文検索してキーワードが含まれる文書を返す)」という意味で使っています。自然な書き方ですね。

クエリ: 複雑

もう少し複雑な条件も自然に書くことができます。例では「タイトルか説明から本文に"Ruby"と"検索"という単語を含んでいる1ヶ月以内のサイト」を検索しています*1。この条件でもRubyで使われている演算子を同じ意味で使っているので、自然に書くことができます。

検索条件をRubyで指定すると検索が遅くなるのではと思うかもしれません。しかし、その心配は不要です。ブロックの中身は1度だけ評価され、groongaの条件式オブジェクトにコンパイルされます。groongaはそのC言語で記述された条件式オブジェクトを使って検索を行うので、rroongaを使わない場合と同様に高速に検索します。

ORマッパーでは検索条件はSQL文字列を組み立てることに相当しますが、rroongaでは直接groongaの条件式を作ります。検索エンジンがSQL文字列をパースする必要がないため、より少ないコストで検索のための下準備をすることができます。

利用シーン

rroongaは様々な場面で有用です。例えば、データを加工しながらgroongaに登録するインデクサーを作る場合です。サイト検索システムなら各ページで共通のヘッダー部分とフッター部分は検索対象から外したいでしょう。そのとき、Rubyでそのあたりの処理を実装してそのままgroongaに登録できると便利です。

他にも、groongaに入っているデータを利用しながら新しいデータを登録する場合もrroongaを使うと便利です。

利用例

rroongaはるりまサーチでも利用されているので、実際にどのように使われているかを調べたいときは、るりまサーチのソースコードを見てください。

rroongaは以下のコマンドでインストールできます。

% sudo gem install rroonga

groongaがインストールされていない場合は自動的にダウンロード・ビルドして利用します。便利ですね。

ChupaText

ChupaText

ChupaTextはテキスト抽出ツールです。「複数の入力形式を1つの操作方法」で扱えることを目標に開発が進められています。

ChupaTextはchupatextコマンドとC言語用のAPIを提供しています。chupatextコマンドは抽出したテキストをMIME形式で出力します。MIME形式を採用したのは以下の理由からです。

  • 広く普及している形式である。
  • メタデータとテキストを扱える。
    • メタデータはヘッダーとして表現。ヘッダーの項目は拡張可能なこともちょうどよい。
    • テキストは本文として表現。
  • 複数のファイルを扱える。アーカイブからテキストを抽出した 場合にこれがうれしくなる。

拡張性

入力形式はこれからも増えていくでしょう。それに対応するため、ChupaTextは拡張しやすい設計になっています。ChupaText本体を変更しなくても新しい入力形式に対応するためのモジュールを後付けで追加することができます。

現在はC言語で共有ライブラリを作成するか、Rubyスクリプトを作成して、所定の位置に置くことで入力形式を追加することができます。Rubyスクリプトでも拡張できるのは、ChupaText内にRubyインタプリタが組み込まれているからです。

ChupaTextはaptitudeでインストールできます。

% sudo aptitude install chupatext

詳細はChupaTextのインストールドキュメントを見てください。

ChupaRuby

ChupaRuby

ChupaRubyはChupaTextをRubyのライブラリとして使用できるようにします。ChupaTextをHTTP経由で利用するためのインターフェイスを追加する予定です。これが追加されると、「テキスト抽出Webサービス」を構築することができるようになります。

ChupaRubyは以下でインストールすることができます。

% sudo gem install chuparuby

ChupaTextは自動的にインストールされないので、事前にインストールしておいてください。

ActiveGroonga

ActiveGroonga

ActiveGroongaはRails 3用のモデルライブラリです。groongaをモデルのデータストアとして利用することができます。ActiveRecordが提供しているような以下の機能を提供します。

  • ActiveGroonga::Base(ActiveRecord::Base相当)
  • rake groonga:*(rake db:*相当)
  • ジェネレーション
  • マイグレーション
  • バリデーション
  • リレーション

ActiveGroongaは以下でインストールすることができます。

% sudo gem install activegroonga

ActiveGroongaに関するより詳しいことは別の機会に紹介します。

racknga

racknga

rackngaは上記のパッケージに入れるには適切ではないな、というものが詰まっているユーティリティパッケージです。RackのミドルウェアやPassenger用Muninプラグインなどがここに入っています。

rackngaは以下でインストールすることができます。

% sudo gem install racknga

文書検索ラングバ

文書検索ラングバ

文書検索ラングバはこれまで紹介したライブラリ・ツールを利用した全文検索システムです。groongaの特徴を活かしたWebベースの検索インターフェイスを提供します。例えば、るりまサーチでも提供しているドリルダウン機能などです。

文書検索ラングバはそろそろリリースされる予定です。もうしばらくお待ちください。

まとめ

groongaのRuby関連の情報を紹介しました。便利なライブラリやツールが充実していっているので、ぜひ、Rubyとgroongaを使って全文検索システムを作ってみてください。

お知らせ

*1  この資料を使ったのは2010年11月29日です。

つづき: 2010-12-29
タグ: Ruby
2010-12-01

Rubyでmilterを作れる - milter manager 1.6.2リリース

(まだMLではアナウンスしていませんが)milter manager 1.6.2をリリースしました。

このリリースではRubyでmilterを書くために必要な機能がすべて実装されました。これまではメールを拒否したり、隔離する機能は提供していたのですが、差出人を変更する、宛先を追加する、本文を変更するなどメッセージを変更する機能は部分的にしか対応していませんでした。このリリースからメッセージを変更する機能も提供されるようになったので、ほとんどのmilterはRubyでも実装できるようになりました。

Rubyでmilterを書くとどのようになるかはhbstudy#15の発表内容で紹介しています。

milter manager 1.6.2のインストール方法・アップグレード方法は以下にまとまっています。

milter managerを使ってRubyでメールフィルターを作ってみてはいかがでしょうか?

タグ: milter manager | Ruby
2010-11-24

「全文検索エンジンgroongaを囲む夕べ #1」のお知らせ

るりまサーチでも使っている全文検索エンジンgroongaとその周辺技術についての勉強会が開催されます。

現時点でもうすでに定員(50名)オーバーの98名が登録していますが、現在、参加人数を増やせるように調整中なので、諦めずに早めに登録しておくことをオススメします。参加人数が増えたときに定員に入れるかもしれませんよ。

内容は以下の通り、現在のgroonga関連のことが網羅できる内容になっています。

  • groonga本体について。groongaの前身Sennaとの違いは?
  • groongaのRubyバインディングrroongaと関連Rubyライブラリについて。
  • groongaのMySQLバインディングgroongaストレージエンジンについて。
  • groongaのPostgreSQLバインディングtextsearch_groongaについて。

1ヶ月くらい先ですが、今から楽しみですね。ぐるぐる。

タグ: Ruby
2010-10-26

git-utilsをGitHubへ移動

git用のコミットメール配信システムであるgit-utilsのリポジトリをGitHubへ移動しました。こんなこともあろうかとGitHubにクリアコードアカウントを取得しておいたのです。

0.0.1以降リリースしていませんが、少しずつ機能を追加していて、GitHubのPost-Receive Hooksを用いたGitHub上のリポジトリのコミットメール配信にも対応しています。tDiaryのコミットメールgroongaのコミットメールなどがgit-utilsを利用しています。GitHub上のリポジトリでコミットメールを配信したい場合はお問い合わせフォームからご連絡ください。(無償・無保証で対応しています。)

github-post-reciever/以下を利用して自分でPost-Receive Hooksを受け付けるサーバを立ち上げることもできます。具体的な手順のかかれたインストールドキュメントはありませんが、RackアプリケーションなのでPassengerなどを設定したことがある方は問題なく設置できるでしょう。

GitHubに移動したことでforkしたりpull requestを出しやすくなっているので、自由に利用したり変更したりしてください。改良やドキュメント作成などフィードバックは積極的に取り込んでいく予定です。ライセンスはGPLv3 or laterです。

つづき: 2010-12-29
タグ: Ruby
2010-09-27

hbstudy#15発表資料: milter managerで簡単迷惑メール対策

hbstudy#15でmilterについて発表しました。

milter managerで簡単迷惑メール対策

公開しているスライドの内容は実際に使ったものと異なっています*1。実際に使ったものや当日の雰囲気などが気になる人はUstreamの録画を観てください。

スライドのPDFやソース、当日使ったmilterなどはスライドページからダウンロードできます。milterはスライドのソースと同じアーカイブに含まれています。

スライドはRabbitというRubyで書かれたフリーソフトウェアで作成しています。Ruby界隈ではとても有名なプレゼンツールなのですが、インフラ界隈ではあまり有名ではないので、当日使ったRabbitの機能を簡単に説明しておきます。

スライドの下にでていたうさぎとかめは、うさぎがページ数を、かめが経過時間を示しています。うさぎが前を走っていればペースが速い、かめが前を走っていれば間に合わない、というようにプレゼンテーションの進み具合が視覚的に一瞬でわかるのがよいところです。「残り7:35」と出てもいい感じで進んでいるのかどうかがわからないですよね。また、タイマー用のテキストがスライドの中にあると不自然ですが、うさぎとかめがスライドの中にいても不自然ではないので、発表者用のビューではなく、表示用のビューに表示することができるのもよいところです。PCの画面とディスプレイの内容を同じ内容にできるので、ディスプレイ側の表示だけおかしくなっている、という状態を防ぐことができます。ただし、聞いている人が発表よりもうさぎとかめの方が気になってしまうという問題があります。ここは発表者の腕でなんとかする必要があります。

また、携帯電話でRabbitを遠隔操作していました。仕組みを図示したのものがとちぎRuby会議02の資料公開にあります。今回も大体これと同じ構成です。処理の流れは以下の通りです。

  1. 携帯電話のi-modeブラウザで「次のページへ遷移」リンクを辿る ーHTTP→
  2. サーバ上で動いているHTTPサーバ(Rabrick: Rubyで書かれている)がノートPC上のRabbitプロセスの「次のページへ遷移メソッド」をdRubyを使って呼び出す ーdRuby→
  3. ーSSHトンネル→
  4. ーdRuby→ ノートPC上のRabbitプロセスの「次のページへ遷移メソッド」が呼び出されて、次のページへ遷移

システムとしてはカッコイイのですが、目の前のノートPC上で動いているRabbitを操作するためにインターネットを経由しているので多少タイムラグがあります。

それでは、以下に発表内容を簡単にまとめておきます。省略している部分が気になる場合は上述のUstreamの録画を観てください。

概要

話すこと

タイトルは「milter managerで簡単迷惑メール対策」でしたが、参加者がそれほどメール環境になじみの深い人ばかりではなさそうだったので、milter managerそのものの話は最後に少しする程度にしました。内容のほとんどはmilter managerがベースとしている技術であるmilterについてです。

milterを言葉や図だけで理解するのは少し大変なので、今回は実際にありうる例を動かしながら理解していきます。

milterについて

milterとは

milterとはSendmailが作ったメールフィルターの仕組みです。実際には、この仕組みの中で使われるネットワークプロトコルや、メールフィルターを開発するためのAPI、メールフィルターそのもののことも「milter」と呼ぶことがあります。ただ、多くの場合は文脈からどれを指しているかがわかるので、それほど混乱することはありません。

milterは2001年9月にリリースされたSendmail 8.12.0から正式サポートされています。9年の歴史がある枯れた技術といえます。また、SendmailだけではなくPostfixでもサポートされています。Postfixでは2006年6月にリリースされた2.3.0からサポートされ、現時点の最新版2.7.1ではmilterのほとんどの機能がサポートされています。こちらも4年の歴史があり、もう十分に実践に投入できるほど使われています。

つまり、現時点ではmilter*2を用いてメールフィルター機能を実現することは「実験的な試み」ではなく「いくつかある有力な選択肢の1つ」といえます。

しかし、日本語でのmilterの情報が少ないことは事実です。milterに関する英語の情報はmilter.orgに集まっています。milter.orgにはmilter*3を検索する機能や開発者向けの情報なども載っています。

たとえば、Technical Overview - Control Flowに載っている以下の擬似コードを見れば、複数のmilter*4を同時に用いたときにどのような動作になるかはわかります*5

For each of N connections {
  For each filter
    process connection (xxfi_connect)
  For each filter
    process helo (xxfi_helo)
  MESSAGE:For each message in this connection (sequentially) {
    For each filter
      process sender (xxfi_envfrom)
    For each recipient {
      For each filter
        process recipient (xxfi_envrcpt)
    }
    For each filter {
      process DATA (xxfi_data)
      For each header
        process header (xxfi_header)
      process end of headers (xxfi_eoh)
      For each body block
        process this body block (xxfi_body)
      process end of message (xxfi_eom)
    }
  }
  For each filter
    process end of connection (xxfi_close)
}

日本語でもこれと同じような情報を読むことができれば、milter利用の敷居が低くなるかもしれません。ここでは、実際に動かしながらmilter*6がどのように動くのかを確認していきます。

milterの挙動

milterの挙動: 3行で

上記のmilterの動作フローをざっくりとまとめると、以下のようになります。

  • 本文は直列(前のmilterの影響を受ける)
  • 本文以外は並列(前のmilterの影響を受けない)
  • 詳細な結果は最後に返す

milterは迷惑メール対策に使われることがほとんどです。迷惑メールの手法は複雑化しているので、1つの迷惑メール対策方法で完璧ということは不可能です。複数のmilter(迷惑メール対策方法)を組み合わせて効果的な迷惑メール対策システムを構築する必要があります。

しかし、milterは上記のような動作のため、milter1の結果を使ってmilter2の挙動を変えるということが難しくなっています。milter1の詳細な結果を使いたければ、メール全体の処理が終わるのを待たなくてはいけません。milter2がエンベロープ情報(送信元や宛先)を取得した段階で実行する迷惑メール対策方法*7を採用している場合は、メール全体の処理が終わるまで待つのは効率的ではありません。

milterがどのように動作するかがわかれば、複数のmilterをどのように組み合わせることが効果的かを検討することができます。そのため、milterを用いて効果的な迷惑メール対策システムを構築する場合はmilterの挙動を理解しておく必要があります。

milterの挙動の詳細

milterプロトコル

miltrの挙動を理解するためには、以下の3つの要素があることを理解するのが早道です。要素の名前はここでの説明のために便宜的に付けたもので、milterで一般的な用語ではないので注意してください。

  • ステージ: milterが処理を行うタイミング
  • アクション: milterが処理を行ったときにメールサーバに返す結果
  • メッセージ変更機能: メールフィルターとして行えること

ステージ

milterが処理を行うタイミングはSMTPのコマンド+αだけあります。どのようなタイミングかを一言解説を付けています*8

  • connect: SMTPクライアントが接続してきたとき。
  • helo: SMTPクライアントがHELO/EHLOコマンドを実行したとき。
  • mail from: SMTPクライアントがMAIL FROMコマンドを実行したとき。
  • rcpt to: SMTPクライアントがRCPT TOコマンドを実行したとき。
  • data: SMTPクライアントがDATAコマンドを実行したとき。
  • header: メールサーバがメールのヘッダー1つを解析したとき。ヘッダーの数だけn回発生する。
  • end of header: メールサーバがメールのヘッダーをすべて解析し終わったとき。
  • body: メール本文を処理しているとき。
  • end of message: メール全体を処理したとき。

「メッセージ変更機能」は「end of message」のときしか実行できません。「メッセージ変更機能」は後述します。

アクション

milterは処理を行うたびにメールサーバに結果を返さないといけません。メールサーバに返せる結果は以下の通りです。

  • continue: 処理を続行する。普通はこれ。
  • accept: 受信する。このmilterは以降の処理を行わない。
  • reject: メールを受信拒否する。SMTPレベルでは500番台の受信拒否になる。
  • tempfail: メールを一時受信拒否する。SMTPレベルでは400番台の受信拒否になる。
  • discard: メールを受信するが、配送せずにそのまま廃棄する。このmilterは以降の処理を行わない。
  • quarantine: メールを受信するが、配送はしない。(本当はアクションではないので、end of messageステージのときしか使えない。)

メッセージ変更

以下のようなメッセージ変更機能があります。メールフィルターとしては十分な機能です。

  • From変更
  • To追加
  • To削除
  • 本文変更
  • ヘッダー追加
  • ヘッダー削除
  • ヘッダー変更

それでは、以下、実例を元に実際の動作を確認します。

ケース1: mail fromでaccept

実例は5つ用意しましたが、ここでは1つだけ紹介します。他はスライドやUstreamの録画を観てください。

ケース1: accept

mail fromステージでacceptアクションを返した場合です。SMTP Authをしている場合は何も処理をしないmilterが多くあります。その場合はこのような動作になります。

mail fromステージでacceptアクションを返すmitlerはRubyで実装するとこのようになります。これはスライドのアーカイブの中のmilters/milter-accept.rbに入っています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'milter'

class MilterAccept < Milter::ClientSession
  def envelope_from(from)
    p from
    accept
  end

  def envelope_recipient(to)
    p to
  end
end

command_line = Milter::Client::CommandLine.new
command_line.run do |client, _options|
  client.register(MilterAccept)
end

このとき、SMTPクライアントがRCPT TOコマンドを送ったらどうなるでしょうか。

ケース1: 答え

この場合、milterはacceptを返しているのでメールを受信します。ただし、SMTPクライアントがMAIL FROMより後のRCPT TO以降のコマンドを送ってもmitler側では何も起こりません。それ以降のステージではメールサーバがmilterに通知しないからです。

発表時には実際にSMTPを話しながら動作を確認しています。この様子はUstreamの録画を観てください。また、他の例もUstreamの録画やスライドページを見てください。

milter managerが便利なところ

milter managerの特徴

例を使ってmilterの動作を確認した通り、milterを連携させることが難しいケースがあります。milter managerを使うことでそこを補うことができます。

例えば、「milter評価モード」機能を使うことによって、「milter1がreject/temp failを返した」という情報をmilter2で利用することができます。通常はend of messageまで待たなければ情報を利用することができませんが、reject/temp failはどのステージでも利用することができるので、もっと早い段階で情報を利用することができます。

以下、milter managerの利用例を1つ紹介します。他の利用例などはまた別の機会にでも紹介できるとよいですね。

管理例: ユーザ毎の設定

milter managerを使うことによってユーザ毎に迷惑メール対策方法を変えることができるシステムを構築することができます。例えば、MySQLにユーザ毎の設定を格納しておきます。格納した情報は各milterではなくmilter managerが参照してmilterの挙動を動的に制御します。各milterがそれぞれMySQLの情報を参照するようなシステムにすることもできますが、そうすると、複数のmilterを使う場合に大変です。それぞれのmilterがMySQLに対応し、さらに、MySQL内の情報をどのように使うかを判断しなければいけません。milter managerを使うことにより、その部分を一括管理することができます。

まとめ

管理例: ユーザ毎の設定

迷惑メールが多様化しているため、複数のmilterでそれに対応する必要があります。ただし、複数のmilterを利用した場合の挙動は少し複雑です。まるで理解しがたいというものではなく、少し落ち着いて考えれば理解できるものなので、milterを利用する場合は一度くらいは落ち着いてmilterの挙動を確認しましょう。

複数のmilterを利用する場合はmitler managerも一緒に導入するとより便利です。

お知らせ

宣伝1: クリアコードではプログラミングが好きな開発者を2名募集中です。興味がある人は応募してみてください。よさそうな人を知っていたら教えてあげてください。

宣伝2: クリアコードの開発者は全員オープンソースソフトウェアの開発に関わって磨いてきた技術力を持っています。オープンソースソフトウェアに関して技術的にお困りのことがあったらぜひご相談ください。もちろん、milterに関することもOKです。

*1  その場だけで使う用のものや再配布用ではないものを抜いている。

*2  これは仕組みのこと。

*3  この「milter」はmilterの仕組みを使って実現されたメールフィルターそのもののこと。

*4  これもメールフィルターそのもののこと。

*5  ちょっとウソ。milterの他のことも少し知らないとこれだけだとわからない。

*6  これもメールフィルターのこと。これ以降は注釈をつけませんがわかるはずです。

*7  例えば、SPFやGreylisting(グレイリスト)。SPFは迷惑メール対策にも使われることが多い。

*8  わかりやすさを重視したので、厳密には違う場合もあるので注意してください。

タグ: milter manager | Ruby
2010-09-20

メタプログラミングRuby

日本Ruby会議2010A Metaprogramming Spell BookというタイトルのRubyの魔術の話がありましたが、それはこの本の内容がベースになっています。

メタプログラミングRuby
Paolo Perrotta/角征典
アスキー・メディアワークス
¥ 2,940

Rubyはとても動的な言語で、ほとんどのことが実行時に決まります。このRubyの特徴はメタプログラミングと非常に相性がよいのです。「メタプログラミングとは、コードを記述するコードを記述すること」です。プログラムを実行して動的にプログラムを作っていくのです。

1冊まるごとRubyのメタプログラミング機能について書いてあります。RailsでWebアプリケーションが作れるようになってきて、少し余裕がでてきた人あたりにあっているのではないでしょうか。Railsはよくも悪くもメタプログラミングしまくっています。少しレールを外れたことをしようとしたらRailsの中に飛び込んで中を調べるようになるでしょう。そのとき、この本に書いてあることが役に立つでしょう。

この本のすごいところは、訳がとてもこなれていて読みやすいことと、サンプルコードがきちんと整形されていて読みやすいことです。

訳はMartin Fowler's Blikiの翻訳でお馴染みの角さんなので安心して読めるのは驚くことではありませんね。

コードがきちんとインデントされていて、スタイルが一貫しているのはとても好感が持てます。プログラミング関連の書籍にはきちんと整形されていなくて汚いソースコードが載っていることがあります*1が、コードもきれいに整形できない人が書いていることを誰が信用できるでしょうか。いくら役立つことを書いていたとしても、いくらすごいことをするプログラムであっても、信用することはできません。

この本は、日本語としてもRubyとしても読みやすくできているため、内容に集中することができます*2。Rubyのメタプログラミングについて知りたくなったら信用できるこの本を読んでみるとよいでしょう。Rubyをちゃんと知って、信用できるようになれば、「ダークサイド」に堕ちることもないでしょう。

*1  翻訳のものの方がそんな傾向が多い印象。

*2  最近読んだ技術書の中で誤植を見つけられなかったのはこの本くらいです。

タグ: Ruby
2010-09-13

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

注: 長いです。

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

るりまサーチの作り方

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

2010-08-30
再生: 210
コメント: 20
マイリスト: 5

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

すごー buzztterはgrongaを使 rackngaが便利そう 検索:rrooonga HTTPでも出来るー 検索selec...

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

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

話すこと

話すこと

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

るりまサーチとは

るりまサーチとは

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

ポイント: ドリルダウン

ポイント: ドリルダウン

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

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

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

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

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

ポイント: URL

ポイント: URL

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

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

ポイント: キャッシュ

ポイント: キャッシュ

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

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

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

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

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

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

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

ポイント

ポイント

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

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

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

全文検索システム

全文検索システム

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

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

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

るりまサーチの場合

るりまサーチの場合

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

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

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

groonga: 特徴

groonga: 特徴

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

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

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

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

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

定義例: るりまサーチ

定義例: るりまサーチ

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

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

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

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

  • テーブル
  • カラム

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

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

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

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

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

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

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

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

groongaのDDL:

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

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

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

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

rroongaのDDL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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:

1
2
3
4
5
6
7
8
9
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:

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

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

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

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

racknga

racknga

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

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

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

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

エラー通知

エラー通知

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

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

config.ru:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
require 'racknga'

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

JSONP

JSONP

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

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

config.ru:

1
2
3
4
5
6
7
8
9
10
require 'racknga'
require 'racknga/middleware/cache'

use Rack::Middleware::JSONP

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

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

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

まとめ

まとめ

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

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

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

お知らせ

*1  数十秒以上かかる。

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

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

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

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

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

つづき: 2010-12-29
タグ: Ruby
2010-09-01

プログラミングRuby 1.9

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

前作: プログラミングRuby 第2版 言語編
Dave Thomas/Chad Fowler/Andy Hunt/田和 勝/まつもと ゆきひろ
オーム社
¥ 3,990

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

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

1
2
3
4
5
6
7
8
9
module MyLibrary
  class MyClass
    # ...
  end
end

class MyLibrary::MyClass
  # ...
end

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

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

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

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

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

プログラミングRuby 1.9 −言語編−
Dave Thomas with Chad Fowler and Andy Hun/まつもとゆきひろ/田和 勝
オーム社
¥ 3,990

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

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

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

タグ: Ruby
2010-08-23

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

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

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

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

@kdmsnr

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

@kdmsnr改

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

使っているもの

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

cairo

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

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

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

1
2
3
4
5
6
7
8
9
10
def render(context)
  # 実際のサイズの描画
end

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

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

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

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

Pango

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

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

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

1
2
3
4
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
2010-08-18

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

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

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

内容

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

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

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

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

まとめ

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

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

お知らせ

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

タグ: Ruby
2010-08-11

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

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

Deviseとは

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

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

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

インストール

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

% sudo gem1.9.1 install rails --pre

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

% ruby1.9.1 rails new taiyaki
% cd taiyaki

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

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

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

Gemfile:

1
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:

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

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

config/routes.rb:

1
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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(変更後):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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:

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
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:

1
2
class Users::SessionsController < Devise::SessionsController
end

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

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

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

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

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

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

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

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

<h1>Welcome#index</h1>

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

サーバを起動します。

% ruby1.9.1 script/rails server

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

トップページ

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

ログインフォーム

OpenID対応

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

% sudo gem1.9.1 install warden-openid

Gemfileにも追記します。

Gemfile:

1
gem 'warden-openid'

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

config/initializers/devise.rb:

1
2
3
4
5
6
Devise.setup do |config|
  ...
  config.warden do |manager|
    manager.default_strategies(:openid, :scope => :user)
  end
end

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

config/initializers/openid.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class User < ActiveRecord::Base
  REQUIRED_FIELDS = {
    :nickname => "nickname",
  }

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

  class << self
    def required_open_id_fields
      REQUIRED_FIELDS.values
    end

    def optional_open_id_fields
      OPTIONAL_FIELDS.values
    end
  end

  def password_required?
    false
  end

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

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

ログイン成功

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

まとめ

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

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

つづき: 2010-12-29
タグ: Ruby
2010-07-13

ActiveLdap 1.2.2 - Rails 2.3.8対応

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

% sudo gem install activeldap

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

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

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

つづき: 2010-12-29
タグ: Ruby
2010-07-05

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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

以下のように使います。

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

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

タグ: Ruby
2010-06-23

Passenger用Muninプラグイン

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

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

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

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

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

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

インストール方法

Debian GNU/Linux前提です。

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

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

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

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

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

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

[passenger_*]
  user root

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

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

munin-nodeを再起動します。

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

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

まとめ

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

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

つづき: 2010-12-01
タグ: Ruby
2010-06-14

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

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

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

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

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

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

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

つづき: 2010-12-29
タグ: Ruby | Mozilla
2010-06-08

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

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

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

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

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

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

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

つづき: 2011-05-19
タグ: Ruby
2010-05-31

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

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

るりまサーチ

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

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

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

機能

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

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

ドリルダウン

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

ドリルダウン

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

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

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

条件解除

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

条件解除

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

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

実装

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

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

スキーマ定義

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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とも呼ばれ、やりすぎる人も出るほどです。例えば、上記のような記述を以下のようにすることもできますが、これは少しやりすぎではないかと感じます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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」であるレコードを検索するときは以下のようになります。

1
2
3
entries.select do |record|
  record.name == "Regexp"
end

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

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

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

1
2
3
4
5
6
7
entries.select do |record|
  target = record.match_target do |match_record|
    (match_record.name * 100) |
    (match_record.description)
  end
  target =~ "encoding"
end

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

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

まとめ

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

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

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

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

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

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

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

つづき: 2010-12-29
タグ: Ruby
2010-04-27

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

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

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

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

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

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

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

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

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

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

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

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

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

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

% sudo gem install rake-compiler

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

% sudo aptitude install -y mingw32

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
diff -ru ruby-1.9.1-p378.orig/win32/win32.c ruby-1.9.1-p378/win32/win32.c
--- ruby-1.9.1-p378.orig/win32/win32.c        2009-12-05 18:40:53.000000000 +0900
+++ ruby-1.9.1-p378/win32/win32.c        2010-04-20 23:10:13.000000000 +0900
@@ -4604,7 +4604,7 @@

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

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

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

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

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

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

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

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

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

1
2
3
4
5
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ベストプラクティス -プロフェッショナルによるコードとテクニック
Gregory Brown/高橋 征義(監訳)/笹井 崇司
オライリージャパン
¥ 3,360

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

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

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

タグ: Ruby
2010-04-21

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

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

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

1
2
3
4
5
6
7
8
9
10
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では範囲の最初から最後まで順に繰り返し、その中に引数と==な値が含まれるかどうかを確認します。コードで表すとこんな感じです。

1
2
3
4
5
6
7
8
9
10
11
12
13
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を返すとき)。

1
(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?と比べてだいぶ遅くなるというわけです。

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

解決法

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

1
2
3
4
5
6
7
8
9
10
require 'time'
require 'benchmark'

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

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

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

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

高速なRange#include?

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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のことをもっと知って上手に付き合ってみてはいかがでしょうか。

タグ: Ruby
2010-03-25

るびま0029号

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

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

ActiveLdapを使ってみよう

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

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

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

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

とちぎRuby会議02

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

Rubyベストプラクティス

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

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

まとめ

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

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

タグ: Ruby
2010-03-16

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

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

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

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

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

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

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

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

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

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

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

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

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

タグ: Ruby | Cutter | テスト
2010-03-11

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

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

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

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

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

内容

話題

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

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

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

ゴール

今日のみなさんのゴール

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

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

メールフィルタ

メールフィルタ

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

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

MTAにプラグイン

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

メールフィルタのまとめ

メールフィルタのまとめ

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

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

milterについて

milter?

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

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

milterシステム

milter関連用語

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

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

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

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

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

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

SMTPの概要

簡単?

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

  • HELO
  • MAIL FROM
  • RCPT TO
  • DATA

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

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

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

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

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

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

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

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

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

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

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

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

> QUIT
< 221 2.0.0 Bye

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

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

SMTPとmilterプロトコル

SMTPとmilterプロトコル

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

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

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

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

コマンド: メタ情報

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

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

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

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

コマンド: DATA

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

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

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

milterサンプル: メール検索

扱うもの

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

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

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

カラム指向

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

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

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

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

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

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

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

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

パトリシアトライ

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

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

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

groongaでキーワード検出

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

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

1
2
3
4
5
6
7
8
9
10
11
target_text = "..."
keywords = request["keywords"].split

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

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

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

スキーマ

スキーマ: Messages

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

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

スキーマ: Terms

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Groonga::Schema.define do |schema|
  schema.create_table("Messages") do |table|
    ...
    table.text("text")
  end

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

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

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

Rubyでmilterを作る

Rubyでmilter

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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を作ることは簡単だということです。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
query = "Ruby" # <- 検索キーワード
messages = Groonga["Messages"]
result = messages.select do |record|
  record["subject"].match(query) |
    record["body"].match(query)
end
result.sort([["_score", :desc]]).each do |message|
  puts "-" * 78
  puts "score: #{message.score}"
  puts "Subject: #{message.subject}"
  puts "From: #{message.from}"
  puts "To: #{message.to}"
  puts
  puts message.body
  puts "-" * 78
end

まとめ

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

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

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

あわせて読みたい

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

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

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

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

つづき: 2010-12-29
タグ: milter manager | Ruby
2010-02-15

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

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

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

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

% sudo gem install groonga

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

0.9.x

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

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

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

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

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

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

まとめ

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

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

タグ: Ruby
2010-02-09

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

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

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

内容

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

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

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

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

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

まとめ

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

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

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

タグ: milter manager | Ruby
2010-01-25

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

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

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

概要

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

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

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

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

内容

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

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

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

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

まとめ

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

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

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

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

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

つづき: 2009-12-21
タグ: Ruby
2009-12-15

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

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

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

内容

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

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

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

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

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

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

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

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

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

札幌でたいやきってな w

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

FAQ

Q: 練習したのですか?

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

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

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

Q: 少し速いですね。

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

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

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

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

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

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

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

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

札幌Ruby会議02全体の内容

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

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

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

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

Reject Talks

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

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

まとめ

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

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

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

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

あわせて読みたい

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

タグ: Ruby
2009-12-07

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

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

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

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

内容

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

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

まとめ

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

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

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

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

儲かるRuby - 支えるRuby

システム構成

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

システム構成

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

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

内容

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

雰囲気がよい

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

まとめ

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

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

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

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

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

タグ: Ruby
2009-10-25

Ruby/groonga 0.0.7

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

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

% sudo gem install groonga

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

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

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

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

キーワードリンク

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- coding: utf-8 -*-
require 'groonga'

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# "description"カラムに「ruby」または「groonga」が入っているレコードを検索
query = "ruby OR groonga"
records = table.select do |record|
  record["description"].match(query)
end

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

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

まとめ

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

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

つづき: 2010-02-09
タグ: Ruby
2009-10-02

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

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

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

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

手順

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

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

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

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

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

config/routes.rb:

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

app/controllers/books_controller.rb:

1
2
3
class BooksController < ApplicationController
  active_scaffold :book
end

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

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

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

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

日本語メニューのActiveScaffold

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

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

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

まとめ

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

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

タグ: Ruby
2009-09-28

るびま0027号

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

ささださんのコメント

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

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

おすすめ記事

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

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

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

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

まとめ

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

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

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

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

タグ: Ruby
2009-09-14

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

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

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

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

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

タグ: Ruby
2009-09-11

もうすぐRetrospectiva 2.0がリリース

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

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

簡単インストール

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

アジャイル開発支援

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

git対応

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

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

まとめ

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

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

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

タグ: Ruby
2009-08-07

groongaをRackに載せて全文検索

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

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

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

機能

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

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

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

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

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

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

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

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

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

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

1
2
3
4
5
records = documents.select do |record|
  (record["content"] =~ keyword1) &
    (record["content"] =~ keyword2) &
    ...
end

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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を使うとこうなります。

1
2
3
4
5
6
7
8
9
10
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行です。

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

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

検索キーワードの正規化

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# スキーマ定義開始
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ではスニペットと呼んでいます。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# キーワードを囲むタグ
open_tag = "<span class=\"keyword\">"
close_tag = "</span>"
# スニペットオブジェクトの作成
snippet = Groonga::Snippet.new(:width => 100,
                               :default_open_tag => open_tag,
                               :default_close_tag => close_tag,
                               :html_escape => true,
                               :normalize => true) # キーワードを正規化
# 検索キーワードを登録
request["query"].split.each do |word|
  snippet.add_keyword(word)
end

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

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

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

簡単ですね。

まとめ

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

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

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

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

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

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

つづき: 2009-12-21
タグ: Ruby
2009-07-31

日本Ruby会議2009の資料公開

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

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

つづき: 2009-12-21
タグ: Ruby
2009-07-19

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

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

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

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

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

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

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

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

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

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

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

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

まとめ

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

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

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

タグ: milter manager | Ruby
2009-07-15

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

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

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

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

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

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

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

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

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

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

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

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

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

まとめ

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

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

つづき: 2009-07-19
タグ: Ruby
2009-07-06

るびま0026号

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

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

仙台Ruby会議01

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

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

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

RubyNews

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

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

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

まとめ

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

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

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

タグ: Ruby
2009-07-02

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

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

資料: milter manager

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

動画: OSC 2009 Hokkaido milter manager

内容

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

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

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

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

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

Ruby札幌

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

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

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

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

まとめ

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

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

つづき: 2009-12-21
タグ: milter manager | Ruby
2009-06-21

ActiveLdap 1.0.9リリース

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

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

1.0.9

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

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

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

まとめ

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

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

タグ: Ruby
2009-06-08

Ruby/groonga 0.0.2リリース

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

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

メソッドの個別化

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

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

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

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

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

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

1
2
3
4
5
6
7
8
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つのメソッドでたくさんの状況を考慮しなければいけないとこのようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

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

1
2
3
4
5
6
7
8
9
10
11
def fast_search(options={})
  valid_keys = [...]
  ...
end

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

...

あとは、総称的なメソッドを1つ用意すればメソッド分割前と同じように使えます。

1
2
3
4
5
6
7
8
9
def search(type, options={})
  case type
  when :fast
    fast_search(options)
  when :remote
    remote_search(options)
  ...
  end   
end

ここまできたらもう一歩です。オブジェクト指向プログラミングでcase whenやswitch caseで分岐している時はオブジェクトが足りない匂いを感じとってください。このような場合はそれぞれの条件毎にオブジェクトを作り、それぞれのオブジェクトで同じ名前のメソッドを定義します。

1
2
3
4
5
6
7
8
9
10
11
class FastSearcher
  def search(options={})
    ...
  end
end

class RemoteSearcher
  def search(options={})
    ...
  end
end

これで、条件分岐がなくなり、総称的なメソッドも定義しなくてもよくなります。それらは言語がやってくれるからです。

1
2
searcher = FastSearcher.new
searcher.search(:query => ...)

と、だいぶ遠回りをしましたが、Ruby/groonga 0.0.2では以上のようなAPI設計ポリシーに従って、オブジェクト毎にメソッドを実装するようになりました。これにより使いやすさが向上しています。

また、メソッドが分割されることにより、ドキュメントを書きやすくなります。読む側も読みやすくなります。

まとめ

Ruby/groonga 0.0.2はAPIの使いやすさが向上しています。これは、適切な粒度に実装を分割したからです。使いやすいAPIを検討しているのであれば、実装の粒度を細かくすることを検討してみてください。無駄がなくすっきりして使いやすいAPIになるかもしれません。

タグ: Ruby
2009-06-05

Ruby 1.9.1とREXMLとXML宣言のエンコーディング

Ruby 1.9.1付属のREXMLではXML宣言のエンコーディングの扱いに問題があるためvalidなXMLでもパースできない場合があるという話です。

問題

Ruby 1.9では文字列や正規表現がエンコーディング情報を持つため、REXMLのように正規表現ベースでXMLをパースしている場合は、エンコーディングを適切に設定しないとパースに失敗することがあります。

例えば、tDiaryのseach-yahoo.rbプラグインがこの問題に遭遇しています。

原因

REXMLは内部でUTF-8を用いています。そのため、パース対象のXMLのエンコーディングをUTF-8に変換しながらパースします。この処理はREXML::SourceまたはREXML::IOSourceで行われます。

しかし、REXML::IOSourceに問題があり、UTF-8に変換しないままパースしてしまう場合があります。これは、入力XMLのエンコーディングがUTF-8に設定されていない、かつ、XML宣言のエンコーディングがUTF-8になっている場合です。ちなみに、REXML::Sourceではこの問題は起きません。

tDiaryのsearch-yahoo.rbでは入力XMLのエンコーディングがASCII-8BITでXML宣言のエンコーディングがUTF-8になっていたため問題に遭遇しました。

search-yahoo.rbではopen-uriを使って入力XMLをHTTP経由で取得しています。open-uriはContent-Typeを見て適切なエンコーディングを設定してくれますが、今回はcharsetが指定されていなかったとのことです。このため、open-uriで取得した入力XMLがASCII-8BITになっていました。

1
2
3
xml = open("http://.../xxx.xml") {|f| f.read}
xml.encoding # => ASCII-8BIT
document = REXML::Document.new(xml) # => パースエラー

解決法

この問題に遭遇してしまった場合は、以下のような解決法があります。

  • 入力XMLのエンコーディングをUTF-8に設定する。
  • REXML::IOSourceの代わりにREXML::Sourceを使う。
  • パッチ付きでバグ報告済みなので修正されるのを待つ。

入力XMLのエンコーディングをUTF-8に設定する場合は以下のようになります。

1
2
3
xml = open("http://.../xxx.xml") {|f| f.read}
xml.force_encoding("utf-8")
document = REXML::Document.new(xml)

REXML::Sourceを使う場合は以下のようになります。

1
2
xml = open("http://.../xxx.xml") {|f| f.read}
document = REXML::Document.new(REXML::Source.new(xml))

修正されるのを待つ場合は、修正されるまで待ってください。

まとめ

Ruby 1.9で正規表現ベースのコードがうまく動かない場合はマッチ対象の文字列のエンコーディングを確認しましょう。

ちなみに、REXML::IOSource#matchではエンコーディング関係のエラーを握りつぶしているため、実際に発生するREXML::ParseExceptionだけ見てもエンコーディングミスマッチがどこで起こっているかはわかりません。問題が発生したときは問題解決につながるエラーメッセージを提供したいものですね。

タグ: Ruby
2009-05-11

Ruby/groonga 0.0.1リリース

データベース機能も備える全文検索エンジンgroongaをRubyから利用するための拡張ライブラリRuby/groongaがリリースされました。

Ruby/groongaはRubyGemsに対応しているので、以下のようにコマンド一発でインストールできます。(事前にmakeやgccやRubyのヘッダファイルなど拡張ライブラリのビルドに必要なソフトウェアを揃えておいてください。)

% sudo gem install groonga

Ruby/groongaを利用するためには最新のgroonga 0.0.4が必要ですが、もし、システムにインストールされていない場合は自動的にダウンロードし、groongaのRubyGemsディレクトリの中にインストールします。この場合、最適化オプション(gccの-O2オプション)付きでビルドされますが、最適化オプション付きでgroongaをビルドすると、とても時間がかかります。(30分とか。)慌てずにのんびり待ってください。*1

サンプル

Ruby/groongaでは、よりRubyらしい読み書きしやすいAPIでgroongaを利用できるようにすることを目的としています。例えば、groongaのテーブルはRubyのHashのように扱うことができます。また、テーブルやカラムの型に応じてRubyとgroonga上のデータを適切に変換することにより、特別なことを意識せずにgroongaのデータベース機能を使うことができます。

Cで書かれたインデックスを自動更新するプログラムをRubyで書くとこうなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

require 'rubygems'
require 'groonga'

# 初期化
Groonga::Context.default_options = {:encoding => :utf8}
Groonga::Database.create

# テーブル定義
## <bookmarks>テーブルとそのカラムたちを定義
bookmarks = Groonga::Array.create(:name => "<bookmarks>")
bookmarks.define_column("uri", "<shorttext>")
bookmarks.define_column("comment", "<shorttext>")

## <lexicon>テーブルとそのカラムたちを定義
lexicon = Groonga::Hash.create(:name => "<lexicon>",
                               :key_type => "<shorttext>")
## MeCabで検索用単語を切り出す
lexicon.default_tokenizer = "<token:mecab>"
comment_index_column = lexicon.define_column("comment-index", bookmarks,
                                             :type => "index")

# ポイント: インデックス自動更新の設定
comment_index_column.source = "<bookmarks>.comment"

# ブックマークの登録: 3件
def add_bookmark(bookmarks, uri, comment)
  bookmark = bookmarks.add
  bookmark["uri"] = uri
  bookmark["comment"] = comment
end

add_bookmark(bookmarks,
             "http://groonga.org/",
             "an open-source fulltext search engine and column store")
add_bookmark(bookmarks,
             "http://qwik.jp/senna/",
             "an embeddable fulltext search engine")
add_bookmark(bookmarks,
             "http://cutter.sourceforge.net/",
             "a unit testing framework for C")


# 検索: 2回
def search(comment_index_column, word)
  result = comment_index_column.search(word)
  puts("search result: <#{word}>: #{result.size}")
  puts("uri\t\t\t | comment")
  result.each do |record|
    bookmark = record.key
    puts("#{bookmark['uri']}\t | #{bookmark['comment']}")
  end
  puts
end

search(comment_index_column, "search")
# 結果: <search>で検索したら2件ヒット
# search result: <search>: 2
# uri                         | comment
# http://groonga.org/         | an open-source fulltext search engine and column store
# http://qwik.jp/senna/         | an embeddable fulltext search engine

search(comment_index_column, "testing")
# 結果: <testing>で検索したら1件ヒット
# search result: <testing>: 1
# uri                         | comment
# http://cutter.sourceforge.net/         | a unit testing framework for C

注意事項

Ruby/groongaには以下のような既知の問題があります。

  • RubyのGCでgroongaのオブジェクトを開放しすぎてしまう
  • それほど高速化のためのチューニングをしていない
  • リファレンスマニュアル・チュートリアルが半分位しか用意されていない
  • APIがまだ流動的

GCの問題は、groongaのgrn_ctxというメモリ管理機能を提供するオブジェクトとRubyがオブジェクトをどの順番でGCするかはわからないという動作のために起きています。これはgroonga本体とも協調しながら解決する予定です。ちなみに、通常のアプリケーションでこの問題が発生する可能性があるのはプロセスの終了時だけです。そのため、この問題のためにデータが壊れてしまうということはないと考えられます。

今回のリリースではよりRubyらしいAPIの提供の優先度を高くしたため、高速化のためのチューニングはそれほど行われていません。いくつかチューニング案があるので、それらを適用することにより、Rubyの読み書きしやすいAPIを利用しながらより高速な全文検索機能とデータベース機能を利用できるようになるでしょう。

ドキュメントが完備されていないのは、APIがまだ流動的なことやgroongaのすべての機能を網羅していないこととも関係があります。groonga本体もまだAPIが改良され続けています。それに追従したり、より使いやすいAPIを目指してRuby/groongaのAPIはこれから変更されるでしょう。その過程でドキュメントも充実していく予定です。

次のステップ

ラングバプロジェクトではRuby/groongaの開発に参加してくれる人を募集しています。興味のある方は開発者向け情報をご覧ください。

クリアコードではRubyの拡張機能を書けるプログラミングが好きな開発者を募集しています。興味のある方は採用情報をご覧ください。

*1  -O0オプション(非最適化オプション)をつけるとすぐにビルドできます。

つづき: 2009-12-21
タグ: Ruby
2009-04-30

ActiveLdap: ldap_mapping

これまで、ActiveLdapにはまとまった日本語の情報がありませんでしたが、id:tashenさんがActiveLdapのチュートリアルを翻訳してくれています。原文に最新の状況に追従していない部分があるため、いくつか古い情報もあるのですが、現在、最新の状況に追従するように作業が進んでいます。(最初の方はわりと最新の状況に追従しています。)

せっかくなので、ldap_mappingのあたりを簡単に説明します。できるなら、この内容を本家のドキュメントにうまくマージしたいと思っています。

ldap_mapping

ActiveRecordでは何もしなくてもカラムへアクセスすることができるのですが、ActiveLdapではldap_mappingでLDAPとRubyのオブジェクトを対応させる必要があります。ActiveRecordでは1つのテーブルが1つのクラスに対応し、各レコードがインスタンスに対応しますが、LDAPではそのような対応関係を自動的に判断することが難しいためです。どのようなエントリの集合を1つのクラスに対応させるかはアプリケーション毎に異なります。そのため、ActiveLdapでは明示的にユーザに指定してもらう方法をとっています。適切なデフォルト値がない場合はデフォルト値を提供しない方が混乱しないと思います。

ldap_mappingでは以下の3つの情報を使ってクラスに対応するエントリの集合を決めます。

  • objectClass
  • 検索対象のツリー
  • 検索範囲

objectClass

オブジェクトクラスposixGroupに属しているエントリを対象とする場合は以下のようになります。

1
2
3
class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"], ...
end

これで、posixGroupに属するLDAPエントリそれぞれがGroupクラスのインスタンスに対応することになります。

もし、すべてのLDAPエントリを扱いたい場合はオブジェクトクラスtopを指定します。

1
2
3
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のエントリの集合は検索対象のツリーでも絞り込むことができます。以下のように、それぞれのツリーで扱いが異なる場合に役立ちます。

1
2
3
4
5
6
7
8
9
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の世界でも利用するということです。

このようにクラスをわけることによって、メソッド内で管理者グループかどうかで処理を振り分けなくてもよくなります。

1
2
3
4
5
6
7
8
9
10
11
class Group < ActiveLdap::Base
  def users
    # 一般ユーザの配列を返す
  end
end

class AdministratorGroup < ActiveLdap::Base
  def users
    # 管理者ユーザの配列を返す
  end
end

例としてusersメソッドを出しましたが、実は、ActiveLdapにはActiveRecordのhas_manyのように、LDAPエントリ間の関連をRubyで簡単に扱えるようにする機能があり、usersのようなメソッドは自分で定義する必要はありません。別の機会にでも紹介したいと思います。

検索範囲

検索対象のツリーのうち、どのツリーを検索範囲にするのかを指定できます。

1
2
3
4
5
class Group < ActiveLdap::Base
  ldap_mapping :classes => ["posixGroup"],
               :prefix => "ou=Groups",
               :scope => :sub, ...
end

この例では、検索ツリー以下にあるサブツリー全体からエントリを検索します。もし、サブツリーの直下にだけ必要なエントリがある場合は:oneを指定することにより、よけいな検索を避けることができます。アプリケーションにあわせて適切な検索範囲を指定して下さい。

まとめ

ActiveLdapでのLDAPのエントリとRubyのオブジェクトを関連付けるためのメソッドldap_mappingについて簡単に紹介しました。ここでは以下の3つについてだけ触れましたが、他にdn_attributeという重要なオプションがあります。

  • objectClass
  • 検索対象のツリー: LDAPでのbase
  • 検索範囲: LDAPでのscope

より詳しくはActiveLdapの日本語チュートリアルを見てください。

タグ: Ruby
2009-04-14

問題解決につながるエラーメッセージ

プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。

ここでは、Rubyでの例をまじえながら問題解決に有用なエラーメッセージを紹介します。ライブラリなど多くの人が使うようなプログラムを作成する場合は参考になるかもしれません。

問題解決への道

問題に遭遇してから問題を解決するまでには以下の順で作業をする必要があります。

  1. 問題の把握
  2. 問題の原因の調査
  3. 原因の解決方法の検討
  4. 解決方法の実装

役立つエラーメッセージがあると「1. 問題の把握」、「2. 問題の原因の調査」、「3. 原因の解決方法の検討」がはかどります。

問題の値を示す

エラーが発生すれば問題が起こっている事実は把握できます。次にすることは、どのような問題が起こっているかを調査することです。

String#gsubにはいくつかの使い方がありますが、その1つは以下のように正規表現と文字列を引数にする使い方です。

1
2
>> "abcde".gsub(/c/, "C")
=> "abCde"

もちろん、違うオブジェクトを渡すとエラーが発生します。

1
2
3
4
>> "abcde".gsub([:first], [:second])
TypeError: can't convert Array into String
        from (irb):2:in `gsub'
        from (irb):2

配列を文字列に変換できなかったといっています。しかし、ここでは引数に配列を2つ指定しています。このエラーメッセージでは「配列を文字列に変換できなかった」ことはわかりますが、「どの配列を文字列に変換できなかった」かはわかりません。

正規表現のリテラルでも、正規表現の構文が間違っている場合はエラーが発生します。

1
2
3
4
5
>> 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オブジェクトにします。

1
2
3
4
>> require 'time'
=> true
>> Time.iso8601("2009-04-10T12:02:54+09:00")
=> Fri Apr 10 03:02:54 UTC 2009

不正なフォーマットの場合はエラーが発生します。

1
2
3
4
>> 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つも引数を与えない場合はエラーが発生します。

1
2
3
4
5
6
>> "abcde".delete("a")
=> "bcde"
>> "abcde".delete
ArgumentError: wrong number of arguments
        from (irb):2:in `delete'
        from (irb):2

エラーメッセージを見ると「引数の数が違う」ということがわかります。これで「問題の原因」を把握することができます。

しかし、「問題の原因」はわかってもどうすればその問題を解決できるかはわかりません。引数の数を変えればよいということはわかりますが、いくつにすればよいかがわからないのです。

期待している値を示すと、問題を解決しやすくなります。

1
2
3
4
>> "abcde".gsub
ArgumentError: wrong number of arguments (0 for 2)
        from (irb):3:in `gsub'
        from (irb):3

このエラーメッセージからはString#gsubが2つの引数を期待していることがわかるので、解決案として「引数を2つ渡す」というアイディアが浮かびます。次にすることは「引数に何を2つ渡すか」を考えることです。

エラーメッセージに「期待していること」を含めると、解決案が浮かびやすくなります。できるだけ、期待していることも含めるようにしましょう。

まとめ

Rubyを例にして問題解決に役立つエラーメッセージについて紹介しました。

問題解決に役立つエラーメッセージの特長は、テストの実行結果にもあてはまります。クリアコードが開発に関わっているテスティングフレームワークではテストの実行結果にこだわっています。

  • Cutter: C言語用テスティングフレームワーク
  • UxU: Firefoxアドオン開発用テスティングフレームワーク

あなたが使っているテスティングフレームワークは問題解決に役立つような情報を提供していますか?

タグ: Ruby | UxU | Cutter
2009-04-10

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

すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したプログラムが入ったSubversionリポジトリリポジトリの更新状況のRSS)の公開を始めました。

クリアコードでは既存のフリーソフトウェアの開発に参加したり、新しくmilter managerなどのフリーソフトウェアを開発したりしていますが、それらの開発成果の公開場所はケースバイケースとなっています。

  • 既存のフリーソフトウェアの開発に参加する場合は、基本的に、開発成果はアップストリームに還元しています。
  • 新しくフリーソフトウェアを開発する場合は、基本的には関連コミュニティで標準的なホスティングサイトを利用しています。例えば、Ruby関連のソフトウェアであればRubyForge、GNOME関連のソフトウェアであればgnome.orgといった具合です。
  • 関連するソフトウェアがGitHubGoogle Codeを利用している場合は、それらのサイトを利用することもあります。
  • 特に標準的なホスティングサイトが無い場合はSourceForgeを利用しています。

このように、クリアコードの開発成果のソースコードは様々なホスティングサイトのリポジトリにて管理、および公開されています。

まもなくクリアコードは設立から3年が経とうとしていますが、その間、プロジェクトを作るまでもないような小規模なソースコードがいくつかたまってきました。この度、そのようなソースコードをSubversionリポジトリで公開することにしました。

このリポジトリには現在、ページの一部を折りたたむfolding.jsや、このククログを生成するためのtDiary関連のスクリプト(日記のデータをSubversionで管理するIOバックエンド日記を静的なHTMLに変換するスクリプトなどの記事で述べた物)、Thunderbird用の各種アドオンのソースコードが入っています。誰でも自由にチェックアウトできますので、注意事項をご了承の上でどうぞご利用ください。

以下、現在入っているプログラムを簡単に紹介します。

注意事項

  • これらのプログラムはすべて無保証です。
  • プログラムは予告なく追加・削除・変更されることがあります。

JavaScript関連

tDiary関連

ククログだけではなく、milter managerのブログでも使っています。使い方はmilter managerのtdiary.confが参考になると思います。

  • /tdiary/subversionio.rb: tDiaryのSubversionバックエンド
  • /tdiary/gitio.rb: ↑のSubverionバックエンドのgitバージョンです。
  • /tdiary/html-archiver.rb: tDiaryのデータをHTML化するで紹介した物ですが、その後、カテゴリに対応するなどいくつかの点で改良されています。
  • /tdiary/patches/customizable-style-path.rb: スタイルファイルのパスをカスタマイズできるようにします。RDスタイルなど、標準では有効になっていないスタイルを使うときに便利です。本家に提案したのですが、rejectされたのでここに入っています。
  • /tdiary/plugin/classed-category-list.rb: class付きでカテゴリリストを生成します。ククログのトップに並んでいる「タグ: ...」の部分です。
  • /tdiary/plugin/date-to-tag.rb: 日付を本文の下に生成します。今思えば名前が悪いですね。後で変えるかもしれません。
  • /tdiary/plugin/link-subtitle.rb: サブタイトルをその記事のリンクにします。通常は日付がリンクになるのですが、このククログでは日付は本文の下に置いてあるので、代わりにサブタイトルをリンクにしています。
  • /tdiary/plugin/multi-icon.rb: ページアイコンとして、favicon.ico(ICO形式の画像)とfavicon.png(PNG形式の画像)を両方指定できるようにします。
  • /tdiary/plugin/title-navi-label.rb: 「前の日記」「次の日記」リンクのラベルをリンク先の日記のタイトルにします。
  • /tdiary/plugin/zz-permalink-without-section-id.rb: section_footerプラグインが生成するpermalinkから「#pXX」を削除します。ククログでは、1記事を1セクションにして同じ日には複数の記事を書かないという方針で運営しており、セクションIDが必要ないため、このプラグインを作成しました。ファイル名の「zz-」は、このプラグインの読み込み順序を最後の方にさせるためのものです。

Thunderbird関連

Thunderbird Add-ons - クリアコードで公開している、Thunderbirdのバグを回避するパッチや挙動の変更を行う拡張機能です。公開ページにも書いてありますが、これらの拡張機能は無保証です。業務上の必要性からの導入をお考えの場合は、Mozilla Firefox & Mozilla Thunderbird保守・サポートサービスのご利用もご検討ください。

おまけ

リポジトリの更新状況を配信しているRSSは、Subversionに標準添付のcommit-email.rbで生成しています。今回、RSSのタイトルや説明を日本語にしたかったので、Subversionのtrunkに--rss-titleと--rss-descriptionオプションを追加しました。また、--repository-uriで指定されたリポジトリのURIをコミットメールのX-SVN-Repositoryヘッダに設定するようにもしています。

Subversionリポジトリの整形表示にはRepos Styleを利用しています。mod_dav_svnにはSVNIndexXSLTというオプションがあって、それを利用しています。

タグ: Ruby | JavaScript | Mozilla
2009-04-06

2009年3月の肉の会

クリアコードでは毎月29日頃に肉の会と呼ばれる社内食事会を行っています。 肉の会というだけあって肉を食べることが多いのですが、今回は肉ではなく、社内でたこやきを食べまくりました。

今回はゲストとしてITproの高橋さんに参加してもらえました。(忙しい中、ありがとうございます!)

高橋さんがおみやげとして発売したばかりの「Ruby技術者認定試験 公式ガイド」を持ってきてくれました。「クリアコードさんへ」とサインももらいました。ありがとうございます!

高橋さんのサイン

この公式ガイドには模擬試験が2回分ついています。Rubyには自信があったのでやってみたところ、90%くらいしか正解できませんでした。普段使わないメソッドや引数が問題になっていると間違えてしまいます。

ひっかけっぽい問題も何題かあり、ひっかけようとしているなぁとニヤニヤしながら解いていたのですが、何題か間違えてしまいました。油断してはいけません。

この模擬試験をやってみて、Hash#invertをはじめて知りました。もうすでにかなりRubyを知っている人も、Ruby技術者認定試験の問題を解いてみると知らないことが見つかるかもしれません。問題を見て、ひっかけっぽいな、とニヤニヤしながら解くのも楽しいと思います。

模擬試験の前にはコンパクトにまとめられたRubyの解説がついているので、まだRubyに詳しくない人のとっかかりにもよさそうな気がします。mapとcollectをきちんと対等に扱っているので、変に偏らなそうなのもよいと思います。*1 Rubyで開発されたデスクトップ・アプリケーションとしてRabbitが挙がっていることも、とてもよいと思います。

Ruby技術者認定試験 公式ガイド (ITpro BOOKs)
伊藤忠テクノソリューションズ/ITpro/Rubyアソシエーション
日経BP社
¥ 2,100

*1  私はcollectに偏っています。

タグ: Ruby
2009-04-02

Rails 2.3.2でActiveLdapを使う

2009/3/24時点でのActiveLdapの最新リリースは1.0.2ですが、ActiveLdap 1.0.2はRails 2.3.2には対応していません。これは、ActiveLdap 1.0.2の方がRails 2.3.2より早くリリースされたからです。基本的に、ActiveLdapはリリース時点での最新のRailsに対応していますが、未来のRailsには対応できていません。

Rails 2.3.2とRuby 1.9.1とActiveLdap

さて、そんなActiveLdapですが、trunkではRails 2.3.2に対応しています。(Issue 18 - ruby-activeldap - [Rails 2.3 Support] :: Running WEBrick Hangs - Google Code

また、Rails 2.3.2だけではなく、Ruby 1.9.1にも対応しています。(Issue 20 - [Ruby 1.9 Support] :: Running Tests - Google Code) 対応の過程でAlexey.Chebotarさんが、Ruby/LDAPをRuby 1.9.1に対応しました。

Ruby 1.9.1対応Ruby/LDAP

Ruby/LDAPはOpenLDAPのRubyバインディングで最終リリースが2006/8/9です。Alexeyさん以外にもRuby 1.9.1対応した成果をフィードバックしている人がいるのですが、まだ対応してもらえていないようです。

Alexeyさんもメンテナの方に連絡をとってみたということですが、まだ対応してもらえていないようです。パッチの形で転がっているよりも、パッチが取り込まれた形の方が利用しやすいということと、AlexeyさんがRuby/LDAPをメンテナンスする意志があるということだったので、ActiveLdapのリポジトリにRuby 1.9.1対応版のRuby/LDAPが入っています。(リポジトリ)Ruby/LDAPをRuby 1.9.1で使いたいという方はこれを使ってみるとよいと思います。

Rails 2.3.2とActiveLdapのtrunk

少し脱線しましたが、ActiveLdapのtrunkをRailsアプリケーションで使う方法がActiveLdapプロジェクトのWikiにあります。(UsingTrunkWithRailsJa) Rails 2.3.2でActiveLdapを使いたい場合はこの方法を参考にしてください。

当初は次のリリースは1.1.0と考えていたのですが、1.1.0の前に、Rails 2.3.2に対応している現時点のものを1.0.3をリリースすることも考えています。いくつか予定されていた項目がまだ実現されていないので1.1.0を出すことはできないのですが、その前にRails 2.3.2対応版を出すことには価値があるかもしれないと思うようになってきました。*1

1.1.0に予定されていた項目の1つにドキュメントの整備というものがあります。この項目もまだ実現できていない項目の1つなのですが、現在、id:tashenさんがチュートリアルの日本語訳を行ってくれています。(ありがとうございます!)

ActiveLdapは活発に開発が行われているライブラリなので、RubyでLDAPを便利に操作したい場合はActiveLdapを使うことを検討してみてください。そして、より使いやすく有用なライブラリにするために、バグ修正やドキュメント作成などでプロジェクトに協力してくれる人を募集しています。お待ちしています。

*1  Rails 2.3.2に対応していないというIssueが2つ登録されたりしたので。

タグ: Ruby
2009-03-24

Railsで画像アップロード

Railsで画像をアップロードするときはどうやっているんでしょうか。

Fleximageというプラグインがあります。よいAPIだと思うのですが、あまり使っている人がいないようなので紹介します。

ここでは1からサンプルRailsアプリケーションを作成しながらFleximageの使い方を紹介します。順番にコマンドを実行・コードを変更していくと動くように書いてあります。

下準備

まず、sampleというRailsアプリケーションを作成します。

% cd /tmp
% rails sample
% cd sample

次に、Fleximageをインストールし、画像用のテーブルを作成します。

% script/plugin install git://github.com/Squeegy/fleximage.git
% script/generate scaffold photo title:string image_filename:string image_width:integer image_height:integer
% rake db:migrate

以下のカラムは特別なカラムです。Fleximageは以下のカラムが定義されていると、そこに値を設定してくれます。

image_filename
アップロードした画像のファイル名
image_width
画像の幅
image_height
画像の高さ

これらはすべて省略可能です。今回は、せっかくなので、すべて定義しました。

Fleximageを使うため、モデルを以下のように変更します。

app/models/photo.rb:

class Photo < ActiveRecord::Base
  acts_as_fleximage :image_directory => 'public/images/uploaded'
end

これで、アップロードされた画像は"#{RAILS_ROOT}/public/images/uploaded"以下に保存されます。

ここからは、コードを変更し、アプリケーションを動かし、動作を確認しながら進めていきます。そのため、ここでサーバを起動しておきます。

% script/server

アップロード

http://localhost:3000/photos/にアクセスすると以下のような見慣れたscaffoldの画面になるので、「New Photo」リンクから画像アップロードフォームへ進みます。

indexページ

scaffoldのフォームでは画像をアップロードできないので、app/views/photos/new.htmlを以下のように変更します。

app/views/photos/new.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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:

1
2
3
<p>
  <%= image_tag(photo_path(@photo, :format => "png")) %>
</p>

PNGフォーマットを指定しているので、コントローラ側でPNGフォーマットを受けつけるようにします。

app/controllers/photos_controller.rb:

1
2
3
4
5
6
7
8
9
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:

1
2
3
# -*- 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:

1
map.resources :photos, :member => {:thumbnail => :get}

これで、thumbnail_photo_pathが使えるようになるので、index.html.erbを書き換えます。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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:

1
2
3
4
5
6
7
def thumbnail
  @photo = Photo.find(params[:id])

  respond_to do |format|
    format.gif
  end
end

サムネイルのGIF画像用のビューは以下のようになります。とてもすっきり書けています。

app/views/photos/thumbnail.gif.flexi:

1
2
3
4
# -*- ruby -*-
@photo.operate do |image|
  image.resize('80x60')
end

変更した一覧ページを表示するとこうなります。

サムネイル付き画像一覧ページ

アップロードした画像がサムネイルとして表示されています。

まとめ

すっきりしたAPIのRails用画像アップロードプラグインFleximageを紹介しました。

ここでは触れませんでしたが、Fleximageは機能が豊富でカスタマイズ性にも優れています。例えば、アップロードされた画像を表示するときに影をつけたり、文字を入れたりといろいろ加工することができます。また、ファイルではなく、データベースに画像を保存することもできます。

Fleximageは日本語の情報がほとんどないのが不思議ですね。あまり使われていないのかもしれません。

おまけ

ImageMagickを使って画像に影をつけるシェルスクリプトです。これを使うと、このページのスクリーンショットについているような影をつけることができます。

add-shadow.sh:

1
2
3
4
5
6
7
8
9
#!/bin/sh

if [ $# != 2 ]; then
    echo "Usage: $0 INPUT SHADOWED_OUTPUT"
    exit 1
fi

convert $1  \( +clone -background black -shadow 80x6 \) \
    +swap -background none -layers merge +repage $2

このように使います。

% ./add-shadow.sh image.png shadowed-image.png
つづき: 2009-12-21
タグ: Ruby
2009-03-23

仙台Ruby会議01

先日、仙台Ruby会議01で1コマ話してきました。

まず好きなこと、そしてそれを続けること

東北にいたころにお世話になっている人たちも観にきてくれて、たくさんの方に聞いてもらえました。ありがとうございます。

何人か直接感想を伝えてくれた方もいたので、少しでも言いたかったことが伝わったような気がしています。仙台で話せてよかったです。ただ、いくつか言い損ねたこともありました。

使うこと

前半では、作る側である前に使う側という立場であったから、作る側の考え方が養われたということに触れたのですが、後半の「今日からできること」ではそれに触れませんでした。使うことでよりソフトウェアに愛着がもてるかもしれないので、使うということも大事なことだと思います。

今なら、Ruby 1.9.1を使うというのもエキサイティングでよいかと思います。使うことでバグを踏んで、直す機会に巡りあえるかもしれません。

手を動かしている人は私だけではない

自分の例を使って、東北にいてもプログラミングを続けることはできるし、認めてもらえることもあるという話をしました。そして、プログラミングして直すということがどちらにも有効ではないかと続くのですが、そこで、とてもよい実例をあげそこねてしまいました。

ちょうど1つ前に話していた藤岡さんがまさにそのよい例だと思います。藤岡さんはcgi.rbを直すことで認められ、Rubyのコミッタになっています。

当時はまだ東北にいましたが今は東北にいない自分の例だけよりも、今も東北にいて活躍されている藤岡さんに触れなかったことが悔やまれます。より身近にいる藤岡さんのことに結びつけて聞いてもらえていたら、もっと実感して聞いてもらえたかもしれません。

最近は地方でもすごい

都会に行かないと話せる人がいないという少し昔の話をしましたが、今はその状況は大きく変わっていると思います。それは、仙台Ruby会議01に参加した人たちならわかるはずです。前述の藤岡さんもいますし、仙台Ruby会議01を先頭に立って引っ張ってくれた片平さんもいます。

これは、はっきりと言うべきでした。

感謝

今回、仙台Ruby会議01で話すことができたのは、角谷さんと片平さんのおかげでした。仙台Ruby会議01の話者がすべて決まった後なのに、私も入れてくれました。入ってきた私のために、1コマ増やせるように動いてくれ、実際に1コマ増やしてもらえました。本当にありがとうございました。

東北にいるたくさんのすばらしいRubyistを知ることができた仙台Ruby会議01は、とてもすばらしいRuby会議だったと思います。

Ruby会議2009は、私個人だけではなく、スポンサーとしてクリアコードでも参加したいと思います。

タグ: Ruby
2009-01-26

tDiaryのデータをHTML化する

tDiaryをローカルなネットワークに配置して、tDiaryが表示する内容を静的なHTMLとして公開したい場合はよくありますよね。ククログもそんなよくある使い方の1つです。

tDiaryには静的なHTMLを生成するためのsqueezeプラグインがありますが、squeezeプラグインが出力するHTMLは以下の点でCGIで表示される内容と異なります。

  • 各日付のページしか生成しない
    • 最新の日記n件ページや月別ページやカテゴリページは生成しない
  • リンクがCGI用のリンクのままで、次の日記のページに移動するリンクが壊れている
  • テーマファイルや画像はコピーしてくれないので、生成したHTMLの入ったディレクトリ以下だけでは完結しない

ただし、これはsqueezeプラグインが検索エンジンへの入力データとしてのHTML生成を目的としているためです。よくある使い方では、生成されたHTMLはCGIで出力されているように表示できることが目的なので、上記のようなミスマッチが発生します。

そこで、ククログではhtml-archiver.rbという静的なHTMLを生成するスクリプトを使っています。html-archiver.rbは最後の方に載せています。

html-archiver.rbの使い方

html-archiver.rbを使うと、CGIで出力されている内容と同じように表示されるHTMLが生成されます。生成例は今見ているこのページです。

使い方はこうなります。

% ruby html-archiver.rb --tdiary tdiayr.rbのあるディレクトリ --conf tdiary.confのあるディレクトリ 出力先ディレクトリ

例えば、以下のような場合を考えます。

  • tdiary.rbは~tdiary/work/ruby/tdiary/core/にある
  • tdiary.confは~tdiary/public_html/にある
  • HTMLは~tdiary/public_html/html/以下に出力する

この場合はこのようなコマンドになります。

% ruby html-archiver.rb --tdiary ~tdiary/work/ruby/tdiary/core/ --conf ~tdiary/public_html/ ~tdiary/public_html/html/

機能

  • 日付ページの生成:
  • 最新n件ページの生成:
  • 月別ページの生成:
  • RSS 1.0の生成:
  • テーマファイルのコピー
  • 画像のコピー

制限

  • ツッコミが生成されるかどうかは試していない
  • カテゴリ一覧ページがきちんと生成されるかは(最近は)試していない
  • タブインデント(tDiary本体のコーディングスタイルに合わせているため)
  • 思ったほど使う場面が少ないかもしれない(もしかしたら、tDiaryが表示する内容を静的なHTMLとして公開することがそんなにないかもしれない)

ライセンス

GPL3あるいは3以降の新しいバージョンのGPL

html-archiver.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
#!/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
タグ: Ruby
2008-12-05

tDiaryのSubversionバックエンド

みなさんはtDiaryの日記データをどのようにバックアップしているのでしょうか。cronでアーカイブしていたり、dbi_ioでデータベースに保存して、データベースの内容をアーカイブしていたりしているのでしょうか。

開発者の人なら日記のデータもソースコードと同じようにバックアップしたいですよね。つまり、バージョン管理をして、レポジトリの内容をアーカイブするバックアップです。そんな人はこのようなSubversionIOはいかがでしょうか。保存する毎にSubversionリポジトリに日記データをコミットします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
require 'tdiary/defaultio'

module TDiary
        class SubversionIO < DefaultIO
                def transaction( date, &block )
                        dirty = TDiaryBase::DIRTY_NONE
                        result = super( date ) do |diaries|
                                dirty = block.call( diaries )
                                diaries = diaries.reject {|_, diary| /\A\s*\Z/ =~ diary.to_src}
                                dirty
                        end
                        unless (dirty & TDiaryBase::DIRTY_DIARY).zero?
                                run( "svn", "add", File.dirname( @dfile ) )
                                run( "svn", "add", @dfile )
                                Dir.chdir( @data_path ) do
                                        run( "svn", "ci", "-m", "update #{date.strftime('%Y-%m-%d')}" )
                                end
                        end
                        result
                end

                private
                def run( *command )
                        command = command.collect {|arg| escape_arg( arg )}.join(' ')
                        result = `#{command} 2>&1`
                        unless $?.success?
                                raise "Failed to run #{command}: #{result}"
                        end
                        result
                end

                def escape_arg( arg )
                        "'#{arg.gsub( /'/, '\\\'' )}'"
                end
        end
end

# Local Variables:
# ruby-indent-level: 3
# tab-width: 3
# indent-tabs-mode: t
# End:

使い方は以下の2ステップです。

  1. tdiary.confで@io_classに指定する
  2. 日記のデータディレクトリをSubversionのワーキングコピーにする

tdiary.confの設定

まず、上記のソースコードをsubversionio.rbとしてどこかに保存してください。ここでは/home/tdiary/lib/以下に保存したとします。

次にtdiary.confに以下の内容を追記します。

1
2
3
subversion_io_dir = "/home/tdiary/lib" # <- 保存した場所にあわせて変更
require "#{subversion_io_dir}/subversionio"
@io_class = TDiary::SubversionIO

データディレクトリの設定

データディレクトリ(tdiary.conf内の@data_pathで指定したディレクトリ)は/home/tdiary/data/として進めます。また、作業しているユーザはtDiaryのCGIを動かすユーザとします。(ここではtdiaryユーザ)

まず、レポジトリに日記データ用のパスを作ります。既存のSubversionリポジトリを利用する場合は、例えばこのようになります。

[tdiary]% svn mkdir -m 'create tDiary data path' https://.../repos/tdiary-data

ローカルに新しくSubversionのリポジトリを作成して、そこに日記データをコミットするようにする場合はこのようになります。新しく作成するリポジトリは/home/tdiary/repos/に作ることにします。

[tdiary]% svnadmin create /home/tdiary/repos

Subversionリポジトリから日記データ保存用のパスを、tDiaryのデータディレクトリにチェックアウトします。今すでにあるtDiaryのデータディレクトリはどこかによけておきます。

[tdiary]% mv /home/tdiary/data /home/tdiary/data.bak
[tdiary]% svn co file:///home/tdiary/repos /home/tdiary/data

既存のデータをワーキングコピーに移動し、日記データだけをレポジトリにコミットします。

[tdiary]% cd /home/tdiary/data
[tdiary]% cp -rp ../data.bak/* ./
[tdiary]% svn add 200*
[tdiary]% svn add 199* # <- もし2000年より前のデータがあるなら
[tdiary]% svn ci -m 'import'

完了

以上で設定は完了です。日記を保存するとリポジトリにコミットされます。

制限

日記本文しか対応していません。ツッコミや画像には対応していません。

svnコマンドをインストールしている必要があります。

tDiary本体のコーディングスタイルに合わせているためタブインデントになっています。

ライセンス

AGPL3あるいは3以降の新しいバージョンのAGPL

つづき: 2009-04-06
タグ: Ruby
2008-11-13

Test::Unit 2.0.1リリース

Test::Unit 2.0.1が RubyForge上でリリースされました。RubyGemsも提供されているの で以下のようにインストールできます。

% sudo gem install test-unit

経緯

Test::UnitはRuby 1.8.xに標準添付されている単体テストフレーム ワークです。しかし、Ruby 1.9.1からは miniunitが標準 添付され、Test::UnitはRubyForgeで開発が継続されることになりま した。これからもTest::Unitを使うときはRubyGemsでインストール することになるでしょう。

Ruby 1.8.xに標準添付されているTest::Unitは互換性のために、 Test::Unit 1.2.3としてリリースされています。Ruby 1.9.1でも Ruby 1.8.xに標準添付されているTest::Unitと同じTest::Unitを使 用したい場合は以下のようにします。

% sudo gem install test-unit --version '= 1.2.3'

テストファイル内(変更前):

1
2
require 'test/unit'
...

テストファイル内(変更後):

1
2
3
4
require 'rubygems'
gem 'test-unit', '= 1.2.3'
require 'test/unit'
...

余談ですが、Ruby 1.9.1でTest::Unitが標準添付から外れ、 miniunitが標準添付になったのはTest::Unitのソースがメンテナン スしづらくなっていたのが主な理由です。

Test::Unit 2.x

Ruby 1.8.xに標準添付されているTest::Unitは長い間メンテナンス はされていましたが、特に機能拡張などは行われていませんでした。 しかし、その間にもテスト環境を便利にするライブラリが公開され てきました。例えば、RSpecのような BDD用のフレームワークや、 expectations のような軽量の単体テストフレームワーク、 Shoulda/ test/spec/ Mochaのように Test::Unitを拡張するライブラリなどです。Test::Unitは少し時代 遅れになってしまったのです。

最近のテスト用のフレームワークは DSL化の方向に向かっているようにも見えます。 これはRSpecの影響が大きいのでしょう。expectationsやShouldaもテスト用の DSLを提供します。

しかし、Test::UnitはDSLを提供しません。テストを「英語らしく」 ではなく「Rubyプログラムらしく」書きます。好みにもよりますが、 これはTest::Unitのメリットの1つと言えます。

Test::Unitに他のフレームワークやライブラリの機能を、 Test::Unitの「Rubyプログラムらしく」テストを書ける特性を活か したまま追加すれば、Test::Unitはもっと便利で使いやすいテスト フレームワークになるでしょう。Test::Unit 2.x系列はRuby 1.8.x に標準添付されていた頃とは違い、そのような方針の元で活発に開 発されていく系列になります。

例えば、以下のような機能が他のフレームワークやライブラリから 移植されています。

  • 差分表示
  • ネストしたテスト定義
  • 色付け
  • C-cでテスト中断時にもテスト結果を表示
  • 複数のsetup/teardown
  • ...

ここでは「差分表示」と「ネストしたテスト定義」だけ紹介します。 *1

差分表示

RSpecでは比較結果が異なった場合に差分を表示して違いをわかり やすく表示してくれます。

diff_spec.rb:

1
2
3
4
5
6
7
8
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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
5
6
7
8
9
10
require 'rubygems'
gem 'test-unit'
require 'test/unit'

class TestDiff < Test::Unit::TestCase
  def test_diff
    assert_equal(["I", "am", "a", "boy"].join(" "),
                 ["I", "was", "a", "boy"].join(" "))
  end
end

実行結果(差分表示部分のみ):

% ruby test_diff.rb
...
diff:
- I am a boy
?    ^
+ I was a boy
?   + ^
...

Test::Unit 2.0.1では必要なら同じ行のうち、どの列が異なってい るのかも表示します。RSpecでは行単位の差分までで列単位までの 差分は表示しません。

余談ですが、この差分表示形式はPythondifflib ライブラリで使われている形式です。

ネストしたテスト定義

Shouldaではcontextをネストさせることにより、便利にテストを書 くことができます。以下はShouldaのページからの引用です。 *2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UserTest < Test::Unit::TestCase
  context "A User instance" do
    setup do
      @user = User.find(:first)
    end

    should "return its full name" do
      assert_equal 'John Doe', @user.full_name
    end

    context "with a profile" do
      setup do
        @user.profile = Profile.find(:first)
      end

      should "return true when sent #has_profile?" do
        assert @user.has_profile?
      end
    end
  end
end

ネストされた"with a profile"のcontext内では上位の"A User instance"のcontext内のsetupが実行された後に実行されます。つ まり、以下のような実行順序になります。

  • setup: "A User instance" context
    • should: "return its full name"
  • setup: "A User instance" context
    • setup: "with a profile" context
      • should: "return true when sent #has_profile?"

実行されるフィクスチャ(setup)がネストで自然に表現されていま す。

Test::Unit 2.0.1では以下のように書きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UserTest < Test::Unit::TestCase
  def setup
    @user = User.find(:first)
  end

  def test_full_name
    assert_equal('John Doe', @user.full_name)
  end

  class ProfileTest < UserTest
    def setup
      super
      @user.profile = Profile.find(:first)
    end

    def test_profile
      assert_true(@user.has_profile?)
    end
  end
end

これは以下のように実行されます。

  • UserTest#setup
    • UserTest#test_full_name
  • UserTest#setup
    • UserTest::ProfileTest#setup
      • UserTest::ProfileTest#test_profile

実行されるフィクスチャ(setup)がネストとクラス階層で自然に表現 されています。

まとめ

Test::UnitはRuby 1.9.1からは標準添付ではなくなりましたが、 Test::Unit 2.xとして活発に開発が続けられています。既存の他の フレームワークやライブラリのよいところは積極的に導入している ため、Ruby 1.8.xに標準添付されているTest::Unitよりもはるかに 使いやすくなっています。

今回は紹介しませんでしたが、他のフレームワークやライブラリに はないTest::Unit 2.x独自の便利な機能もあります。Test::Unitの 「Rubyプログラムらしい」テストの書き方が好きな場合はこれから もTest::Unitを使ってみてはいかがでしょうか。

*1  「C-cでテスト中断時にもテスト結果を表示」は開発が進んでい き、たくさんのテストがある場合には地味ですがとても便利な機能 なのです。RSpecにも実装されています。

*2  ただし、Test::UnitはTest::Unit::TestCaseに修正してある。

タグ: Ruby | テスト
2008-11-10

Ruby-GetText-Packageとrake gems:installの共存

Ruby-GetText-Packageだけというわけではないですが、app/controllers/application.rbで何かを行う*1gemを使っているとrake gems:installで足りないgemをインストールできません。経験したことがあるけど別に手動でインストールすればいいやということで、おそらく、わりとうやむやにされていることが多い問題ではないでしょうか。

例えば、Ruby-GetText-Packageだと以下のようにapp/controllers/application.rbを変更する必要があります。

1
2
3
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を使わない」という方法があります。 あまりきれいな方法ではありませんが、紹介します。

1
2
3
4
5
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_を定義しておくとよいでしょう。

1
2
3
4
5
6
7
8
9
class ApplicationController < ActionController::Base
  if Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?
    init_gettext "blog"
  else
    class ActiveRecord::Base
      def self.N_(*args); end
    end
  end
end

これでRuby-GetText-Packageを使っているときでもrake gems:installが動くようになります。

% rake gems:install
(in /tmp/blog)
gem install gettext
Bulk updating Gem source index for: http://gems.rubyforge.org/
Successfully installed gettext-1.93.0
1 gem installed
Installing ri documentation for gettext-1.93.0...
Installing RDoc documentation for gettext-1.93.0...

以下、「足りないgemがあるとき」の判断方法について少し書いてみます。

足りないgemがあるとき

Railsでは「足りないgemがあるかどうか」を示すAPIを提供しているのはRails::Initializerです。ただ、残念ながらconfig/environment.rbの中で作ったRails::Initializerはどこにも保存されていないので、無理やり引っ張り出す必要があります。具体的には以下のようなコードになります。

1
2
3
4
5
6
initializer = nil
ObjectSpace.each_object(Rails::Initializer) do |object|
  initializer = object
  break
end
initializer.gems_dependencies_loaded

しかし、この方法ではRails::InitializerがGCされてしまっていると動きません*2

また、ObjectSpaceはできれば使いたくないものです。そのため、もう少し安全で、何をしているのかがまだわかりそうな方法の方がよさそうです。そのための「足りないgemがあるかどうか」を判断する方法が以下のようになるというわけです。

1
Rails.configuration.gems.reject {|gem| gem.loaded?}.empty?

ただ、この方法はRails 2.1.2(や2008-10-30でのmaster)では動きますが、もし、Rails内部の「足りないgemがあるかどうか」を判断する方法が変わった場合は動かなくなる可能性もあります。 「あまりきれいな方法ではありませんが」と書いたのはこのためです。

まとめ

Ruby-GetText-Packageを使っている場合でもrake gem:installを利用する方法を紹介しました。

同じような問題は他のライブラリでも起こりうると思うので、そのような場合も同じように問題を回避できると思います。

*1  もう少しいうと、読み込まれた時に実行される場所(例えばクラス定義の中)で何かを行う場合。メソッド定義の中などその場では実行されないものは関係ない。

*2  多くの場合はそんな状況にはならないでしょう

タグ: Ruby
2008-10-30

Rabbit 0.5.8リリース

Ruby-GNOME2を使って実装されているプレゼンテーションツールRabbit 0.5.8がリリースされました。

0.5.8では部分的にClutterをサポートしています。

Clutter

Clutterは高速で、視覚的にリッチで、アニメーションするGUIを作成するためのライブラリです。Clutterのこれらの特徴はOpenGLをバックエンドに使う事で実現されています。 ClutterはLinux/Mac OS X/Windowsなどマルチプラットフォームで動作します。さらに、組み込み環境でも動作し(OpenGL ESを利用)、デモ動画も公開されています。

ライブラリを使用する視点で見ると、GStreamer/cairo/Pango/GTK+などGNOME関連のライブラリと親和性が高いこともあり、便利で使いやすいAPIになっています。

ClutterにはRuby/Python/Perl/Valaなど各種言語用のバインディングがあります。リッチなインターフェイスを作成したい場合にClutterを利用してみてはどうでしょうか。

タグ: Ruby
2008-10-20

Ruby-GNOME2 0.18.0リリース

0.17.0のリリースから1ヶ月も経っていませんが、Ruby-GNOME2バージョン0.18.0がリリースされました。

Ruby-GNOME2はGTK+を含むGNOME関連ライブラリのRubyバインディング集です。

目玉

このリリースの目玉はメモリリークの修正と、新規バインディングの追加です。

メモリリークはRuby/GLibの中にもあり、Ruby-GNOME2関連ライブラリ全体で影響を受ける可能性が高いものでした。0.17.0を利用している場合は0.18.0に更新することをおすすめします。

新規バインディングとして以下の2つが追加されました。ただし、まだどちらも「実験的」マークがついていて、今後APIが変更される可能性があります。

  • Ruby/GtkSourceView2
  • Ruby/GooCanvas
Ruby/GtkSourceView2

Ruby/GtkSourceView2はソースコードハイライトウィジェットであるGtkSourceView 2.x系列をサポートします。以前のリリースにも含まれているRuby/GtkSourceViewはGtkSourceView 1.x系列をサポートしていて、2.x系列はサポートしていませんでした。

今回、別ライブラリになっているのはAPIに非互換性が発生したためです。それぞれのライブラリはrequireが異なります。Ruby/GtkSourceViewからRuby/GtkSourceView2へ移行する場合は以下のように変更する必要があります。

変更前:

1
require 'gtksourceview'

変更後:

1
require 'gtksourceview2'

GtkSourceView 1.x系列は開発が終了していて、現在は2.x系列が開発されています。今後のことを考えるとRuby/GtkSourceViewからRuby/GtkSourceView2へ移行を検討した方がよいのではないかと思います。

Ruby/GooCanvas

Ruby/GooCanvasは描画にcairoを用いるキャンバスウィジェットであるGooCanvasのバインディングです。

キャンバスウィジェットとは図形や他のウィジェットなどを自由に配置できるウィジェットです。Inkscapeなどのようなグラフィックツールを思い浮かべるとイメージしやすいかもしれません。Inkscapeでは丸や四角などの図形を好きな場所に配置することができます。キャンバスウィジェットを用いることで、そのような機能を持つアプリケーションを簡単に開発することができます。

現在、GTK+にはキャンバスウィジェットが含まれていませんが、将来のGTK+ではGooCanvasがGTK+のキャンバスウィジェットとして取り込まれるのではないかと予想しています。GTK+ではProjectRidley/CanvasOverview - GNOME Live!で検討しているようです。GooCanvasを含むいくつかのキャンバスウィジェットを比較しています。

ちなみに、Ruby-GNOME2にはRuby/GnomeCanvas2というGnomeCanvasのバインディングがあります。ただし、GnomeCanvasは非推奨ライブラリになっています。そのため、Ruby/GnomeCanvas2も将来的にRuby-GNOME2から削除される可能性があります。

これから新しくキャンバスウィジェットを用いたアプリケーションを開発する場合はRuby/GooCanvasも候補のひとつに入れた方がよいかもしれません。ただし、まだ「実験的」な段階なのでAPIが変更される可能性があることに注意する必要があります。

協力のお願い

アナウンスメールにもありますが、Ruby-GNOME2プロジェクトでは協力してくれる方を募集しています。例えば、バインディングを開発してくれる方、ドキュメントを書いてくれる方、英語のドキュメントを日本語化してくれる方、リリース作業をしてくれる方などを募集しています。

興味のある方はruby-gnome2-devel-ja MLまでお願いします。

0.17.0がリリースされてから何人かの方がドキュメント関連作業で協力してくれています。ありがとうございます!

つづき: 2009-07-31
タグ: Ruby
2008-10-01

rcairo 1.8.0リリース

2008/09/26にマルチプラットフォームで動作するベクトルベースの グラフィックライブラリであるcairo 1.8.0がリリースされました。また、同日のうちにcairoを Rubyから利用するためのライブラリ rcairo 1.8.0が リリースされました。([ruby-list45520] [ANN] rcairo 1.8.0 )rcairo 1.8.0はcairo 1.8.0に対応しています。rcairoの基本的 な使い方はるびまの記事「cairo: 2 次元画像描画ライブラリ」 にまとまっ ています。

ちなみに、誰も気づいていないかもしれませんが、cairoの最新API への対応は各種言語バインディングの中ではrcairoが最速です。

cairo 1.8.0

cairo 1.8.0ではテキストの扱いが改良されています。例えば、テキ ストを検索、選択、コピペできるようなPDFを出力するようになって います。また、「ユーザフォント」という機能が導入されています。

「ユーザフォント」はその名の通り、ユーザ(cairoを使う開発者) 独自のフォントを定義・利用できる機能です。利用例として、SVGフォ ントやFlashフォントなど標準化されていないフォーマットのフォン トの実装があげられています。通常は利用することはないと思いま すが、cairoがより広く利用される機会を増やす機能になるかもし れません。

もちろん、rcairoでは「ユーザフォント」もサポートしています。

rcairoの今後

rcairoには、cairoのバインディングだけではなく、rcairo独自の cairoをもっと便利に使うための機能が追加されています。例えば、 Cairo::Color もそのひとつです。

rcairoをもっと便利に使えるようにするためには、レンダリングエ ンジンのような機能が必要だと考えています。例えば、以下のよう にすれば表を描画できるというような機能を考えています。

1
2
3
4
5
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)

現在考えているサポートしたい描画対象は以下の通りです。もし興 味があったらぜひ手伝ってください。

タグ: Ruby
2008-09-27

ActiveScaffoldの地域化

現在、Railsに対応した国際化の仕組みがいくつかあります。しかし、それぞれが 独自の方法で実現しているため、それらを組み合わせて使うと混沌 とした状態に陥ることも少なくありません。

ここでは、モデルから動的にきれいな画面とコントローラ部分を生 成するActiveScaffoldを用 いた場合の国際化(i18n)と地域化(l10n)の実現方法のひとつを 紹介します。この方法では、 ActiveScaffoldLocalizeRuby-GetText-Package を組み合わせます。混沌とする部分はそれなりになじませます。

国際化の仕組み

Railsで使用できる国際化の仕組みの比較はRails Wiki (英語)が詳しいです。

Ruby-GetText-Package には、以下のような地域化対象のメンテナン スのことを考慮した機能があるので、地域化対象メッセージが増加 したり更新される場合には有力な候補になるでしょう。

  • 地域化対象のメッセージを抽出する機能
    • テーブルにカラムを追加した場合、画面に表示するメッセージを追加・更新した場合などに利用
  • 抽出したメッセージを既存の翻訳済みメッセージにマージする機能
    • 翻訳者が新しい地域化対象のメッセージを翻訳する場合に利用
  • wikipedia:gettext用の翻訳支援ツールを利用可能(.po のフォーマットがGNU gettextと互換性があるため)
    • Emacs用のpo-modeや.po専用のエディタ

Railsやプラグインなどが提供しているメッセージだけを地域化した いなど、地域対象メッセージが変化しない場合はその他の仕組みも 有力な候補になるでしょう。例えば、ActiveScaffold用の ActiveScaffoldLocalizeがその場合です。

ActiveScaffoldLocalize

ActiveScaffoldは、国際化の仕組みとしてObject#as_を提供してい ます。その仕組みを利用して国際化・地域化を実現しているのが ActiveScaffoldLocalizeです。

ActiveScaffoldLocalizeには日本語用のメッセージも含まれている ので、以下のようにすればActiveScaffoldのメッセージを日本語に することができます。

% rails shelf
% cd shelf
% script/generate resource book title:string
% rake db:migrate
% script/plugin install git://github.com/activescaffold/active_scaffold.git
% script/plugin install git://github.com/edwinmoss/active_scaffold_localize.git

config/routes.rb:

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

app/controllers/books_controller.rb:

1
2
3
class BooksController < ApplicationController
  active_scaffold :book
end

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!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:

1
2
3
4
5
6
7
8
9
class ApplicationController < ActionController::Base
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    ActiveScaffold::Localization.lang = "ja-jp"
    true
  end
end

サーバを起動してhttp://localhost:3000/books/にアクセスします。

% script/server
% firefox http://localhost:3000/books/

ActiveScaffold + ActiveScaffoldLocalize

見ての通り、「検索」などのメニューは日本語になりますが、テー ブル名からきている「Books」やカラム名の「Title」などは日本語 になりません。

ActiveScaffoldLocalizeの方針では、これらを日本語にするために以下のような内容の config/initializers/lang/ja-jp.rb*1を作成します。

config/initializers/lang/ja-jp.rb:

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-

ActiveScaffold::Localization.define('ja-jp') do |lang|
  lang["Books"] = "本一覧"
  lang["Book"] = ""
  lang["Title"] = "タイトル"
end

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + ActiveScaffoldLocalize + モデルの地域化

(「本を作成」ではなく「本一覧を作成」になっているのはこ のパッチ で直ります。)

ActiveScaffoldLocalizeのこのやり方は手軽ですが、地域化対象の メッセージが変更になった場合(例: 「Title」から「Name」に変更) や、地域化対象のメッセージをtypoした場合(例: 「Title」ではな く「title」としていた)に気づきにくいという問題があります。 このような問題に対してはRuby-GetText-Packageが有効です。

ということで、ActiveScaffoldのメッセージは ActiveScaffoldLocalizeで地域化し、それ以外は Ruby-GetText-Packageで地域化するようにします。

Ruby-GetText-Package

ActiveScaffoldLocalizeとRuby-GetText-Packageのすみわけは上述 の通りですが、エラーメッセージの地域化はRuby-GetText-Package ではなく、ActiveScaffldLocalizeに任せます。これは、 ActiveScaffoldがエラーメッセージ部分を上書きしているため、 Ruby-GetText-Packageが提供するエラーメッセージ国際化処理とな じまないためです。

また、Ruby-GetText-Packageが取得したロケール情報を使って ActiveScaffoldLocalizeのlangを設定していることもコツのひとつ です。

config/environment.rb:

1
2
3
4
5
6
# ...
Rails::Initializer.run do |config|
  # ...
  config.gem "gettext", :lib => "gettext/rails"
  # ...
end

lib/active_scaffold_gettext.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
class ApplicationController < ActionController::Base
  init_gettext GETTEXT_DOMAIN
  # ...
  private
  before_filter :localize_active_scaffold
  def localize_active_scaffold
    posix_locale = GetText.locale.to_posix
    posix_locale = "#{posix_locale}-#{posix_locale}" if /_/ !~ posix_locale
    lang = posix_locale.gsub(/_/, '-').downcase
    ActiveScaffold::Localization.lang = lang
    true
  end
end

翻訳メッセージのファイルpoを作って翻訳します。

% rake gettext:po:update
% mkdir po/ja
% msginit -i po/your-rails-application.pot -o po/ja/your-rails-application.po -l ja_JP
# 途中でメールアドレスを聞かれるので入力する

po/ja/your-rails-application.po:

# ...
#: app/models/book.rb:-
msgid "Book"
msgstr "本"

#: app/models/book.rb:-
msgid "Books"
msgstr "本一覧"
# ...
#: app/models/book.rb:-
msgid "Book|Title"
msgstr "タイトル"
# ...

翻訳メッセージをmoにコンパイルしてアクセスするとテーブル名や カラム名などが日本語になります。

% rake gettext:mo:create

config/initializers/以下を変更したので、サーバを再起動してか ら再度アクセスすると、日本語で表示されます。

ActiveScaffold + Ruby-GetText-Package

まとめ

ActiveScaffoldLocalizeとRuby-GetText-Packageを使って、 ActiveScaffoldを用いたアプリケーションの国際化・地域化を実現する方法 のひとつを紹介しました。

基本的に複数の国際化のしくみを同時に使うと問題が起きますが、 今回は以下のようにそれぞれの長所を活かすようにすみわけて、問 題を回避しています。

  • ActiveScaffoldが利用する固定のメッセージは ActiveScaffoldLocalizeで地域化
  • モデル関連や追加・更新が行われるメッセージについては Ruby-GetText-Packageで地域化

*1  config/initializers/lang/以下にファイルを作るというのはActiveScaffoldLocalizeの方針ではありません。ファイルの場所は特に方針はないようです。

つづき: 2009-09-28
タグ: Ruby
2008-08-12

Rabbit 0.5.7リリース

昨日、 RDるびまの記事 )でスラ イドが書けるプレゼンテーションツール Rabbit がリリースされました。

サンプル

Rabbitではスライドを画像 HTML(+画像) PDF一覧表示 )などで出力することができます。

発表した後に資料を公開する場合や配布資料を作成する場合に利用 するとよいでしょう。

対象ユーザ

RDで書けることがウリというくらいなので、テキストエディタでス ライドを作成したい人が対象になります。おそらく、そのような人 はプログラマであることが多いと思うので、Rabbitはプログラマ向 けのプレゼンテーションツールといえるかもしれません。

スライドをテキストで作成すると以下のような利点があります。

  • バージョンコントロールシステムとの親和性が高い (diffの表示など)
  • 使い慣れたテキストエディタで編集できるため、編集作業の効 率がよい
  • 単なるテキストなので、専用のスライド表示ソフトウェアを用 いなくても内容を確認できる

一方、GUIの編集インターフェイスを備えたプレゼンテーションツー ル(wikipedia:PowerPointwikipedia:Keynoteなど)と比較すると、以下のような欠 点があります。

  • 見た目を微調整しずらい
  • 簡単な図を挿入することが面倒
    • 画像作成ソフトを起動して図を作成し、スライドに挿入

RabbitはRDで書かれたテキストだけではなく、PDFを入力としても 受け付けます。つまり、PDFビューアにもなります。

そこで、上記のような編集時の欠点を解決するために、別途PDF出 力ができるソフトウェアでスライドを作成し、Rabbitで表示すると いうことができます。RabbitをPDFビューアとして使うことにより、 Rabbitのユニークで実用的なユーザインターフェイスを使うことが できます。Rabbitの使い勝手に興味がある場合はこの方法を試して みるとよいかもしれません。

Rabbitのユーザインターフェイスに関してはまた別の機会にしてお きます。

まとめ

Rabbitの外面だけを紹介しました。難易度が高いと言われているイ ンストール方法や特徴的なユーザインターフェイスなどについては 触れませんでした。

タグ: Ruby
2008-08-01

ActiveLdap 1.0.1リリース

LDAPのエントリを ActiveRecord風のAPIでア クセスするためのライブラリ、 ActiveLdap 1.0.1がリリースされました。

ActiveLdapとは

ActiveRecord風のAPIとは1エントリを1オブジェクトとして扱える ということです。例えば、ユーザの説明を変更する場合は以下のよ うになります。

1
2
3
alice = User.find("alice")
alice.description = "New user"
alice.save!

ActiveRecordと同じように、各クラス間の関係を設定して便利にア クセスすることもできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User < ActiveLdap::Base
  belongs_to :groups, :many => "memberUid"
end

class Group < ActiveLdap::Base
  has_many :users, :wrap => "memberUid"
end

alice = User.find("alice")
alice.groups # => [Group("friend"), Group("office"), ...]
alice.groups << Group.find("home")
alice.groups # => [Group("friend"), Group("office"), Group("home"), ...]

friend = Group.find("friend")
friend.users # => [User("alice"), User("bob"), ...]

ActiveRecordと同じように、Ruby on Railsと使用することもでき ます。

% script/plugin install http://ruby-activeldap.googlecode.com/svn/tags/r1.0.1/rails/plugin/active_ldap
% script/generate scaffold_active_ldap
% vim config/ldap.yml

ActiveLdapは以下のライブラリをバックエンドとして利用できます。

  • Ruby/LDAP
    • 拡張ライブラリ(速い、インストールが大変かもしれない)
  • Net::LDAP
    • Rubyのみで実装(遅い、インストールは簡単)
    • 2008/06/17時点の最新版0.0.4では動かない。 Subversion 版を利用する必要がある。
  • JNDIのLDAPサービスプロバイダ (実験的)
    • JRubyでのみ利用可能。

ベンチマーク

以下はActiveLdapに付属するベンチマークの結果です。ベンチマー クでは100エントリを検索しています。「Rehearsal(リハーサル)」 を行って、それぞれ2回ずつ実行しているのは、以前はキャッシュ などで2回目以降の結果がよくなることなどがあったためです。現 在はあまり意味がありませんが、歴史的に残っています。

% ruby benchmark/bench-al.rb --config benchmark/config.yaml
Populating...
Rehearsal -------------------------------------------------------
  1x: AL              0.080000   0.010000   0.090000 (  0.098738)
  1x: AL(No Obj)      0.010000   0.000000   0.010000 (  0.016623)
  1x: LDAP            0.000000   0.000000   0.000000 (  0.008674)
  1x: Net::LDAP       0.030000   0.000000   0.030000 (  0.045199)
---------------------------------------------- total: 0.130000sec

                          user     system      total        real
  1x: AL              0.080000   0.020000   0.100000 (  0.100959)
  1x: AL(No Obj)      0.010000   0.010000   0.020000 (  0.020697)
  1x: LDAP            0.000000   0.000000   0.000000 (  0.010129)
  1x: Net::LDAP       0.030000   0.000000   0.030000 (  0.042075)
Entries processed by Ruby/ActiveLdap: 100
Entries processed by Ruby/ActiveLdap (without object creation): 100
Entries processed by Ruby/LDAP: 100
Entries processed by Net::LDAP: 100
Cleaning...

各項目はそれぞれ以下の通りです。

  • AL: Ruby/LDAPバックエンドのActiveLdapで検索を行い、各エ ントリをオブジェクト化する(ActiveRecord風のAPIを利用す る場合)
  • AL(No Obj): Ruby/LDAPバックエンドのActiveLdapで検索を行 い、各エントリの結果をオブジェクト化しない(エントリを配 列やハッシュなどを使って表現)
  • LDAP: Ruby/LDAPで検索を行う
  • Net::LDAP: Net::LDAPで検索を行う

上記の結果からは以下のことが言えます。

  • 本当に速度が重要な場合にはRuby/LDAPを直接利用する方がよ い。
  • 利用できるならば、Net::LDAPよりもRuby/LDAPバックエンドを 利用した方がよい。
  • Net::LDAPを直接利用するよりも、オブジェクト化しない ActiveLdap + Ruby/LDAPバックエンドの方が速い。

多くの場合、1度に100エントリを処理することは少ないでしょう。 そのため、通常はActiveLdapで各エントリをオブジェクト化しても 問題は少ないといえます。

もし、1度に多くのエントリを扱う場合で、読み込み専用ならば、 オブジェクト化しない方法で利用することでパフォーマンスを改善 することができます。

まとめ

ActiveLdapを利用することでLDAPのエントリをオブジェクト指向的 なAPIで自然に処理することができます。

ActiveLdapは複数のLDAPバックエンドに対応しており、Rubyがイン ストールされている環境さえあれば動かすこともできます。 (Net::LDAPバックエンド使用時。ただしそんなに速くない)また、 JRubyでもほとんどの機能が動きます。

もし、Ruby/LDAPを利用できる環境であれば、Net::LDAPを直接利用 するよりも、ActiveLdap + Ruby/LDAPバックエンドを利用した方が よりオブジェクト指向らしいAPIでLDAPのエントリを操作できます。 また、速度が要求される場合であれば、オブジェクト化を行わない (オブジェクト指向らしいAPIを利用しない)ことにより、より高 速にLDAPのエントリを読み込むことができます。

つづき: 2009-02-25
タグ: Ruby
2008-06-15

コミットログでRetrospectivaと連携

Rubyで実装されたSubversion*1リポジトリブラウザ(兼ITS(Issue Tracking System)/BTS(Bug Tracking System))としてRetrospectivaがあります。

Retrospectivaにはコミットログを解析してより便利にRetrospectivaを使うための機能がいくつかあります。Retrospectivaを採用しているプロジェクトであれば、コミットログの書き方をRetrospectivaの解釈できる書き方にすることにより、さらにRetrospectivaを便利に使うことができます。

チケットへのリンク

RetrospectivaはチケットベースのITS機能を備えています。コミットログには「XXX番のチケットの問題を修正した」というようなログを書くことも多いでしょう。その時、以下のフォーマットでチケットの番号を書くことにより、そのコミットログをRetrospectiva上で見ると該当するチケットにリンクが張られます。(

[#XXX]

この機能はブラウザ上から変更履歴を見ているときにとても便利です。

チケットの更新

RetrospectivaにはExtensionという拡張機能の仕組みがあります。この仕組みを利用したSCM Ticket Updateという拡張機能を導入することにより、コミットログでチケットの状態を更新することができます。

SCM Ticket Updateの導入方法は以下の通りです。(現時点(2008-05-23)のtrunkを利用している場合)

% RAILS_ENV=production ruby script/rxm checkout http://retrospectiva.googlecode.com/svn/extensions/1-1/scm_ticket_update
% RAILS_ENV=production ruby script/rxm install scm_ticket_update
% # Retrospectivaを再起動

拡張機能をインストールした場合はRetrospectivaを再起動することを忘れないでください。

この拡張機能を入れた後は以下のような書式でコミットログを書くことにより、コミットと一緒にチケットも更新することができます。

チケット#123の状態を修正済みに変更:

クラッシュバグを修正 [#123] (status:fixed)

チケット#29の割り当てユーザをaliceに変更:

[#29] テストを追加 (assigned:alice)

他にも以下のような書式が使えます。

[#N] (NAME1:VALUE1 NAME:VALUE2 ...) ログ

また、もし変更後の値に空白が入っている場合は「"..."」とダブルクォートで囲みます。

[#2929] (status:fixed milestone:"2.9 (バラ)") fix a trivial bug.

この機能を使うと、コミットした後にブラウザからチケットを変更する作業がなくなるのでチケットのクローズし忘れも減るかもしれません。また、コミットメールで(コミットログから)チケットがクローズされたことが分かるのも便利な点です。

まとめ

このようにコミットログの書き方を少しRetrospectivaよりにするだけでもっと便利にRetrospectivaが使えるようになります。

Retrospectivaが要求している書き方を使ってもコミットログが見づらくなるわけではないので、少し意識して使ってみてはいかがでしょうか。

*1  Gitにも微妙に対応している

つづき: 2009-08-07
タグ: Ruby
2008-05-23

最新
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|
タグ:
RubyKaigi2008Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer