ククログ

株式会社クリアコード > ククログ > groongaでN-gramを使って全文検索

groongaでN-gramを使って全文検索

groongaは活発に開発が続けられており、リポジトリ上のgroongaでは性能改善だけではなくAPIも改善されています。APIの変更点を紹介しつつ、N-gramを用いた全文検索の仕方を紹介します。ただし、継続的に改善されているので、APIはこれからも変わっていきます。ここで紹介する内容もしばらくするとすぐに古くなることに注意してください。

ここでは、groogaのインデックスを自動更新で作成したサンプルアプリケーションを2009/05/14時点でのgroongaのAPIにあわせた上で、MeCabではなくN-gram(bi-gram)でインデックスを作成するように変更します。

GRN_OBJ_INIT()にドメイン指定が必須

GRN_OBJ_INIT()に引数が一つ増えて「ドメイン」も受けとるようになっています。ドメインとはそのオブジェクトがとりうる値の範囲を示しているオブジェクトのことです。例えば、カラムのドメインが<int>であれば、そのカラムは32ビットの整数を持つということを表します。また、テーブルであれば、そのカラムはドメインに指定したテーブルのレコードIDを持つということを表します。カラムのドメインにテーブルを指定することにより、テーブル間の関連付けが行えます。

さて、サンプルアプリケーションではバルクオブジェクトを初期化するためにGRN_OBJ_INIT()を使っていました。バルクオブジェクトとは少し賢い文字列のようなものです。バイト列を持っていますが、バイト列の長さを取得できたり、バイト列に割り当てる領域を再利用できたりします。groongaでは値の受け渡しにバルクオブジェクトを使うことが多いので、実は、結構大事なオブジェクトです。

サンプルアプリケーション内では、単なるバイト列として使っていたのでバルクオブジェクトのドメインはGRN_ID_NILとします。ドメインにはオブジェクトそのものではなく、オブジェクトのIDを指定するのですが、GRN_ID_NILは存在しないオブジェクトを表すIDになります。LispやRubyを触ったことのある人なら、名前からすぐに想像がつきますね。

変更点は以下のようになります。

--- groonga/auto-index-update.c	2009-04-26 17:20:28 +09:00 (rev 32)
+++ groonga/auto-index-update.c	2009-05-14 17:35:56 +09:00 (rev 33)
@@ -64,7 +64,7 @@
     grn_id source_id;

     source_id = grn_obj_id(context, comment_column);
-    GRN_OBJ_INIT(&source, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
+    GRN_OBJ_INIT(&source, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY, GRN_ID_NIL);
     GRN_BULK_SET(context, &source, &source_id, sizeof(grn_id));

     grn_obj_set_info(context, comment_index_column, GRN_INFO_SOURCE, &source);
@@ -78,11 +78,11 @@

     id = grn_table_add(context, bookmarks);

-    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
+    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY, GRN_ID_NIL);
     GRN_BULK_SET(context, &value, uri, strlen(uri));
     grn_obj_set_value(context, uri_column, id, &value, GRN_OBJ_SET);

-    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY);
+    GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY, GRN_ID_NIL);
     GRN_BULK_SET(context, &value, comment, strlen(comment));
     grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);
 }

エンコーディング引数廃止

これまでは、テーブルやカラムなどオブジェクトを生成するときの引数にエンコーディング(grn_encoding)を指定していましたが、エンコーディングはコンテキスト(grn_ctx)から取得することになりました。エンコーディングの扱いは、メーリングリストに投稿された[groonga-dev,00056] Re: GRN_ENC_DEFAULTの扱いが詳しいです。

同一アプリケーション(同一コンテキスト)内ではエンコーディングを統一することが多いという観点からこのようになりました。引数が減ってすっきりしました。

この変更に対応するためには、単にエンコーディング引数を削除します。

--- groonga/auto-index-update.c	2009-05-14 17:35:56 +09:00 (rev 33)
+++ groonga/auto-index-update.c	2009-05-14 17:37:01 +09:00 (rev 34)
@@ -30,8 +30,7 @@
                                  NULL,
                                  GRN_OBJ_TABLE_NO_KEY,
                                  NULL,
-                                 0,
-                                 GRN_ENC_DEFAULT);
+                                 0);
     uri_column = create_column(context, bookmarks, "uri",
                                lookup(context, "<shorttext>"),
                                0);
@@ -48,8 +47,7 @@
                                NULL,
                                GRN_OBJ_TABLE_PAT_KEY,
                                lookup(context, "<shorttext>"),
-                               0,
-                               GRN_ENC_DEFAULT);
+                               0);

     comment_index_column = create_column(context, lexicon, "comment-index",
                                          bookmarks,
@@ -127,8 +125,7 @@
                               NULL,
                               GRN_OBJ_TABLE_HASH_KEY,
                               lexicon,
-                              0,
-                              GRN_ENC_DEFAULT);
+                              0);

     query = grn_obj_open(context, GRN_BULK, 0, 0);
     grn_bulk_write(context, query, word, strlen(word));
@@ -148,7 +145,7 @@
     grn_ctx context;

     grn_init();
-    grn_ctx_init(&context, 0, GRN_ENC_UTF8);
+    grn_ctx_init(&context, 0);
     grn_db_create(&context, NULL, NULL);

     define_bookmarks_table(&context);

