はじめに

SQLiteの全文検索の拡張FTSは、まだ実験的な段階でfts1, fts2と作っては破棄(?)してきて、今はfts3が最新のようです。(sqlite3.5.1)。ただし、コンパイル済みバイナリの配布は止めてしまったようです。実験段階なので欲しい人は自分でコンパイルしてね♥みたいな。このftsで日本語が使えないとか色々言われているのに加えて、ちょっとプロトタイプをでっち上げるのに必要になりそうな雰囲気なので、日本語でもftsが使えるようにしてみました。

そもそもの動機

SQLite の全文検索を Python から使ってみる (3)』を読んだ。ここでftsのデフォルトの英文用のtokenizer(空白で単語を区切る)で日本語を無理やり使うためにMeCabを使って分かち書きをして、その文字列を挿入し、取り出したあとに連結とかしてた・・・。誰も知らないようだが、実はfts2のころから簡単にユーザーが独自のtokenizerを作って組み込めるようになっていたのですよ・・・。MeCabで分かち書きをした文字列を無駄に持つくらいなら、そのままMeCabをtokenizerとして使えばいいですやん?というわけです。

いきなりですがコードです

fts3_mecab.c

/* 
 * This file implements a tokenizer for fts3 based on the MeCab library.
 */

#if !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3)

#include <assert.h>
#include <string.h>
#include <sqlite3ext.h>
#include <fts3_tokenizer.h>
#include <mecab.h>
SQLITE_EXTENSION_INIT1

#ifdef WIN32
#define DLLIMPORT __declspec(dllimport)
#define DLLEXPORT __declspec(dllexport)
#else
#define DLLIMPORT
#define DLLEXPORT
#endif

typedef struct MecabTokenizer {
    sqlite3_tokenizer base;
    mecab_t *mecab;
} MecabTokenizer;

typedef struct MecabCursor {
    sqlite3_tokenizer_cursor base;
    mecab_node_t *node;
    char *buf;
    int buflen;
    int offset;
    int pos;
} MecabCursor;

/*
 * Create a new tokenizer instance.
 */
static int mecabCreate(
    int argc,                       /* Number of entries in argv[] */
    const char * const *argv,       /* Tokenizer creation arguments */
    sqlite3_tokenizer **ppTokenizer /* OUT: Created tokenizer */
){
    MecabTokenizer *p;
    mecab_t *mecab;
    p = (MecabTokenizer*) malloc(sizeof(MecabTokenizer));
    if(p == NULL) {
        return SQLITE_NOMEM;
    }
    memset(p, 0, sizeof(MecabTokenizer));
    
    p->mecab = mecab_new(argc, (char**)argv);
    if (p->mecab == NULL) {
        return SQLITE_ERROR; 
    }

    *ppTokenizer = (sqlite3_tokenizer *)p;

    return SQLITE_OK;
}

/* 
 * Destroy a tokenizer
 */
static int mecabDestroy(sqlite3_tokenizer *pTokenizer){
    MecabTokenizer *p = (MecabTokenizer *)pTokenizer;
    mecab_destroy(p->mecab);
    free(p);
    return SQLITE_OK;
}

/*
 * Prepare to begin tokenizing a particular string.  The input
 * string to be tokenized is pInput[0..nBytes-1].  A cursor
 * used to incrementally tokenize this string is returned in 
 * *ppCursor.
 */
static int mecabOpen(
    sqlite3_tokenizer *pTokenizer,      /* The tokenizer */
    const char *pInput,                 /* Input string */
    int nInput,                         /* Length of pInput in bytes */
    sqlite3_tokenizer_cursor **ppCursor /* OUT: Tokenization cursor */
) {
    MecabTokenizer *p = (MecabTokenizer *)pTokenizer;
    MecabCursor *pCsr;
    mecab_node_t *node;

    *ppCursor = 0;

    pCsr = (MecabCursor *)malloc( sizeof(MecabCursor));
    if(pCsr == NULL){
        return SQLITE_NOMEM;
    }
    memset(pCsr, 0, sizeof(MecabCursor));

    node = mecab_sparse_tonode2(p->mecab, pInput, strlen(pInput)+1);
    if (node == NULL) {
        return SQLITE_ERROR;
    }
#define DEFAULT_CURSOR_BUF 256
    pCsr->node = node;
    pCsr->buf = malloc(DEFAULT_CURSOR_BUF);
    pCsr->buflen = DEFAULT_CURSOR_BUF;
    pCsr->offset = 0;
    pCsr->pos = 0;

    *ppCursor = (sqlite3_tokenizer_cursor *)pCsr;
    return SQLITE_OK;
}

