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