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の処理による物のようです。
個人利用で扱う程度のテキスト量なら、このくらいの処理速度で十分実用に耐えられるでしょう。

2010/10/27

設定ファイルにウィンドウ位置などの情報を保存する

ウィンドウを表示して入力の受け付けや情報の表示を行うタイプのプログラムでは、ウィンドウを表示する際、前回のウィンドウの位置・サイズを記憶して再現してくれるようにすると便利です。

このような設定情報の保存を行う際には、pref()関数を使います。
この関数は、適切なユーザー設定ディレクトリを自動認識して、設定ファイルを表すFile/Directoryオブジェクトを返してくれます。

設定ファイルの形式は、JavaScriptのオブジェクトリテラルの表記と同じ書式のテキストで保存するJSONという形式を使うと良いでしょう。
FileオブジェクトのloadJSON()やsaveJSON()というメソッドを使えば、簡単にオブジェクトとして読み込んだり保存できます。

下記の例は、ウィンドウを閉じようとしたときに位置とサイズを記憶して次回実行時に再現するだけのスクリプトです。
var config=(pref("windowPosTest.json")||{loadJSON:function()({})}).loadJSON();
var w=require('Window').Window.create({
 title:"test",
 width:100,height:100,
 x:config.windowX||0, y:config.windowY||0,
 events:{
  close:function(){
   config.windowX=this.x;
   config.windowY=this.y;
   pref("windowPosTest.json",true).saveJSON(config);
  },
 },
}).show();;

最初のpref()では、既に存在している設定ファイルを表すFileオブジェクトを取得しています。
この時、ファイルが存在していなくてnullが返された場合でもエラーにならずにデフォルトのオブジェクトがセットされるようにしておく必要があります。

Windowオブジェクトのcloseイベントでは、設定オブジェクトにウィンドウの現在位置をセットして保存を行っています。
保存時のpref()の呼び出しでは、第2引数をtrueにする必要があります。

スクリプトが不要になったらプログラムのディレクトリを削除するだけで完全に削除できるように、通常はスクリプトのあるディレクトリ内の「users」ディレクトリにユーザー名のディレクトリを作って保存されます。
しかし、スクリプトのあるディレクトリが書き込み禁止になっていたりする場合は、Application Dataディレクトリ内のスクリプトファイル名を名前にしたディレクトリが保存先として選択されます。
また、スクリプトと同じディレクトリに「config」というディレクトリがあれば、そちらが保存先になります。これはプログラムをリムーバブルメディアに入れて持ち出して複数の環境で利用したい場合などのための機能です。

なお、プログラム用のドライブに頻繁な書き込みをしたくないとか、まとめてバックアップ出来た方がいいなどの理由で、設定がApplication Dataディレクトリに保存されることを好むユーザーは、あらかじめApplication Dataディレクトリ内に該当するディレクトリを作成しておくことで、そちらに保存させることも出来ます。

これらの仕様により、pref()関数を使用すれば、簡単にユーザーの好みや実行環境に合った最適な場所にデータを保存できるようになっています。

2010/10/25

自前ウィンドウの任意の部分をドラッグして移動できるようにする

NILScriptの自前ウィンドウ生成機能では、タイトルバーや枠のないウィンドウも生成できます。
しかし、タイトルバーや枠のドラッグによる移動・リサイズが行なえないので、別途移動やリサイズのための動作を用意する必要があります。

ウィンドウの任意の部分をドラッグして移動できるようにするには、hitTestイベントを利用します。
このイベントは、カーソル下にあるのがウィンドウのどの部位かという問い合わせを受取ったときに実行されます。
ここでカーソル下がタイトルバーであると応答するようにすれば、マウスの左ボタンでドラッグしたときに、タイトルバーをドラッグしたときと同じようにウィンドウが移動されるようになります。

これを利用して前回のCPU使用率グラフ表示のウィンドウをドラッグ可能にしたのが、以下のスクリプトになります。
var img=require('Image').Image.create(100,100,"black");
var w=require('Window').PlainWindow.create({
    width:100,height:100,
    graphics:img,
    transparentColor:0x000000,toolWindow:true,
    topmost:true,
    events:{
        hitTest:function(o){
            o.result="caption";
            return(true);
        },
        mbuttonUp:function(o){
            this.close();
        },
    },
});
Thread.create(function(){
    var mon=new (require('SystemMonitor').CPU)(), rate=0;
    while(true){
        w.request(function(){
            img.fillEllipse(1,1,98,98,0x9999FF);
            img.fillPie(1,1,98,98,-Math.PI/2,(Math.PI*2)*rate,0x99FF99,true);
            img.strokeEllipse(1,1,98,98,0x010101,true);
            img.drawTo(w);
        });
        sleep(1000);
        rate=mon.get().usage;
    }
});
w.show();

自前ウィンドウ上に表示した画像を更新・再描画する

Imageオブジェクトの画像をウィンドウに表示するには、Window.create()のオプションオブジェクトのgraphicsメンバにセットするというのは以前説明しましたが、今回は表示する画像の内容を更新する方法について説明します。

ウィンドウの画像を更新するには、Imageオブジェクトの各種メソッドで画像を更新した後、描画対象のWindowオブジェクトを引数にしてdrawTo()メソッドを呼び出します。

この時、複数スレッドからImageオブジェクトに同時アクセスしないように注意する必要があります。
ウィンドウを所有するスレッド上では、Imageオブジェクトを使用した再描画が不特定のタイミングで発生するので、他のスレッド上でImageオブジェクトを操作すると、更新途中の画像が表示されてしまったり、更新処理に失敗してしまったりします。
これを避けるためには、画像の更新処理もウィンドウを所有するスレッド上で実行しなければなりません。

Windowオブジェクトのイベントハンドラ内で更新・再描画を行う場合は特に気にする必要はありませんが、他のスレッドから更新を行う場合は、Windowオブジェクトのrequest()メソッドを使用してください。
request()メソッドは、指定された関数をウィンドウを所有するスレッド上で実行します。この関数内でImageオブジェクトを操作すれば、安全に画像を更新できます。

下記のスクリプトは、SystemMonitorユニットのCPUクラスを利用して取得したCPU使用率を円グラフでウィンドウ上に表示するという例です。
var img=require('Image').Image.create(100,100,"black");
var w=require('Window').PlainWindow.create({
    width:100, height:100,
    transparentColor:0x000000,topmost:true,
    graphics:img,
});

Thread.create(function(){
    var mon=new (require('SystemMonitor').CPU)(), rate=0;
    while(true){
        w.request(function(){
            img.fillEllipse(1,1,98,98,0x9999FF);
            img.fillPie(1,1,98,98,-Math.PI/2,(Math.PI*2)*rate,0x99FF99,true);
            img.strokeEllipse(1,1,98,98,0x010101,true);
            img.drawTo(w);
        });
        sleep(1000);
        rate=mon.get().usage;
    }
});
w.show();