/*
 * Close a tokenization cursor previously opened 
 * by a call to mecabOpen(). 
 */
static int mecabClose(sqlite3_tokenizer_cursor *pCursor){
    MecabCursor *pCsr = (MecabCursor *)pCursor;
    free(pCsr->buf);
    free(pCsr);
    return SQLITE_OK;
}

/*
 * Extract the next token from a tokenization cursor.
 */
static int mecabNext(
    sqlite3_tokenizer_cursor *pCursor,/* Cursor returned by mecabOpen */
    const char **ppToken,   /* OUT: *ppToken is the token text */
    int *pnBytes,           /* OUT: Number of bytes in token */
    int *piStartOffset,     /* OUT: Starting offset of token */
    int *piEndOffset,       /* OUT: Ending offset of token */
    int *piPosition         /* OUT: Position integer of token */
){
    mecab_node_t *node;
    int nlen;

    MecabCursor *pCsr = (MecabCursor *)pCursor;
    node = pCsr->node;
    while (node->next != NULL && node->length == 0) {
        node = node->next;
    }
    
    nlen = node->length;
    if (node->length > pCsr->buflen) {
        char *buf = realloc(pCsr->buf, node->length + 1);
        pCsr->buf = buf;
        pCsr->buflen = node->length;
    }
    strncpy(pCsr->buf, node->surface, node->length);
    pCsr->buf[node->length] = '\0';

    *ppToken = pCsr->buf;
    *pnBytes = node->length;
    *piStartOffset = pCsr->offset;
    *piEndOffset = pCsr->offset + node->length;
    *piPosition = pCsr->pos++;

    if (node->next == NULL) {
      return SQLITE_DONE;
    }
    pCsr->node = node->next;
    pCsr->offset += node->rlength;
    return SQLITE_OK;
}

/*
 * The set of routines that implement the MeCab tokenizer
 */
static const sqlite3_tokenizer_module mecabTokenizerModule = {
    0,                          /* iVersion */
    mecabCreate,                /* xCreate  */
    mecabDestroy,               /* xCreate  */
    mecabOpen,                  /* xOpen    */
    mecabClose,                 /* xClose   */
    mecabNext,                  /* xNext    */
};


static int registerTokenizer(
    sqlite3 *db, 
    char *zName, 
    const sqlite3_tokenizer_module *p
){
    int rc;
    sqlite3_stmt *pStmt;
    const char zSql[] = "SELECT fts3_tokenizer(?, ?)";
    
    rc = sqlite3_prepare_v2 (db, zSql, -1, &pStmt, 0);

    if( rc!=SQLITE_OK ){
        return rc;
    }

    sqlite3_bind_text(pStmt, 1, zName, -1, SQLITE_STATIC);
    sqlite3_bind_blob(pStmt, 2, &p, sizeof(p), SQLITE_STATIC);
    sqlite3_step(pStmt);

    return sqlite3_finalize(pStmt);
}

/*
 * entry point
 */
DLLEXPORT
int sqlite3_extension_init (
    sqlite3 *db,          /* The database connection */
    char **pzErrMsg,      /* Write error messages here */
    const sqlite3_api_routines *pApi  /* API methods */
) {
    SQLITE_EXTENSION_INIT2(pApi)

    return registerTokenizer(db, "mecab", &mecabTokenizerModule);
}

#endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3) */

コンパイルするにはsqlite、sqlite/ext/fts3、mecab、iconvが必用です。 共有ライブラリとしてコンパイルします。mingwでDLLを作りました。

gcc -O2 \
    -shared fts3_mecab.c -o fts3mecab.dll \
    -I. \
    -I/home/reddog/src/sqlite \
    -I/home/reddog/src/sqlite/src \
    -I/home/reddog/src/sqlite/ext/fts3 \
    -I/home/reddog/src/mecab-0.96/src \
    -I/home/reddog/src/mecab-0.96/src \
    -L/home/reddog/src/mecab-0.96/src/.libs \
    -lmecab
strip fts3mecab.dll
cp fts3mecab.dll c:/bin

以上です。

いきなりですがバイナリです

http://reddog.s35.xrea.com/software/fts3mecab.zip
libmecab-1.dllをパスの通ったところに置いてください。またlibiconv2.dllが 必用なので、LibIconv for Windows からBinariesをダウンロードしてください。このiconv名前が変な気がする……。 fts3.dllとfts3mecab.dllはsqliteから直接ロードするのでどこに置いてもいいです。

