2010/11/21

SQLiteのFTSを応用して長文の要点を抜き出してみる

SQLiteの全文検索機能であるFTS3には、snippet()という関数が用意されており、検索ワードが含まれていた箇所の前後の文章を取得できます。
これを応用すれば、長文から要点っぽい部分を抜き出す機能が実現できるのではないかと思い、Segmenterで分かち書きしたテキストをDBに格納し、出現頻度上位の単語を検索した時のsnippet()を取得するというスクリプトを作成してみました。
use('Clipboard','Segmenter','SQLite');
try{
    var seg=Segmenter.create("ja");
    var db=DB.open();
    db.execute("CREATE VIRTUAL TABLE docs USING fts3(body)");
    var a=seg.segment(Clipboard.text.replace(/[\s\r\n\t]+/g,' '));
    db.table('docs').insert([{body:a.join(' ')}]);
    var re=/[ぁ-ん]{4,}|[々〇\u303B\u3400-\u9FFF\uF900-\uFAFF0-90-9a-za-zァ-ヴーア-ン゙ー]{2,}/i;
    var k=$G(a).groupBy(void(0),function(w,g)({w:w,c:g.count()}))
               .filter(function(o)(re.test(o.w)))
               .orderByDesc('c').map('w').head(3).toArray();
    println(k);
    println(db.select(
        [{d:'snippet(docs,"<<",">>","...",0,64)'},'docs','body MATCH $q'],
        "",-1,0,{q:k.join(' ')}
    ).first().d.replace(/([^\w])\s|\s([^\w])/g,'$1$2'));
}finally{
    free(seg,db);
}
最初にSegmenterオブジェクトとオンメモリDBオブジェクトを生成し、クリップボードのテキストを分かち書きした配列をDBに格納します。
次に、ジェネレータの拡張メソッドで分かち書き配列を加工し、出現頻度の高い上位3件の単語を得ます。
この時、記号や助詞などの不要な単語を除外するため、正規表現で大雑把にフィルタリングしています。
そして、その頻出単語をキーワードとしてDBから検索を行い、sinppet()を取得し、不要な空白を除去して出力します。
なお、snippet()の引数ではテーブル名、マッチ箇所を囲む開始・終了文字列、列番号(0~)、抜き出したい単語数を指定できます。単語数を負の数にすると、抜き出す箇所が複数に分かれる場合に、合計ではなくそれぞれの部分の単語数が指定した数になるように抜き出されるようです。

適当にいくつかのニュースやブログ記事で試してみたところ、期待通りに頻出単語とそれを含む部分を抜き出せました。
文章によっては本題ではない部分が抜き出されてしまうこともありましたが、文の書き出しなどを決め撃ちで抜き出す単純な方法よりは、有用な部分を抜き出せる可能性が高いような気がします。

2010/11/19

Segmenterで分かち書きしたテキストをSQLiteで全文検索

NILScriptには軽量データベースエンジンSQLiteを扱うクラスが用意されています。
SQLiteには文字列中の単語をインデックス化して高速な全文検索を行うFTS3という機能が用意されていますが、標準搭載されている単語分割処理は文字列を空白文字で分割するだけの簡易的なものなので、そのままでは日本語のテキストをまともに処理できません。

そこで、日本語の文字列を分かち書きする機能を提供する「Segmenter」クラスを新たに用意しました。
アルゴリズム本体はプラグイン方式で追加できるようになっており、現在はTinySegmenter- Javascriptだけで実装されたコンパクトな分かち書きソフトウェア高速化のための修正を加えたものにSpiderMonkey独自機能を利用したチューニングを加えて作成したプラグインが用意されています。
TinySegmenterは簡易的な物なので、「すもももももももものうち」のように平仮名が連続していたりすると「すも | も | も | もも | も | ももの | うち」のようなおかしな結果になってしまうことがありますが、検索に利用するだけなら十分でしょう。(検索語句の「すもも」も「すも | も」として検索されるため)
より正確な分かち書きが必要な場合は、MeCabなどのエンジンを利用したプラグインを作成するとよいでしょう。

以下のスクリプトは、SegmenterとSQLiteを組み合わせて全文検索を行うサンプルです。
var segmenter=require('Segmenter').Segmenter.create("ja");
var db=require('SQLite').DB.open();
db.execute("CREATE VIRTUAL TABLE docs USING fts3(title TEXT, body TEXT)");
db.begin();
Main.directory.directory('doc').files.execute(function(f){
    db.table('docs').insert([{title:f.baseName,body:segmenter.segment(f.load()).join(' ')}]);
});
db.end();

var q;
while(q=prompt("search query","")){
    println("search result for: "+q);
    db.select([,'docs','body MATCH $q'],"title",-1,0,{q:'"'+segmenter.segment(q).join(' ')+'"'}).execute(function(o){
        println(o.title);
    });
}
free(db,segmenter);
各クラスの基本的な使い方については、同梱のドキュメントなどを参照してください。

全文検索型のテーブルを作成するには、「"CREATE VIRTUAL TABLE name USING fts3(colnames)」のようにします。
そして、挿入する値を指定する際に、分かち書きした値を指定します。Segmenterオブジェクトのsegment()メソッドは配列を返すので、空白で連結する必要があります。

検索時にMATCH演算子を使うことで、インデックスを利用した全文検索を行なえます。
検索語句は挿入時と同様にSegmenterで分割して空白区切りにし、前後に「"」を付加する必要があります。
MATCH演算子の詳しい使い方はSQLite FTS3 Extensionなどを参照してください。

上記のスクリプトを実行すると、キーワード入力用のプロンプトが表示され、入力した語句を含むドキュメントが列挙されます。
プロンプト表示までに発生する2秒ほどの待ち時間は、主にSegmenterの処理による物のようです。
個人利用で扱う程度のテキスト量なら、このくらいの処理速度で十分実用に耐えられるでしょう。