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

ククログ

«前月 最新 翌月»
タグ:

UxUで外部テキストエディタを使う時のおすすめ設定

みなさん、テストしてますか?(挨拶)

UxUでは、テスト失敗時に表示されるスタックトレースからテキストエディタを起動することができます。この時、利用するテキストエディタがコマンドライン引数による行指定に対応していれば、エラーが発生した行を直接開いて編集できます。きちんと設定しておけば、テストを実行して、編集して、またテストして、といったサイクルで開発を進められるので非常に便利です。

以下に、有名なテキストエディタ向けの設定の例をいくつか挙げてみました。UxUの設定ダイアログの「MozUnitテストランナー」タブでエディタ起動用のコマンドとして入力してください。(エディタの実行ファイルのパスは必要に応じて読み替えてください)

秀丸エディタ
"C:\Program Files\Hidemaru\Hidemaru.exe" /j%L,%C "%F"
TeraPad
"C:\Program Files\TeraPad\TeraPad.exe" /j=%L "%F"
サクラエディタ
"C:\Program Files\sakura\sakura.exe" "%F" -X=%C -Y=%L
EmEditor
"C:\Program Files\EmEditor\EmEditor.exe" /l %L /cl %C "%F"
xyzzy
"C:\Program Files\xyzzy\xyzzycli.exe" -l "%F" -g %L -c %C
萌エディタ
"C:\Program Files\moeditor\moe.exe" "%F" -m %L,%C
gedit
/usr/bin/gedit +%L "%F"
Vim(vi)
/usr/bin/vim +%L "%F"
Emacs
/usr/bin/gnuclient +%L "%F"

なお、%Lは行番号、%Cは列番号、%Fはファイルのパスへと、それぞれ自動的に置換されます。

タグ: Mozilla | テスト | UxU
2009-04-01

2009年3月の肉の会

クリアコードでは毎月29日頃に肉の会と呼ばれる社内食事会を行っています。 肉の会というだけあって肉を食べることが多いのですが、今回は肉ではなく、社内でたこやきを食べまくりました。

今回はゲストとしてITproの高橋さんに参加してもらえました。(忙しい中、ありがとうございます!)

高橋さんがおみやげとして発売したばかりの「Ruby技術者認定試験 公式ガイド」を持ってきてくれました。「クリアコードさんへ」とサインももらいました。ありがとうございます!

高橋さんのサイン

この公式ガイドには模擬試験が2回分ついています。Rubyには自信があったのでやってみたところ、90%くらいしか正解できませんでした。普段使わないメソッドや引数が問題になっていると間違えてしまいます。

ひっかけっぽい問題も何題かあり、ひっかけようとしているなぁとニヤニヤしながら解いていたのですが、何題か間違えてしまいました。油断してはいけません。

この模擬試験をやってみて、Hash#invertをはじめて知りました。もうすでにかなりRubyを知っている人も、Ruby技術者認定試験の問題を解いてみると知らないことが見つかるかもしれません。問題を見て、ひっかけっぽいな、とニヤニヤしながら解くのも楽しいと思います。

模擬試験の前にはコンパクトにまとめられたRubyの解説がついているので、まだRubyに詳しくない人のとっかかりにもよさそうな気がします。mapとcollectをきちんと対等に扱っているので、変に偏らなそうなのもよいと思います。*1 Rubyで開発されたデスクトップ・アプリケーションとしてRabbitが挙がっていることも、とてもよいと思います。

RUBY技術者認定試験 公式ガイド (ITpro BOOKs)
伊藤忠テクノソリューションズ/Rubyアソシエーション/ITpro
日経BP社
¥ 2,160

*1 私はcollectに偏っています。

タグ: Ruby
2009-04-02

ページの内容を折りたたむスクリプトを公開しました

UxUのページの過去のバージョンの詳細情報など、そのまま表示すると長くなってしまう内容を折りたたむめの簡単なスクリプトを書いてみました。せっかくなので、AGPLv3にて公開することにします。

