今さらferret

WEBサイトをクロールして集めたデータの検索にgroonga(rroonga)を使っていたが、32bitのWindowsという、推奨されない環境のため、何万件もデータ突っ込んだら、エラーでどうにもならなくなったので、代わりに使えそうなferretを試してみた。solr-rubyにはそのうち挑戦したい。

rubyはrubyinstaller + devkitのやつを使っているので、普通にインストールできた。

gem install ferret

1.9でビルドできるようにしてくれたのはgithubにあるようだが、文字コードとかには対応してないので、実際に使うには、処理をかませる必要がありそう。

gem install sdsykes-ferret

一通り使い方を勉強してみた。

require 'ferret'
include Ferret
require 'strscan'


class MyAnalyzer < Analysis::Analyzer
  def token_stream(field, str)
    case field
    when :content
      MyTokenizer.new(str)
    else
      Analysis::WhiteSpaceTokenizer.new(str)
    end
  end
end

class MyTokenizer
  attr_reader :text
  def initialize(str)
    self.text = str
  end
  
  def text=(str)
    @text = str
    @scnr = StringScanner.new(str)
  end
  
  def next
    c = @scnr.getch
    c ? Analysis::Token.new(c, @scnr.pos - c.bytesize, @scnr.pos) : nil
  end
end

fis = Index::FieldInfos.new(:store => :yes, :index => :yes, :term_vector => :no)
fis.add_field(:year)
fis.add_field(:content, :term_vector => :with_positions_offsets)

index = Index::Index.new(
  :id_field => :year,
  #:key => :year,
  :path => "index.db",
  #:create => true,
  :fieldinfos => fis,
  :analyzer => MyAnalyzer.new
)

index << {:year => "0710", :content => '平城京に都を移す。'}
index << {:year => "0743", :content => '墾田永年私財法が出される。'}
index << {:year => "0784", :content => '長岡京に都を移す。'}
index << {:year => "0794", :content => '平安京に都を移す。'}
index << {:year => "0797", :content => '坂上田村麻呂が征夷大将軍になる。'}

p index[0].load
#=> {:year=>"0710", :content=>"平城京に都を移す。"}
p index["0743"].load
#=> {:year=>"0743", :content=>"墾田永年私財法が出される。"}

# 検索する
query = "+都 +year:>0750"
total = index.search_each(query, :sort => "year DESC") {|idx, score|
  data = index[idx].load
  hi = index.highlight(query, idx, :field => :content, :pre_tag=>"<", :post_tag=>">")
  p [data[:year], hi, score]
}
#=> ["0794", ["平安京に<都>を移す。"], 0.404452621936798]
#=> ["0784", ["長岡京に<都>を移す。"], 0.404452621936798]

自分の使い方だと、MeCabでは検索漏れが多くなるので、bi-gramのTokenizer適当に作ればすぐに使えそうな感じ。

KyotoCabinetをrubyで使う (キーをたどる)

require 'kyotocabinet'

db_hash = KyotoCabinet::DB.new
db_hash.open("*")

db_hash["apple"] = "りんご"
db_hash["lemon"] = "檸檬"
db_hash["orange"] = "オレンジ"
db_hash["grape"] = "ぶどう"

# 全ての項目を辿る each each_pair each_key each_value
db_hash.each {|k,v|
  p [k,v]
}
#=> ["orange", "オレンジ"]
#=> ["apple", "りんご"]
#=> ["grape", "ぶどう"]
#=> ["lemon", "檸檬"]

db_hash.each {|k,v|
  p [k,v]
  break # (注意) breakしない
}

db_hash.each_key {|k| p k } # 配列で来る
#=> ["orange"]
#=> ["apple"]
#=> ["grape"]
#=> ["lemon"]

db_hash.each_value {|v| p v } # 配列で来る
#=> ["オレンジ"]
#=> ["りんご"]
#=> ["ぶどう"]
#=> ["檸檬"]

# カーソル
cur = db_hash.cursor
cur.jump # カーソルを先頭に移動
while true
  k,v = cur.get(true) # 取得してカーソルを次に移動
  break if !k
  p [k,v]
end
cur.disable # 後始末?
p cur #=> #<KyotoCabinet::Cursor:(disabled)>


# 後始末付き
db_hash.cursor_process {|cur|
  cur.jump
  while true
    k,v = cur.get(true)
    break if !k
    p [k,v]
  end
}


# キーの検索
db_hash["apricot"] = "アプリコット"
p db_hash.match_prefix("ap") #=> ["apricot", "apple"] 前方一致
p db_hash.match_regex("ap") #=> ["apricot", "apple", "grape"]
p db_hash.match_regex("ap", 2) #=> ["apricot", "apple"] 2個まで取得
p db_hash.match_regex("(.)\\1") #=> ["apple"]


# ツリー
db_tree = KyotoCabinet::DB.new
db_tree.open("%")

db_tree["あひる"] = 0
db_tree["あせくさい"] = 1
db_tree["あかさか"] = 2
db_tree["あかばね"] = 3
db_tree["あくましんかん"] = 4

