Feature #1257

簡単にトークナイザを開発できるようにする

Added by Susumu Yata almost 6 years ago. Updated over 5 years ago.

Status:担当者作業中Start date:01/26/2012
Priority:NormalDue date:
Assignee:Susumu Yata% Done:

0%

Category:-
Target version:アイスボックス

Description

h3. 問題

groonga ではトークナイザをプラグインとして追加できるようになっていますが,外部には公開されていない関数が必要であり,さらには暗黙的な知識が必要になるなど,開発のために超えなくてはならないハードルが極めて高くなっています.

h3. 期待する状態

  • 非公開の関数を使わずにトークナイザを開発できること
  • ヘッダとサンプルを見てトークナイザを開発できること
  • 開発したトークナイザの組み込み方法が分かること

h3. 解決方法

  • トークナイザの開発に必要なマクロ・関数を公開します.
  • コメント付きの分かりやすいヘッダとサンプルを用意します.
  • ドキュメントを用意します.

History

#1 Updated by Susumu Yata almost 6 years ago

とりあえず,以下のような型・関数・マクロを提供すれば TokenMecab と同等のトークナイザを実装できそうです.

/*
  GRN_TOKENIZER_MALLOC() allocates `size' bytes and returns a pointer to the
  allocated memory space. Note that the memory space is associated with `ctx'.
 */
#define GRN_TOKENIZER_MALLOC(ctx, size) \
  grn_tokenizer_malloc((ctx), (size), __FILE__, __LINE__, __FUNCTION__)
/*
  GRN_TOKENIZER_FREE() frees a memory space allocated by
  GRN_TOKENIZER_MALLOC(). This means that `ptr' must be a pointer returned by
  GRN_TOKENIZER_MALLOC().
 */
#define GRN_TOKENIZER_FREE(ctx, ptr) \
  grn_tokenizer_free((ctx), (ptr), __FILE__, __LINE__, __FUNCTION__)

/*
  GRN_TOKENIZER_LOG() reports a log of `level'. Its error message is generated
  from `format' and the varying number of arguments. See grn_log_level in
  "groonga.h" for more details of `level'.
 */