このスクリプトを読み込んだ後で以下のようにすると、XPath式で示された要素が折りたたみ可能となります。複数のインスタンスを生成すれば、異なる種類の折りたたみ項目を作ることもできます。

1
2
3
4
5
new Folding(
  '/descendant::*[@class="items-should-be-folded"]',
  '詳細を表示',
  '詳細を隠す'
);

また、折りたたまれた要素をボタンのクリックで表示すると、URIの末尾にその情報が付け加えられた状態となりますので、その状態のページをブックマークしたり、特定の項目を表示した状態のページへリンクしたりすることもできます。

DOM3 XPathを使用しているため、IE6などのレガシーなWebブラウザ向けにはJavaScript-XPathを併用する必要があることに注意してください。

この手のスクリプトはアニメーション効果が派手な物などすでに色々あると思いますが、シンプルな物が好みな場合や、社内Wikiのようにあまり飾り気が無くても良い場面などに使ってみると良いのではないでしょうか。また、折りたたみの対象となる要素をXPath式で自由に指定できるので、すでにあるページの内容を変更せずに簡単に導入できるという利点もあります。

AGPL

ところで、皆さんはAGPLというライセンスについてはご存じでしょうか? 

AGPL(Affero GPL)はGPLの派生ライセンスの1つで、Webサービス用のプログラムで使われることを想定しています。このライセンスが適用されたプログラムのコードを利用したWebサービスは、そのWebサービスのユーザに対して、サービスを構成するすべてのプログラムのソースコードを公開する義務があります。それ以外の点は通常のGPLと同一です。

誤解している方もおられるかもしれませんが、AGPLやGPLでライセンスされたコードを利用すると、すぐにソースコードを公開する義務があるということはありません。AGPLやGPLは、プログラムのユーザ(AGPLの場合はそのサービスのユーザも含む)に対してはソースコード開示の義務がありますが、それ以外の関係ない人にまでソースコードを公開する義務はありません。例えば社内Wikiに上のスクリプトを組み込んだ場合であれば、社内の人にはソースコードを公開しなくてはいけませんが、社外にまで公開する必要はありません。「よく分からないけどGPLこわい!」と尻込みしてしまわずに、色々なコードを用途に応じて使い分けてみると良いでしょう。

つづき: 2009-04-06
タグ: JavaScript
2009-04-03

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

すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したプログラムが入った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

問題解決につながるエラーメッセージ

プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。

ここでは、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 | Cutter | UxU
2009-04-10

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

milter manager 1.0.0リリース

milter manager初の安定版1.0.0をリリースしました。→milter managerの紹介

また、クリアコード初のプレスリリースもしました。→迷惑メール対策システムの構築を簡単・低コストにする『milter manager』をリリース

今日をリリース日にしたのは、今日が大安だったからです。リリース作業はそれほど大きなトラブルもなく行えたので、よい日だったと思います。みなさんも、大安にリリースしてみてはいかがでしょうか。

2009-04-16

groongaのインデックスを自動更新

Sennaの後継となる組み込み型全文検索エンジンgroongaでインデックスを自動更新する方法を見つけたので紹介します。

「見つけた」という風に書いているのは、「ドキュメントには書いていないけどソースを見たらやり方がわかった」からです。

groonga

Sennaは転置インデックス関連の機能のみを提供していましたが、groonaでは転置インデックスだけではなく、データ管理の機能も提供しています。そのため、DBMSなど他のデータ管理機能を持つソフトウェアと組み合わせなくても、groongaだけでデータ管理と高速な全文検索機能を実現することができます。

groongaはGitHub上で開発されていて、groongaに関するドキュメントgroongaのAPIのドキュメントもGitHub上にあります。

また、Sennaとgroongaの比較groongaデータベースAPIも読んでおくとよいと思います。

