mal_blue@tumblrでFirefox3に載っているSQLiteに全文検索機能がついたのを知りました。昨年12月にGoogle Japan Blog: Google デベロッパー交流会 ( 第 4 回 )に参加させていただいたとき、Google Gearsの開発者がGoogle Gearsでサポートされているlocal storage(実体はsqliteのデータベースでSQLを使ってデータを読み書きできます)について非ASCIIのfull-text searchをサポートしたい
と言われていたのを思い出しました。
その後どうなったかなー、と思ってちょっと検索してみたらGoogle Gearsと直接関係ないけどSQLite Full Text Search with MeCab - mynoteという記事が。この記事はSQLiteのCのAPIにはtokenizerを指定することができて、あらかじめtokenizeする必要もなく日本語で全文検索ができる、というのが趣旨ですが、tokenizeしてあげれば日本語もふつうに全文検索可能、ということならFirefoxの拡張機能からも日本語での全文検索機能が利用できるはずー、と思って試してみたらできました。
USING fts3とつけるのがミソ。
var storageService = Components.classes["@mozilla.org/storage/service;1"]
.getService(Components.interfaces.mozIStorageService);
var Connect = storageService.openUnsharedDatabase(file);
Connect.executeSimpleSQL(
"CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)"
);
これに全文検索したい日本語の文字列をあらかじめtokenizeしてからINSERTします。
分け入っても分け入っても青い山
大銀杏散りつくしたる大空
松はみな枝垂れて南無観世音
まったく雲がない笠をぬぎ
やつぱり一人がよろしい雑草
捨てきれない荷物のおもさまへうしろ
すべつてころんで山がひつそり
待つでも待たぬでもない雑草の月あかり
ともかくも生かされてはゐる雑草の中
この道しかない春の雪ふるころり寝ころべば青空
を入れると | で区切られてtokenizeした結果が得られます。
分け入っ | て | も | 分け入っ | て | も | 青い山 | | 大銀 | 杏散り | つく | し | たる | 大空 | | 松 | はみ | な | 枝垂 | れ | て | 南無 | 観世音 | まっ | たく | 雲 | が | ない | 笠 | を | ぬぎ | | やつぱり | 一人 | が | よろしい | 雑草 | | 捨 | て | きれ | ない | 荷物 | の | おも | さま | へうしろ | | すべつ | て | ころん | で | 山 | が | ひつそり | | 待つ | でも | 待 | た | ぬ | でも | ない | 雑草 | の | 月 | あかり | | ともかく | も | 生か | さ | れ | て | はゐる | 雑草 | の | 中 | | この | 道し | か | ない | 春 | の | 雪ふる | ころり | 寝ころべば | 青空
これをスペース区切りに書き換えて、1レコードに1俳句でSQLiteのdbに入れました。
var uc = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
uc.charset = 'UTF-8';
var tokenized = document.getSelection();
tokenized.split(/\|/).map( function (s) {
s = s.replace(/\s/g, "");
return s ? s : '\n'
} ).join(" ").split(/\n/).map( function (s) {
documentTitle = uc.ConvertFromUnicode(documentTitle);
console.log(s);
s = uc.ConvertFromUnicode(s);
var statement = Connect.createStatement("INSERT INTO recipe VALUES (?1,$2)");
statement.bindUTF8StringParameter(0, documentTitle);
statement.bindUTF8StringParameter(1, s);
statement.execute();
} );

MATCHを使います。ここだけbindUTF8StringParameter
でへんな問題がありました(テストに使ったのはFirefox3beta3です)。
ほんとうは
var q = "青空";
sql = "SELECT docid,ingredients FROM recipe WHERE ingredients MATCH $1";
statement.bindUTF8StringParameter(0, q);
とすればうまく検索できるはずなのですが、bindUTF8StringParameterを使ってパラメータを与えると、パラメータがASCIIであるか日本語であるかに関わらず、全文検索が正しくできませんでした。でもSQLの中に直接このように
sql = "SELECT docid,ingredients FROM recipe WHERE ingredients MATCH '\u9752\u7A7A' "; // 青空
記述するとちゃんと検索することができました(Unicodeエスケープしているのは、今回スクリプトを実行するのに使ったJSActionsが使っているloadSubScriptに起因する文字化け回避のためです。参考: Bug 377498 – loadSubScript does not support JavaScript 1.7 and does not support specifying the character encoding)。
実行するとこのとおり、青空
を含んでいるこの道しかない春の雪ふるころり寝ころべば青空
がマッチします。

大銀杏散りつくしたる大空
この道しかない春の雪ふるころり寝ころべば青空
sql = "SELECT docid,ingredients FROM recipe WHERE ingredients MATCH "+"'\u7A7A'"; // 空
で検索してみると

bindUTF8StringParameterが使えず、日本語を扱うにはへんな書き方をする必要がありそうです(でも明らかにバグなので新しいバージョンでは直っているかもしれません)。
TinySegmenter: Javascriptだけで実装されたコンパクトな分かち書きソフトウェアと組み合わせて使用すれば、Firefoxのみで日本語の全文検索をすることが可能になります。
ThumbStrips :: Firefox Add-onsは見たページの全文をSQLiteに保存しておいて検索することができるextensionですが、日本語に対しては正しく機能しません。上述のように、日本語をtokenizeしてからSQLiteに保存する方法を使えば、日本語のページでも閲覧したページの全文を保存しておいてあとから検索することができます。
var console = {
log: function () {
Firebug.Console.log(arguments);
}
};
var file = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Components.interfaces.nsIProperties)
.get("ProfD", Components.interfaces.nsIFile);
file.append("FTS3.sqlite");
var storageService = Components.classes["@mozilla.org/storage/service;1"]
.getService(Components.interfaces.mozIStorageService);
var Connect = storageService.openUnsharedDatabase(file);
if(!Connect.tableExists("recipe")){
try {
Connect.executeSimpleSQL(
"CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)"
);
}catch(e) {
console.log(e);
}
} else {
var selectedText = (document.getSelection());
var documentTitle = document.title;
var uc = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
uc.charset = 'UTF-8';
if ( selectedText ) {
var tokenized = document.getSelection();
tokenized.split(/\|/).map( function (s) {
s = s.replace(/\s/g, "");
return s ? s : '\n'
} ).join(" ").split(/\n/).map( function (s) {
documentTitle = uc.ConvertFromUnicode(documentTitle);
console.log(s);
s = uc.ConvertFromUnicode(s);
var statement = Connect.createStatement("INSERT INTO recipe VALUES (?1,$2)");
statement.bindUTF8StringParameter(0, documentTitle);
statement.bindUTF8StringParameter(1, s);
statement.execute();
} );
} else {
var sql;
//sql = "DELETE FROM recipe";
//sql = "SELECT * FROM recipe";
sql = "SELECT docid,ingredients FROM recipe WHERE ingredients MATCH "+"'\u9752\u7A7A'";
console.log(sql);
sql = uc.ConvertFromUnicode(sql);
statement = Connect.createStatement(sql);
//statement.bindStringParameter(0, 'XPCOM');
//statement.bindUTF8StringParameter(0, q);
statement.execute();
var i = 0;
while ( statement.executeStep() ) {
var title = statement.getString(0);
var data = uc.ConvertToUnicode( statement.getString(1) );
Firebug.Console.log([i++, {text: data }]);
}
console.log("hit(s)", i);
}
}
トラックバック元エントリにこのエントリへのリンクがない場合はトラックバックを受け付けません。
http://labs.gmo.jp/mt/mt-tb.cgi/208
comments