みなさん、テストしてますか?(挨拶)
UxUでは、テスト失敗時に表示されるスタックトレースからテキストエディタを起動することができます。この時、利用するテキストエディタがコマンドライン引数による行指定に対応していれば、エラーが発生した行を直接開いて編集できます。きちんと設定しておけば、テストを実行して、編集して、またテストして、といったサイクルで開発を進められるので非常に便利です。
以下に、有名なテキストエディタ向けの設定の例をいくつか挙げてみました。UxUの設定ダイアログの「MozUnitテストランナー」タブでエディタ起動用のコマンドとして入力してください。(エディタの実行ファイルのパスは必要に応じて読み替えてください)
なお、%Lは行番号、%Cは列番号、%Fはファイルのパスへと、それぞれ自動的に置換されます。
クリアコードでは毎月29日頃に肉の会と呼ばれる社内食事会を行っています。 肉の会というだけあって肉を食べることが多いのですが、今回は肉ではなく、社内でたこやきを食べまくりました。
今回はゲストとしてITproの高橋さんに参加してもらえました。(忙しい中、ありがとうございます!)
高橋さんがおみやげとして発売したばかりの「Ruby技術者認定試験 公式ガイド」を持ってきてくれました。「クリアコードさんへ」とサインももらいました。ありがとうございます!
この公式ガイドには模擬試験が2回分ついています。Rubyには自信があったのでやってみたところ、90%くらいしか正解できませんでした。普段使わないメソッドや引数が問題になっていると間違えてしまいます。
ひっかけっぽい問題も何題かあり、ひっかけようとしているなぁとニヤニヤしながら解いていたのですが、何題か間違えてしまいました。油断してはいけません。
この模擬試験をやってみて、Hash#invertをはじめて知りました。もうすでにかなりRubyを知っている人も、Ruby技術者認定試験の問題を解いてみると知らないことが見つかるかもしれません。問題を見て、ひっかけっぽいな、とニヤニヤしながら解くのも楽しいと思います。
模擬試験の前にはコンパクトにまとめられたRubyの解説がついているので、まだRubyに詳しくない人のとっかかりにもよさそうな気がします。mapとcollectをきちんと対等に扱っているので、変に偏らなそうなのもよいと思います。*1 Rubyで開発されたデスクトップ・アプリケーションとしてRabbitが挙がっていることも、とてもよいと思います。
RUBY技術者認定試験 公式ガイド (ITpro BOOKs)
日経BP社
¥ 22,995
*1 私はcollectに偏っています。
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(Affero GPL)はGPLの派生ライセンスの1つで、Webサービス用のプログラムで使われることを想定しています。このライセンスが適用されたプログラムのコードを利用したWebサービスは、そのWebサービスのユーザに対して、サービスを構成するすべてのプログラムのソースコードを公開する義務があります。それ以外の点は通常のGPLと同一です。
誤解している方もおられるかもしれませんが、AGPLやGPLでライセンスされたコードを利用すると、すぐにソースコードを公開する義務があるということはありません。AGPLやGPLは、プログラムのユーザ(AGPLの場合はそのサービスのユーザも含む)に対してはソースコード開示の義務がありますが、それ以外の関係ない人にまでソースコードを公開する義務はありません。例えば社内Wikiに上のスクリプトを組み込んだ場合であれば、社内の人にはソースコードを公開しなくてはいけませんが、社外にまで公開する必要はありません。「よく分からないけどGPLこわい!」と尻込みしてしまわずに、色々なコードを用途に応じて使い分けてみると良いでしょう。
すでにお気づきの方もいるかもしれませんが、先日から、クリアコードで開発したプログラムが入ったSubversionリポジトリ(リポジトリの更新状況のRSS)の公開を始めました。
クリアコードでは既存のフリーソフトウェアの開発に参加したり、新しくmilter managerなどのフリーソフトウェアを開発したりしていますが、それらの開発成果の公開場所はケースバイケースとなっています。
このように、クリアコードの開発成果のソースコードは様々なホスティングサイトのリポジトリにて管理、および公開されています。
まもなくクリアコードは設立から3年が経とうとしていますが、その間、プロジェクトを作るまでもないような小規模なソースコードがいくつかたまってきました。この度、そのようなソースコードをSubversionリポジトリで公開することにしました。
このリポジトリには現在、ページの一部を折りたたむfolding.jsや、このククログを生成するためのtDiary関連のスクリプト(日記のデータをSubversionで管理するIOバックエンドや日記を静的なHTMLに変換するスクリプトなどの記事で述べた物)、Thunderbird用の各種アドオンのソースコードが入っています。誰でも自由にチェックアウトできますので、注意事項をご了承の上でどうぞご利用ください。
以下、現在入っているプログラムを簡単に紹介します。
ククログだけではなく、milter managerのブログでも使っています。使い方はmilter managerのtdiary.confが参考になると思います。
Thunderbird Add-ons - クリアコードで公開している、Thunderbirdのバグを回避するパッチや挙動の変更を行う拡張機能です。公開ページにも書いてありますが、これらの拡張機能は無保証です。業務上の必要性からの導入をお考えの場合は、Mozilla Firefox & Mozilla Thunderbird保守・サポートサービスのご利用もご検討ください。
リポジトリの更新状況を配信しているRSSは、Subversionに標準添付のcommit-email.rbで生成しています。今回、RSSのタイトルや説明を日本語にしたかったので、Subversionのtrunkに--rss-titleと--rss-descriptionオプションを追加しました。また、--repository-uriで指定されたリポジトリのURIをコミットメールのX-SVN-Repositoryヘッダに設定するようにもしています。
Subversionリポジトリの整形表示にはRepos Styleを利用しています。mod_dav_svnにはSVNIndexXSLTというオプションがあって、それを利用しています。
プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。
ここでは、Rubyでの例をまじえながら問題解決に有用なエラーメッセージを紹介します。ライブラリなど多くの人が使うようなプログラムを作成する場合は参考になるかもしれません。
問題に遭遇してから問題を解決するまでには以下の順で作業をする必要があります。
役立つエラーメッセージがあると「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を例にして問題解決に役立つエラーメッセージについて紹介しました。
問題解決に役立つエラーメッセージの特長は、テストの実行結果にもあてはまります。クリアコードが開発に関わっているテスティングフレームワークではテストの実行結果にこだわっています。
あなたが使っているテスティングフレームワークは問題解決に役立つような情報を提供していますか?
これまで、ActiveLdapにはまとまった日本語の情報がありませんでしたが、id:tashenさんがActiveLdapのチュートリアルを翻訳してくれています。原文に最新の状況に追従していない部分があるため、いくつか古い情報もあるのですが、現在、最新の状況に追従するように作業が進んでいます。(最初の方はわりと最新の状況に追従しています。)
せっかくなので、ldap_mappingのあたりを簡単に説明します。できるなら、この内容を本家のドキュメントにうまくマージしたいと思っています。
ActiveRecordでは何もしなくてもカラムへアクセスすることができるのですが、ActiveLdapではldap_mappingでLDAPとRubyのオブジェクトを対応させる必要があります。ActiveRecordでは1つのテーブルが1つのクラスに対応し、各レコードがインスタンスに対応しますが、LDAPではそのような対応関係を自動的に判断することが難しいためです。どのようなエントリの集合を1つのクラスに対応させるかはアプリケーション毎に異なります。そのため、ActiveLdapでは明示的にユーザに指定してもらう方法をとっています。適切なデフォルト値がない場合はデフォルト値を提供しない方が混乱しないと思います。
ldap_mappingでは以下の3つの情報を使ってクラスに対応するエントリの集合を決めます。
オブジェクトクラス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という重要なオプションがあります。
より詳しくはActiveLdapの日本語チュートリアルを見てください。
milter manager初の安定版1.0.0をリリースしました。→milter managerの紹介
また、クリアコード初のプレスリリースもしました。→迷惑メール対策システムの構築を簡単・低コストにする『milter manager』をリリース
今日をリリース日にしたのは、今日が大安だったからです。リリース作業はそれほど大きなトラブルもなく行えたので、よい日だったと思います。みなさんも、大安にリリースしてみてはいかがでしょうか。
Sennaの後継となる組み込み型全文検索エンジンgroongaでインデックスを自動更新する方法を見つけたので紹介します。
「見つけた」という風に書いているのは、「ドキュメントには書いていないけどソースを見たらやり方がわかった」からです。
Sennaは転置インデックス関連の機能のみを提供していましたが、groonaでは転置インデックスだけではなく、データ管理の機能も提供しています。そのため、DBMSなど他のデータ管理機能を持つソフトウェアと組み合わせなくても、groongaだけでデータ管理と高速な全文検索機能を実現することができます。
groongaはGitHub上で開発されていて、groongaに関するドキュメントやgroongaのAPIのドキュメントもGitHub上にあります。
また、Sennaとgroongaの比較やgroongaデータベースAPIも読んでおくとよいと思います。
ここでは、上記のドキュメントには書いていなかったインデックスを自動更新する方法を紹介します。
コメント付きブックマークを管理し、コメントで全文検索できるプログラムを作成します。
ブックマークテーブル(<bookmarks>テーブル)には以下の2つのカラムがあります。
全文検索用の単語を登録するテーブル(<lexicon>テーブル)には以下の1つのカラムがあります。
ここでは、「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; } |
何度かここにも書いてあるように、クリアコードはWindows Mobile上で動作するブラウザFennecの改善に力をいれています。
改善した結果は積極的に本家にフィードバックしており、次のFennecのリリースには取り込まれる予定です。具体的には、以下の点が改善されます。
フォントまわりの改善で、日本語の表示問題が修正されたり、表示速度が高速化されたりします。起動まわりの改善で、使い勝手がよくなったりします。
MozillaサポートやMozilla本体・拡張機能の開発に関心のある方はinfo@clear-code.comまでお問い合わせ下さい。
2月26日のエントリのエントリでお伝えした通り、以前のWindows Mobile版Fennecには日本語表示に致命的なバグが存在していましたが、弊社エンジニアがこれを修正し、本家にも既にこの修正が取り込まれております。このため、現在mozilla.orgで公開されているNightlyビルドでも、バイナリを修正することなく日本語を表示することが可能です。 ただし、現在のバージョンではまだフォントの自動選択処理に不具合があるため、設定を正しく行っていない場合、インストールしたフォントや訪れたサイトによっては、文書の一部あるいは全てが文字化けする可能性があります。 そこで今回は、4/28現在のNigtlyビルドにおいて日本語を正しく表示する設定を紹介致します。
mozilla.orgのFTPサイトから最新のcabファイルを取得し、インストールします。その後、一度Fennecを起動して、プロファイルを作成します。プロファイルは \Application Data\Mozilla\Fennec\Profiles\xxxxxxxx.default フォルダ以下に作成されます。この後、設定ファイルを手動で変更しますので、一旦Fennecを終了します。
現在のFennecは、Windows Mobile日本語版に標準で搭載されているAC3形式のフォントに対応していません。このため、別途日本語TTFフォントをインストールする必要があります。ここでは例として、VLゴシックをインストールします。
プロファイルフォルダ以下の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に変更します。
以上でほぼ日本語の表示は可能なのですが、これでもなおフォーム内の文字が化ける場合があります。今回はユーザー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ビルドにはまだ含まれていないものの、日本語入力時の未確定文字列が表示されない問題等についても修正を行い、本家にフィードバックしています。
データベース機能も備える全文検索エンジン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には以下のような既知の問題があります。
GCの問題は、groongaのgrn_ctxというメモリ管理機能を提供するオブジェクトとRubyがオブジェクトをどの順番でGCするかはわからないという動作のために起きています。これはgroonga本体とも協調しながら解決する予定です。ちなみに、通常のアプリケーションでこの問題が発生する可能性があるのはプロセスの終了時だけです。そのため、この問題のためにデータが壊れてしまうということはないと考えられます。
今回のリリースではよりRubyらしいAPIの提供の優先度を高くしたため、高速化のためのチューニングはそれほど行われていません。いくつかチューニング案があるので、それらを適用することにより、Rubyの読み書きしやすいAPIを利用しながらより高速な全文検索機能とデータベース機能を利用できるようになるでしょう。
ドキュメントが完備されていないのは、APIがまだ流動的なことやgroongaのすべての機能を網羅していないこととも関係があります。groonga本体もまだAPIが改良され続けています。それに追従したり、より使いやすいAPIを目指してRuby/groongaのAPIはこれから変更されるでしょう。その過程でドキュメントも充実していく予定です。
ラングバプロジェクトではRuby/groongaの開発に参加してくれる人を募集しています。興味のある方は開発者向け情報をご覧ください。
クリアコードではRubyの拡張機能を書けるプログラミングが好きな開発者を募集しています。興味のある方は採用情報をご覧ください。
*1 -O0オプション(非最適化オプション)をつけるとすぐにビルドできます。