ここでは、上記のドキュメントには書いていなかったインデックスを自動更新する方法を紹介します。

サンプルプログラムの概要

コメント付きブックマークを管理し、コメントで全文検索できるプログラムを作成します。

ブックマークテーブル(<bookmarks>テーブル)には以下の2つのカラムがあります。

  • uri: ブックマークしたURI
  • comment: ブックマークへのコメント

全文検索用の単語を登録するテーブル(<lexicon>テーブル)には以下の1つのカラムがあります。

  • comment-index: インデックスに登録された単語を含むブックマークIDのリスト

ここでは、「comment」カラムにコメントを登録すると自動的にコメントのインデックスを更新して検索可能にする方法を紹介します。ちなみに、「コメントのインデックスを更新」とは、<lexicon>の単語とcomment-indexに入っているブックマークIDのリストを更新する、ということです。

処理の流れ

コメントを入れたプログラムをボトムアップで読んでいきながら説明します。まずは、main()です。

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
int
main (int argc, char **argv)
{
    grn_ctx context;

    /* 初期化 */
    grn_init();
    grn_ctx_init(&context, 0, GRN_ENC_UTF8);
    grn_db_create(&context, NULL, NULL);

    /* テーブル定義 */
    define_bookmarks_table(&context);
    define_lexicon_table(&context);
    /* ポイント: インデックス自動更新の設定 */
    assign_source(&context);

    /* ブックマークの登録: 3件 */
    add_bookmark(&context,
                 "http://groonga.org/",
                 "an open-source fulltext search engine and column store");
    add_bookmark(&context,
                 "http://qwik.jp/senna/",
                 "an embeddable fulltext search engine");
    add_bookmark(&context,
                 "http://cutter.sourceforge.net/",
                 "a unit testing framework for C");

    /* 検索: 2回 */

    search(&context, "search");
    /* 結果: <search>で検索したら2件ヒット
     * search result: <search>: 2
     * uri                         | comment
     * http://qwik.jp/senna/         | an embeddable fulltext search engine
     * http://groonga.org/         | an open-source fulltext search engine and column store
     */

    search(&context, "testing");
    /* 結果: <testing>で検索したら1件ヒット
     * search result: <testing>: 1
     * uri                         | comment
     * http://cutter.sourceforge.net/         | a unit testing framework for C
     */

    /* 後始末 */
    grn_ctx_fin(&context);
    grn_fin();

    return 0;
}

コメントに書いてある通りの処理の流れです。全部の関数を説明しようかと思ったのですが、大事なところはassign_source()のところなので、そこだけ説明します。

インデックスの元データの設定

assign_source()では、comment-indexカラムは<bookmarks>のcommentカラムの値からインデックスを生成している、ということを教えています。こうすることにより、commentカラムの値を更新すると、groongaが中で自動でcomment-indexも更新してくれるというわけです。groongaでは、この関係を指定するためにcomment-indexカラムのGRN_INFO_SOURCEというメタデータとして、commentカラムのIDを設定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void
assign_source (grn_ctx *context)
{
    grn_obj source;
    grn_id source_id;

    /* comment_columnは<bookmarks>のcommentカラムオブジェクト */
    /* grn_obj_id()でcommentカラムのIDを取得 */
    source_id = grn_obj_id(context, comment_column);

    /* GRN_INFO_SOURCEの値として設定する領域を初期化 */
    GRN_OBJ_INIT(&source, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
    /* GRN_INFO_SOURCEの値としてcommentカラムオブジェクトのIDを指定 */
    GRN_BULK_SET(context, &source, &source_id, sizeof(grn_id));

    /* comment-indexカラムのGRN_INFO_SOURCEにcommentカラムオブジェクトのIDを指定 */
    grn_obj_set_info(context, comment_index_column, GRN_INFO_SOURCE, &source);
}

効果

comment-indexがcommentカラムを基にしていると設定することでインデックスの登録の手間が省けます。

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
static void
add_bookmark (grn_ctx *context, const char *uri, const char *comment)
{
    grn_id id;
    grn_obj value;

    /* <bookmarks>テーブルにレコードを追加 */
    id = grn_table_add(context, bookmarks);

    /* <bookmarks>テーブルのuriカラムにURIを設定 */
    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
    GRN_BULK_SET(context, &value, uri, strlen(uri));
    grn_obj_set_value(context, uri_column, id, &value, GRN_OBJ_SET);

    /* <bookmarks>テーブルのcommentカラムにコメントを設定 */
    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
    GRN_BULK_SET(context, &value, comment, strlen(comment));
    grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);

    /* commentカラムのインデックスを作成 */
    /* 不要
    grn_column_index_update(context, comment_index_column, id, 1,
                            NULL, &value);
     */
}