grn_table_add()へキー指定が必須

テーブルにはキーがあるテーブル(ハッシュテーブルとパトリシアトライ)とキーがないテーブル(配列)がありました。これまでは、キーがないテーブルにレコードを追加する場合はgrn_table_add()を用いて、キーがあるテーブルにレコードを追加する場合はgrn_table_lookup()を用いていました。

このAPIの変更でどのテーブルにもgrn_table_add()でレコードを追加できるようになりました。grn_table_add()でキーがあるテーブルにもレコードが追加できるようになったため、キー関連の引数が増えています。

一部では、grn_table_lookup()でレコードが追加できるなんて気づかないよ!という声もあったのですが、この変更で機能と名前が一致したよいAPIになったのではないかと思います。

サンプルアプリケーションではキーがないテーブルにだけレコードを追加していました。キーがないレコードの場合はキー関連の引数にNULLなどを指定します。

--- groonga/auto-index-update.c	2009-05-14 17:37:01 +09:00 (rev 34)
+++ groonga/auto-index-update.c	2009-05-14 17:40:35 +09:00 (rev 35)
@@ -74,7 +74,7 @@
     grn_id id;
     grn_obj value;

-    id = grn_table_add(context, bookmarks);
+    id = grn_table_add(context, bookmarks, NULL, 0, NULL);

     GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY, GRN_ID_NIL);
     GRN_BULK_SET(context, &value, uri, strlen(uri));

BULK -> TEXT

前半でも登場した実は結構大事なバルクオブジェクトですが、バルクオブジェクトを操作する便利マクロがGRN_BULK_プリフィックスからGRN_TEXT_プリフィックスに変更になっています。これらの便利マクロが文字列関連の機能を提供していたのでこうなったのだと思います。機能を反映した名前に変更されていると思います。

ただ、このAPIはまだまだ変更される可能性があるので、注意してください。

この変更に対応するにはBULKをTEXTに置換します。

--- groonga/auto-index-update.c	2009-05-14 17:40:35 +09:00 (rev 35)
+++ groonga/auto-index-update.c	2009-05-14 17:41:26 +09:00 (rev 36)
@@ -63,7 +63,7 @@

     source_id = grn_obj_id(context, comment_column);
     GRN_OBJ_INIT(&source, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY, GRN_ID_NIL);
-    GRN_BULK_SET(context, &source, &source_id, sizeof(grn_id));
+    GRN_TEXT_SET(context, &source, &source_id, sizeof(grn_id));

     grn_obj_set_info(context, comment_index_column, GRN_INFO_SOURCE, &source);
 }
@@ -77,11 +77,11 @@
     id = grn_table_add(context, bookmarks, NULL, 0, NULL);

     GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY, GRN_ID_NIL);
-    GRN_BULK_SET(context, &value, uri, strlen(uri));
+    GRN_TEXT_SET(context, &value, uri, strlen(uri));
     grn_obj_set_value(context, uri_column, id, &value, GRN_OBJ_SET);

     GRN_OBJ_INIT(&value, GRN_BULK, GRN_OBJ_DO_SHALLOW_COPY, GRN_ID_NIL);
-    GRN_BULK_SET(context, &value, comment, strlen(comment));
+    GRN_TEXT_SET(context, &value, comment, strlen(comment));
     grn_obj_set_value(context, comment_column, id, &value, GRN_OBJ_SET);
 }

@@ -105,8 +105,8 @@

         uri = grn_obj_get_value(context, uri_accessor, result_id, NULL);
         comment = grn_obj_get_value(context, comment_accessor, result_id, NULL);
-        GRN_BULK_PUTC(context, uri, '\0');
-        GRN_BULK_PUTC(context, comment, '\0');
+        GRN_TEXT_PUTC(context, uri, '\0');
+        GRN_TEXT_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);

N-gramでインデックスを作成

API変更に追従するための変更は以上です。それでは、MeCabでインデックスを作成してした部分をN-gram(bi-gram)で作成するようにします。

まず、デフォルトトークナイザとして<token:mecab>を指定していた部分を<token:bigram>に変更します。

--- old
+++ new
     grn_obj_set_info(context, lexicon, GRN_INFO_DEFAULT_TOKENIZER,
-                     lookup(context, "<token:mecab>"));
+                     lookup(context, "<token:bigram>"));

そして、ここがドキュメントに載っていない重要なことなのですが、インデックス用のカラムにGRN_OBJ_WITH_POSITIONを指定して位置情報も記録するようにします。これを指定しないと2文字の検索語にしかマッチしなくなります。(bi-gramで切り出しているため)

--- old
+++ new
    comment_index_column = create_column(context, lexicon, "comment-index",
                                         bookmarks,
-                                        GRN_OBJ_COLUMN_INDEX);
+                                        GRN_OBJ_COLUMN_INDEX |
+                                        GRN_OBJ_WITH_POSITION);

これで、リポジトリ上の最新groongaを用いたN-gramベースの全文検索ができるようになります。

変更後のソースコードはクリアコードのリポジトリにあります。

まとめ

最新groongaではAPIが改善されていることを紹介したついでに、ドキュメントに書かれていないN-gram使用時の注意も紹介しました。

もちろん、Ruby/groongaのtrunkは最新groongaに対応しているので、最新groongaを最新Ruby/groongaから使うこともできます。