辞書ファイル

sqlite3の文字コードとmecabの辞書の文字コードをUTF-8にすることにします。 UTF-8の辞書の作り方はMeCabのマニュアルに書いてあるのでここでは書きません。 デカイので鯖にも上げません。各自勝手に作って適当なところに配置してください。 俺はmecabの引数で辞書のパスを渡すことにして、mingwを使ってmake installした デフォルトの C:\msys\1.0\local\lib\mecab\dic\ipadicに置きました。なんか mecabはvcビルドだと勝手にレジストリ使って辞書の位置を得ようとするのが 気に食わない。なんかmingwビルドの辞書の配置の仕方がよくわからない。うーむ。

使ってみる

Tclの対話インターフェースから使ってみます。

package require sqlite

sqlite db test.db

# 拡張機能を有効にする
db enable_load_extension 1

# FTS3を読み込む
db eval "SELECT load_extension('c:/bin/fts3.dll');"

# mecab tokenizerを読み込む
db eval "SELECT load_extension('c:/bin/fts3mecab.dll');"


# mecab tokenizerを使ってftsの仮想テーブルを作成する。
# mecab tokenizerの引数はmecabで使える引数と全く同じにしました。
# よくわからんので辞書ファイルやmecabrcファイルの指定をしています。
# -O wakatiのオプションは必須です。
db eval {
    CREATE VIRTUAL TABLE t USING FTS3 (
        str TEXT,
        tokenize mecab 
            '-O' 'wakati' 
            '-r' 'C:\msys\1.0\home\reddog\src\mecab-0.96\mecabrc' 
            '-d' 'C:\msys\1.0\local\lib\mecab\dic\ipadic'
        );
}
 
db eval {
    BEGIN;
    INSERT INTO t VALUES ('分け入っても分け入っても青い山');
    INSERT INTO t VALUES ('大銀杏散りつくしたる大空');
    INSERT INTO t VALUES ('松はみな枝垂れて南無観世音');
    INSERT INTO t VALUES ('まったく雲がない笠をぬぎ');
    INSERT INTO t VALUES ('やつぱり一人がよろしい雑草');
    INSERT INTO t VALUES ('捨てきれない荷物のおもさまへうしろ');
    INSERT INTO t VALUES ('すべつてころんで山がひつそり');
    INSERT INTO t VALUES ('待つでも待たぬでもない雑草の月あかり');
    INSERT INTO t VALUES ('ともかくも生かされてはゐる雑草の中');
    INSERT INTO t VALUES ('この道しかない春の雪ふる');
    INSERT INTO t VALUES ('ころり寝ころべば青空');
    END;
}

全件取得してみると、

foreach str [db eval "SELECT str FROM t"] {puts $str}
分け入っても分け入っても青い山
大銀杏散りつくしたる大空
松はみな枝垂れて南無観世音
まったく雲がない笠をぬぎ
やつぱり一人がよろしい雑草
捨てきれない荷物のおもさまへうしろ
すべつてころんで山がひつそり
待つでも待たぬでもない雑草の月あかり
ともかくも生かされてはゐる雑草の中
この道しかない春の雪ふる
ころり寝ころべば青空

で、

foreach str [db eval "SELECT str FROM t WHERE str MATCH '山'"] {puts $str}
分け入っても分け入っても青い山
すべつてころんで山がひつそり

それから、

foreach str [db eval "SELECT str FROM t WHERE str MATCH '雑草'"] {puts $str}
やつぱり一人がよろしい雑草
待つでも待たぬでもない雑草の月あかり
ともかくも生かされてはゐる雑草の中

この辺はちょっと微妙かもしれないが、どう処理するのが適切なのか不明。

foreach str [db eval "SELECT str FROM t WHERE str LIKE '%空%'"] {puts $str}
大銀杏散りつくしたる大空
ころり寝ころべば青空
foreach str [db eval "SELECT str FROM t WHERE str MATCH '空'"] {puts $str}
# No Result

一応使えているように見えますがどうでしょう。
とりあえずlibiconv2.dllやめたい。きれいに書き直したい。

コメントをどーぞ



CategoryTclTk


|New|Edit|Freeze|Diff|History|Attach|Copy|Rename|
Last-modified: 2011-04-28 (Thu) 16:04:54
HTML convert time: 0.018 sec.