ククログ

株式会社クリアコード > ククログ > GroongaのNormalizerNFKC*ノーマライザーにオプションを追加する(ステートレス編)

GroongaのNormalizerNFKC*ノーマライザーにオプションを追加する(ステートレス編)

Groongaのノーマライザーについてお勉強中の児玉です。

今回は、GroongaのNormalizerNFKC*ノーマライザーに新しいオプションを追加した際の手順についてお話しします。 はじめてこの作業に取り組んだとき、どこから手をつけるべきか悩みました。 そこで、同じようにノーマライザーにオプションを追加したいと考えている方に向けて、私が学んだことをもとにその方法を紹介します!

Groonga開発者に聞け!(グルカイ!)第68回でも解説しているので、あわせてご覧ください!

この記事では、NormalizerNFKC*ノーマライザーにオプションを追加して機能を拡張する具体的な実装方法を紹介します。 他にもノーマライザーに機能追加する方法はあるのですが、詳しくはGroongaのノーマライザーへ機能追加する方針についてをご覧ください。

ノーマライザーに新しいオプションを追加する方法

それでは、新しいオプションを追加する方法を見ていきましょう。ノーマライザーにオプションを実装する際は、以下の手順で進めます。 各ステップについて詳しく解説します。

  1. オプションを保存する場所を作成する
  2. ユーザーの入力からオプションを取得して保存する
  3. オプションのロジックを実装する

オプションを保存する場所を作成する

ユーザーがオプションを指定した際に、そのオプション情報を保持しておく場所が必要になります。 実際に、unify_latin_alphabet_withオプションを追加した際のコード例を元に一緒に見ていきましょう。

まず、ユーザーが指定したオプションを保持するための場所を用意します。 新しいオプションを追加する場合は、オプション情報を保持している構造体に新しいオプション用のメンバーを追加します。

typedef struct {
  // ...
  bool unify_latin_alphabet_with;
  // ...
} grn_nfkc_normalize_options

このgrn_nfkc_normalize_optionsが、ノーマライザーのオプション情報を保持している構造体になります。 なのでこの例では、unify_latin_alphabet_withメンバーをgrn_nfkc_normalize_options構造体に追加しています。 ここまでで、オプションを保存する場所を用意できました。

オプションを保存する場所を作成したら、次にそのオプションの初期値も設定する必要があります。 なぜなら、ユーザーがオプションを指定しなかった場合、デフォルト値を設定しておかないと、どのような値に初期化されるか不定だからです。

そのため次にオプションを初期化しデフォルト値を設定する部分を実装します。

void
grn_nfkc_normalize_options_init(grn_ctx *ctx,
                                grn_nfkc_normalize_options *options,
                                grn_nfkc_char_type_func char_type_func,
                                grn_nfkc_decompose_func decompose_func,
                                grn_nfkc_compose_func compose_func)
{
  // ...
  options->unify_latin_alphabet_with = false;
  // ...
}

これを行うのがgrn_nfkc_normalize_options_init関数です。この関数は、各オプションにデフォルトの値を設定し初期化します。 この例では、unify_latin_alphabet_withfalseを設定しており、デフォルトではこのオプションを無効にしています。 ここまでで、オプションを保存する場所と、そのオプションを初期化する準備が整いました。次は、ユーザーの入力からオプションを取得し、保存する方法を見ていきます。

ユーザーの入力からオプションを取得して保存する

次に、ユーザーの入力からオプションを取得して保存する処理を実装します。そのためにはユーザーからの入力を解析し、その値を保存する必要があります。 ここでは、grn_nfkc_normalize_options_apply関数を使用して、ユーザーが指定したunify_latin_alphabet_withオプションの値を取得し、保存する方法を見ていきます。

