ククログ

株式会社クリアコード > ククログ > Groonga用のMariaDBのutf8mb4_uca1400照合順序互換ノーマライザー

Groonga用のMariaDBのutf8mb4_uca1400照合順序互換ノーマライザー

Mroongaの開発をしている須藤です。MroongaはMySQL/MariaDB/Percona Serverからいい感じにGroongaを使えるようにするストレージエンジンです。Groongaは全文検索エンジンなので、MySQL/MariaDB/Percona Serverだけで(ゼロETLで!っていうとナウいんでしょ!?)Elasticsearchとかなしにいい感じに高速高機能全文検索機能を実現できます。

そんなMroongaは10年くらい前からMariaDBにバンドルされています。MySQL・MariaDB作者のMontyさんがなんかの日本のイベントに来ていたときになんか話して「バンドルするか!」みたいな流れになったような気もするし、それとは別のところでなんやかんやしたような気もして、よく覚えていないのですが、バンドルされています。

なんだろう、思い出せないーと思ってインターネットを漁ってみたら、2014年のMyNA会で使った「MariaDBにバンドルされていないMroonga」になんか関連情報がありますね。歴史に興味がある人は眺めてみてね。

まぁ、そんなMroongaですが、MariaDBにバンドルされているMroongaの更新が大変で全然更新できていません。でも、それはまずいよねぇということでまた数年ぶりにチャレンジしています。で、今回はMroongaのコピーをMariaDBに入れるんじゃなくて、git submoduleでMroongaを入れようとしています。コピーだと、MariaDBが持っているMroongaの方にだけ最新MariaDBの対応が入って、本家のMroongaでは気づかずに「あれー新しいMariaDBでビルドできなくなってるー」みたいなことになるからです。

だいぶ前置きが長くなりましたが、MariaDBにバンドルするMroongaを更新するために、最新のMariaDBでも動くようにする作業をしています。

2025年4月のリリース自慢会の後半でもこのあたりのことを説明しているので、テキストだけだとピンとこない方は動画もどうぞ。

最新のMariaDB

最新のMariaDBというのはPreviewとかRCとかでリリースされているMariaDBではなくて、MariaDBのリポジトリーのmainブランチのMariaDBのことです。LTS以降に入った変更もありますし、リリース前の変更も入っています。

そんな変更の中でMroongaに大きく影響するものがMariaDB 11.5で入ったuca1400系の照合順序です。これはUnicode 14.0.0をベースにした照合順序です。(UCAはUnicode Collation Algorithmの略。)MySQLは8.0で0900系のUnicode 9.0.0ベースの照合順序が入りましたが、これのMariaDBバージョンみたいな感じです。(たぶん。私はそう思っています。)ちなみに、MariaDBでもMySQLの0900系の照合順序を使えますが、これはuca1400系の照合順序の別名になっています。

最新のMariaDBに対応するということはこのuca1400系の照合順序にも対応しないといけないということです。

照合順序

しれっと「照合順序」と連呼していましたが、一応、照合順序とはなにかを簡単に説明しておきます。

照合順序というのは文字列をどのように並べるかというルールです。並べ方のルールなので、照合順序が変わると文字列のソート結果が変わります。並び方のルールには、どちらが先でどちらが後という情報だけではなく、どれとどれは同じか、という情報も含まれています。Mroongaではここだけが重要です。全文検索は、ざっくりいうと「指定された文字列が含まれている文書を返す処理」なので、2つの文字列が同じかどうかという情報だけが大事なのです。どっちが前か後ろかとかは大事ではありません。

なお、「や」と「や」のように、同じ文字列かどうかだけで「同じ」かどうかを判断しているわけではありません。「や」と「ゃ」のように大文字・小文字を無視して「同じ」かどうかを判断する?しない?とか、「ハ」と「パ」のように半濁点の有無を無視して「同じ」かどうかを判断する?しない?のようにカスタマイズして使うこともできるようになっています。

これがどのように実現されているかをざっくりと説明すると、「重み(weight)」と「レベル(level)」です。各文字にはNレベルの重みが定義されています。何番目のレベルまでを使って比較するかどうかで挙動をカスタマイズします。

