Sennaの後継となる組み込み型全文検索エンジンgroongaでインデックスを自動更新する方法を見つけたので紹介します。
「見つけた」という風に書いているのは、「ドキュメントには書いていないけどソースを見たらやり方がわかった」からです。
groonga
Sennaは転置インデックス関連の機能のみを提供していましたが、groonaでは転置インデックスだけではなく、データ管理の機能も提供しています。そのため、DBMSなど他のデータ管理機能を持つソフトウェアと組み合わせなくても、groongaだけでデータ管理と高速な全文検索機能を実現することができます。
groongaはGitHub上で開発されていて、groongaに関するドキュメントやgroongaのAPIのドキュメントもGitHub上にあります。
また、Sennaとgroongaの比較やgroongaデータベースAPIも読んでおくとよいと思います。
ここでは、上記のドキュメントには書いていなかったインデックスを自動更新する方法を紹介します。
サンプルプログラムの概要
コメント付きブックマークを管理し、コメントで全文検索できるプログラムを作成します。
ブックマークテーブル(
- uri: ブックマークしたURI
- comment: ブックマークへのコメント
全文検索用の単語を登録するテーブル(
- comment-index: インデックスに登録された単語を含むブックマークIDのリスト
ここでは、「comment」カラムにコメントを登録すると自動的にコメントのインデックスを更新して検索可能にする方法を紹介します。ちなみに、「コメントのインデックスを更新」とは、
処理の流れ
コメントを入れたプログラムをボトムアップで読んでいきながら説明します。まずは、main()です。
int
main (int argc, char **argv)
{
grn_ctx context;
/* 初期化 */
grn_init();
grn_ctx_init(&context, 0, GRN_ENC_UTF8);
grn_db_create(&context, NULL, NULL);
/* テーブル定義 */
define_bookmarks_table(&context);
define_lexicon_table(&context);
/* ポイント: インデックス自動更新の設定 */
assign_source(&context);
/* ブックマークの登録: 3件 */
add_bookmark(&context,
"http://groonga.org/",
"an open-source fulltext search engine and column store");
add_bookmark(&context,
"http://qwik.jp/senna/",
"an embeddable fulltext search engine");
add_bookmark(&context,
"http://cutter.sourceforge.net/",
"a unit testing framework for C");
/* 検索: 2回 */
search(&context, "search");
/* 結果: <search>で検索したら2件ヒット
* search result: <search>: 2
* uri | comment
* http://qwik.jp/senna/ | an embeddable fulltext search engine
* http://groonga.org/ | an open-source fulltext search engine and column store
*/
search(&context, "testing");
/* 結果: <testing>で検索したら1件ヒット
* search result: <testing>: 1
* uri | comment
* http://cutter.sourceforge.net/ | a unit testing framework for C
*/
/* 後始末 */
grn_ctx_fin(&context);
grn_fin();
return 0;
}
コメントに書いてある通りの処理の流れです。全部の関数を説明しようかと思ったのですが、大事なところはassign_source()のところなので、そこだけ説明します。
インデックスの元データの設定
assign_source()では、comment-indexカラムは
static void
assign_source (grn_ctx *context)
{
grn_obj source;
grn_id source_id;
/* comment_columnは<bookmarks>のcommentカラムオブジェクト */
/* grn_obj_id()でcommentカラムのIDを取得 */
source_id = grn_obj_id(context, comment_column);
/* GRN_INFO_SOURCEの値として設定する領域を初期化 */
GRN_OBJ_INIT(&source, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
/* GRN_INFO_SOURCEの値としてcommentカラムオブジェクトのIDを指定 */
GRN_BULK_SET(context, &source, &source_id, sizeof(grn_id));
/* comment-indexカラムのGRN_INFO_SOURCEにcommentカラムオブジェクトのIDを指定 */
grn_obj_set_info(context, comment_index_column, GRN_INFO_SOURCE, &source);
}
効果
comment-indexがcommentカラムを基にしていると設定することでインデックスの登録の手間が省けます。
static void
add_bookmark (grn_ctx *context, const char *uri, const char *comment)
{
grn_id id;
grn_obj value;
/* <bookmarks>テーブルにレコードを追加 */
id = grn_table_add(context, bookmarks);
/* <bookmarks>テーブルのuriカラムにURIを設定 */
GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
GRN_BULK_SET(context, &value, uri, strlen(uri));
grn_obj_set_value(context, uri_column, id, &value, GRN_OBJ_SET);
/* <bookmarks>テーブルのcommentカラムにコメントを設定 */
GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
GRN_BULK_SET(context, &value, comment, strlen(comment));
grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);
/* commentカラムのインデックスを作成 */
/* 不要
grn_column_index_update(context, comment_index_column, id, 1,
NULL, &value);
*/
}
追加するときは新しくインデックスを生成するだけでよいですが、更新する場合は、以前の値のインデックスを削除してから新しいインデックスを生成しなければいけません。
static void
update_bookmark_comment (grn_ctx *context, grn_id id, const char *comment)
{
grn_obj *old_value;
grn_obj value;
/* commentカラムの以前の値を取得 */
/* 不要
old_value = grn_obj_get_value(context, comment_column, id, NULL);
*/
/* commentカラムの値を更新 */
GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
GRN_BULK_SET(context, &value, comment, strlen(comment));
grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);
/* commentカラムのインデックスを更新 */
/* 不要
grn_column_index_update(context, comment_index_column, id, 1,
&old_value, &value);
*/
}
GRN_ELEMENT_INFOを設定していない場合は、値を更新する前に古い値を取得しておいてからインデックスを更新しなければいけません。
1つしかインデックスを使っていない場合はGRN_ELEMENT_INFOを使わなくても気にならない手間かもしれませんが、たくさんインデックスを使っているときはだいぶ楽になると思います。
まとめ
groongaにはドキュメントには書かれていない便利機能があります。GRN_ELEMENT_INFO以外ではaccessorにもびっくりしました。
ちなみに、groongaはテスティングフレームワークとしてCutterを採用しています。
ソースコード
ここで使用したサンプルコードです。ざっくりとコメントを入れておきました。
#include <stdio.h>
#include <string.h>
#include <groonga.h>
/* <bookmarks>テーブルとそのカラムたち */
static grn_obj *bookmarks, *uri_column, *comment_column;
/* <lexicon>テーブルとそのカラムたち */
static grn_obj *lexicon, *comment_index_column;
/* 名前からオブジェクトを取得するための便利関数 */
static grn_obj *
lookup (grn_ctx *context, const char *name)
{
return grn_ctx_lookup(context, name, strlen(name));
}
/* カラムを作成するための便利関数 */
static grn_obj *
create_column (grn_ctx *context, grn_obj *table,
const char *name, grn_obj *value_type, grn_obj_flags flags)
{
/* 一時カラム: ファイルには保存しない */
return grn_column_create(context, table,
name, strlen(name),
NULL, flags,
value_type);
}
/* <bookmarks>テーブルとそのカラムたちを定義 */
static void
define_bookmarks_table (grn_ctx *context)
{
/* 一時テーブル: ファイルには保存しない */
bookmarks = grn_table_create(context,
"<bookmarks>", strlen("<bookmarks>"),
NULL,
GRN_OBJ_TABLE_NO_KEY,
NULL,
0,
GRN_ENC_DEFAULT);
uri_column = create_column(context, bookmarks, "uri",
lookup(context, "<shorttext>"),
0);
comment_column = create_column(context, bookmarks, "comment",
lookup(context, "<shorttext>"),
0);
}
/* <lexicon>テーブルとそのカラムたちを定義 */
static void
define_lexicon_table (grn_ctx *context)
{
/* 一時テーブル: ファイルには保存しない
* GRN_OBJ_TABLE_PAT_KEYかGRN_OBJ_TABLE_HASH_KEYにすること
* GRN_OBJ_TABLE_NO_KEYは使えない
*/
lexicon = grn_table_create(context,
"<lexicon>", strlen("<lexicon>"),
NULL,
GRN_OBJ_TABLE_PAT_KEY,
lookup(context, "<shorttext>"),
0,
GRN_ENC_DEFAULT);
/* MeCabで検索用単語を切り出す */
grn_obj_set_info(context, lexicon, GRN_INFO_DEFAULT_TOKENIZER,
lookup(context, "<token:mecab>"));
comment_index_column = create_column(context, lexicon, "comment-index",
bookmarks, GRN_OBJ_COLUMN_INDEX);
}
/* comment-indexカラムとcommentカラムを関連付ける */
static void
assign_source (grn_ctx *context)
{
grn_obj source;
grn_id source_id;
/* comment_columnは<bookmarks>のcommentカラムオブジェクト */
/* grn_obj_id()でcommentカラムのIDを取得 */
source_id = grn_obj_id(context, comment_column);
/* GRN_INFO_SOURCEの値として設定する領域を初期化 */
GRN_OBJ_INIT(&source, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
/* GRN_INFO_SOURCEの値としてcommentカラムオブジェクトのIDを指定 */
GRN_BULK_SET(context, &source, &source_id, sizeof(grn_id));
/* comment-indexカラムのGRN_INFO_SOURCEにcommentカラムオブジェクトのIDを指定 */
grn_obj_set_info(context, comment_index_column, GRN_INFO_SOURCE, &source);
}
/* ブックマーク追加 */
static void
add_bookmark (grn_ctx *context, const char *uri, const char *comment)
{
grn_id id;
grn_obj value;
/* <bookmarks>テーブルにレコードを追加 */
id = grn_table_add(context, bookmarks);
/* <bookmarks>テーブルのuriカラムにURIを設定 */
GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
GRN_BULK_SET(context, &value, uri, strlen(uri));
grn_obj_set_value(context, uri_column, id, &value, GRN_OBJ_SET);
/* <bookmarks>テーブルのcommentカラムにコメントを設定 */
GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
GRN_BULK_SET(context, &value, comment, strlen(comment));
grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);
}
/* 検索結果の表示 */
static void
print_result (grn_ctx *context, grn_obj *result)
{
grn_table_cursor *cursor;
grn_id result_id;
grn_obj *uri_accessor, *comment_accessor;
/* アクセサ! */
uri_accessor = grn_table_column(context, result,
".comment-index.uri",
strlen(".comment-index.uri"));
comment_accessor = grn_table_column(context, result,
".comment-index.comment",
strlen(".comment-index.comment"));
printf("uri\t\t\t | comment\n");
/* カーソルで一行ずつ処理 */
cursor = grn_table_cursor_open(context, result, NULL, 0, NULL, 0, 0);
while ((result_id = grn_table_cursor_next(context, cursor)) != GRN_ID_NIL) {
grn_obj *uri, *comment;
uri = grn_obj_get_value(context, uri_accessor, result_id, NULL);
comment = grn_obj_get_value(context, comment_accessor, result_id, NULL);
/* 登録したURIとコメントはNULL終端していないので'\0'を追加 */
GRN_BULK_PUTC(context, uri, '\0');
GRN_BULK_PUTC(context, comment, '\0');
printf("%s\t | %s\n", GRN_BULK_HEAD(uri), GRN_BULK_HEAD(comment));
grn_obj_close(context, uri);
grn_obj_close(context, comment);
}
grn_table_cursor_close(context, cursor);
}
/* 検索して結果を表示 */
static void
search (grn_ctx *context, const char *word)
{
grn_obj *result;
grn_obj *query;
/* 検索結果を格納する一時テーブル
* キーにヒットしたレコードのIDが入るので、
* GRN_OBJ_TABLE_NO_KEYは使えない。
* GRN_OBJ_TABLE_HASH_KEYを指定すればよい
*/
result = grn_table_create(context,
NULL, 0,
NULL,
GRN_OBJ_TABLE_HASH_KEY,
lexicon, /* <lexicon>テーブルのレコードIDが入る */
0,
GRN_ENC_DEFAULT);
/* 検索 */
query = grn_obj_open(context, GRN_BULK, 0, 0);
grn_bulk_write(context, query, word, strlen(word));
grn_obj_search(context, comment_index_column, query, result,
GRN_SEL_OR, NULL);
grn_obj_close(context, query);
printf("search result: <%s>: %d\n", word, grn_table_size(context, result));
print_result(context, result);
grn_obj_close(context, result);
printf("\n");
}
int
main (int argc, char **argv)
{
grn_ctx context;
/* 初期化 */
grn_init();
grn_ctx_init(&context, 0, GRN_ENC_UTF8);
grn_db_create(&context, NULL, NULL);
/* テーブル定義 */
define_bookmarks_table(&context);
define_lexicon_table(&context);
/* ポイント: インデックス自動更新の設定 */
assign_source(&context);
/* ブックマークの登録: 3件 */
add_bookmark(&context,
"http://groonga.org/",
"an open-source fulltext search engine and column store");
add_bookmark(&context,
"http://qwik.jp/senna/",
"an embeddable fulltext search engine");
add_bookmark(&context,
"http://cutter.sourceforge.net/",
"a unit testing framework for C");
/* 検索: 2回 */
search(&context, "search");
/* 結果: <search>で検索したら2件ヒット
* search result: <search>: 2
* uri | comment
* http://qwik.jp/senna/ | an embeddable fulltext search engine
* http://groonga.org/ | an open-source fulltext search engine and column store
*/
search(&context, "testing");
/* 結果: <testing>で検索したら1件ヒット
* search result: <testing>: 1
* uri | comment
* http://cutter.sourceforge.net/ | a unit testing framework for C
*/
/* 後始末 */
grn_ctx_fin(&context);
grn_fin();
return 0;
}