近況報告
第五十一回
最近は研究が忙しいのと,RailsをせっかくさわってるんだからBlogもRailsアプリにするべきじゃない?という短絡的な思考でtypoでBlogを作ってみました.
acts_as_ludia や,acts_as_find_or_initialize_by といったプラグインのこともちょこっと書いてます.
私自身が非常に飽きっぽいのでいつまで続くか分かりませんが,お時間があればのぞいてあげてください.
新しく作ったBlogはもちろんtypoがベースになっていますが,実はRuby-GetText-Packageを利用して日本語化しようと目論んでいたりします.設定画面の一部などが今は日本語になってきています.その話もおいおい.
typoベースの私のブログ
LudiaでWikipedia日本語版を対象にインデックスを構築するとかかる時間
第五十回
[myname@localhost wikipedia]$ rake db:migrate (in /home/myname/rails/wikipedia) == AddLudiaIndex: migrating =================================================== -- execute("CREATE INDEX plain_text_index ON documents USING fulltext(plain_text);") -> 3594.9286s == AddLudiaIndex: migrated (3594.9307s) =======================================
環境とか
VMWareServer1.01上で仮想サーバを構築して試しました.
ホストOS
- WindowsXP
- CPU 3.4GHz
- メモリ 2GB
ゲストOS
- Fedora Core 5
- 割り当てメモリ 1GB
- MeCab 0.95
- senna 1.0.7
- PostgreSQL 8.1.8
- Ludia 1.1.0
データの詳細
- Wikipedia:データベースダウンロード - Wikipediaの最新版 jawiki-latest-pages-articles.xml.bz2 を利用
- HyperEstraierの平林さんが作られた wpxmltoest を参考にテキスト抽出してデータベースへ格納
- 文書数は373,666件(countにも結構時間がかかる)
感想とか
ほぼ1時間でインデックス作成ってのは早いのかなどうなのかな.ちなみにHyperEstraierでは,
早速、「estcmd gather -xl -cs 640 casket .」でインデクシングしてみたところ、45分2秒で完了し、さらに「estcmd gather -um casket」でMeCabの補助インデックスを作ったところ、11分3秒で完了した。
とのことらしいので,ちょっとだけHyperEstraierの方が早・・と思ったけどこの記事は2005/12/28の記事でした.同じ記事には以下のようにも書いてあって,
wpxmltoestは62分22秒で完了した。162030件(1527MB)の文書を抽出できた。1文書当たり9.65KBだから、ASCIIコードも少し混じっているから、各文書の文字数は平均でだいたい4000文字くらいかな。
ということなので,私は倍以上の文書に対してインデックスを構築したようです.
しかし,WikipediaのXMLファイルからテキストを抽出してデータベースへ格納するのにはだいたい丸一日くらいかかりました.Railsが遅いのでしょうか.ものっすごいメモリ使いましたし.(私の書いたコードが悪いといううわさもチラホラ(笑)
データベースへのデータ格納用Railsコード
xml2sql --postgresql というやりかたも考えたんですが,事前に必要なPostgreSQLのテーブル構成がいまいちよく分からなかったのでRailsのActiveRecordに頼りました.こういう点ではORMのメリットですよね.
以下にデータベースへWikipediaの文書を格納するコードを載せておきます.ここはこうした方が早くなるとかあれば教えてください.
app/model/document.rb
class Document < ActiveRecord::Base class << self def import(start_id = 1) file = Pathname.new(RAILS_ROOT) + "db/data/jawiki-latest-pages-articles.xml" list = MyListener.new(start_id) source = File.new(file) REXML::Document.parse_stream(source, list) end class MyListener include REXML::StreamListener BASEURL = "http://ja.wikipedia.org/wiki/" TEXTMINSIZE = 256 def initialize(start_id) @document = nil @buf = nil @start = start_id.to_s @flag = false @count = 0 end def tag_start(name, attrs) if name == "page" @document = nil @buf = StringIO.new end return nil if skip? @buf.write(%|<#{REXML::Text::normalize(name)}|) attrs.each do |pair| @buf.write(%| #{REXML::Text::normalize(pair[0])}="#{REXML::Text::normalize(pair[1])}"|) end @buf.write(">") end def text(text) return nil if skip? @buf.write(REXML::Text::normalize(text)) end def tag_end(name) return nil if skip? @buf.write(%|</#{REXML::Text::normalize(name)}>|) if name == "page" begin proc_document(Hpricot.XML(@buf.string)) rescue => e puts e return ensure @count +=1 end if @document.plain_text @document.wiki_text = @buf.string @document.save end @buf.close exit if @count == 2 end end private def proc_document(doc) id = doc.at(:id).inner_text title = doc.at(:title).inner_text raise "no title error" if title.blank? if @flag elsif id == @start @flag = true else raise "not modified: count = #{@count}" end raise "no indexing document" if title =~ /(Media|特別|Wikipedia|利用者|ノート|画像|Template|Category|Portal)(:|;|-|=|‐)/ @document = Document.find_or_new_by_entity_id(id) @document.created_time = Time.parse(doc.at(:timestamp).inner_text) raise "not modified: #{@document.id ? @document.id : ''}" if @document.created_time < (@document.updated_on || Time.parse('2001-05-20T00:00:00Z')) @document.title = title if author = doc.at(:username) || doc.at(:ip) @document.author = author.inner_text else @document.author = "anonymous" end text = doc.at(:text).inner_text @document.plain_text = trimming(REXML::Text::unnormalize(text)) @document.url = BASEURL + CGI.escape(@document.title) end def trimming(text) return nil if self.blank? || (text.size < TEXTMINSIZE) || (text =~ /^#REDIRECT/) text.gsub!(/^=+([^=]+)=+/){ $1 } text.gsub!(/<[^>]+>/, "") text.gsub!(/^\s*[\*#:|;-]+\s*/, "") text.gsub!(/\[\[[^\]\|]+\|([^\]]+)\]\]/){ $1 } text.gsub!(/\[\[([a-zA-Z-]+:)?([^\]]+)\]\]/){ $2 } text.gsub!(/\{\{([^\}\|]+)\|[^\}]+\}\}/){ $1 } text.gsub!(/\{\{([^\}]+)\}\}/){ $1 } text.gsub!(/\[http:[^ \]]+ ([^\]]+)\]/){ $1 } text.gsub!(/'{2,}/, "") text.gsub!(/^ *\{?|/, "") text.gsub!(/^ *[\!\|\}]/, "") text.gsub!(/^\*+/, "") text.gsub!(/[a-zA-Z]+=\"[^\"].*\"/, "") text.gsub!(/[a-z][a-z]+=[0-9]+/, "") text.gsub!(/.*border-style.*/, "") text.gsub!(/.*valign=.*/, "") text.gsub!(/\&[a-zA-Z]+;/, "") text.gsub!(/.*(利用者|会話|ノート):.*/, "") text.gsub!(/(Wikipedia|Category):/, "") text.gsub!(/.*語:/, "") text.gsub!(/^thumb\|/, "") text.gsub!(/画像:/, "") text.gsub!(/^[ +]*[\|]*/, "") text.gsub!(/\|\|/, " ") text.gsub!(/\s/, " ") return text end def skip? return true unless @buf return true if @buf.closed? end end end end
acts_as_ludia を使ってLudiaのデモWebアプリケーションを作りました
第四十九回
先日のエントリ「Ludia 用の Rails プラグイン acts_as_ludia を作りました - のほほん徒然」で紹介したacts_as_ludiaプラグインを使って,簡単な検索アプリケーションをRailsで作ってみましたので紹介します.
はてなブックマーク全文検索
http://www.unraki.com:3000/bookmarks
これはなに?
id:uchiuchiyamaこと,私がはてなブックマークしたWebページを対象にLudiaを使って全文検索を行うアプリケーションです.
どうやって使うの?
リンク先のページで検索ボックスに適当なキーワードを入れて検索ボタンを押してみてください.例えばid:uchiuchiyamaのはてなブックマークから「rails」を含むWebページを検索したりできます.
また,検索キーワードを空白で区切ればAND検索,「 OR 」で区切るとOR検索になります.例えば,id:uchiuchiyamaのはてなブックマークから「rails」を含み,「ludia」か「postgresql」か「senna」を含むWebページを検索することもできます.
なにがうれしいの?
はてなブックマークでは,タグによる検索と,タイトルからのキーワード検索はサポートしていますが,ブックマークしたWebページ本文を対象としたキーワード検索は(私の知る限り)できません.
そこで,「確かCSSのセレクタについて書いてあるページをブックマークしたはずなんだけど・・」というときや,「JavaScriptに詳しいb:id:amachangさんのはてなブックマークから,mochikitのことを書いてあるWebページがほしい」という場合に有用なのではないでしょうか.
つまり,その道の第一人者や一家言を持つ人の知識を検索できる,ということになります.これは,Google Coopのように,限定された(有用な)情報源から検索を可能にすると言えるのではないでしょうか.私は,はてなブックマークのようにソーシャルな情報蓄積サイトをうまく利用すれば,より簡単に精度の高い検索が行えるのではないかと思っています.あと,情報を知りたい時には「知ってる人」を探すというアプローチは面白そうだなーとか妄想してます.
制限事項
現在,b:id:uchiuchiyamaのはてなブックマークのうち,だいたい600件くらいが検索対象になっています.また,他のユーザのはてなブックマークを対象とした検索は,アイデアはあるものの時間がなくて作ってません.時間が出来れば作りたいなと思ってます.
聞いてきました:Googleの大規模日本語データ公開に関する特別セッション
第四十七回
写真はGigazineのマネです(笑)
3月に滋賀で行われる言語処理学会全国大会で、グーグルが
特別セッションをやるそうです。大規模日本語データについて。
たつをさんのブログで知ったGoogleの特別セッション.
グーグル株式会社では、日本語の言語処理研究推進のため大規模日本語データの公開を検討しています。つきましては仕様を決定するにあたり、実際にデータを御利用頂く研究者 / 技術者の皆様の「生の声」を是非お伺いしたく存じます。今回、言語処理学会様の御好意により、下記のとおりデータ仕様に関する特別セッションを設けて頂ける事になりました。
はてなブックマークでも話題になっているGoogleの大規模日本語データ公開に関する特別セッション@NLP2007に,家が近いこともあり参加してきましたので,その詳細を書きます.
アンケート詳細:Googleの大規模日本語データ公開に関する特別セッション
第四十八回
Googleは日本語の言語処理研究のためにWebインデックスから作成したコーパスデータの公開を予定しており,そのデータの形式や内容を含めた概要のたたき台を公開し,これらに対する研究者の意見を広く募りたい.
先日のエントリでお伝えしたように,Googleが日本語Webコーパスデータを学術用に公開するようです.そのときの公開データの形式や内容について,言語処理学会参加者からアンケートとして意見を募っていました.
このエントリでは,そのアンケートの内容を広く公開します.直接提出する先などはありませんが,Webのデータの話なので,Web上で広く議論してみるのも良いのではないでしょうか.
Ludia 用の Rails プラグイン acts_as_ludia を作りました
第四十六回
使い方や機能など,詳しくは後で書きます書きました.
acts_as_ludiaの更新情報もご覧下さい.
acts_as_ludiaの概要と機能
LudiaによるPostgreSQLの高速全文検索機能をRuby on Railsから利用するためのプラグインです.名前はacts_as_ludiaといいます(そのまんまですいません).
今までRailsからLudiaを使う場合,findのconditionsオプションにクエリをごりごり書く必要がありました.
Model.find(:all, :conditions => "body @@ '*D+ ruby rails'")
これが,acts_as_ludiaを利用すると以下のように簡潔にコードを書くことができます.もちろん,AND/OR検索や日本語での検索も可能です.
Model.find_fulltext(:body => "ruby rails")
また,Ludia 1.0.0から追加された検索単語周辺の文章(いわゆるスニペット(snippet))を簡単な書式で検索結果と同時に取得することができます.
Model.find_fulltext({:body => "ruby rails"}, :snippet => true)
このエントリの続きには,acts_as_ludiaの導入方法,使い方について書いてあります.
Sennaのクエリ書式がよくわかりません
第四十四回
最近,Ludiaを使ってデータベースの全文検索を行っているのですが,Sennaのクエリ書式がよく分かりません.というか,MySQLのBOOLEAN MODEがよく分かってないのかもしれません.
test=# select *from table1; col1 | col2 ------------------------------+-------------------- すもももももももものうち | あの壺はよいものだ ももから生まれた桃太郎 | あの壷はよいものだ ももんが飛んだら木が揺れた | あの壺は悪いものだ あなたももうけ話が聞きたいの | あの壷は悪いものだ 昨夜もももの缶詰を開けた | この壷はよいものだ にわにはにわにわとりがいる | この壺はよいものだ (6 rows)
こんなLudiaのテスト用テーブルがあるとき,以下のようなクエリを投げると結果がおかしい?というか納得いかないものになります.
test=# select *from table1 where col2 @@ '*D+ あの よい OR もの'; col1 | col2 ------------------------------+-------------------- すもももももももものうち | あの壺はよいものだ ももから生まれた桃太郎 | あの壷はよいものだ ももんが飛んだら木が揺れた | あの壺は悪いものだ あなたももうけ話が聞きたいの | あの壷は悪いものだ 昨夜もももの缶詰を開けた | この壷はよいものだ にわにはにわにわとりがいる | この壺はよいものだ (6 rows) test=# select *from table1 where col2 @@ '*D+ あの よい OR 壺'; col1 | col2 --------------------------+-------------------- すもももももももものうち | あの壺はよいものだ ももから生まれた桃太郎 | あの壷はよいものだ (2 rows) test=# select *from table1 where col2 @@ '*D+ あの よい OR 壺は'; col1 | col2 ----------------------------+-------------------- すもももももももものうち | あの壺はよいものだ にわにはにわにわとりがいる | この壺はよいものだ ももんが飛んだら木が揺れた | あの壺は悪いものだ ももから生まれた桃太郎 | あの壷はよいものだ (4 rows)
単純な AND と OR だけの書式なんですが,「壺」と「壺は」で結果が違うのはなぜなんでしょうか.
もしかすると,形態素解析に利用しているMeCabの語の区切り方のせいなのでは,と思ってmecabで解析してみるとこんな風になります.
$ mecab あの壺はよいものだ あの 連体詞,*,*,*,*,*,あの,アノ,アノ 壺 名詞,一般,*,*,*,*,壺,ツボ,ツボ は 助詞,係助詞,*,*,*,*,は,ハ,ワ よい 形容詞,自立,*,*,形容詞・アウオ段,基本形,よい,ヨイ,ヨイ もの 名詞,非自立,一般,*,*,*,もの,モノ,モノ だ 助動詞,*,*,*,特殊・ダ,基本形,だ,ダ,ダ EOS
うーん,解析はうまくいってるようです.わからない.もしなにかお分かりになる方はコメントなどいただけると幸いです.
環境
- PostgreSQL 8.1.8
- Senna 1.0.1
- MeCab 0.93
- Ludia 1.0.0
追記(2007/03/24 00:30)
Senna開発者のid:tasukuchanさんから直々にコメントとトラックバックで解説をいただきました.少し長めですが引用させていただきます.
「これ OR 壺」といったクエリを実行した場合を考えます。
まず、「これ」というトークンを含む文書を検索します。
2文字のトークンなので、検索結果が存在します。
次に、「壺」というトークンを含む文書を検索します。
1文字のトークンなので、検索結果が通常は存在しません。
そして、2つの検索結果をORでつなぐと、
結局「これ」というトークンを含む文書セットが結果となってしまいます。
その検索結果件数は0ではないので、部分一致検索が行われることがなく、
そのまま検索結果を出力してしまいます。
詳しくは引用元のエントリに記載されていますので,ご覧ください.id:tasukuchanさんありがとうございました.