# ツリーはキーに並び順あり
db_tree.each_key {|k,x| p k }
#=> "あかさか"
#=> "あかばね"
#=> "あくましんかん"
#=> "あせくさい"
#=> "あひる"


# ツリーは逆順にさかのぼれる
db_tree.cursor_process {|cur|
  cur.jump_back # カーソルを最後に移動
  while true
    k,v = cur.get # 取得。カーソル位置はそのまま
    cur.step_back # カーソルを前に戻す
    break if !k
    p [k,v]
  end
}
#=> ["あひる", "0"]
#=> ["あせくさい", "1"]
#=> ["あくましんかん", "4"]
#=> ["あかばね", "3"]
#=> ["あかさか", "2"]


# 文字順じゃなく、数値順にする
db_tree_n = KyotoCabinet::DB.new
db_tree_n.open("%#rcomp=dec")
db_tree_n["1212"] = 1
db_tree_n["123"] = 1
db_tree_n["11111"] = 1
db_tree_n.each_key {|k,x| p k }
#=> "123"
#=> "1212"
#=> "11111"

KyotoCabinetをrubyで使う(基本)

require 'kyotocabinet'

db = KyotoCabinet::DB.new
db.open("*") # オンメモリハッシュDB

# 項目の設定 set 
db.set("apple", "りんご")
db["banana"] = "ばなーな"

# 項目の取得 get 
p db["apple"] #=> "りんご"
p db.get("banana") #=> "ばなーな"
p db["grape"] #=> nil

# 項目の数
p db.count #=> 2

# 項目の追加 add (すでにあるものは変更されない)
p db.add("orange", "オレンジ") #=> true
p db.add("banana", "バナナ") #=> false
p db["banana"] #=> "ばなーな"

# 項目の変更 replace
p db.replace("banana", "ばにゃにゃ") #=> true
p db.replace("grape", "ぶどう") #=> false

# 項目の削除 remove seize
p db.remove("apple") #=> true
p db.remove("lemon") #=> false
p db["apple"] #=> nil
p db.seize("orange") #=> "オレンジ"
p db.seize("lemon") #=> nil
p db["orange"] #=> nil

# 値に追加 append
db.append("banana", "")
db.append("lemon", "")
p db["banana"] #=> "ばにゃにゃ★"
p db["lemon"] #=> "★"

# 全部削除 clear
db.clear
p db.count #=> 0

KyotoCabinetをrubyで使う前に

まずはKyotoCabinetのデータベースを知らねば。

参照

mixi Engineers' Blog » 京都収納棚紅玉束縛: Rubyで簡単、DBプログラミング
http://alpha.mixi.co.jp/blog/?p=1795

データベースの種類

- prototype hash database
+ prototype tree database
: stash database
* cache hash database
% cache tree database
kch file hash database
kct file tree database
kcd directory hash databas
kcf directory tree database
kcx plain text database

チューニングパラメータ

db = KyotoCabinet::DB.new
db.open('items.kch#opts=c#zcomp=lzo', KyotoCabinet::DB::OWRITER | KyotoCabinet::DB::OCREATE)
- + : * % kch kct kcd kcf kcx
log ログファイルのパスを指定する。
"-": 標準出力
"+": 標準エラー出力
o o o o o o o o o o
logkinds "debug", "info", "warn", or "error" o o o o o o o o o o
logpx prefix of each log message o o o o o o o o o o
opts "s": use 32-bit addressing
"l": use linear collision chaining
"c": compress each record
o o o o o o
zcomp "zlib": ZLIB raw compressor
"def": ZLIB deflate compressor
"gz": ZLIB gzip compressor
"lzo": LZO compressor
"lzma": LZMA compressor
"arc": Arcfour cipher
o o o o o o
zkey the cipher key of the compressor o o o o o o
bnum ハッシュ表のバケット数。デフォルトは100万くらい。格納するレコードの総数の2倍くらいがオススメ。 o o o o o
capcnt 格納するレコード数の上限。溢れたはLRUなものから削除される。デフォルトは制限なし。 o
capsiz 格納するレコードサイズの合計の上限。溢れた分はLRUなものから削除される。デフォルトは制限なし。 o
psiz B+木のページサイズ。デフォルトは8192バイト。4096でもOK。 o o o
rcomp B+木の比較関数。”lex”(lexical)か “dec”(decimal)。デフォルトはlexical。
"lex": lexical comparator
"dec": decimal comparator
"lexdesc": lexical descending comparator
"decdesc": decimal descending comparator
o o o
pccap ページキャッシュのサイズの合計の上限。デフォルトは64MB。 o o o
apow レコードのアラインメント。2の冪で指定。デフォルトは3(ハッシュ)か8(ツリー)。 o o
fpow フリーブロックプールのサイズ。2の冪で指定。デフォルトは10。 o o
msiz mmapする領域のサイズ。デフォルトは64MB。 o o
dfunit 動的デフラグの単位。デフォルトはなし。動的デフラグをしたい場合は8くらいがオススメ。 o o

