読者です 読者をやめる 読者になる 読者になる

shobylogy

叩けシンプルの杖

iOSアプリでSQLiteを使い日本語の全文検索をする。

iOSアプリで全文検索

iOSアプリで日本語の全文検索したいこと、ありますよね。私はあります。

そもそもiOSアプリ内で日本語の全文検索をするという需要がないみたいで、情報が少なかったのでまとめました。

SQLiteの全文検索拡張

実はSQLiteにはFTS3 and FTS4 extensionsという全文検索拡張が存在します。

以前は、iOS SDKに含まれているSQLiteライブラリで無効だったため、自前でビルドする必要があったのですが、 iOS 6 SDKからはデフォルトで有効です。

基本的なFTSの使い方

テーブル

CREATE VIRTUAL TABLE article USING fts4 (title, body);

完全一致、前方一致検索を高速にできます。

INSERT INTO article(title, body) VALUES ('Apple', 'I have iPhone4 and iPhone5');

SELECT * FROM article WHERE body MATCH 'iPhone5'; 
SELECT * FROM article WHERE body MATCH 'iPhone* '; 

ただし、日本語の場合、デフォルトで使用可能なtokenizerは日本語をうまくtokenに分けることができません。*1

そのため、tokenizerが理解できる形式として、あらかじめ半角スペースでtokenに分けたデータを挿入することで対応します。

テーブル構造

CREATE TABLE article (id, title, body);
CREATE VIRTUAL TABLE articleTokens USING fts4 (articleId, bodyTokens);

元データを保持するテーブルと、token分割したデータを保持するテーブルを分けます。

INSERT

INSERTしたいのはこんなデータ

INSERT INTO article(id, title, body) VALUES (1, 'アップル', '私はiPhone4とiPhone5を持っています');
INSERT INTO articleTokens(articleId, bodyTokens) VALUES (1, '私は iPhone4 と iPhone5 を 持っています');

これを目標にSQLを組み立てます。

トークン分割

tokenに分けるのはCFStringTokenizerを使用します。

トークン分割メソッド

- (NSArray *)tokenArrayWithString:(NSString *)string
{  
  NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"ja"];
  CFRange range = CFRangeMake(0, CFStringGetLength((CFStringRef)string));
  CFStringTokenizerRef tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, (CFStringRef)string, range, kCFStringTokenizerUnitWordBoundary, (CFLocaleRef)locale);
  
  NSMutableArray *tokenArray = [NSMutableArray array];
  
  while(CFStringTokenizerAdvanceToNextToken(tokenizer) != kCFStringTokenizerTokenNone) {
        CFRange tokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer);
        if(range.location != kCFNotFound) {
            NSString *token = [string substringWithRange:NSMakeRange(tokenRange.location, tokenRange.length)];
            [tokenArray addObject:token];
        }
    }
  
  CFRelease(tokenizer);
  
  return tokenArray;
}

得られたNSArrayを半角スペースで連結して使用します。

[tokenArray componentsJoinedByString:@" "];

SELECT

SELECT * FROM article JOIN (
SELECT articleId FROM articleTokens WHERE bodyTokens MATCH 'iPhone*'
) AS result ON article.id = result.articleId

こんな感じでデータが取得できます。

まとめ

SQLiteのFTSを使用して、日本語で全文検索を使用する方法を紹介しました。

日本語の場合、CFStringTokenizerを使用してINSERTするデータ自体を分割してしまうことによって、 わりと簡単に全文検索を使用する事ができます。

さらにranking用関数を作成して、マッチした順にデータを返すことも可能です。

CoreDataにも不向きな用途があるので、パフォーマンスが求められる部分にはSQLiteを使うのも良いと思います。

参考リンク

*1:icu tokenizerはiOS 7 SDKに含まれるライブラリでも使用できませんでした。