FtsLite

FtsLiteは組み込み型お手軽全文検索エンジンのRubyライブラリです。 基本的にはSQLite3 FTS4のラッパーで、日本語のbigramやtrigram、wakachi_bigramなどのトークナイザーをRubyのレイヤで実装したものです。

qarc.info で使われています。

Ruby 1.9.2 以降と、FTS4 に対応してる SQLite3 が必要で、SQLite3 はできれば 3.7.7 以降がよいです(FTS4の仮想テーブルに対するINSERT OR REPLACEが実装されているのでsetのパフォーマンスがよい)。

Installation

Add this line to your application's Gemfile:

gem 'fts_lite'

And then execute:

$ bundle

Or install it yourself as:

$ gem install fts_lite

Usage

# -*- coding: utf-8 -*-
require 'fts_lite'

FtsLite::Index.open("./index.sqlite3") do |db|
  # set(docid, text, sort_value = nil)
  db.set(1, "なぜナポリタンは赤いのだろうか?")
  db.set(2, "昼飯のスパゲティナポリタンを眺めながら、積年の疑問を考えていた。")

  # docid_array = search(query, options = {})
  docids = db.search("ナポリタン")
  puts docids.join(",")

  docids = db.search("赤い ナポリタン")
  puts docids.join(",")

  # update_sort_value(docid, sort_value)
  db.update_sort_value(1, 2)
  db.update_sort_value(2, 1)

  docids = db.search("ナポリタン", :order => :asc, :limit => 1)
  puts docids.join(",")
end

set(docid, text, sort_value)でデータを登録します。 docidはレコードを表すIDで search ではこのIDの配列を検索結果として返します。 textは全文検索インデックスのためのテキストデータです。取り出すことはできないので、元のデータは別のデータベースにあることを想定しています。 sort_valueはソート用の値で、searchの時にこの値でソートしたり、ソートしたうえで上位N件を取り出したりできます。

search(query, options) で検索できて、queryは空白区切りでAND検索です。このへんの仕様は用途によっていろいろだと思うけど、僕はテキトウにANDだけできればいいやと思っているので、いじりたい人は lib/tokenizer.rb の各トークナイザで定義してある query というメソッドをいじってください。 optionsは :order に :desc か :asc を指定すると sort_value で昇順ソートまたは降順ソートします。 :order が指定されない場合は docid の昇順になります。:limit => N を指定すると検索結果の上位N件だけを返します。

Railsで使う

まず config/application.rb あたりで

QUESTION_FTS = FtsLite::Index.open(File.join(Rails.root.to_s, "fts", "index.sqlite3"), :table_name => "questions")
THREAD_FTS = FtsLite::Index.open(File.join(Rails.root.to_s, "fts", "index.sqlite3"), :table_name => "threads")
# ...

とコネクションを作ってグローバルにアクセスできるようにしておきます。 モデルでしか使わない場合は、モデルの中で定義したほうがいいかもしれません。 :table_name を指定するとひとつのDBファイルに複数の全文検索インデックスが持てます。

あとは、たとえば、Question という ActiveRecord のモデルがあって、全文検索用のテキストデータ(内容やタイトルなんかを適当に結合した文字列)を作成する make_ft というメソッドとソート用の表示数 view_count というカラムがあるとすると

class Question < ActiveRecord::Base
   LIMIT = 1000
   after_save :set_ft

   def set_ft
     QUESTION_FTS.set(id, make_ft, view_count)
   end
   def search(query)
      find(:all,
           :conditions => ["id in (?)", QUESTION_FTS.search(query, :order => :desc, :limit => LIMIT)],
           :order => "view_count DESC")
   end
   # def make_ft ...
end

まず after_save でレコードの更新時に全文検索インデックス側も更新するようにしておきます。 search というメソットでは、全文検索を行って、その結果(IDの配列)を含むレコードをさらにDBに問い合わせることで検索結果となるレコードを返しています。

この実装だとレプリケーションしている場合に、別のサーバーでレコードが更新されるとトリガーが効かなくてローカルの全文検索インデックスが更新されないことに注意してください。 そういう場合は、定期ジョブで適当に同期すればいいと思います。