追加するときは新しくインデックスを生成するだけでよいですが、更新する場合は、以前の値のインデックスを削除してから新しいインデックスを生成しなければいけません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void
update_bookmark_comment (grn_ctx *context, grn_id id, const char *comment)
{
    grn_obj *old_value;
    grn_obj value;

    /* commentカラムの以前の値を取得 */
    /* 不要
    old_value = grn_obj_get_value(context, comment_column, id, NULL);
    */

    /* commentカラムの値を更新 */
    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
    GRN_BULK_SET(context, &value, comment, strlen(comment));
    grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);

    /* commentカラムのインデックスを更新 */
    /* 不要
    grn_column_index_update(context, comment_index_column, id, 1,
                            &old_value, &value);
     */
}

GRN_ELEMENT_INFOを設定していない場合は、値を更新する前に古い値を取得しておいてからインデックスを更新しなければいけません。

1つしかインデックスを使っていない場合はGRN_ELEMENT_INFOを使わなくても気にならない手間かもしれませんが、たくさんインデックスを使っているときはだいぶ楽になると思います。

まとめ

groongaにはドキュメントには書かれていない便利機能があります。GRN_ELEMENT_INFO以外ではaccessorにもびっくりしました。

ちなみに、groongaはテスティングフレームワークとしてCutterを採用しています。

ソースコード

ここで使用したサンプルコードです。ざっくりとコメントを入れておきました。

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
#include <stdio.h>
#include <string.h>

#include <groonga.h>

/* <bookmarks>テーブルとそのカラムたち */
static grn_obj *bookmarks, *uri_column, *comment_column;
/* <lexicon>テーブルとそのカラムたち */
static grn_obj *lexicon, *comment_index_column;

/* 名前からオブジェクトを取得するための便利関数 */
static grn_obj *
lookup (grn_ctx *context, const char *name)
{
    return grn_ctx_lookup(context, name, strlen(name));
}

/* カラムを作成するための便利関数 */
static grn_obj *
create_column (grn_ctx *context, grn_obj *table,
               const char *name, grn_obj *value_type, grn_obj_flags flags)
{
    /* 一時カラム: ファイルには保存しない */
    return grn_column_create(context, table,
                             name, strlen(name),
                             NULL, flags,
                             value_type);
}

/* <bookmarks>テーブルとそのカラムたちを定義 */
static void
define_bookmarks_table (grn_ctx *context)
{
    /* 一時テーブル: ファイルには保存しない */
    bookmarks = grn_table_create(context,
                                 "<bookmarks>", strlen("<bookmarks>"),
                                 NULL,
                                 GRN_OBJ_TABLE_NO_KEY,
                                 NULL,
                                 0,
                                 GRN_ENC_DEFAULT);
    uri_column = create_column(context, bookmarks, "uri",
                               lookup(context, "<shorttext>"),
                               0);
    comment_column = create_column(context, bookmarks, "comment",
                                   lookup(context, "<shorttext>"),
                                   0);
}

