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

ゲストOS

データの詳細

感想とか

ほぼ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文字くらいかな。

ということなので,私は倍以上の文書に対してインデックスを構築したようです.

しかし,WikipediaXMLファイルからテキストを抽出してデータベースへ格納するのにはだいたい丸一日くらいかかりました.Railsが遅いのでしょうか.ものっすごいメモリ使いましたし.(私の書いたコードが悪いといううわさもチラホラ(笑)

データベースへのデータ格納用Railsコード

xml2sql --postgresql というやりかたも考えたんですが,事前に必要なPostgreSQLのテーブル構成がいまいちよく分からなかったのでRailsActiveRecordに頼りました.こういう点では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