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

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


## FTS
SQLiteにはFTSという全文検索用モジュールがサポートされているのでそれを使う。
最新版はFTS4で、Android APIレベル11以上なら標準で使えるようなのでFTS4を使う。

公式ドキュメント
[SQLite FTS3 and FTS4 Extensions](http://www.sqlite.org/fts3.html)



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

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

```java: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テーブルにわけて作成する例。


```sql
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](http://www.sqlite.org/fts3.html#section_9)



## 保存

今回の例ではメインのテーブルとFTSテーブルにわけているので、両方のテーブルに保存する（メインのほうには通常のテキスト、FTSテーブルのほうにはN-gram変換したテキスト）。
※保存するテキストは、ユーザーの入力値になることがほとんどだと思うので、実際の開発時にはプリペアードステートメント等でSQLインジェクション対策する。 

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

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


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



## 検索

MATCHを使用する。

SQLは以下のような感じになる。検索キーワードにN-gram変換したテキストを使用する。
※検索キーワードはユーザーの入力値になることがほとんどだと思うので、実際の開発時にはプリペアードステートメント等でSQLインジェクション対策する。


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


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

```sql
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]
(http://www.sqlite.org/fts3.html#section_3)


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

普通にLIKE


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

CPU Time: user 0.104006 sys 0.240015
```


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

```sql
SELECT count(*) from hoge_fts WHERE words MATCH "ho og ge"
 
CPU Time: user 0.000000 sys 0.000000
```


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

```sql
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 前方一致でメインテーブルとの結合あり

```sql
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