クリアコードはプログラミングが好きなソフトウェア開発者を2名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyに関連した開発を得意としています。
注: 長いです。
日本Ruby会議2010でるりまサーチの作り方について発表しました。
|
2010-08-30 |
[rk10][29S06] るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索 (32:59) cache!! |
ステージから見た感じだと立ち見の人もいたようでした。セッションに参加してくれたみなさん、会場を担当してくれたりレポートしてくれたスタッフのみなさん、ありがとうございました。
時間の関係で省略したことも含めてまとめておきます。
資料の中では、まず、るりまサーチについて説明し、その後、全文検索システムとしてのるりまサーチをどう作るのかを説明しています。
るりまサーチはRubyリファレンスマニュアル刷新計画 (通称るりま)の成果物であるRuby本体のリファレンスマニュアルを全文検索するためのWebアプリケーションです。るりまサーチが必要とされていた理由は、既存のリファレンスマニュアル閲覧Webアプリケーションに組み込まれていた検索機能の速度が遅かった*1からです。せっかく有益なリファレンスマニュアルがあっても、目的のエントリにたどりつくのが難しければ、有効に活用することができません。検索機能の面からリファレンスマニュアルの有効活用を支援する全文検索システムがるりまサーチです。
るりまサーチはRubyのリファレンスマニュアルに特化した小さな全文検索システムですが、最近の全文検索システムにとって重要なエッセンスが含まれています。全文検索システムを開発する場合はこれらのエッセンスを含めることを検討してみてください。
まず1つ目はドリルダウンと呼ばれる機能です。Solrなど他の全文検索システムによってはファセットと呼ぶこともあります。ドリルダウンとは、通常の検索結果に加えて、別のパラメータでの絞り込み結果も同時に提供する機能です。スライド中では「Rubyのバージョンで絞り込んだ結果、何件ヒットするか」という情報も表示しています。
この機能で嬉しいことは以下の2点です。
どちらもユーザの使い勝手を向上させるインターフェイスにつながります。ショッピングサイトなどでも使われているインターフェイスですね。
2つ目はURLのパスに絞り込み条件を含めることです。これは、内部ネットワーク用の全文検索システムではなく、インターネット上に公開する全文検索システム向けです。
最近ではURLにUTF-8でエンコードされたページ情報を含めることは一般的になってきました。WikipediaやAmazonでも行っています。Web検索エンジンはURLからも検索用の情報を抽出しているようなので、SEOになると考えられます。
3つ目はキャッシュです。より快適に検索・絞り込みを行うにはできるだけ速いレスポンスが求められます。レスポンスを高速化するためには、以下のような方法があります。
手軽に高速化する場合は後者のキャッシュ機能が便利です。キャッシュをする場合はキャッシュを無効化するタイミングを慎重に検討する必要があります。このタイミングを誤ると、期待した結果が返ってこないという問題が発生します。
キャッシュを無効にするタイミングはアプリケーションに依存します。一般的に、データが変更されるまでは同じキャッシュを利用できます。るりまサーチの場合は1日1回バッチ処理で元データを更新しています。そのため、同じキャッシュを1日使いまわすことができます。これにより高速にレスポンスを返すことができます。
また、キャッシュの効果を高めるためには、処理の内部よりもクライアントに近いところでキャッシュする必要があります。その方がより多くの計算を省略することができるからです。るりまサーチはログインせずに使えるシステムなので、同じ検索リクエストの結果はクライアントに関わらず同一になります。そのため、レスポンスをまるごとキャッシュすることができ、とても高い効果があります。
ログインが必要なシステムの場合は、クライアント毎に変更される部分のみJavaScriptで動的に生成したり、iframeを用いて別HTMLにすることにより、ログインによって変更されない部分ではキャッシュを利用することができます。それが難しい場合はもっと処理の内部でキャッシュをすることになります。この場合はキャッシュの効果が薄くなります。
キャッシュを用いることにより劇的にレスポンス速度を改善することができますが、キャッシュの有効期限とキャッシュする場所についてはよく検討する必要があります。
るりまサーチに含まれている最近の全文検索システムに重要なエッセンスは以下の3つです。
それでは、このようなエッセンスを含む全文検索システムるりまサーチの作り方について説明します。
全文検索システムは以下の5つの要素からなります。
まず、検索対象からクローラーが検索対象とする文書を収集します。次に、それらからインデクサーがテキストやメタ情報を抽出して全文検索エンジンに登録します。全文検索エンジンに登録したデータからユーザが求めるデータを検索して提示するのが検索インターフェイスです。
るりまサーチの場合は以下のようになります。
この中で、るりまサーチの重要な部分である全文検索エンジンgroongaについて説明します。
発表当日に初のメジャーバージョン1.0.0がリリースされたgroongaは、MySQLとの組み合わせで広く利用されているSennaの後継プロジェクトです。Sennaでのよいところを維持しつつ、さらに改良が加えられています。
Sennaは妥協しない転置索引実装と参照ロックしない更新アルゴリズムによるリアルタイム検索の実現が大きな特徴でした。Senna自体はデータストア機能を持たず、MySQLなど外部のデータストアと連携します。MySQLとSennaを連携させるソフトウェアはTritonnと呼ばれ、SQLで高速な全文検索機能を利用できることから広く使われています。しかし、MySQL側のロックモデルのため常に検索可能な状態で更新処理を行うことができません。そのため、せっかくのSennaの参照ロックフリーな更新アルゴリズムの特徴を活かしきれませんでした。
そこで、groongaでは独自のデータストア機能を提供し、外部のシステムによる制限を回避してgroongaの性能を発揮できるようにしました。データストアはドリルダウンを高速に実現できるカラム指向を採用しています。
また、HTTP/memcached/独自プロトコルなどのネットワークプロトコルも実装し、Solrのように検索サーバとして利用することもできるようになっています。
その他にも、より大規模な文書に対してもスケールするような性能改善や、モバイル端末の普及により重要性が増している位置情報データに対応するなど新規機能が含まれています。ただし、これらの改善のためにSennaとの互換性がなくなっています。Sennaの後継としてgroongaと名前を変更した理由はこのためです。
それでは、るりまサーチのケースを例にしてgroongaの使い方を説明します。手順は以下の通りです。
RDBと同じようにgroognaでも、まず、スキーマを定義します。
スキーマはRDBと同じように以下の3つの要素から構成されます。
RDBではさらに索引もでてきますが、groongaでは↑の3つの要素を使って索引を作成するので、RDBより特別な存在ではありません。
スキーマを定義するときは、まず、検索対象がなにかを考えます。そして、その対象がどのくらいの粒度で1エントリになるかを考えます。るりまサーチではリファレンスマニュアルが検索対象で、メソッドやクラスそれぞれが1つのエントリになります。検索対象全体をテーブルとし、エントリをテーブルの各レコードにします。るりまサーチでは検索対象全体を扱う「Entries」テーブルを定義しています。
テーブルには検索結果に表示したい内容と検索時に利用する内容をカラムとして定義します。るりまサーチの場合にはメソッド名やクラス名を格納する「name」カラムやドキュメントを格納する「description」カラムなどを定義しています。
検索対象用のテーブルを定義したら索引を定義します。ここがRDBと異なる部分です。全文検索用の索引では単語と文書を対応させる語彙表が必要になりますが、同じトークナイザー*2を利用している場合は同じ語彙表を共有して省スペース化したり、同じテキストに複数のトークナイザーを適用して検索精度や検索漏れのトレードオフを調整したり、といったRDBよりも細かい制御ができます。
単にヒットしたかどうかではなく、検索結果の重み付けも重要です。有用な検索結果を提供するためには、クエリに適していると思われる結果ほど上位に提示する必要があります。しかし、どのように重み付けをするのが適切かは全文検索システムに大きく依存します。そのため、groongaでは索引毎に重み付けをカスタマイズする機能を提供しています。
るりまサーチではメソッド名やクラス名に完全一致した場合はよりマッチしていると判断するように*3、名前と完全一致だけする語彙表「Names」テーブル*4を定義し、そこに「name」カラムの索引を定義します。検索時にはこの索引にマッチした場合は重み付けを大きくします。
ドキュメント部分(「summary」カラムと「description」カラム)はトークナイザーを設定した全文検索用の語彙表「Terms」テーブルを共有しています。こっちの索引にマッチした場合は重み付けを小さくします。
スキーマはgroongaが提供している組み込みのDDLで定義する方法と、groongaのRubyバインディングであるrroongaが提供するDSLで定義する方法があります。
groongaのDDL:
# 検索対象のテーブル table_create Entries TABLE_HASH_KEY ShortText # 全文検索用の語彙表。トークナイザーとしてN-gramを使用。 table_create Terms TABLE_PAT_KEY ShortText --default_tokenizer TokenBigram # 完全一致検索用の語彙表。トークナイザーはなし。 table_create Names TABLE_HASH_KEY ShortText # 検索対象のデータ格納場所 column_create Entries name COLUMN_SCALAR Names column_create Entries summary COLUMN_SCALAR Text column_create Entries description COLUMN_SCALAR Text # 全文検索用の索引 column_create Terms Entries_summary COLUMN_INDEX Entries summary column_create Terms Entries_description COLUMN_INDEX Entries description # 完全一致検索用の索引 column_create Names Entries_name COLUMN_INDEX Entries name
rroongaのDDL:
Groonga::Schema.define do |schema| # 完全一致検索用の語彙表。トークナイザーはなし。 schema.create_table("Names", :type => :hash, :key_type => "ShortText") do |table| end # 検索対象のテーブル schema.create_table("Entries", :type => :hash, :key_type => "ShortText") do |table| table.reference("name", "Names") table.text("summary") table.text("description") end # 全文検索用の語彙表。トークナイザーとしてN-gramを使用。 schema.create_table("Terms", :type => :patricia_trie, :key_type => "ShortText", :default_tokenizer => "TokenBigram", :key_normalize => true) do |table| # 全文検索用の索引 table.index("Entries.summary") table.index("Entries.description") end schema.change_table("Names") do |table| # 全文検索用の索引 table.index("Entries.name") end end
スキーマを定義したらデータを登録します。索引は自動で更新されるため、データ用のカラムにデータを登録するだけで動作します。
データの登録方法はgroongaのloadコマンドを使う方法と、rroongaを使う方法があります。
groongaのloadコマンド:
load --table Entries [ ["_key", "name", "summary", "description"], ["String#sub", "sub", "置換", "1つ置換"], ["String#gsub", "gsub", "置換", "全部置換"] ]
rroonga:
entries = Groonga["Entries"] entries.add("String#sub", name: "sub", summary: "置換", description: "1つ置換") entries.add("String#gsub", name: "gsub", summary: "置換", description: "全部置換")
Rubyで登録データの前処理を行う場合はrroongaを使う方がよいでしょう。Ruby以外で処理を行う場合はデータからJSONを生成し、groongaのloadコマンドを使う方がよいでしょう。るりまサーチはRubyで前処理*5をしているのでrroongaでデータを登録しています。
全文検索する場合は検索対象のカラムを指定する方法と、明示的に利用する索引を指定する方法の2通りあります。カラム単位で重み付けをしたい場合はカラムを指定し、索引単位で重み付けをしたい場合は索引を指定します。両方の指定方法を混ぜ合わせることもできます。
データの登録方法はgroongaのselectコマンドを使う方法と、rroongaを使う方法があります。
groongaのselectコマンド:
# 「description」カラムに「1つが」含まれているエントリを検索 select Entries description "1つ" [[...], [[[...], [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]], [..., "String#sub", "sub", "置換", "1つ置換", ...], ...]] # 「sub」が含まれているエントリを検索。ただし、「name」が # 「sub」だった場合は重みを大きくする。 select Entries "name * 100 | summary | description" "sub" [[...], [[[...], [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]], [..., "String#sub", "sub", "置換", "1つ置換", ...], ...]]
groongaのselectコマンド(HTTP経由):
# 「description」カラムに「1つが」含まれているエントリを検索 % wget -O - 'http://localhost:10041/d/select?table=Entries&match_columns=description&query=1つ' [[...], [[[...], [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]], [..., "String#sub", "sub", "置換", "1つ置換", ...], ...]] # 「sub」が含まれているエントリを検索。ただし、「name」が # 「sub」だった場合は重みを大きくする。 % wget -O - 'http://localhost:10041/d/select?table=Entries&match_columns=name*100|summary|description&query=sub' [[...], [[[...], [..., ["_key", ...], ["name", ...], ["summary", ...], ["description", ...], ...]], [..., "String#sub", "sub", "置換", "1つ置換", ...], ...]]
rroonga:
entries = Groonga["Entries"] # 「description」カラムに「1つ」が含まれているエントリを検索 result = entries.select do |record| record.description =~ "1つ" end # 「sub」が含まれているエントリを検索。ただし、「name」が # 「sub」だった場合は重みを大きくする。 result = entries.select do |record| target = record.match_target do |match_record| (match_record["name"] * 100) | (match_record["summary"]) | (match_record["description"]) end target =~ "sub" end
PHPなどRuby以外の言語から利用する場合はgroongaサーバを立てて、HTTP経由で検索するのがよいでしょう。Rubyから利用する場合は、selectコマンドで十分ならselectコマンドを利用、より複雑なことをしたい場合はrroongaを利用するのがよいでしょう。selectコマンドでもドリルダウンはサポートされて入るので、多くの場合はselectコマンドで十分でしょう。
るりまサーチでは、selectコマンドが提供するクエリ書式を利用したくない、rroongaが提供するページネーション機能を利用したい、などの理由でselectコマンドではなくrroongaを使っています。rroongaを利用してドリルダウンを実現する例にもなっています。
るりまサーチを例にして、groongaを用いて全文検索システムを開発する場合の基本的な流れを説明しました。より詳しいことはGitHubるりまサーチのリポジトリにあるソースコードを見てください。
るりまサーチの検索WebインターフェイスはRuby 1.9とRackの上に構築されています*6。るりまサーチを開発した際に、るりまサーチ以外でも使えそうな部分がでてきたので、rackngaという名前でるりまサーチと別パッケージとして公開しています。
rackngaにはRackのミドルウェアとMuninプラグインが含まれています。MuninのプラグインはPassengerの以下の情報を収集します。
Rackのミドルウェアは1つずつ説明します。
アプリケーション内でエラーが発生した場合にメールでその内容を通知するミドルウェアです。RailsのException NotifierのRack用です。
以下のように利用します。
config.ru:
require 'racknga' notifier_options = { "host" => 127.0.0.1, "from" => "rurema@example.com", "to" => "developer@example.com", "charset" => "iso-2022-jp", "subject_label" => "[るりまサーチ] ", } notifiers = [Racknga::ExceptionMailNotifier.new(notifier_options)] use Racknga::Middleware::ExceptionNotifier, :notifiers => notifiers # ... run your_application
できるだけ多くのエラーを検出するためになるべく最初の方でuseしてください。
主にサーバ1台や2台などで処理できる程度の中規模のPassenger環境で利用することを想定したキャッシュミドルウェアです。ヘッダーやボディを含めHTTPのレスポンス全体をgroongaのデータストアにキャッシュします。Passengerでは複数のインスタンスが別プロセスで起動しますが、groongaは複数プロセス間で同一のデータベースを操作することができるため、別のインスタンスがキャッシュした内容を他のインスタンスから参照することができます。以下のように利用します。
config.ru:
require 'racknga' require 'racknga/middleware/cache' # ... # use Rack::Deflater # use Rack::ConditionalGet # ... base_dir = Pathname.new(__FILE__).dirname.cleanpath.realpath cache_database_path = base_dir + "var" + "cache" + "db" use Racknga::Middleware::Cache, :database_path => cache_database_path.to_s run your_application
他のミドルウェアと組み合わせやすいように、なるべくアプリケーションに近い部分に置くことをよいでしょう。
複数のサーバ間でキャッシュを共有したい場合は別の仕組みを利用することをオススメします。
ネットワーク帯域を節約するためには、レスポンスを圧縮して返すことが有効です。しかし、Internet Explorer 6では問題があることがわかっています。そのため、Internet Explorer 6の場合は常に圧縮しないようにするのがこのミドルウェアです。Rack::Deflaterのラッパーです。以下のように利用します。
config.ru:
require 'racknga' # ... use Racknga::Middleware::Deflater # use Rack::ConditionalGet # ... run your_application
Web APIとしてサービスを提供する場合、JSON形式で結果を返すことが多くなっています。クライアント側でWeb APIにアクセスする場合はJSONPを利用することになります。
このミドルウェアはJSONPに対応しておらず単にJSONデータを返すだけのアプリケーションをJSONPに対応させることができます。また、以下のような配置にすることにより、キャッシュを有効にしたままJSONP対応にすることができます。
config.ru:
require 'racknga' require 'racknga/middleware/cache' use Rack::Middleware::JSONP base_dir = Pathname.new(__FILE__).dirname.cleanpath.realpath cache_database_path = base_dir + "var" + "cache" + "db" use Racknga::Middleware::Cache, :database_path => cache_database_path.to_s run your_application # "Content-Type: application/json"のレスポンスを返す
現在、るりまサーチはWebサービスを提供していませんが、将来の拡張を念頭においてこのミドルウェアがrackngaに含まれています。
るりまサーチはドリルダウンやキャッシュを利用することにより、快適に目的のドキュメントへ到達できるような工夫をしています。るりまサーチ以外にもリファレンスマニュアルを利用するツールがあるので有効活用しましょう。
ドリルダウンを効果的に利用した高速な全文検索システムにはgroongaが適しています。Rubyとの親和性も高いgroongaで全文検索システムを開発してみてはいかがでしょうか。汎用ユーティリティであるrackngaも一緒に用いることにより開発・運用が改善されるでしょう。
最後にお知らせです。クリアコードではプログラミングが好きな開発者を募集しています。プログラミングが好きな人は検討してみてください。
前作プログラミングRubyのRuby 1.9対応版です。
前作:
オーム社
¥ 3,990
オーム社
¥ 4,410
このシリーズはページ数からもわかる通り、Rubyについて細かいところまで網羅しているのが特徴です。例えば、Proc.newとlambdaの違いはわかりますか?以下のようなクラス定義の書き方による参照解決の違いはわかりますか?
module MyLibrary class MyClass # ... end end class MyLibrary::MyClass # ... end
これまでのシリーズと同様、本書でもそのあたりの細かいところにも触れています。もちろん、1.8と1.9で変わった部分についても触れています。このシリーズでRubyを覚えたという人もわりといるようですが、それはこの網羅性が役に立ったのではないでしょうか。
おそらく、1回読んだだけではすべてを覚えることは無理でしょう。読んで、実際にRubyのコードを書いて、気になったところをまた調べ直す、そのサイクルができるのがこのシリーズです。このシリーズでRubyを覚えた人はこのようなサイクルを使ったのではないでしょうか。先日1.9.2がリリースされ、これから1.8から1.9への移行がより進むと考えられます。そのときに、つまずいたところを本書で調べて理解していくというサイクルに使えるでしょう。
Rubyは(わりと)読みやすいコードを(わりと)書きやすい言語仕様になっています*1が、複雑なこともいろいろできる仕様も多く含まれています。ほとんどの場合は複雑なことはしなくても済むはずですし、そのように書いておく方がよい場合の方が圧倒的に多いです。一度、(わからない部分があったとしても)全体を一通り読んでおいて、Rubyの動作はどうなっているかをざっくりと知っておくとよいでしょう。複雑なことをしそうになったときに、別の方法もあった気がする、と気付けるようになるくらいで十分です。気付けたら本書なりるりまなりで詳しく調べることができます。
本書は分量の多さから言語編とライブラリ編の2編構成になっています。言語編に比べてライブラリ編の内容が手薄になっているので気をつけてください。必ずしもそれぞれのライブラリの最新の情報に追従できているわけではありません。本書をきっかけにしてるりまやWeb上での情報などで補正する必要があるでしょう。
試しにRubyを勉強してみたい、という人には敷居が高いでしょう*2。しかし、Rubyを使いこなそうというくらいの気持ちがあるのであれば、助けになってくれることでしょう。
オーム社
¥ 3,990
オーム社
¥ 4,620
早いもので来週末は日本Ruby会議2010です。日本Ruby会議では、たくさんいる(会ったことはないけど名前を知っている)参加者がお互いを認識しやすいように大きな名札をつけることが恒例となっています。
RubyKaigi日記でも名札には名前を大きく書きましょうと呼びかけています。この中で、「あらかじめ太くて大きなフォントで、黒々と印刷してきたものを持参して、名札に貼り付けるのはいかがでしょう。」と提案しています。しかし、自分でデザインするのはわりと面倒なものです。
そこで、kdmsnrさんが名札には名前を大きく書きましょうジェネレータを作りました。これはtwitter IDを指定するだけでRubyKaigi日記で提案されているようなデザインの画像を生成してくれます。
でも、印刷するならPDFの方が嬉しいよね、ということでPDFを出力できるように改造したのが名札には名前を大きく書きましょうジェネレータ改です。
PDFも出力できるようにした他に、フォントを選べたり、細かく調整するためにSVGも出力できるようにしています。それでは、どのように実現しているかを説明します。
描画にはcairo、文字の配置にはPango、画像の読み込みにはGdkPixbufを用いています。どれもLinux、*BSD、Mac OS X、Windowsなど多くの環境で動作するライブラリです。ここでは、cairoとPangoだけ説明します。
cairoは2次元グラフィックを生成するためのライブラリで以下のような特長があります。
ベクトルベースのAPIとなっているということは品質を落とさずに拡大・縮小ができるということです。ジェネレータ改では、実際のサイズの画像とサムネイル画像を生成しますが、このようなことが以下のように描画処理を変更せずに実現できます。
def render(context) # 実際のサイズの描画 end # 実際のサイズを描画するとき render(context) # 1/3サイズのサムネイルを描画するとき context.scale(1 / 0.3, 1 / 0.3) # 描画処理の前にこれを呼ぶだけでOK render(context)
描画処理のコードを変更せずに出力先を変えることができると、描画結果はPNGにしてブラウザで確認、印刷するときはPDF、編集する時はSVG、というように用途にあわせたグラフィックのフォーマットを提供することが簡単にできるということです。これは今回のようなWeb上で印刷物を生成する場合はとても便利な機能です。いちいちPDFで確認するのは面倒ですよね。サムネイルで一覧表示する場合もPNG+ブラウザの方が便利です。
cairoの詳しい使い方はRubyist Magazine - cairo: 2 次元画像描画ライブラリを見てください。
Pangoは多言語に対応したテキストの配置を行うライブラリです。フォントの扱いなどテキストの配置に関することを抽象化してくれるので、TTFやOTFなどフォントフォーマットの違いや、フォントファイルをどこに置くかなどをプログラム側で気にする必要がありません。
ジェネレータ改ではインストールされているフォントを列挙したり、できるだけ大きいテキストサイズを自動検出するためにPangoを利用しています。テキストを中央揃えにするのもPangoの機能を利用しています。
システムにインストールされているフォントの一覧は以下のように取得できます。
font_families = Pango::CairoFontMap.default.families.collect do |family| family.name end p font_families # => ["Mona", "梅明朝S3", "衡山毛筆フォント草書", ...]
残念ながらPangoを利用するためのまとまった日本語の資料はありません。興味のある人はソースコードを見てください。
名札には名前を大きく書きましょうジェネレータ改をネタにしてcairoとPangoを紹介してみました。cairoとPangoはFirefoxやGTK+などデスクトップで使われることが多いライブラリですが、Webアプリケーションのようにサーバサイドでも有用なライブラリです。ジェネレータ改のように用途にあわせて画像のフォーマットを使い分けたい場合は、cairoとPangoを使ってみてはいかがでしょうか。また、日本Ruby会議2010に参加する人はジェネレータで作った名札を印刷して持っていってはいかがでしょうか。
名札には名前を大きく書きましょうジェネレータ改のソースコードはGitHubにあります。
毎年開催規模が大きくなっている日本Ruby会議に今年も参加します。今年も去年と同じくスポンサーと発表者として参加します。
発表タイトルは「るりまサーチの作り方 - Ruby 1.9でgroonga使って全文検索」です。
発表内容はるりまサーチという、Rubyリファレンスマニュアル刷新計画 (通称るりま)の成果物であるRubyのドキュメントを全文検索するWebアプリケーションの関連技術を紹介するというものです。もう少し具体的にいうと以下のような話題になります。
Twitterなどを見てもわかる通り、世界には情報がどんどん増えていきます。そうすると、その中から必要な情報を選ぶことが重要になっていきます。しかし、情報が溢れた世界では人力のみで効率よく情報を選択することは困難です。最近Twitterが提供をはじめた「おすすめユーザー」という機能も、溢れかえった情報の中から必要な情報を見つけることを支援するための機能と言えます。
必要としている人が必要な情報を見つけやすくしたい、そんなアプリケーションを作りたいと考えている人に聞いて欲しい内容です。もしかしたら、groongaが提供する必要な情報を見つけるための機能でそれを実現できるかもしれません。発表日時は最終日8/29(日)の13:30-14:00で、場所は中ホールです。同じ時間帯に、別の場所ではかずひこさんの外国で暮らすRubyistだけど何か質問ある?、TermtterKaigi、MSWin32版Ruby野良ビルダー養成塾などありますが、こちらは30分なので、こちらの発表の時間だけ抜け出すことも考えてみてください。
日本Ruby会議2010で発表する予定の内容を紹介しました。面白そうだと思った方はぜひ参加してみてください。チケットはまだ少し残っているようです。また、チケットを譲りたいという方もいるので、まだチケットを持っていない方は連絡をとってみるのもよいかもしれません。
それでは、日本Ruby会議2010でお会いできることを楽しみにしています。
採用を再開しました。ソフトウェア開発者を2名募集しています。応募条件はプログラミングが好きなことだけです。学歴や年齢などは関係ありません。勤務地は東京都文京区または栃木県小山市になる予定です。詳しくは採用情報を見てください。日本Ruby会議2010の会場にはクリアコードの人が3人はいるはずなので、そのときに声をかけてもらえればその場でも説明します。
groongaなどを使って全文検索システムを作るときは、PDFやオフィス文書などからテキスト情報を抜きだして検索用インデックスを作る必要があります。Windowsでテキストを抽出するソフトウェアとしてはxdoc2txtなどがありますが、ここでは、Linuxサーバ上でテキストを抽出する方法を紹介します。
Linux上でPDFを閲覧する場合は、昔はXpdfでしたが、最近はEvinceやOkularの方がよく使われているようです。どちらもPDFの処理にはXpdfからforkしたPopplerというライブラリを使っています。
popplerにはPDFからテキストを抽出するpdftotextというコマンドが付属しているため、それを利用してPDFからテキストを抽出できます。
% pdftotext hello.pdf hello.txt
これでhello.pdfのテキスト情報がhello.txtに出力されます。
WordやOpenOffice.org Writerで作成した文書はAbiWordでテキスト情報を抽出できます。
AbiWordは通常はGUI付きで起動しますが、オプションを指定することによりフィルタコマンドとしても利用できます。
Word文書からテキストを抽出するコマンドは以下の通りです。
% abiword --to txt --to-name hello.txt hello.doc
OpenDocumentFormatテキスト文書からテキストを抽出する場合も同じオプションです。
% abiword --to txt --to-name hello.txt hello.odt
ExcelやOpenOffice.org Calcで作成したスプレッドシートはGnumericに付属するssconvertでCSVに変換できます。
ExcelスプレッドシートをCSVに変換するコマンドは以下の通りです。
% ssconvert --export-type Gnumeric_stf:stf_csv hello.xls hello.csv
OpenDocumentFormatスプレッドシートをCSVに変換する場合も同じオプションです。
% ssconvert --export-type Gnumeric_stf:stf_csv hello.ods hello.csv
PowerPointやOpenOffice.org Impressで作成したスライドをテキストに変換する方法はいくつかあるのですが、OpenOffice.orgを使う方法を紹介します。OpenOffice.orgを使う方法はWordやExcelの時も使えますが、事前に準備が必要なので、多少面倒です。
OpenOffice.orgを使って変換する方法にはOpenOffice.orgを変換サーバとして使う方法とコマンドとして使う方法がありますが、ここではコマンドとして使う方法を紹介します。コマンドとして使う場合もいろいろやり方がありますが、今回は、まずPDFに変換し、PDFからは上述のPopplerを使って変換することにします。
まず、以下の内容の~/.openoffice.org/3/user/basic/Standard/Export.xbaを作ります。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd"> <script:module xmlns:script="http://openoffice.org/2000/script" script:name="Export" script:language="StarBasic"> sub WritePDF(url as string) dim document as object dim dispatcher as object document = ThisComponent.CurrentController.Frame dispatcher = createUnoService("com.sun.star.frame.DispatchHelper") dim args1(1) as new com.sun.star.beans.PropertyValue args1(0).Name = "URL" args1(0).Value = url args1(1).Name = "FilterName" args1(1).Value = "writer_pdf_Export" dispatcher.executeDispatch(document, ".uno:ExportDirectToPDF", "", 0, args1()) document.close(true) end sub </script:module>
そして、同じディレクトリにある~/.openoffice.org/3/user/basic/Standard/script.xlbに登録して、以下のような内容にします。<library:element>のところを追加しています。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE library:library PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "library.dtd"> <library:library xmlns:library="http://openoffice.org/2000/library" library:name="Standard" library:readonly="false" library:passwordprotected="false"> <library:element library:name="Export"/> </library:library>
これで準備ができました。以下のようにスライドをPDFに変換できます。
% ooffice -headless hello.ppt 'macro:///Standard.Export.WritePDF("file:///tmp/hello.pdf")'
PowerPointのファイル以外でもWord文書などOpenOffice.orgで開けるファイルを指定すれば、それらもPDFに変換できます。
以前はXがない状態では動かないため、Xvfbなど仮想的なXサーバを使ったものですが、最近のOpenOffice.orgはXがなくても動作するようです。すばらしい。
PDFに変換したら後は上述の方法でテキストを抽出できますね。
Linuxサーバ上でPDFやオフィス文書からテキストを抽出する方法を紹介しました。以前はwvTextやxlhtml、ppthtmlなどを使うことが多かったでしょうが、最近はもっと精度よくテキストを抽出できるようになっているので、ここで紹介したツールも試してみてはいかがでしょうか。
数ヶ月前、すでにPythonを知っている人向けのPythonの本が出版されました。オリジナルは2008年に海外で出版されたもので、これはその翻訳です。
アスキー・メディアワークス
¥ 3,780
さらにステップアップしたいPython開発者向けの内容なので、構文の説明など初級者用の内容はなく*1、どうやってPythonを使うのがよいかを書いてあります。プログラミングだけではなく、開発全体を意識しているのが実務的といえます。パッケージを作り方を説明するところでも、単にEggを作るだけではなく、PyPIに登録するところまで説明しています。他にもよい名前について1章使ったり、テスト駆動開発に1章使ったりと、しっかりと大事な話題はおさえています。ドキュメントについての章があるのもPythonらしいですね。
ただし、幅広く扱っている分、少し物足りない部分もあります。例えば、テスト駆動開発の部分は標準添付のunittestよりもnoseの方により重みをおいて説明した方がより実務的でしょう。
一番有用なのは付録のPython 2とPython 3のUnicode文字列についてまとめたところかもしれません。この部分は日本語版用に翻訳者たちが書き下ろしたもので、マルチバイト文字列を扱う機会の多い日本のPython開発者には特に有用でしょう。Python 2とPython 3は互換性がないため、文字列の扱いを移行するときにこの付録が役に立つことでしょう。
また、オリジナルが出版されてから現在までにPython界隈の状況も変わってきていますが、それについて訳注で補足されているのもうれしいところでしょう。新しく書くコードでは最新の状況にあわせたコードにしたいものです。
エキスパートPythonプログラミングの内容を簡単に紹介しました。
エキスパートPythonプログラミングではまったく触れられていませんが、単体テストフレームワークはunittestやnoseよりもPikzieがオススメです。
*1 むしろ最初にインストール方法があるのが違和感。ただ、MinGWなどもインストールするように書いてあるので、初心者用ではない。
とあるRails 3を使っているたいやき用のCMSでDeviseを使ってOpenID認証をするようにしたので、そのやり方を紹介します。RubyはRuby 1.9.2 RC2も出ていますが、今回はRuby 1.9.1を使います。
DeviseはRackベースの認証システムです。バックエンドにWardenを利用しているため、Basic認証やOpenID、OAuthなど認証方法を切り替えることができます。
ただ、以下の説明を読んでみてもらってもわかる通り、動き出すまでにそこそこの作業が必要になります。機能は豊富なので、動き出したらカスタマイズしてアプリケーションの要求に合わせていくことができるでしょう。日本語での情報もあまりありませんが、探せばいくつかはあるので、試してみてはいかがでしょうか。
とはいえ、今回はDeviseのデフォルトの認証方法ではなく、OpenIDでのみ認証することにします。また、未登録のユーザがログインしようとしたときは自動的に新規ユーザを作成することにします。このようにも使えるという例ということで読むとよいかもしれません。
まず、Rails 3.0 beta4をインストールします。
% sudo gem1.9.1 install rails --pre
サンプル用のアプリケーションを作ります。
% ruby1.9.1 rails new taiyaki % cd taiyaki
次にDeviseをインストールします。
% sudo gem1.9.1 install devise --version=1.1.rc2
インストールしたDeviseを利用するため、以下のようにGemfileに追記します。
Gemfile:
gem 'devise', "1.1.rc2"
アプリケーションにDeviseが動作するために必要なファイルをインストールします。config/initializers/devise.rbなど主に設定ファイルです。
% ruby1.9.1 script/rails generate devise:install
いくつかは手動で設定する必要があります。それぞれ以下の通りです。
Deviseはパスワードの再設定をする機能もあり、そのときはユーザにメールを送信します。そのような機能を使うときはActionMailerのURL生成オプションを設定する必要があります。例えば、開発時のホスト情報を設定する場合は以下のようになります。
config/environments/development.rb:
config.action_mailer.default_url_options = {:host => 'localhost:3000'}
Deviseはリダイレクト先のURLを生成するときなどにデフォルトではroot_pathを使うので、rootパスへのマッピングを追加します。以下の例ではwelcome#indexを指定しているので、後でWelcomeControllerを作ります。
config/routes.rb:
root :to => "welcome#index"
Deviseはnoticeとalertのflashを設定するので、レイアウトに追加しておくとよいでしょう。例えば、以下のようにyieldの前に追加します。
app/views/layouts/application.html.erb:
<%# ... %> <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p> <%= yield %> <%# ... %>
これでインストールは完了したので、コントローラーやモデルを作成します。
まず、ユーザー用のモデルを作成します。
% ruby1.9.1 script/rails generate devise User
このとき生成されるスキーマは、以下のようにデータベース上にパスワードのダイジェストなどの情報を持ち、それを利用して認証することになります。
db/migrate/XXXX_devise_create_users.rb:
class DeviseCreateUsers < ActiveRecord::Migration def self.up create_table(:users) do |t| t.database_authenticatable :null => false t.recoverable t.rememberable t.trackable # t.confirmable # t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both # t.token_authenticatable t.timestamps end add_index :users, :email, :unique => true add_index :users, :reset_password_token, :unique => true # add_index :users, :confirmation_token, :unique => true # add_index :users, :unlock_token, :unique => true end def self.down drop_table :users end end
しかし、今回は自分では認証情報を持たずにOpenIDで認証するので、データベース上に認証情報を持たないようにします。代わりにOpenID用のカラムを追加します。
db/migrate/XXXX_devise_create_users.rb(変更後):
class DeviseCreateUsers < ActiveRecord::Migration def self.up create_table(:users) do |t| t.string :email t.string :nickname t.string :identity_url t.string :fullname t.string :birth_date t.integer :gender t.string :postcode t.string :country t.string :language t.string :timezone t.rememberable t.trackable t.confirmable t.lockable :lock_strategy => :failed_attempts, :unlock_strategy => :both t.token_authenticatable t.timestamps end add_index :users, :identity_url, :unique => true add_index :users, :email, :unique => true add_index :users, :confirmation_token, :unique => true add_index :users, :unlock_token, :unique => true end def self.down drop_table :users end end
変更したらスキーマを反映させます。
% rake1.9.1 db:migrate
モデルのコードにもデータベースで認証するためのコードが入っています。今回は必要のないユーザ登録用の:registerableオプションやパスワードの入力チェックなどをする:validatableオプションなどは外します。password_required?メソッドをオーバーライドしているのは、OpenIDで認証するためパスワードが必要がないからです。
app/models/user.rb:
class User < ActiveRecord::Base ... # devise :database_authenticatable, :registerable, # :recoverable, :rememberable, :trackable, :validatable devise :database_authenticatable, :rememberable, :trackable ... def password_required? false end end
Deviseではログイン画面などデフォルトのビューも提供してくれますが、今回はOpenIDを使った認証にするためビューをカスタマイズします。ビューをカスタマイズする場合は、コントローラーごとカスタマイズする方法と、ビューだけカスタマイズする方法がありますが、今回はコントローラーごとカスタマイズする方法にします。
コントローラーをカスタマイズするにはconfig/routes.rbに追加されたdevise_forで:controllersオプションを指定します。以下のように指定するとUsers::SessionsControllerコントローラーを使います。
config/routes.rb:
devise_for(:users,
:controllers => {:sessions => "users/sessions"})
コントローラーを作成します。
% ruby1.9.1 script/rails generate controller Users::Sessions
コントローラーをカスタマイズする場合は、ApplicationControllerではなくDevise::SessionsControllerを継承します。
app/controllers/users/sessions_controller.rb:
class Users::SessionsController < Devise::SessionsController end
ログインフォームではOpenID用の識別子を入力してもらうようにします。
app/views/users/sessions/new.html.erb:
<%= form_for(resource,
:as => resource_name,
:url => session_path(resource_name)) do |f| %>
<p>
<label for="openid_identifier" >OpenID URL:</label>
<%= text_field_tag :openid_identifier %>
</p>
<p><%= f.label :remember_me %> <%= f.check_box :remember_me %></p>
<p><%= f.submit "Login" %></p>
<% end %>
あとは、トップページを準備すれば画面を確認することができます。
トップページ用のコントローラーを生成します。
% ruby1.9.1 script/rails generate controller welcome index % rm public/index.html
トップページではログインページに移動できるようにします。ログイン時はログイン中のユーザ情報を表示します。
app/views/welcome/index.html.erb:
<h1>Welcome#index</h1>
<% if user_signed_in? %>
<p>ようこそ<%= current_user.nickname %>さん</p>
<%= link_to("ログアウト", destroy_user_session_path) %>
<% else %>
<%= link_to("ログイン", new_user_session_path) %>
<% end %>
サーバを起動します。
% ruby1.9.1 script/rails server
http://localhost:3000/にアクセスすると以下のような画面になります。

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