grn_rc
grn_nfkc_normalize_options_apply(grn_ctx *ctx,
                                 grn_nfkc_normalize_options *options,
                                 grn_obj *raw_options)
{
  GRN_OPTION_VALUES_EACH_BEGIN(ctx, raw_options, i, name, name_length) {
    grn_raw_string name_raw;
    name_raw.value = name;
    name_raw.length = name_length;

    if (/* Checking the other option's condition. */) {
      // ...
    } else if (GRN_RAW_STRING_EQUAL_CSTRING(name_raw,
                                     "unify_latin_alphabet_with")) {
      options->unify_latin_alphabet_with =
        grn_vector_get_element_bool(ctx,
                                    raw_options,
                                    i,
                                    options->unify_latin_alphabet_with);
    }
  } GRN_OPTION_VALUES_EACH_END();

  return ctx->rc;
}

この関数では、ユーザーが指定したオプション情報がraw_optionsオブジェクトに格納されています。 raw_options内に特定のオプション(この例ではunify_latin_alphabet_with)が存在する場合、 その値をノーマライザーのオプション情報を保持している構造体であるoptionsに保存します。 optionsの対応するメンバーに、ユーザーの入力値が反映される仕組みです。 結果として、unify_latin_alphabet_withの値がユーザー指定の値に更新されます。

ここまでで、ユーザーの入力からオプション情報を取得し、それを保存する処理までを実装しました。 しかし、オプション情報が保存されただけでは、そのオプションの機能自体は動作しません。 なぜなら、そのオプションのロジック部分を実装していないからです。次は、実際にオプションのロジックを実装する方法を見ていきましょう!

オプションのロジックを実装する

ノーマライザーオプションのロジックを実装する上で、代表的な実装パターンについて説明してから、オプションの具体的な実装について紹介します。 パターンを知ることでオプションの実装イメージが付きやすくります。ここでは、次の流れに沿って紹介します。

  • ノーマライザーオプションの実装パターン
  • ステートレスなノーマライザーオプションの実装方法

ノーマライザーオプションの実装パターン

ノーマライザーオプションの実装パターンについて、次の2つのパターンを紹介します。 この記事では、タイトルにある通り「ステートレス」な正規化の実装に焦点を当てていきますが、それぞれ見ていきます。

  • ステートフルな正規化をするパターン
  • ステートレスな正規化をするパターン
ステートフルな正規化をするパターン

ステートフルな正規化とは、前後の文字の影響を受けて正規化処理が変化する正規化を指します。 たとえば、カタカナやひらがなをローマ字に正規化するunify_to_romajiオプションが該当します。以下の実行例を見てみましょう。

normalize 'NormalizerNFKC150("unify_to_romaji", true)' "キャッチ"
[
  [
    0,
    0.0,
    0.0
  ],
  {
    "normalized": "kyatchi",
  }
]

この例では、「キャッチ」という文字列が「kyatchi」に変換されています。 「キャ」の部分が「kya」に変換されるのは、「キ」の後ろに「ャ」がある場合に前後関係を考慮して「kya」に変換するからです。 もし、前後関係を無視したステートレスな処理を行った場合は、「キ」は単体で「ki」、「ャ」は単体で「xya」と解釈されるため、「kixya」という結果になってしまいます。

このように、文字の前後関係に応じて処理が変わる場合を、ステートフルな正規化と呼びます。

次に、ステートレスな正規化について説明します。

ステートレスな正規化をするパターン

ステートレスな正規化とは、前後の文字を考慮せずに、各文字をそれぞれ正規化処理する正規化を指します。 たとえば、発音区別符号を取り除くunify_latin_alphabet_withオプションが該当します。以下の実行例を見てみましょう。

normalize 'NormalizerNFKC150("unify_latin_alphabet_with", true)' "ngoằn nghoèo"
[
  [
    0,
    0.0,
    0.0
  ],
  {
    "normalized": "ngoan nghoeo",
  }
]

この例では、「ngoằn nghoèo」という文字列が「ngoan nghoeo」に正規化されています。 この場合、発音区別符号付きの文字がそれぞれ独立に処理され、符号が取り除かれています。

このように、前後関係に依存せず各文字を独立して変換する正規化処理をステートレスな正規化と呼びます。