/* <lexicon>テーブルとそのカラムたちを定義 */
static void
define_lexicon_table (grn_ctx *context)
{
    /* 一時テーブル: ファイルには保存しない
     * GRN_OBJ_TABLE_PAT_KEYかGRN_OBJ_TABLE_HASH_KEYにすること
     * GRN_OBJ_TABLE_NO_KEYは使えない
     */
    lexicon = grn_table_create(context,
                               "<lexicon>", strlen("<lexicon>"),
                               NULL,
                               GRN_OBJ_TABLE_PAT_KEY,
                               lookup(context, "<shorttext>"),
                               0,
                               GRN_ENC_DEFAULT);

    /* MeCabで検索用単語を切り出す */
    grn_obj_set_info(context, lexicon, GRN_INFO_DEFAULT_TOKENIZER,
                     lookup(context, "<token:mecab>"));

    comment_index_column = create_column(context, lexicon, "comment-index",
                                         bookmarks, GRN_OBJ_COLUMN_INDEX);
}

/* comment-indexカラムとcommentカラムを関連付ける */
static void
assign_source (grn_ctx *context)
{
    grn_obj source;
    grn_id source_id;

    /* comment_columnは<bookmarks>のcommentカラムオブジェクト */
    /* grn_obj_id()でcommentカラムのIDを取得 */
    source_id = grn_obj_id(context, comment_column);

    /* GRN_INFO_SOURCEの値として設定する領域を初期化 */
    GRN_OBJ_INIT(&source, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
    /* GRN_INFO_SOURCEの値としてcommentカラムオブジェクトのIDを指定 */
    GRN_BULK_SET(context, &source, &source_id, sizeof(grn_id));

    /* comment-indexカラムのGRN_INFO_SOURCEにcommentカラムオブジェクトのIDを指定 */
    grn_obj_set_info(context, comment_index_column, GRN_INFO_SOURCE, &source);
}

/* ブックマーク追加 */
static void
add_bookmark (grn_ctx *context, const char *uri, const char *comment)
{
    grn_id id;
    grn_obj value;

    /* <bookmarks>テーブルにレコードを追加 */
    id = grn_table_add(context, bookmarks);

    /* <bookmarks>テーブルのuriカラムにURIを設定 */
    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
    GRN_BULK_SET(context, &value, uri, strlen(uri));
    grn_obj_set_value(context, uri_column, id, &value, GRN_OBJ_SET);

    /* <bookmarks>テーブルのcommentカラムにコメントを設定 */
    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
    GRN_BULK_SET(context, &value, comment, strlen(comment));
    grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);
}

/* 検索結果の表示 */
static void
print_result (grn_ctx *context, grn_obj *result)
{
    grn_table_cursor *cursor;
    grn_id result_id;
    grn_obj *uri_accessor, *comment_accessor;

    /* アクセサ! */
    uri_accessor = grn_table_column(context, result,
                                    ".comment-index.uri",
                                    strlen(".comment-index.uri"));
    comment_accessor = grn_table_column(context, result,
                                        ".comment-index.comment",
                                        strlen(".comment-index.comment"));

    printf("uri\t\t\t | comment\n");

    /* カーソルで一行ずつ処理 */
    cursor = grn_table_cursor_open(context, result, NULL, 0, NULL, 0, 0);
    while ((result_id = grn_table_cursor_next(context, cursor)) != GRN_ID_NIL) {
        grn_obj *uri, *comment;

        uri = grn_obj_get_value(context, uri_accessor, result_id, NULL);
        comment = grn_obj_get_value(context, comment_accessor, result_id, NULL);
        /* 登録したURIとコメントはNULL終端していないので'\0'を追加 */
        GRN_BULK_PUTC(context, uri, '\0');
        GRN_BULK_PUTC(context, comment, '\0');
        printf("%s\t | %s\n", GRN_BULK_HEAD(uri), GRN_BULK_HEAD(comment));
        grn_obj_close(context, uri);
        grn_obj_close(context, comment);
    }
    grn_table_cursor_close(context, cursor);
}