挙動を見る感じでは、2番目のレベルの重み(secondary weight)はアクセント(「Ả」のようにアルファベットにつくやつとか、日本語の濁点とか半濁点とか)の違いを表していて、3番目のレベルの重み(tertiary weight)は大文字・小文字の違いを表しているっぽいです。1番目のレベルの重み(primary weight)はそういうアクセントとか大文字・小文字とかの違いを抜いたところを表しているっぽいです。

なので、「や」と「ゃ」は1番目のレベルの重みは同じで3番目のレベルの重みが違います。比較するときに1番目のレベルの重みだけを使うと同じ文字判定になって、3番目のレベルの重みも使うと別の文字判定になります。

「ハ」と「パ」の場合は、1番目のレベルは同じで2番目のレベルの重みが違います。比較するときに1番目のレベルの重みだけを使うと同じ文字判定になって、2番目のレベルの重みも使うと別の文字判定になります。

そういう感じです。

MySQLとかMariaDBではutf8mb4_0900_ai_ciとかutf8mb4_0900_as_csとかutf8bm4_uca1400_ai_ciとかutf8bm4_uca1400_as_csとかというように照合順序名の中にai/as/ci/csが入っています。この各文字は次のことを表しています。

  • a:アクセント
  • c:ケース(大文字・小文字のこと)
  • i:無視する(insensitive)
  • s:考慮する(sensitive)

ということで、aiはアクセントを無視、asはアクセントを無視しない、ciは大文字・小文字を無視、csは大文字・小文字を無視しないという意味になります。つまり、ai/asは2番目のレベルの重みを使わない・使う、ci/csは3番目のレベルの重みを使わない・使うということで実現されています。

MroongaはMySQL/MariaDBユーザーが自然に使えるように設計されているので、MySQL/MariaDBレベルでこのような照合順序を使うと指定されている場合は同じように動くようにしたいのです。

groonga-normalizer-mysql

Mroongaは全文検索エンジンGroongaとMySQL/MariaDBをつなぐ部分だけなので、重要な処理はぜんぶGroongaがやっています。文字列の比較もGroongaの仕事です。そのため、GroongaレベルでMySQL/MariaDB互換の文字列比較処理をできないといけないのです。(MySQL/MariaDBの処理を使っちゃダメ。)

文字列比較処理はGroongaでは「ノーマライザー」としてモジュール化されています。そのため、MySQL/MariaDB互換のノーマライザーがあればいい感じに動きます。

groonga-normalizer-mysqlがMySQL/MriaDB互換のGroonga用ノーマライザーを提供するプロダクトです。MariaDBのuca1400系の照合順序をサポートするには、まずはここでuca1400系互換のノーマライザーを用意して、その後、Mroongaでそれを使うようにする、ということが必要です。

現状でどこまでやったかというとgroonga-normalizer-mysqlにuca1400系互換のノーマライザーを用意するところまでです。Mroongaで使うようにするところはまだできていません。uca1400系互換のノーマライザーを作るのは大変だったので、それを自慢するかと思ってこれを書いているわけです。つまり、実はまだ前置きです。長い。。。

MySQL/MariaDB互換のGroonga用ノーマライザーの実装方法

それでは、groonga-normalizer-mysqlがどのようにMySQL/MariaDB互換のGroonga用ノーマライザーを実装しているかを紹介します。

MySQL/MariaDBの照合順序がやっているように、比較する文字列のどちらが先か後かまでがんばる場合は、具体的な重み情報を持たなくてはいけないのですが、同じかどうかだけがんばるならもっと少ない情報だけで済みます。同じ扱いになる文字のグループを作ってそれぞれにそのグループを代表する文字を割り当てるだけです。たとえば、「は」と「ぱ」と「ば」を同じ文字と判定する場合(ai_cs相当)は、「は」も「ぱ」も「ば」も「は」にしちゃうという情報を持てばよいです。

Groongaのノーマライザーはそのような設計になっていて、入力された文字列を一定のルールで変換します。同じ扱いになる文字ならどの文字を入力しても最終的に特定の文字として出力されます。たとえば、「はぱば」は「ははは」と出力します。