それではOpenIDに対応します。DeviseからOpenIDを使うために、warden-openidを使います。
% sudo gem1.9.1 install warden-openid
Gemfileにも追記します。
Gemfile:
gem 'warden-openid'
WargenでOpenIDを使うようにします。
config/initializers/devise.rb:
Devise.setup do |config| ... config.warden do |manager| manager.default_strategies(:openid, :scope => :user) end end
OpenIDの設定をします。warden-openidではOpenIDの認証が成功した時にコールバックが実行され、そこで認証情報に対応したアプリケーション用のユーザを返すことになります。今回は、ここで、ユーザが存在しない場合は自動的に新規ユーザを作成することにします。
config/initializers/openid.rb:
Rails.application.config.middleware.insert(Warden::Manager, Rack::OpenID) Warden::OpenID.configure do |config| config.required_fields = User.required_open_id_fields config.optional_fields = User.optional_open_id_fields config.user_finder do |response| user = User.find_by_identity_url(response.identity_url) if user.nil? user = User.new user.extract_open_id_values(response) unless user.save message = "failed to create user: " message << "#{users.errors.full_messages.inspect}: " message << user.inspect Rails.logger.error(message) user = nil end end user end end
OpenIDの情報とアプリケーションのユーザ情報をマッピングする処理はモデルで行います。
app/models/user.rb:
class User < ActiveRecord::Base REQUIRED_FIELDS = { :nickname => "nickname", } OPTIONAL_FIELDS = { :email => "email", :fullname => "fullname", :birth_date => "dob", :gender => "gender", :postcode => "postcode", :country => "country", :language => "language", :timezone => "timezone" } class << self def required_open_id_fields REQUIRED_FIELDS.values end def optional_open_id_fields OPTIONAL_FIELDS.values end end def password_required? false end def extract_open_id_values(response) profile_data = {} [OpenID::SReg::Response, OpenID::AX::FetchResponse].each do |response_class| data_response = response_class.from_success_response(response) profile_data.merge!(data_response.data) if data_response end [REQUIRED_FIELDS, OPTIONAL_FIELDS].each do |fields| fields.each do |model_key, profile_key| unless profile_data[profile_key].blank? self.send("#{model_key}=", profile_data[profile_key]) end end end self.identity_url = response.identity_url self.nickname ||= identity_url end end
一応、ニックネーム情報は欲しいとリクエストしますが、もらえなくてもなんとなく動くようになっています。この状態でログインページにOpenID識別子を入力して、認証に成功するとアプリケーションにログインする事ができます。