/* 検索して結果を表示 */
static void
search (grn_ctx *context, const char *word)
{
    grn_obj *result;
    grn_obj *query;

    /* 検索結果を格納する一時テーブル
     * キーにヒットしたレコードのIDが入るので、
     * GRN_OBJ_TABLE_NO_KEYは使えない。
     * GRN_OBJ_TABLE_HASH_KEYを指定すればよい
     */
    result = grn_table_create(context,
                              NULL, 0,
                              NULL,
                              GRN_OBJ_TABLE_HASH_KEY,
                              lexicon, /* <lexicon>テーブルのレコードIDが入る */
                              0,
                              GRN_ENC_DEFAULT);

    /* 検索 */
    query = grn_obj_open(context, GRN_BULK, 0, 0);
    grn_bulk_write(context, query, word, strlen(word));
    grn_obj_search(context, comment_index_column, query, result,
                   GRN_SEL_OR, NULL);
    grn_obj_close(context, query);

    printf("search result: <%s>: %d\n", word, grn_table_size(context, result));
    print_result(context, result);
    grn_obj_close(context, result);
    printf("\n");
}

int
main (int argc, char **argv)
{
    grn_ctx context;

    /* 初期化 */
    grn_init();
    grn_ctx_init(&context, 0, GRN_ENC_UTF8);
    grn_db_create(&context, NULL, NULL);

    /* テーブル定義 */
    define_bookmarks_table(&context);
    define_lexicon_table(&context);
    /* ポイント: インデックス自動更新の設定 */
    assign_source(&context);

    /* ブックマークの登録: 3件 */
    add_bookmark(&context,
                 "http://groonga.org/",
                 "an open-source fulltext search engine and column store");
    add_bookmark(&context,
                 "http://qwik.jp/senna/",
                 "an embeddable fulltext search engine");
    add_bookmark(&context,
                 "http://cutter.sourceforge.net/",
                 "a unit testing framework for C");

    /* 検索: 2回 */

    search(&context, "search");
    /* 結果: <search>で検索したら2件ヒット
     * search result: <search>: 2
     * uri                         | comment
     * http://qwik.jp/senna/         | an embeddable fulltext search engine
     * http://groonga.org/         | an open-source fulltext search engine and column store
     */

    search(&context, "testing");
    /* 結果: <testing>で検索したら1件ヒット
     * search result: <testing>: 1
     * uri                         | comment
     * http://cutter.sourceforge.net/         | a unit testing framework for C
     */

    /* 後始末 */
    grn_ctx_fin(&context);
    grn_fin();

    return 0;
}
2009-04-22

2009年4月のFennec

何度かここにも書いてあるように、クリアコードはWindows Mobile上で動作するブラウザFennecの改善に力をいれています。

改善した結果は積極的に本家にフィードバックしており、次のFennecのリリースには取り込まれる予定です。具体的には、以下の点が改善されます。

  • フォントまわり:
    • 480249 gfxWindowsPlatform::ResolveFontName() almost fails since direct access to mName member.
    • 484083 Should load TruType Collection file too
    • 486624 AppendFacesFromFontFile is called twice for the same font file.
    • 489511 gfxFontCache will never hit on Freetype2 backend.
  • 起動まわり:
    • 485031 WinCE Crash on First Startup (or command line environment passing)
    • 487174 Modify nsXULStub to launch from GRE folder on WinCE
    • 485465 wince fennec ignores "command line" options

フォントまわりの改善で、日本語の表示問題が修正されたり、表示速度が高速化されたりします。起動まわりの改善で、使い勝手がよくなったりします。

MozillaサポートやMozilla本体・拡張機能の開発に関心のある方はinfo@clear-code.comまでお問い合わせ下さい。

タグ: Mozilla
2009-04-24

Fennecでの日本語表示設定

Nightlyビルドでの日本語対応状況

