PostgreSQL Collation ja-x-icuの検証

この投稿は、[PostgreSQL Advent Calendar 2019](https://qiita.com/advent-calendar/2019/postgresql) の12日目で公開する記事です。

今日はCollation、照合順序についての記事です。

# なぜこのテーマ

10年ほど業務でPostgreSQL使ってきましたが、今年会社を変わって、MySQLだけ使うようになったのです。
PostgreSQLとMySQLのユーザ会は合同勉強会もあって、知らない仲でもなかったのですが、今年から[MySQL Casual](https://mysql-casual.connpass.com/)や[MyNA](https://mysql.connpass.com/)でも勉強してます。
そこで不思議に思ったのが、MySQLの勉強会ではしばしばCollationの話題が出るのですよ。

とみたさんの令和の記事とか
https://tmtms.hatenablog.com/entry/201904/mysql-reiwa2

- 普通の「令」U+4EE4ともうひとつの「令」U+F9A8
- 異体字セレクタ U+4EE4 U+E0102
- 令和と合字の㋿

このくらいのことなら実用上「そういうもの」と割り切ることもできそうですが、

https://tmtms.hatenablog.com/entry/2017/06/19/mysql-ja-collation

- utf8mb4_0900_ai_ci
- アクセントの違いを無視 (Accent Insensitive)「ハ」=「パ」=「バ」
- 大文字小文字の違いを無視 (Case Insensitive)「や」=「ゃ」

これはさすがに辛い。とみたさんも

> 個人的にはそこまで区別したいんだったら utf8mb4_bin でいいかな…。

PostgreSQL業務では`LC_COLLATE 'C' LC_CTYPE 'C' ENCODING 'UTF8'`で不都合を感じなかったし、あまり界隈で話題にも出なかったのに、一度確認しておこうかと。

# 先行記事

PostgreSQLのCollation記事としては大垣さんの2017/11/29のが先行しています。
[PostgreSQL 10のICUコレーションを使うと日本語を普通にソートでき、更に文字順序までカスタマイズできる](https://blog.ohgaki.net/postgresql-10-icu-locale-collation-enables-natural-japanese-sorting)

> PostgreSQL 10からICU（International Components for Unicode）のロケール/コレーションがサポートされました。
> ja-x-icu コレーションだと概ね一般に期待されるような順序でソートされています。

```
 ア
 ｱ
 い
 ｲ
```

「一般に期待されるような順番」＝別の言い方では国語辞書順とか五十音順です。コード順だと、カタカナとかたかなで別のかたまりになるところ、「音」を第一にする順番です。

大垣さんのを参照している @nora1962jp さんの2019-05-28 の記事もみつかりました。
[PostgreSQLのICUコレーションを使った日本語辞書順ソート](https://qiita.com/nora1962jp/items/07db8739149d9592f13b)

# 参照ドキュメント

- まずは公式
    - [PostgreSQL11文書 23.2. 照合順序サポート](https://www.postgresql.jp/document/11/html/collation.html)
- とみたさんも言及しているJIS
    - [JIS X 4061「日本語文字列照合順番」(辞書順)](http://kikakurui.com/x4/X4061-1996-01.html)
    - 20世紀のうちは、日本規格協会からいいお値段で物理本を買う必要があったはず。今では電子版が公開されていてありがたい。

# 検証環境

PostgreSQLのDockerイメージがでてるので、環境作るのも簡単になりました。

@kimullaaさんの [Docker PostgreSQLイメージを利用する](https://www.kimullaa.com/entry/2019/12/01/133740)を参考にして

```
$ docker run --name postgres12 -e POSTGRES_PASSWORD=________ postgres
...
Status: Downloaded newer image for postgres:latest
...
PostgreSQL init process complete; ready for start up.

2019-11-23 13:37:10.800 UTC [1] LOG:  starting PostgreSQL 12.1 (Debian 12.1-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
2019-11-23 13:37:10.800 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-11-23 13:37:10.801 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2019-11-23 13:37:10.803 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-11-23 13:37:10.816 UTC [52] LOG:  database system was shut down at 2019-11-23 13:37:10 UTC
2019-11-23 13:37:10.820 UTC [1] LOG:  database system is ready to accept connections
```

```
PS C:\Users\masud> docker exec -it postgres12 /bin/bash
root@c8d08e959464:/# psql -U postgres
psql (12.1 (Debian 12.1-1.pgdg100+1))
Type "help" for help.

postgres=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
```

# X4061検証

p.32 参考 2  適合性試験用データ でピックアップ検証してみます。

> この規格に適合している照合処理系は，次に示す文字列のリストを任意に並べ替えた入力リストに対して，次の順序の整列結果を与える。
> 備考  次のリストの中には，互いに同順位のレコードはない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47549/2c12da0c-4769-61cc-19c2-99a20e02757c.png)

## 濁点・半濁点

```
root@c8d08e959464:/# psql --tuples-only -U postgres
psql (12.1 (Debian 12.1-1.pgdg100+1))
Type "help" for help.

postgres=# SELECT 'ブゥり' < 'ぷぅり' collate "C";
f

postgres=# SELECT 'ブゥり' < 'ぷぅり' collate "ja-x-icu";
t
```

仮名文字クラスは、基底文字変換で清音・濁音・半濁音、小文字・大文字、ひらがな・カタカナの区別をなくして同一視します。

1. 上の例ではどちらも'ふうり' 基底文字列では順序がつきません
2. 基底文字列で同じ順位のときは、次に文字列先頭から照合属性評価をします
3. 清音文字＜濁音文字＜半濁音文字
4. 上の例では1文字目で　濁音 < 半濁音の順序がついたので 'ブゥり' < 'ぷぅり'

まだ、カタカナ・ひらがなの区別を見ないうちに順序がついています。
日本語文字列なら、同じものをかなで書いたりカナで書いたりすることはありますが、
濁音・半濁音表記が一般化した後の日本語で、濁音・半濁音にするべきところを清音にしたりその逆にしたりはないでしょう？
優先順位は濁音有無 >> カナ・かな　です。

反例：「 お゛め゛ぇ゛の゛席゛ね゛ぇ゛がら゛あ゛」

反例にたいするコメント：規格の暗黙の対象としては「日本語で」「照合対象」にする「文字列」＝辞書・事典の見出し語や固有名詞などで、口語や感性表現はスコープにいれてないのでしょう。

## 長音処理

```
postgres=# SELECT 'シャーレ' < 'シャイ' collate "C";
f

postgres=# SELECT 'シャーレ' < 'シャイ' collate "ja-x-icu";
t
```

仮名文字クラスの基底文字変換では長音記号変換もします(規格p.9)

'シャーレ' の基底文字変換後は ’しやあれ’となるので、'しやい'の前に来てくれます。 :thumbsup: 

## 繰り返し記号

```
postgres=# SELECT 'ぶヽ' < 'ぶふ' collate "C";
f

postgres=# SELECT 'ぶヽ' < 'ぶふ' collate "ja-x-icu";
t
```

かな・カナの繰り返し記号は、基底文字変換で直前「基底」文字に置き換えます。
'ぶヽ'　も 'ぶふ' も基底変換は 'ふふ' で一致して、さらに清濁比較も一致します。

このときは、さらに第2照合属性（記号種別）長音記号＜小文字＜繰返し記号＜大文字　での順位付けが定義されています。

繰り返し記号は、正字よりも「軽い」というわけでしょうね。字面の見た感じなんでしょうか。
さらに軽いのが小文字=拗音・撥音につかう「捨て仮名」 「ぁぃぅぇぉっゃゅょゎ」で、前の字と併せて一音節となれば、扱いは軽くなるのでしょう。
さらに、長音記号は「ぁぃぅぇぉ」と表記するのを統一簡便表記になって、字面の見た目で一番軽い感じがしてます。

## PostgreSQLデフォールトCollation

```
postgres=# SELECT 'ブゥり' < 'ぷぅり';
 t
postgres=# SELECT 'シャーレ' < 'シャイ';
 t
postgres=# SELECT 'ぶヽ' < 'ぶふ';
 t
```

DockerImageを「ナリ」でいれたままのPostgreSQL12 databaseだと、なんと `collate "ja-x-icu"` なしでもX4061っぽいですぞ。

## MySQLとみたさんのやってた令和

```$xslt
postgres=# SELECT '平成' = '㍻';
 f

postgres=# SELECT '令和' = '㋿';
 f

```
同じにはなりませんね。同じになるcollateがあるかな？

```
postgres=# SELECT collname FROM pg_collation WHERE collname like 'ja%';
  collname
-------------
 ja-JP-x-icu
 ja-x-icu
(2 rows)

postgres=# SELECT '平成' = '㍻' collate "ja-JP-x-icu";
 ?column?
----------
 f
```

同じになるのは作り付けでは無さそうです。

## 半角カナ

大垣さんの検証では半角カナも入れていて、そのソート順は

```
 ア
 ｱ
 い
 ｲ
 う
 ウ
 ｳ
```

他のパターンも検証してみます。

```
postgres=# SELECT 'ｼｬｰﾚ' < 'ｼｬｲ' collate "C";
 t
```
おっと、半角カナの長音は code順で 'ｧ',...'ｰ' 'ｱ' 'ｲ'　の位置にあるのでこれでは比較検証になりません。

```
postgres=# SELECT 'ｼﾕｲ' < 'ｼｭｰﾙ' collate "C";
 f
postgres=# SELECT 'ｼﾕｲ' < 'ｼｭｰﾙ' collate "ja-x-icu";
 f
postgres=# SELECT 'シユイ' < 'シュール' collate "ja-x-icu";
 t
```

半角カナ長音については、直前基底母音扱いをしてないようです。

実はX4061には半角カナが登場しないのです。

p.8 　4.4.10仮名  表 10 に示す 174 文字は，仮名文字クラス
p.16　附属書  各文字クラスの文字の名前 の　p.23 附属書表 10  仮名

規格のスコープに半角カナは入ってないのです。思想背景が「半角カナ滅ぶべし」だったんでしょうが、依然として滅ぶ気配も見えません。

p.11 　4.5 処理系定義による追加  
> 処理系は，文字，文字クラス，文字の照合属性及び文字の照合属性の値を追加して定義してもよい。追加する場合は，次による。

とあって、処理系追加仕様でやることになっています。


# まとめ

- PostgreSQLの日本語Collationは、JIS X4061の辞書順を実装しているが、「ハ」=「パ」=「バ」のような同一視はしていない

ちゃんと実装したlibを使えばそうなってくれると期待していいのです。
「ハ」=「パ」=「バ」の同一視は照合順を評価するときの第1ステップだけのことなんですから。
第1ステップだけでやめたのでは、文字列の一致比較だけじゃなくて、照合順としても不完全です。
元の出現順やらソートバッファサイズによって、同一視したところの順番が不定になる嫌な現象も出ているのでは。

## 蛇足

JIS X4061の仕様は凝りすぎじゃないの？　という声もあります。
ラテン文字でも　アクセント・大文字小文字について最大3パス評価をしますが、JIS X4061だと4パスが必要です。

[ICU Collation Concepts](http://userguide.icu-project.org/collation/concepts)
> Hiragana Processing
>  JIS X 4061 standard requires more levels than provided by the UCA. To offer conformant sorting order, ICU uses the quaternary level to distinguish between Hiragana and Katakana. Hiragana symbols are given smaller values than Katakana symbols on quaternary level, thus causing Hiragana sequences to sort before corresponding Katakana sequences.

> JIS X 4061標準では、UCAが提供するレベルよりも多くのレベルが必要です。適合したソート順を提供するために、ICUは第4レベルを使用して、ひらがなとカタカナを区別します。ひらがな記号には、第4レベルのカタカナ記号よりも小さい値が与えられるため、対応するカタカナシーケンスの前にひらがなシーケンスがソートされます。

たしかにご苦労かけてるようで。
でも日本語の情報処理ならこれ当然でしょ、というのが20世紀の日本の空気だったのですよ。
紙の辞典も事典も電話帳も小異はあれど4レベル五十音順でできてて、それを高速に手繰るのがリテラシーになっていて、身体的記憶と違和感のある情報出力では受け入れ難いものがありました。

JIS X4061の巻末 P.33 にはスタッフロールがあって、委員の所属はこんな範囲まで及んでます。

- 日本電信電話株式会社電話帳事業推進部
- 北海道大学文学部国語学講座
- 読売新聞社制作局制作システム部

21世紀も令和になって、五十音順の紙を手繰る体験はレアなものになってしまって、ディスプレイ上を目でgrepする機会も減ってます。
ピンポイントで情報にたどり着けるようになった今ではわざわざ処理コストかけてやることでもないかな。
でも、この処理コスト削るくらいで性能問題が解決するような局面もめったになさそうです。

X4061-1996から四半世紀経過しようとしてます。半角カナは滅びなかったし絵文字などが増えたし、「日本語」の状況は変化しています。

> 備考  処理系は，独自に追加定義した内容を文書に明示しなければならない（6.2 参照）。

この記述は、処理系拡張仕様を改訂規格に取り込んで現代版に進化するのがあるべき姿と想定しているのだと思います。でも、そんな面倒くさいことにコストかけてくれるところはなかったのです。