ただし、現在リリースされているruby-openidはRuby 1.9のEncodingに対応していないため、認証中にASCII以外の文字列を含むページにアクセスすることになると失敗します。これを修正する方法はRuby 1.9.1 supportで報告済みですが、まだ取り込まれていません。
Rails 3.0 beta4でDeviseを使ってOpenID認証する方法を紹介しました。Rails 3で認証まわりはどうしようか、と考えていている人は試してみるとよいかもしれません。ただ、betaやrcのものを使っているので、これから使い方は変わっていく可能性が高いと考えられます。注意してください。
RubyらしいAPIでLDAPのエントリを操作できるライブラリActiveLdapの新しいバージョンがリリースされました。以下のようにgemでアップデートできます。
% sudo gem install activeldap
ActiveLdapについてはこのあたりを見てください。
今回のリリースではRuby on Railsの最新安定版2.3.8に対応しました。ActiveLdapは国際化対応のためにRuby-GetText-Packageを使っています。そのため、ActiveLdapをRailsで使う場合にlocale_railsと一緒に使っている場合も多いでしょう。しかし、locale_railsの最新版はRails 2.3.8に対応していないので、locale_railsを利用している場合はアップデートするかどうかよく検討してください。(locale_railsのリポジトリ上では2.3.8に対応しているので、locale_railsのリリース版ではなくて未リリースのものを利用するのも対応策の1つです。)
LDAPといえば、日本Ruby会議2010では「Rubyで扱うLDAPのススメ」という企画があります。[ANN]RubyKaigi2010 企画 "Ruby で扱う LDAP のススメ" にご協力頂ける方を募集しています - tashenの日記ということなので、ぜひ、ご協力をお願いします。
クリアコードは6月が期末なので今日が第4期の最終日になります。
今期になって初めて社員が増えました。2名増えたのでクリアコードは全員で7名になりました。
また、今期も前期と同様にいろいろなイベントに発表者として参加し(トップページにリストがあります)、これまで以上にたくさんの方とお話しすることができました。
業務でも継続してフリーソフトウェアの開発に関わってきました。いくつかはククログでも紹介してきました。
明日から第5期になります。これからもクリアコードをよろしくおねがいします。
Muninのプラグインを作るときなど、大きなサイズのログファイルを解析する必要がたまにありますよね。そんなとき、ファイルの先頭から処理をしていくとファイルサイズが増加するにしたがって処理時間も増えていってしまいます。Muninのプラグインの場合は最近5分間のデータだけあれば十分なので、ファイルの先頭からではなく、最後から処理する方が効率的です。最後から処理すると、ファイルサイズが大きくなっても処理時間にはほとんど影響がありません。
ということで、ファイルの最後から1行ずつ読み込む小さなRubyのクラスを作りました。groongaのリポジトリに入っているので、groongaと同じライセンスで利用できます。
class ReverseLineReader def initialize(io) @io = io @io.seek(0, IO::SEEK_END) @buffer = "" @data = "" end def each separator = $/ separator_length = separator.length while read_to_buffer loop do index = @buffer.rindex(separator, @buffer.length - 1 - separator_length) break if index.nil? or index.zero? last_line = @buffer.slice!((index + separator_length)..-1) yield(last_line) end end yield(@buffer) unless @buffer.empty? end private BYTES_PER_READ = 4096 def read position = @io.pos if position < BYTES_PER_READ bytes_per_read = position else bytes_per_read = BYTES_PER_READ end if bytes_per_read.zero? @data.replace("") else @io.seek(-bytes_per_read, IO::SEEK_CUR) @io.read(bytes_per_read, @data) @io.seek(-bytes_per_read, IO::SEEK_CUR) end @data end def read_to_buffer data = read if data.empty? false else @buffer.insert(0, data) true end end end
以下のように使います。
File.open("/var/log/groonga/query.log", "r") do |file| ReverseLineReader.new(file).each do |line| break if no_more_need?(line) # ... end end
ログファイルから直近のログだけを取り出して処理したいときなどに利用してみてはいかがでしょうか。