2月26日のエントリのエントリでお伝えした通り、以前のWindows Mobile版Fennecには日本語表示に致命的なバグが存在していましたが、弊社エンジニアがこれを修正し、本家にも既にこの修正が取り込まれております。このため、現在mozilla.orgで公開されているNightlyビルドでも、バイナリを修正することなく日本語を表示することが可能です。 ただし、現在のバージョンではまだフォントの自動選択処理に不具合があるため、設定を正しく行っていない場合、インストールしたフォントや訪れたサイトによっては、文書の一部あるいは全てが文字化けする可能性があります。 そこで今回は、4/28現在のNigtlyビルドにおいて日本語を正しく表示する設定を紹介致します。

Nightlyビルドのインストール

mozilla.orgのFTPサイトから最新のcabファイルを取得し、インストールします。その後、一度Fennecを起動して、プロファイルを作成します。プロファイルは \Application Data\Mozilla\Fennec\Profiles\xxxxxxxx.default フォルダ以下に作成されます。この後、設定ファイルを手動で変更しますので、一旦Fennecを終了します。

日本語フォントのインストール

現在のFennecは、Windows Mobile日本語版に標準で搭載されているAC3形式のフォントに対応していません。このため、別途日本語TTFフォントをインストールする必要があります。ここでは例として、VLゴシックをインストールします。

  • VLゴシックのサイトからzipアーカイブを取得
  • 上記zipアーカイブからVL-PGothic-Regular.ttfを抽出
  • 上記TTFファイルを \windows フォルダにコピー

prefs.jsの設定

プロファイルフォルダ以下のprefs.jsファイルを開いて、以下ような記述を追加します。

user_pref("font.language.group", "ja");
user_pref("font.name.sans-serif.ja", "VL PGothic");
user_pref("font.name.sans-serif.x-unicode", "VL PGothic");
user_pref("font.name.sans-serif.x-western", "VL PGothic");
user_pref("font.name.serif.ja", "VL PGothic");
user_pref("font.name.serif.x-unicode", "VL PGothic");
user_pref("font.name.serif.x-western", "VL PGothic");
user_pref("intl.accept_languages", "ja");
user_pref("intl.charset.default", "UTF-8");
user_pref("intl.charset.detector", "ja_parallel_state_machine");

TrueTypeフォントであれば、VLゴシックに限らずどのフォントでも使用可能と思われます。例えばIPA Pゴシックを使用する場合は、VL PGothicという記述をIPAPGothicに変更します。

userContent.cssの設定

以上でほぼ日本語の表示は可能なのですが、これでもなおフォーム内の文字が化ける場合があります。今回はユーザーCSSでフォントを指定して、この問題を回避します。 プロファイルフォルダ以下に chrome というフォルダを作成し、userContent.css という名前で以下のような内容のファイルを作成します。

input[name], input[value], select[name], option, textarea, button, fieldset, label, legend, optgroup[label] {
  font-family: 'VL PGothic', sans-serif;
  font-size: 12px;
}

以上で、フォームでも日本語が表示されるようになります。

まとめ

Fennecで日本語を表示することはできたでしょうか? クリアコードでは、上記のような煩わしい設定を行わなくともFennecで日本語を表示できるよう、引き続き改善作業を行っています。また、現在のNightlyビルドにはまだ含まれていないものの、日本語入力時の未確定文字列が表示されない問題等についても修正を行い、本家にフィードバックしています。

タグ: Mozilla
2009-04-28

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

«前月 最新 翌月»
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|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|
タグ:
RubyKaigi 2015 sponsor RubyKaigi 2015 speaker RubyKaigi 2015 committer RubyKaigi 2014 official-sponsor RubyKaigi 2014 speaker RubyKaigi 2014 committer RubyKaigi 2013 OfficialSponsor RubyKaigi 2013 Speaker RubyKaigi 2013 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
RubyKaigi2008Speaker