#define GRN_TOKENIZER_LOG(ctx, level, format, ...) \
  GRN_LOG((ctx), (level), (format), ## __VA_ARGS__)

/*
  GRN_TOKENIZER_ERROR() reports an error of `error_code'. Its error message is
  generated from `format' and the varying number of arguments. See grn_rc in
  "groonga.h" for more details of `error_code'.
 */
#define GRN_TOKENIZER_ERROR(ctx, error_code, format, ...) \
  GRN_TOKENIZER_SET_ERROR(ctx, GRN_LOG_ERROR, error_code, \
                          format, ## __VA_ARGS__)

/*
  grn_tokenizer_mutex is available to make a critical section. See the
  following functions.
 */
typedef struct _grn_tokenizer_mutex grn_tokenizer_mutex;

/*
  grn_tokenizer_mutex_create() returns a pointer to a new object of
  grn_tokenizer_mutex. Memory for the new object is obtained with
  GRN_TOKENIZER_MALLOC(). grn_tokenizer_mutex_create() returns NULL if
  sufficient memory is not available.
 */
grn_tokenizer_mutex *grn_tokenizer_mutex_create(grn_ctx *ctx);

/*
  grn_tokenizer_mutex_destroy() finalizes an object of grn_tokenizer_mutex
  and then frees memory allocated for that object.
 */
void grn_tokenizer_mutex_destroy(grn_ctx *ctx, grn_tokenizer_mutex *mutex);

/*
  grn_tokenizer_mutex_lock() locks a mutex object. If the object is already
  locked, the calling thread waits until the object will be unlocked.
 */
void grn_tokenizer_mutex_lock(grn_ctx *ctx, grn_tokenizer_mutex *mutex);

/*
  grn_tokenizer_mutex_unlock() unlocks a mutex object.
  grn_tokenizer_mutex_unlock() should not be called for an unlocked object.
 */
void grn_tokenizer_mutex_unlock(grn_ctx *ctx, grn_tokenizer_mutex *mutex);

/*
  grn_tokenizer_charlen() returns the length (#bytes) of the first character
  in the string specified by `str_ptr' and `str_length'. If the starting bytes
  are invalid as a character, grn_tokenizer_charlen() returns 0. See
  grn_encoding in "groonga.h" for more details of `encoding'
 */
int grn_tokenizer_charlen(grn_ctx *ctx, const char *str_ptr,
                          unsigned int str_length, grn_encoding encoding);

/*
  grn_tokenizer_isspace() returns the length (#bytes) of the first character
  in the string specified by `str_ptr' and `str_length' if it is a space
  character. Otherwise, grn_tokenizer_isspace() returns 0.
 */
int grn_tokenizer_isspace(grn_ctx *ctx, const char *str_ptr,
                          unsigned int str_length, grn_encoding encoding);

/*
  grn_tokenizer_query is a structure for storing a query. See the following
  functions.
 */
typedef struct _grn_tokenizer_query grn_tokenizer_query;

struct _grn_tokenizer_query {
  grn_str *str;
  const char *ptr;
  unsigned int length;
  grn_encoding encoding;
};

/*
  grn_tokenizer_query_create() parses `args' and returns a new object of
  grn_tokenizer_query. The new object stores information of the query.
  grn_tokenizer_query_create() normalizes the query if the target table
  requires normalization. grn_tokenizer_query_create() returns NULL if
  something goes wrong. Note that grn_tokenizer_query_create() must be called
  just once in the function that initializes a tokenizer.
 */
grn_tokenizer_query *grn_tokenizer_query_create(grn_ctx *ctx,
                                                int num_args, grn_obj **args);

/*
  grn_tokenizer_mutex_destroy() finalizes an object of grn_tokenizer_mutex
  and then frees memory allocated for that object.
 */
void grn_tokenizer_query_destroy(grn_ctx *ctx, grn_tokenizer_query *query);

/*
  grn_tokenizer_token is needed to return tokens. A grn_tokenizer_token object
  stores a token to be returned and it must be maintained until a request for
  next token or finalization comes.
 */
typedef struct _grn_tokenizer_token grn_tokenizer_token;

struct _grn_tokenizer_token {
  grn_obj str;
  grn_obj status;
};

/*
  grn_tokenizer_token_init() initializes `token'. Note that an initialized
  object must be finalized by grn_tokenizer_token_fin().
 */
void grn_tokenizer_token_init(grn_ctx *ctx, grn_tokenizer_token *token);

/*
  grn_tokenizer_token_fin() finalizes `token' that has been initialized by
  grn_tokenizer_token_init().
 */
void grn_tokenizer_token_fin(grn_ctx *ctx, grn_tokenizer_token *token);

/*
  grn_tokenizer_status provides a list of tokenizer status codes.
  GRN_TOKENIZER_CONTINUE means that the next token is not the last one and
  GRN_TOKENIZER_LAST means that the next token is the last one. If a document
  or query contains no tokens, please push an empty string with
  GRN_TOKENIZER_LAST as a token.
 */
typedef enum _grn_tokenizer_status grn_tokenizer_status;

enum _grn_tokenizer_status {
  GRN_TOKENIZER_CONTINUE = 0,
  GRN_TOKENIZER_LAST     = 1
};

/*
  grn_tokenizer_token_push() pushes the next token in `*token'. Note that
  grn_tokenizer_token_push() does not make a copy of the given string. This
  means that you have to maintain a memory space allocated to the string.
  Also note that the grn_tokenizer_token object must be maintained until the
  request for the next token or finalization comes. See grn_tokenizer_status in
  this header for more details of `status'.
 */
void grn_tokenizer_token_push(grn_ctx *ctx, grn_tokenizer_token *token,
                              const char *str_ptr, unsigned int str_length,
                              grn_tokenizer_status status);

/*
  grn_tokenizer_register() registers a plugin to the database which is
  associated with `ctx'. `plugin_name_ptr' and `plugin_name_length' specify the
  plugin name. Alphabetic letters ('A'-'Z' and 'a'-'z'), digits ('0'-'9') and
  an underscore ('_') are capable characters. `init', `next' and `fin' specify
  the plugin functions. `init' is called for initializing a tokenizer for a
  document or query. `next' is called for extracting tokens one by one. `fin'
  is called for finalizing a tokenizer. grn_tokenizer_register() returns
  GRN_SUCCESS on success, an error code on failure. See "groonga.h" for more
  details of grn_proc_func and grn_user_data, that is used as an argument of
  grn_proc_func.
 */
grn_rc grn_tokenizer_register(grn_ctx *ctx, const char *plugin_name_ptr,
                              unsigned int plugin_name_length,
                              grn_proc_func *init, grn_proc_func *next,
                              grn_proc_func *fin);

#2 Updated by daijiro MORI almost 6 years ago

現状のgrn_tokenでは検索時と更新時に必要なトークンの違いを、GRN_TOKEN_REACH_ENDのようなstatusの値で表現していますが、これは確かに複雑で分かりにくいかも知れないですね。 トークナイザを初期化する時点で検索用か更新用かを知らせるようにして、あとはトークナイザの内部処理に任せた方がいいのかもしれない。そうするならstatusは廃止して、代わりに空文字列をpushすることで終端を表してもいいかもしれないですね。

#3 Updated by Susumu Yata almost 6 years ago

メモリ管理(malloc/free),エラー処理(error/log),排他制御(mutex)については,他でも使えそうということで,プラグイン用の API(groonga/plugin.h)に移動しました.この移動にともない,prefix は GRN_PLUGIN および grn_plugin になります.

#4 Updated by Susumu Yata almost 6 years ago

現状の API で問題なくトークナイザを開発できるかどうかを試すため,kytea を使ったトークナイザを入れてみました.ごく小さなデータではありますが,動作確認をおこなっています.

ただ,モデルのサイズや分かち書きにかかる時間という点で MeCab の方が優れているため,特段の理由がなければ MeCab を使った方が良いと思います.現状ではモデルを切り替えることができないのも残念です.

一応,モデル別にトークナイザを用意すれば複数のモデルを使えるのですが,それは面倒にすぎるように思います.とはいえ,モデルを切り替えできるようにすると,環境による影響を受けやすくなります.モデルの有無やパスなどの問題です.また,モデルが更新されることによって,索引構築時とは異なる分かち書きになり,検索漏れが発生するなどの問題が起こる可能性があります.

肝心の API に対する評価としては,「トークナイザを自前で開発するのは難しいかもしれない」となります.少なくとも面倒ではあります.ちょっと試しにやってみようかという程度の覚悟では手を出せないと感じます.排他制御(mutex)が必要になったり,何かと ctx が必要だったり,引数の役割が不鮮明だったりと,開発者を混乱させる要素がそれなりにあります.

たとえば,C++ でトークナイザを開発するとき,メモリの解放やオブジェクトの終了処理が ctx を要求するのは想像以上に厄介でした.エレガントに解決できないのでストレスがたまってしまいます.

#5 Updated by daijiro MORI over 5 years ago

  • Target version changed from release-1.3 to アイスボックス

Also available in: Atom PDF