MySQL の検索結果を Collator でフィルタリングする



「ハハ」と「パパ」が一緒くたに扱われてしまう
-------------------------------------

MySQL の照合順序の 、`utf8mb4_unicode_ci`、`utf8mb4_unicode_520_ci` を利用している場合、「ハハ」と「パパ」、「びょういん」と「びよういん」などが一緒くたに扱われしまいます。具体的な事例は[こちら](https://yoku0825.blogspot.jp/2016/01/mysql-bug-79977-utf8mb4unicode520ci.html)のブログ記事がまとまっています。ほかに、「神」と「神」のように新字体と旧字体を同一視するかの問題もあります。

`utf8mb4_unicode_ci` と `utf8mb4_unicode_520_ci` の違いは依存となる UCA (Unicode Collation Algorithm) のバージョンです。前者が 4.0.0 であるのに対して後者は 5.2.0 です。

mysql クライアントで確認する
-------------------------

実際に「ハハ」と「パパ」が同一視されてしまうのか mysql クライアントから確認してみましょう。

まず文字集合を指定します。

```sql
SET NAMES 'utf8mb4';
```

照合順序も一緒に指定できますが、今回は不要です。

```sql
SET NAMES 'utf8mb4' COLLATE utf8mb4_unicode_520_ci;
```

SELECT 文の末尾に照合順序を指定して、マッチするか確認してみましょう。

```sql
SELECT 'ハハ' = 'パパ' COLLATE utf8mb4_unicode_520_ci;
SELECT 'ハハ' = 'パパ' COLLATE utf8mb4_general_ci;
SELECT 'ハハ' = 'パパ' COLLATE utf8mb4_bin;
```

`utf8mb4_unicode_520_ci` の場合、結果の値が `1` (true) になります。それ以外は `0` (false) になります。


コードポイントからどんな文字が表示されるのか調べたい場合、`CHAR` 関数を使います。

```sql
SELECT CHAR(0x30CF USING UTF32);
```

文字からコードポイントを調べるには `CONVERT` 関数で UTF-32 に変換して `HEX` 関数でバイト列を調べます。

```
SELECT HEX(CONVERT("ハ" USING utf32));
```

文字の比較をするには `CONVERT` 関数を使ってエンコーディングを照合順序に合わせる必要があります。

```sql
SELECT
  CONVERT(CHAR(0x30CF USING utf32) USING utf8mb4) =
  CONVERT(CHAR(0x30D1 USING UTF32) USING utf8mb4)
  COLLATE utf8mb4_unicode_520_ci;
```

文字列の連結や繰り返しには `CONCAT` や `REPEAT` 関数を使います。

照合順序を変えても別の問題があります
------------------------------

前述の問題の対策として照合順序を `utf8mb4_general_ci` (`utf8_general_ci`) や `utf8mb4_bin` (`utf8_bin`) に切り換える選択肢がありますが、ひらがなとカタカタを同一視してくれないなど別の不便さがあります。

UCA のライブラリを利用したことはありますか？
-------------------------------------

日本語に不便な実装をした MySQL がわるいと思ってしまいがちですが、はたして MySQL の責任だけですむ問題でしょうか？
ここで質問です。UCA (Unicode Collation Algorithm) にもとづいたライブラリを使ったことはありますか？

ライブラリの例として ICU (International Components for Unicode) の Collator の PHP バインディング (intl) や Java が挙げられます。これらの日本語マニュアルを読めば、言語や照合強度などさまざまなパラメーターを指定して調整する必要があることがわかります。


言語ごとに調整できないことが問題
---------------------------

ICU の Collator とMySQL を比べると、MySQL の問題の本質はさまざまなパラメーターで調整できないということになります。

言い換えると、本来は言語ごとに照合順序を調整しなければならないのに1つの照合順序で間に合わせようとしたために同一視してほしい文字、同一視してほしくない文字が混在する結果になったということです。

もし MySQL に改善を求めるのであれば、ふるまいを変えるためのパラメーターを指定できるようにする、もしくは言語ごとに特化した照合順序の提供を求めるということが考えられます。

ICU の Collator による次善策
--------------------------

将来のバージョンにおいて MySQL の照合順序の実装が改善されるとしても、当分のあいだは既存の照合順序を使わなければなりません。
また、外部のサービスであるために照合順序を変更できなかったり、古い CMS のために既存の照合順序を使い続けなければならないこともあります。

そこで、次善策として、MySQL の検索結果に対して ICU の Collator もしくはそれに相当するライブラリを使ってフィルタリングする方法を挙げます。利点は古いバージョンの MySQL でも対応できることです。欠点は処理が増えることです。また MySQL にアクセスする方法もしくは出力処理を共通化しないと、場所によって検索結果が異なってしまうおそれがあります。

ICU の Collator は C/C++/Java のライブラリとして提供されており、クライアントサイド JavaScript を含むスクリプト言語のバインディングが利用できます。Node.js でも Collator を使うことはできますが、日本語を扱いたい場合、Node.js のビルドオプションを指定する必要があります。

コードの例
--------

### JavaScript

```js
var collator = new Intl.Collator('ja');
console.log(
  0 === collator.compare('はは', 'ハハ'),
  0 === collator.compare('はは', 'ﾊﾊ'),
  0 === collator.compare('神', '神'),
  0 !== collator.compare('びょういん', 'びよういん'),
  0 !== collator.compare('はは', 'パパ')
);
```

### PHP

ICU4C および intl モジュールをインストールする必要があります。次のコードは照合強度に応じて、同一視される文字がどのように変わるのかを示しています。

```php
$collator = new Collator('ja_JP');

echo '強度', PHP_EOL;
var_dump(
  Collator::TERTIARY === $collator->getStrength()
);

echo PHP_EOL,
  'Collator::TERTIARY',
PHP_EOL;

var_dump(
  0 === $collator->compare('はは', 'ハハ'),
  0 === $collator->compare('はは', 'ﾊﾊ'),
  0 === $collator->compare('神', '神'),
  0 !== $collator->compare('びょういん', 'びよういん'),
  0 !== $collator->compare('はは', 'パパ')
);

echo PHP_EOL,
  'Collator::SECONDARY',
PHP_EOL;

$collator->setStrength(Collator::SECONDARY);

var_dump(
  0 === $collator->compare('びょういん', 'びよういん')
);

echo PHP_EOL,
  'Collator::PRIMARY',
PHP_EOL;

$collator->setStrength(Collator::PRIMARY);

var_dump(
  0 === $collator->compare('はは', 'パパ')
);
```

### Go

Go の場合、`text/collate` を使います。

```go
package main

import (
    "golang.org/x/text/collate"
    "golang.org/x/text/language"
)

func main() {
    c := collate.New(language.Japanese, )

    println(0 == c.CompareString("はは", "ハハ"))
    println(0 == c.CompareString("はは", "ﾊﾊ"))
    println(0 == c.CompareString("神", "神"))

    println(0 != c.CompareString("びょういん", "びよういん"))
    println(0 != c.CompareString("はは", "パパ"))
}
```