Mroongaで凝ったNormalizeをしたときのsnippet取得

※頂いたコメントを元に改訂予定です。必ずコメントまで読んで下さい (2020-12-01)

Mroongaの文字列正規化はオプション豊富な `NFKC100` があって、いい感じの手広く当てにいく検索インデックスを作れます。

[MroongaのNormalizer/Tokenizer/TokenFilter](https://qiita.com/masudakz/items/b09ec98031455f30330b) 2020-02-25

以前の記事で採用した例

- 長音記号を正規化
- 中点を"·" (U+00B7 MIDDLE DOT)に正規化
- "ヴァヴィヴヴェヴォ"を"バビブベボ"に正規化
- 全角ひらがな、全角カタカナ、半角カタカナの文字を同一視
- かなカナの小さな文字を大きな文字と同一視
- ハイフンを"-" (U+002D HYPHEN-MINUS)に正規化

Mroonga には UDF として `mroonga_snippet_html(document, key_word)` が実装されていて、簡単にGoogleみたいな検索結果画面をつくれます。  
※「実験的機能」の但し書きつき

![MroongaByGoogle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47549/e8a23d86-b72c-ba69-09f1-41a290412376.png)

`document` の中から `key_word` を含む周辺のテキストを切り取って、 `key_word` 部分を強調表示のmark-upをつけて返してくれます。

しかし、`mroonga_snippet_html` の中で `document` 中の `key_word` を探すときにはデフォールトのアルファベット大小同一程度の正規化処理になっているようで、インデックス作成時に `NFKC100` を使っていると空振りすることがあります。

「バイオリン」でdocument中の「ヴァイオリン」にヒットしたのに、snippet が出てこないとかです。

## Mroonga v10.07 (2020-10-02)まで

インデクシング時に `NFKC100` で凝った正規化をしたなら、 `mroonga_snippet_html` の引数にはどちらも、同様の正規化変換をした文字列を与える必要があります。

同じく UDF で `mroonga_normalize(string, normalizer_name)` があるのですが、これに `NFKC100` のオプションをつけて実行する方法がわかりませんでした。

`FULLTEXT INDEX` につけてるのはこんな感じですから。

```sql
normalizer "NormalizerNFKC100(
   \'unify_prolonged_sound_mark\', true,
   \'unify_middle_dot\', true,
   \'unify_katakana_v_sounds\', true)"
,
  token_filters "TokenFilterNFKC100(
   \'unify_kana\', true,
   \'unify_kana_case\', true,
   \'unify_hyphen\', true)"
```

そういうときには、一段抽象化を下げて、汎用の `mroonga_command` で groonga command を使います。

```sql
SELECT mroonga_command('normalize \'NormalizerNFKC100(
"unify_prolonged_sound_mark", true,
"unify_middle_dot", true, 
"unify_katakana_v_sounds", true)\' "ヴァイオリン"');

{"normalized":"バイオリン","types":[],"checks":[]}
```

JSONで返ってくるので、SQLの中での取り回しがちょっと難しいです。ホスト言語(ここの場合はJava)で取り回すことにします。

```java
// NormalizeオプションはFULLTEXT INDEXの定義に合わせること
private static final String MROONGA_NORMALIZER = "normalize 'NormalizerNFKC100(" +
			"\"unify_prolonged_sound_mark\", true," +
			"\"unify_middle_dot\", true, " +
			"\"unify_katakana_v_sounds\", true)' \"";
private static final String MROONGA_TOKENFILTER = "normalize 'NormalizerNFKC100(" +
			"\"unify_kana\", true, " +
			"\"unify_kana_case\", true, " +
			"\"unify_hyphen\", true)' \"";

...
	String words = condition.getFulltextExpression();

	StringBuilder command = new StringBuilder();
	command.append(MROONGA_NORMALIZER).append(words).append("\"");
	String normalizedQuery = mapper.mroongaCommand(command.toString());
	command.setLength(0);
	command.append(MROONGA_TOKENFILTER).append(normalizedQuery).append("\"");
	String filteredQuery = mapper.mroongaCommand(command.toString());

	String originalText = mapper.getTextByFulltextId(docInfo.getFullTextId());
	command.setLength(0);
	command.append(MROONGA_NORMALIZER).append(originalText).append("\"");
	String normalizedText = mapper.mroongaCommand(command.toString());
	command.setLength(0);
	command.append(MROONGA_TOKENFILTER).append(normalizedText).append("\"");
	String filteredText = mapper.mroongaCommand(command.toString());
...

```

正規化オプションの適用順序については、ソースまで読まないといけないのかもしれません。
"ヴァヴィヴヴェヴォ"を変換してから、unify_kana変換することが確実になるように、2段変換しています。

Mapper中のSQL

```sql
SELECT mroonga_command(#{command});
...
SELECT mroonga_snippet_html(#{text}, #{keyword});
```

## Mroonga v10.09 (2020-12-02)以降

2020-12-26に追記
@ktou さんから新版 v10.09 を勧めるコメントもらっていながら、他事に紛れてなかなか確認できませんでしたが、ようやく v10.09 でのありがたい新機能を確認しました。

> @ktou
> 次のMroongaからはmroonga_snippet_html(column, 'diaries' as table_name, 'content' as index_name, '...' as query)で指定したインデックスに設定されているノーマライザーの情報を使ってくれるようになります。


### 更新前の状態  
[2.5.18. CentOS 7（MariaDB 10.4のパッケージを利用）](https://mroonga.org/ja/docs/install/centos.html#centos-7-with-mariadb-10-4-package) でインストールした9.12でした。

```sql
MariaDB [(none)]> SHOW VARIABLES LIKE 'mroonga_version';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| mroonga_version | 9.12  |
+-----------------+-------+
```

### yum update

```
$ yum info --enablerepo=epel mariadb-10.4-mroonga
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
196 packages excluded due to repository priority protections
Installed Packages
Name        : mariadb-10.4-mroonga
Arch        : x86_64
Version     : 9.12
Release     : 1.el7
Size        : 534 k
Repo        : installed
...

Available Packages
Name        : mariadb-10.4-mroonga
Arch        : x86_64
Version     : 10.09
...
```

```
$ sudo yum update --enablerepo=epel mariadb-10.4-mroonga
...
Updated:
  mariadb-10.4-mroonga.x86_64 0:10.09-1.el7

Dependency Updated:
  MariaDB-client.x86_64 0:10.4.17-1.el7.centos
  MariaDB-server.x86_64 0:10.4.17-1.el7.centos

Complete!
```

```

```
$ sudo systemctl daemon-reload
$ sudo mysql < /usr/share/mroonga/install.sql
$ sudo mysql
MariaDB [(none)]> SHOW VARIABLES LIKE 'mroonga_version'\G
*************************** 1. row ***************************
Variable_name: mroonga_version
        Value: 10.09
```

### Normalize

> @ktou
ただ、unify_kanaとunify_katakana_v_soundsは一緒には使えない（unify_kanaでカタカナがすべてひらがなになってしまいunify_katakana_v_sounds対象の文字がなくなってしまう）ので、unify_kanaをunify_to_katakanaにしてください。これで、ノーマライザーとトークンフィルターにわけなくてもよくなります。

```sql
ALTER TABLE t_content ADD FULLTEXT INDEX ftx_ngram(t_ngram) COMMENT
'tokenizer "TokenBigram",
 normalizer "NormalizerNFKC100(
   \'unify_prolonged_sound_mark\', true,
   \'unify_middle_dot\', true,
   \'unify_katakana_v_sounds\', true,
   \'unify_to_katakana\', true,
   \'unify_kana_case\', true,
   \'unify_hyphen\', true)"'
;
```

normalizer と token_filters の2段構えにしたのは「よみがな検索」もやってみたくて、`TokenMecab` を使おうとしていたためです。
カタカナとひらがなをどちらかに統制してしまうと、`TokenMecab` のような形態素解析は外来語や外来固有名詞の区切りを文字種変化に頼っているので悲惨なことになります。normalizer - Mecab - token_filters の順で加工する必要があります。今回の更新で `mroonga_snippet_html` が参照するのは normalizer だけみたいなので、 `TokenMecab` のときにかなカナ同一視をしたいなら、前節の後付けのsnippet補完処理が依然として必要です。

`TokenBigram` ならその心配はないので、normalizer に一本化します。

### SQL

```java
snippetCommand = " mroonga_snippet_html(tc.t_ngram, 't_content' as table_name, 'ftx_ngram' as index_name, '" + words + "' as query) ";
```

### 10.09の`mroonga_snippet_html`のメリット

- FULLTEXT INDEXで設定した Normalizeオプションを完全再現した snippet_html取得になる
    - 多数あるオプションを引き渡すのが10.07以前は難しかった
- SQLひとつで検索結果とヒットした近くのテキスト(snippet_html)を同時に得られるようになった
    - 後付けのsnippet補完処理が不要になった
    - SQL request数が最小で済む
- snippet_html内容が元テキストのまま
    - 後付けの補完処理では、 noramlize変換したテキスト上でのnormalize変換した検索語が強調される
    - カナかな同一視の normalizeをかけると日本語テキストして不自然だった

mroonga_snippet_html() 使うにあたって 10.09 への update を強く勧めてもらえて助かりました。