ステートレスなノーマライザーオプションの実装方法

ここでは、ステートレスなノーマライザーオプションの実装方法を具体例を交えて紹介します。 例として、unify_latin_alphabet_withオプションを実装した際のコードを見てみましょう。

static void
grn_nfkc_normalize_unify_stateless(grn_ctx *ctx,
                                   grn_nfkc_normalize_data *data,
                                   grn_nfkc_normalize_context *unify,
                                   bool before)
{
  const unsigned char *current = data->context.dest;
  const unsigned char *end = data->context.d;

  while (current < end) {
    //...
    if (before && data->options->unify_latin_alphabet_with &&
        GRN_CHAR_TYPE(char_type) == GRN_CHAR_ALPHA) {
      unifying = grn_nfkc_normalize_unify_latin_alphabet_with(unifying,
                                                              unified_alphabet);
    }
    //...
  }
}

この関数では、unify_latin_alphabet_withオプションが有効な場合に、アルファベット文字に対して発音区別符号を取り除く処理を行っています。 具体的に見ていきます。unify_latin_alphabet_withはステートレスな正規化をするオプションなので、grn_nfkc_normalize_unify_statelessから呼び出す形で実装されています。 そして、各文字に対してunify_latin_alphabet_withが有効かつアルファベット文字ならば、grn_nfkc_normalize_unify_latin_alphabet_withにて、 発音区別符号を取り除いたラテン文字に正規化する処理をおこなっています。

具体的な処理について、grn_nfkc_normalize_unify_latin_alphabet_with関数を見ていきましょう。

grn_inline static const unsigned char *
grn_nfkc_normalize_unify_latin_alphabet_with(const unsigned char *utf8_char,
                                             unsigned char *unified)
{
  if (grn_nfkc_normalize_unify_latin_alphabet_with_is_a(utf8_char)) {
    *unified = 'a';
    return unified;
  } else if {
    //...
  } else {
    return utf8_char;
  }
}

この関数は、発音区別符号を取り除いたラテン文字に正規化しています。 grn_nfkc_normalize_unify_latin_alphabet_with_is_a()関数を呼びaに発音区別符号がついた文字であるかを判定しています。 判定が正であるならば、aに正規化するという処理を行っています。 同様の処理を各文字に対して行うことで、発音区別符号を取り除いたラテン文字に正規化する処理をおこなっています。

このように、ステートレスな正規化用のオプションを追加する場合は、正規化処理をする関数を定義し、 その関数をgrn_nfkc_normalize_unify_statelessから呼んであげることでステートレスな正規化を行う機能を実装できます。

補足

実際にどのように発音区別符号がついた文字であるかを判定しているのか?と思った方に少しだけ紹介します。 ここでは、grn_nfkc_normalize_unify_latin_alphabet_with_is_a関数の実装を例に見ていきます。

grn_inline static bool
grn_nfkc_normalize_unify_latin_alphabet_with_is_a(
  const unsigned char *utf8_char)
{
  /*
   * Latin-1 Supplement
   * U+00E0 LATIN SMALL LETTER A WITH GRAVE ..
   * U+00E5 LATIN SMALL LETTER A WITH RING ABOVE
   */
  return (utf8_char[0] == 0xc3 && 0xa0 <= utf8_char[1] &&
          utf8_char[1] <= 0xa5) ||
         /*
          * Latin Extended-A
          * U+0101 LATIN SMALL LETTER A WITH MACRON
          * U+0103 LATIN SMALL LETTER A WITH BREVE
          * U+0105 LATIN SMALL LETTER A WITH OGONEK
          */
         (utf8_char[0] == 0xc4 &&
          (0x81 <= utf8_char[1] && utf8_char[1] <= 0x85)) ||
         /*
          * Latin Extended-B
          * U+01CE LATIN SMALL LETTER A WITH CARON
          */
         (utf8_char[0] == 0xc7 &&
          (utf8_char[1] == 0x8e || utf8_char[1] == 0x9f)) ||
         /*
          * Latin Extended Additional
          * U+1E01 LATIN SMALL LETTER A WITH RING BELOW
          */
         (utf8_char[0] == 0xe1 && utf8_char[1] == 0xb8 &&
          utf8_char[2] == 0x81);
}