前に色々実験したところ、(必要最小限にした)UTF-8での6bit単位での変換テーブル(これは自動生成)とそれをいい感じに使う大きなCのswitch文(これも自動生成で、変換テーブルが小さいときはテーブルを持たずにswitch内で処理しちゃう)の組み合わせが一番速度とサイズのバランスがよかったです。ということで、groonga-normalizer-mysqlもそんな実装に。。。なっていませんでした。

Groonga組み込みのNormalizerNFKCそんな実装になっていますが、groonga-normalizer-mysqlはUnicodeのコードページ単位での変換テーブル(MySQL/MariaDB内の情報から自動生成)とそれをいい感じに使う手書きの小さなswitch文で実装していました。理由は覚えていませんが、こっちの方がメンテナンスしやすかったんでしょう。

ということで、MySQL/MariaDBにある情報からコードページ単位での変換テーブルを生成すればMySQL/MariaDB互換のノーマライザーを生成できるということです。伝統的に、MySQL/MariaDBではここらへんの関連情報はstrings/以下にあります。たとえば、MariaDBのuca1400系のやつはstrings/CMakeLists.txtでテーブルを自動生成とかになります。MySQLの0900系は自動生成されたっぽいテーブルがコミットされていましたが、MariaDBのuca1400系のやつはビルド時にUnicodeのデータから自動生成なんですねぇ。

groonga-normalizer-mysqlでもMariaDBと同じようにUnicodeのデータから自動生成してもいいですが、MariaDBがなにか変換しているかもしれないので、自動生成されたデータをパースしてgroonga-normalizer-mysql用のデータを自動生成しましょう。そこらへんの実装はtool/parser.rbにあります。これを使って、ai_ci/ai_cs/as_ci/as_cs用にそれぞれデータを生成しています。aiのときは2番目のレベルの重みを使わない、csのときは3番目のレベルの重みを使う、という感じになります。

こういうのを作っていると、MySQLはai_cs(アクセントは無視で大文字小文字は区別する)を用意していなかったけどMariaDBは用意したのかーとか、MySQLは日本語用の0900系(utf8mb4_ja_0900_as_csとか)を用意していたけどMariaDBは用意しなかったのかーとか、いろいろ発見があります。Supported Character Sets and Collations Character Setsを眺めると他の言語用のやつは各種用意してあるので必要性を感じなかったのかもしれません。

ちなみに、groonga-normalizer-mysqlはutf8mb4_0900_ja_as_cs互換のノーマライザーもutf8mb4_0900_ja_as_cs_ks互換のノーマライザーも実装してあります。ここらへんを対応するためには照合順序をカスタマイズするミニ言語みたいなやつをパースして評価しないといけないのですが、そこらへんも実装して対応してあります。

uca1400系に話を戻すと、なんやかんやで変換用のテーブル(例:ai_ci用のテーブル)が生成されます。これを既存の実装に組み込んでこのテーブルを使うようにすれば動きます。

他にもテストを書いたりなんやかんやありますがもう疲れたので省略します。そうそう、ai/ciとかとは別にパディングするかどうかという亜種もあるのですが、groonga-normalizer-mysqlは常にパディングしないです。なので、厳密にはutf8mb4_uca1400_ai_ciとかと非互換(こいつらはパディングする。utf8mb4_uca1400_nopad_ai_ciとかパディングなしのやつが別途存在する。)なのですが、まぁ、だれも気にしないよね?とか実装が面倒だなとか思って実装していません。必要ないよね?

まとめ

MariaDBにバンドルされているMroongaを最新にするために最新のMariaDBに対応させようとして、そのためにはuca1400系照合順序対応も必要なのかーと気づいたのでgroonga-normalizer-mysqlで対応するところまでの話をしました。

マジメに説明する文章じゃなくて思いつくままに書いた文章なので詳細は伝わらないような気はしますが、がんばっているんだな!というのが伝わればそれで十分です。

そんながんばっている私たちのサポートがあった方がMroongaを使いこなせそう!という場合はGroongaサポートサービスを提供していますのでお問い合わせください。