【Groongaで学ぶ全文検索 2016-02-12】実例を元にしたGroongaのテーブル設計


# はじめに

* 1ヶ月ぶりに[本勉強会](https://groonga.doorkeeper.jp/events/38529)に参加しました。ワークショップ感もありつつ、物凄く充実した勉強会でした。
* 参加者の方の発言でgroonga-httpdの話題にも触れました。Mroongaとの連携等で面白そうな使い方出来そうなので、簡単に記載してます。
* あとGo言語の話題がちょっと出たので、試しに書いてみました。これも良い勉強になりました。
	* [ukitiyan/tsvToJson](https://github.com/ukitiyan/tsvToJson/tree/master)

# 勉強会で学んだこと

## groonga-httpd

* http経由でGroongaにアクセスしたい場合に便利
* エンジンはGroonga、フロントはnginxで動いてる
* 実体はnginxにモジュール機能があって、モジュールとしてGroongaが動いてる
	* 因みにMroongaはMySQLのモジュール機能で動いてる
	* 勿論、エンジンはGroongaで同じモジュール

	* つまり、バックオフィスでMroongaでデータ突っ込んで、フロントの検索をhttpdでアクセスするとかも出来る！
	* これはオモシロイ！
* Groongaの独自実装でhttpアクセス出来る
	* 軽量で速さ重視なので、Basic認証とかhttpsとか出来ない


## 実例を元にした全文検索エンジン（Groonga）のテーブル設計

### 背景
* 今回の勉強会のお題として、@yamamaijpさんが国立国会図書館のデータを全文検索してみたとのことでその設計レビューをすることになりました。
* 今までの勉強会の勉強成果を実践してるというのは素晴らしいなと素直に思います。
* 自分も実践したい。（いや、する！）

### 要件
* インフラは、Vagrant上のCentOS。
* 国立国会図書館の以下TSVデータを何がしかで全文検索する。
	* 裏要件として、日本酒に関する情報を素早く検索したいという思いがあるようです（笑）
* TSVには以下の内容が含まれている。

        NDLサーチ書誌詳細画面URL，ISBN，セットISBN，ISSN，ISSN-L，DOI，NDLJP（国立国会図書館が付与する永続的識別子），タイトル，巻次・部編番号，部編名，別タイトル，シリーズタイトル，版，著者，出版者，出版地，出版年月日，受理日，目次，注記，言語，大きさ・容量，記録形式，掲載誌名，掲載巻，掲号，掲載通号，提供元書誌詳細画面のURL，一次資料へのリンク，原資料へのリンク，関連資料，異版を持つ，掲載誌情報，目次・記事，別の記録形式である，別の記録形式を持つ
* ここからダウンロード。
http://www.ndl.go.jp/jp/data/data_service/jnb/ebej_tsv.html

* レコード数は34.9万件。

### 設計時に考えたこと
* 何が返ってきてほしいか。
* 何で検索したいか。

### レビュー結果

#### テーブル作成
[table_create](http://groonga.org/ja/docs/reference/commands/table_create.html#table-create)

```
table_create --name Books --flags TABLE_HASH_KEY --key_type ShortText
```

* 後述するが前提として、データロードの際、key項目にはURLをセットしている。
* 上記前提からすると、key_typeに`ShortText`を使ってるのは正しい。
* flagsの指定の観点として、更新があるテーブルでキーが必要か？必要な場合、ハッシュテーブルか？パトリシアトライか？で決めるべき。
	* 基本更新がなく、キーが不要な場合は、`TABLE_NO_KEY`を指定する
	* キーが必要でハッシュテーブルの場合は、`TABLE_HASH_KEY`を指定する
	* キーが必要でパトリシアトライの場合は、`TABLE_PAT_KEY`を指定する（自分的には、`TABLE_PAT_KEY`はインデックステーブルの時、指定すると理解しました）
* 今回は洗い替えが基本で、更新が不要なテーブルのため`TABLE_NO_KEY`で良いかもしれない。

#### カラム作成
[column_create](http://groonga.org/ja/docs/reference/commands/column_create.html#column-create)

```
column_create Books title --flags COLUMN_SCALAR --type ShortText
column_create Books author --flags COLUMN_SCALAR --type ShortText
```

* titleは単一の値なので、flagsに指定している`COLUMN_SCALAR`の指定は正しい。
* authorはもしかしたら、複数の値がある可能性があるので、`COLUMN_VECTOR`の方が良いかもしれない（レビュー後、データを確認したところ複数値は存在しないことが確認されたため`COLUMN_SCALAR`で正しかった）。
* 尚`COLUMN_VECTOR`を指定した場合、データ導入時の値は配列型にする必要がある。
* つまり観点としては、カラムに複数の値が入るかどうか（配列かどうか）。
	* flagsには、COLUMN_SCALAR、COLUMN_VECTORが指定可能。
	* COLUMN_SCALARの場合、単一の値が格納できる。
	* COLUMN_VECTORの場合、複数の値を格納できる。

#### データロード
[load](http://groonga.org/ja/docs/reference/commands/load.html#load)

```
$ echo "load --table Books" > data.load
$ cat data.json >> data.load
$ groonga books.db < data.load
```

* tsv to jsonのスクリプトを自作した◎
* key値としてURLを指定したが、これは少し問題との指摘があがった。
	* `ShortText`の上限である4KiB超える可能性がある
	* ISBN（アイエスビーエヌ、International Standard Book Number）の方が適当
	* それかURLをハッシュ化して、それをキーにする
	* ただし、今回のようなデータでURLが4KiBを超えることは無いのでは？との意見が過半数を超えたため、これも問題ないとの結論に至りました。
* 「tsv to jsonのスクリプトはGo言語で書きたいよね！」と@ktouさんが発言していたので、書きました！
	* [ukitiyan/tsvToJson](https://github.com/ukitiyan/tsvToJson/tree/master)
	* @yamamaijpさんへ、今回のケースでは、以下のようなコマンドで欲しいJsonが手に入ると思いますよ。

		`$ go run tsvToJson.go digital_20160130.tsv digital.json 0:_key 7:title 14:author`

#### データ検索（転置インデックス作成前）
[select](http://groonga.org/ja/docs/reference/commands/select.html#select)

```
select Books --query title:@日本酒
```

* 日本酒で検索◎
* `title:@日本酒`はtitleカラムを日本酒で全文検索するという宣言。
* @を外すと完全一致検索になる。
* インデックス作成していないのでフルスキャンになるが、検索結果として0.6秒とこの時点で流石のベンチ。

#### 転置インデックステーブル作成
[table_create](http://groonga.org/ja/docs/reference/commands/table_create.html#table-create)

```
table_create --name Indexes --flags TABLE_PAT_KEY --key_type ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto
```

* flagsに指定した`TABLE_PAT_KEY`は、以下の要件を満たすことが出来たので結果的に正しい。
	* 要件：日本酒バンザイという書籍を「日本酒」じゃなくて「酒」という検索ワードで検索したい。
	* bigramは右記のようにトークナイズする`日本　本酒　酒バ　バン　ンザ　ザイ`
	* `TABLE_PAT_KEY`を指定すると、高度な検索が出来て、前方一致もその高度な検索に含まれてるので検索される。
	* `TABLE_PAT_KEY`は、主キーをパトリシア木に格納することを指示しているようです。
	* 因みにMySQL 5.7からInnodbでサポートされたNgramの場合、上記は検索されない。

#### 転置インデックスカラム作成
[column_create](http://groonga.org/ja/docs/reference/commands/column_create.html#column-create)

```
column_create --table Indexes --name book_title --flags COLUMN_INDEX|WITH_POSITION --type Bookes --source title
```

* typeは対象のテーブルを指定、sourceはそのテーブルのインデックス化したいカラムを指定するので、今回の指定は正しい。
* flagsに指定した`WITH_POSITION`は、検索時にトークナイズされたワードが隣り合ってるのかが考慮される。つまり全文検索したい時は必須なオプション（忘れずに設定すること）
* 隣り合ってるかどうかを考慮するのは`フレーズ検索`と言って、これは以前勉強した内容です。
	* [【全文検索】Groongaで学ぶ全文検索 2015-11-20に参加してきました](http://qiita.com/ukitiyan/items/5fb565595bf1db583651#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E4%BD%95%E3%81%8C%E5%A4%A7%E5%A4%89%E3%81%AA%E3%82%93%E3%81%A0%E3%81%A3%E3%81%91)

#### データ検索（転置インデックス作成後）
[select](http://groonga.org/ja/docs/reference/commands/select.html#select)

```
select Books --query title:@日本酒
```
* 検索結果のレコード数は転置インデックス作成前と一致しており、検索速度が向上しているのでインデックスの作成は問題ないと言える。
* 結果的に転置インデックス作成したら、検索結果が20倍の速さで返ってきたということで、これまた流石。

# さいごに

* RDBSに慣れ親しんだ自分がこの勉強会を聞いていて、全体的に`キーをテーブルと呼称して、バリューをカラムと呼称して`いるよう感じた。
* これは ~~KVS~~ Groonga的には正解な考え方で森大二郎さんのスライドでもその通り紹介されている。
	* [グルンガの紹介](http://slide.rabbit-shocker.org/authors/daijiro/groonga-night-3/)
* 今回の勉強会は時間オーバーする程の盛り上りだったのですが、あと2時間は行けたな−と感じたのでした。