ここでは、aに発音区別符号が付いたUnicodeのコードポイントを列挙し、渡された文字が、 そのUnicodeのコードポイントに対応するUTF-8の文字であるかを1つ1つ確認しています。 対応するのであれば、aに発音区別符号がついた文字であるとみなしています。

Unicodeのコードポイントをどうやって列挙したのかという疑問があると思いますが、 こちらは次のようにRubyを使って便利に列挙できるスクリプトを書くことで対応しました。

発音区別符号が付いた特定のラテンアルファベットのUnicodeのコードポイントと対応するUTF-8でのバイト列を列挙するスクリプトとその実行例になります。よかったら見てみてください!

$ ruby tools/generate-alphabet-diacritical-mark.rb a
## Generate mapping about Unicode and UTF-8
["U+00e0", "à", ["0xc3", "0xa0"]]
["U+00e1", "á", ["0xc3", "0xa1"]]
["U+00e2", "â", ["0xc3", "0xa2"]]
["U+00e3", "ã", ["0xc3", "0xa3"]]
["U+00e4", "ä", ["0xc3", "0xa4"]]
["U+00e5", "å", ["0xc3", "0xa5"]]
["U+0101", "ā", ["0xc4", "0x81"]]
["U+0103", "ă", ["0xc4", "0x83"]]
["U+0105", "ą", ["0xc4", "0x85"]]
["U+01ce", "ǎ", ["0xc7", "0x8e"]]
["U+01df", "ǟ", ["0xc7", "0x9f"]]
["U+01e1", "ǡ", ["0xc7", "0xa1"]]
["U+01fb", "ǻ", ["0xc7", "0xbb"]]
["U+0201", "ȁ", ["0xc8", "0x81"]]
["U+0203", "ȃ", ["0xc8", "0x83"]]
["U+0227", "ȧ", ["0xc8", "0xa7"]]
["U+1e01", "ḁ", ["0xe1", "0xb8", "0x81"]]
["U+1ea1", "ạ", ["0xe1", "0xba", "0xa1"]]
["U+1ea3", "ả", ["0xe1", "0xba", "0xa3"]]
["U+1ea5", "ấ", ["0xe1", "0xba", "0xa5"]]
["U+1ea7", "ầ", ["0xe1", "0xba", "0xa7"]]
["U+1ea9", "ẩ", ["0xe1", "0xba", "0xa9"]]
["U+1eab", "ẫ", ["0xe1", "0xba", "0xab"]]
["U+1ead", "ậ", ["0xe1", "0xba", "0xad"]]
["U+1eaf", "ắ", ["0xe1", "0xba", "0xaf"]]
["U+1eb1", "ằ", ["0xe1", "0xba", "0xb1"]]
["U+1eb3", "ẳ", ["0xe1", "0xba", "0xb3"]]
["U+1eb5", "ẵ", ["0xe1", "0xba", "0xb5"]]
["U+1eb7", "ặ", ["0xe1", "0xba", "0xb7"]]
--------------------------------------------------
## Generate target characters
ÀÁÂÃÄÅàáâãäåĀāĂ㥹ǍǎǞǟǠǡǺǻȀȁȂȃȦȧḀḁẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặÅ

まとめ

今回の記事では、GroongaのNormalizerNFKC*ノーマライザーにステートレスな正規化を行う新しいオプションを追加する方法を紹介しました。 この記事を通して、ノーマライザーにオプションを追加する際の手順が具体的にイメージできるようになれば幸いです。

もし、Groongaのノーマライザーに機能追加やカスタマイズをご希望の方がいらっしゃいましたら、 こちらのお問い合わせよりぜひご連絡ください。