Android
SQLite3
全文検索

SQLiteを使ってAndroid端末内でお手軽に日本語全文検索する

More than 3 years have passed since last update.

タイトルの通り、SQLiteを使ってAndroid端末内でお手軽に日本語全文検索する方法です。


FTS

SQLiteにはFTSという全文検索用モジュールがサポートされているのでそれを使う。

最新版はFTS4で、Android APIレベル11以上なら標準で使えるようなのでFTS4を使う。

公式ドキュメント

SQLite FTS3 and FTS4 Extensions


N-gram

FTS標準のトークン作成処理はスペース区切りにしか対応していないので、日本語の場合、自分でトークンを作成する必要がある。トークン作成には、お手軽にトークンを作成できるN-gram方式を採用する。

以下が指定した値(N)でN-gram変換するだけのNgramクラス。


Ngram.java

/**

* N-gramクラス
* @author shikato
*
*/

public class Ngram {

private Ngram() {
}

/**
* N-gram変換したテキストを返す
* @param aText N-gram変換するテキスト
* @param aN N
* @return N-gram変換されたテキスト
*/

public static String getNgramText( final String aText, final int aN ) {

final StringBuilder ngramText = new StringBuilder();
final int length = aText.length();

if ( length < 1 ) {

return "";
}

if ( length > aN ) {

int roop = length - aN;

for ( int i = 0; i < roop; i++ ) {

ngramText.append( aText.substring( i, i + aN ) );
ngramText.append( " " );

if ( i == ( roop - 1 ) ) {

i++;
ngramText.append( aText.substring( i, i + aN ) );

if ( i % aN >= aN ) {

i++;
ngramText.append( " " );
ngramText.append( aText.substring( i, i + aN ) );
}
}
}

} else {

ngramText.append( aText );
}

return ngramText.toString();
}
}


https://gist.github.com/shikato/7f75d006bd0c614b7eca#file-ngram-java


FTSテーブル作成

FTSテーブルでは値がすべて文字列扱いになってしまうので、実際に使用するときはメインのテーブルと全文検索用のFTSテーブルにわけて使うことが多くなると思う。

以下が上述したように、メインのテーブルと全文検索用のFTSテーブルにわけて作成する例。

CREATE TABLE hoge ( id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT )

CREATE VIRTUAL TABLE hoge_fts USING fts4( words TEXT )

検索対象テキストをメインテーブルのtextに格納し、検索対象テキストをN-gram変換したものを、FTSテーブルのwordsに格納する。

FTSテーブルを作成すると、docidというAUTO INCREMENTなカラムが自動的に用意されるので、メインテーブルと結合するときに使う。ちなみにdocidはrowidの別名なのでテーブル定義にはない。

あと、FTSでテーブルを作ると、~_contentのような名前のテーブル(Shadow Tables)が5つくらい勝手に作られるけど、普通に使う分にはあまり意識する必要はなさそう。

その辺りの詳細は公式ドキュメントに書いてある。

http://www.sqlite.org/fts3.html#section_9


保存

今回の例ではメインのテーブルとFTSテーブルにわけているので、両方のテーブルに保存する(メインのほうには通常のテキスト、FTSテーブルのほうにはN-gram変換したテキスト)。

※保存するテキストは、ユーザーの入力値になることがほとんどだと思うので、実際の開発時にはプリペアードステートメント等でSQLインジェクション対策する。

INSERT INTO hoge ( text ) VALUES( "hoge" ) 

INSERT INTO hoge_fts ( words ) VALUES( "ho og ge" )

同様に削除や更新の際も、両方のテーブルに対して実行する。


検索

MATCHを使用する。

SQLは以下のような感じになる。検索キーワードにN-gram変換したテキストを使用する。

※検索キーワードはユーザーの入力値になることがほとんどだと思うので、実際の開発時にはプリペアードステートメント等でSQLインジェクション対策する。

SELECT * FROM hoge h INNER JOIN hoge_fts hf ON h.id = hf.docid WHERE words MATCH "ho og ge"

前方一致だとこんな感じになる。

SELECT * FROM hoge h INNER JOIN hoge_fts hf ON h.id = hf.docid WHERE words MATCH "ho og ge*"

他にも色々な検索方法がある。

http://www.sqlite.org/fts3.html#section_3


パフォーマンス

SQLiteの.timerオプションを使用して測定。 投入したデータは5万件ほど。

普通にLIKE

SELECT count(*) from hoge WHERE text LIKE "%hoge%"

CPU Time: user 0.104006 sys 0.240015

FTS メインテーブルとの結合なし

SELECT count(*) from hoge_fts WHERE words MATCH "ho og ge"

CPU Time: user 0.000000 sys 0.000000

FTS メインテーブルとの結合あり

SELECT count(*) FROM hoge h INNER JOIN hoge_fts hf ON h.id = hf.docid WHERE words MATCH "ho og ge"

CPU Time: user 0.004000 sys 0.000000

FTS 前方一致でメインテーブルとの結合あり

SELECT count(*) FROM hoge h INNER JOIN hoge_fts hf ON h.id = hf.docid WHERE words MATCH "ho og ge*"

CPU Time: user 0.004001 sys 0.000000

やっぱりLIKEより速い。


まとめ

LIKEで検索するより速いし、別途ライブラリ等もいらないので、お手軽に端末内で日本語全文検索するときは使うと良いと思う。

元記事:http://alexam.hateblo.jp/entry/2014/06/21/192511