近況報告

第五十一回

最近は研究が忙しいのと,RailsをせっかくさわってるんだからBlogもRailsアプリにするべきじゃない?という短絡的な思考でtypoでBlogを作ってみました

acts_as_ludia や,acts_as_find_or_initialize_by といったプラグインのこともちょこっと書いてます.
私自身が非常に飽きっぽいのでいつまで続くか分かりませんが,お時間があればのぞいてあげてください.

新しく作ったBlogはもちろんtypoがベースになっていますが,実はRuby-GetText-Packageを利用して日本語化しようと目論んでいたりします.設定画面の一部などが今は日本語になってきています.その話もおいおい.

typoベースの私のブログ

screenshothttp://blog.fulltext-search.biz/

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

acts_as_ludia を使ってLudiaのデモWebアプリケーションを作りました

第四十九回

先日のエントリ「Ludia 用の Rails プラグイン acts_as_ludia を作りました - のほほん徒然」で紹介したacts_as_ludiaプラグインを使って,簡単な検索アプリケーションをRailsで作ってみましたので紹介します.

はてなブックマーク全文検索

screenshothttp://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

うーん,解析はうまくいってるようです.わからない.もしなにかお分かりになる方はコメントなどいただけると幸いです.

環境

追記(2007/03/24 00:30)

Senna開発者のid:tasukuchanさんから直々にコメントとトラックバックで解説をいただきました.少し長めですが引用させていただきます.

「これ OR 壺」といったクエリを実行した場合を考えます。

まず、「これ」というトークンを含む文書を検索します。

2文字のトークンなので、検索結果が存在します。

次に、「壺」というトークンを含む文書を検索します。

1文字のトークンなので、検索結果が通常は存在しません。

そして、2つの検索結果をORでつなぐと、

結局「これ」というトークンを含む文書セットが結果となってしまいます。

その検索結果件数は0ではないので、部分一致検索が行われることがなく、

そのまま検索結果を出力してしまいます。

詳しくは引用元のエントリに記載されていますので,ご覧ください.id:tasukuchanさんありがとうございました.