connection mode

OWRITER as a writer
OREADER as a reader

The following may be added to the writer mode by bitwise-or:

OCREATE it creates a new database if the file does not exist
OTRUNCATE it creates a new database regardless if the file exists
OAUTOTRAN each updating operation is performed in implicit transaction
OAUTOSYNC each updating operation is followed by implicit synchronization with the file system

The following may be added to both of the reader mode and the writer mode by bitwise-or:

ONOLOCK it opens the database file without file locking
OTRYLOCK locking is performed without blocking
ONOREPAIR the database file is not repaired implicitly even if file destruction is detected

KyotoCabinetのruby拡張をmingwでビルド

ちょっと試してみたかっただけなのに、わからないことばかりで苦労した。速攻で忘れるだろうから、書き残しておく。

参考サイト

blog.k11i.biz: [メモ]MinGW/MSYS 環境で Kyoto Cabinet Core & Java バインディングをビルドする
http://blog.k11i.biz/2010/12/mingwmsys-kyoto-cabinet-core-java.html

Ruby BDB - |▽ ̄)ノ なページ再帰 - livedoor Wiki(ウィキ)
http://wiki.livedoor.jp/niloufar/d/Ruby%20BDB#content_11_10

1.準備

Mingwのインストール

rubyinstaller の devkit は使わずに、Mingw(mingw-get-inst-20110802.exe)をインストールした。こっちのほうが便利そうだったので。

C++Compiler
□MSYS Basic System
MinGW Developer ToolKit
↑にチェックしてインストール。本当に何が必要なのかはわかってない。

msys.batを起動すると、ホームディレクトリが作られるのでそこで作業することにする。(環境変数のHOMEが設定してあれば、そこ。)

regexのビルド・インストール

参考サイトを見てスタティックな libregex.a を作る。

ochuki@aho ~/mingw-libgnurx-2.5.1
$ ./configure --prefix=/mingw
$ make
$ rm libregex.a
$ ar rcs libregex.a regex.o
$ make install
zlib のビルド・インストール

こちらも参考サイトを見て、こんな感じに。

ochuki@aho ~/zlib-1.2.5
$ make -f win32/Makefile.gcc
$ export BINARY_PATH=/mingw/bin
$ export INCLUDE_PATH=/mingw/include
$ export LIBRARY_PATH=/mingw/lib
$ make -f win32/Makefile.gcc install

2. Kyoto Cabinet のビルド・インストール

この辺のことはちょっとわからなかったので、何度もgoogleで検索して何とかビルドできた。

configure.in を編集

pthreadは使わないみたいなので、configure.in の該当部分をコメントアウトして、autoconf → configure再作成。

245|#AC_CHECK_LIB(pthread, main)
270|#AC_CHECK_HEADER(pthread.h, true, AC_MSG_ERROR([pthread.h is required]))
libstdc++.aをコピー

libstdc++.dll.aをリンクしないように、libstdc++.aをもってくる。本当はどうするのがよいのかわからなかった。

_WIN32_WINNT=0x0500の定義も必要。

ochuki@aho ~/kyotocabinet-1.2.70
$ autoconf
$ cp /mingw/lib/gcc/mingw32/4.5.2/libstdc++.a libstdc++.a
$ export CPPFLAGS=-D_WIN32_WINNT=0x0500
$ ./configure --prefix=/mingw --enable-static 
$ make
$ make install

3. KyotoCabinet の ruby拡張をビルド・インストール

1.8と1.9で何か違うようだが、深く考えないことにする。

kyotocabinet.cc の修正

参考サイトにあるように、修正する。

extconf.rb の修正

extconf.rb の $LDFLAGS に -static-libgcc を追加。何か変なのがリンクされちゃったので。

$LDFLAGS = "#{$LDFLAGS} -L. #{kcldflags} -static-libgcc"
libstdc++.aをコピー

libstdc++.dll.aをリンクしないように、libstdc++.aをもってくる。わからないので、ここでもコピーして対処した。

まずは1.9から

ochuki@aho ~/kyotocabinet-ruby-1.27
$ cp /mingw/lib/gcc/mingw32/4.5.2/libstdc++.a libstdc++.a
$ ruby -v
ruby 1.9.2p290 (2011-07-09) [i386-mingw32]
$ ruby extconf.rb
$ make
$ make install
$ ruby test.rb
105 tests were all ok

大丈夫そうな予感。

続いて1.8。パスが通ってない。

ochuki@aho ~/kyotocabinet-ruby-1.27
$ c:/dev/lang/ruby187/bin/ruby -v
ruby 1.8.7 (2011-06-30 patchlevel 352) [i386-mingw32]
$ c:/dev/lang/ruby187/bin/ruby extconf.rb
$ make
$ make install
$ c:/dev/lang/ruby187/bin/ruby test.rb
28/105 tests failed

テストの失敗は、スレッドの関係ですかね。

まとめ

結局の所、rubyしか使えない私にはよくわからない世界でした。