Groongaで学ぶ全文検索 2016-2-12 に参加しました


[Groongaで学ぶ全文検索 2016-2-12](https://groonga.doorkeeper.jp/events/38529)に参加しました。3回目の参加です。

今回は、参加者の[@yamamaijp](http://qiita.com/yamamaijp)さんが試しにGroongaを使ってみたということで、その設計をレビューしようという話になりました。というわけで、今回のテーマは **「実例を基にしたGroongaの設計」**です。

勉強会が終わった後に実際に自分でコマンドをたたいて復習してみました。ここではその時のことも補足でかきたいと思います。

まずはじめに考えたこと
--
####用途を決める
* 今回は「国立国会図書館の蔵書を検索する」というものでした。

####返ってきてほしいものを決める
* 本のタイトルを返したい

####検索したいものを決める
* タイトル、著者名で検索したい

####テーブル名を決める
* テーブル名は「Books」

####検索したいもののカラムを決める
* タイトルは「title」、著者は「author」とします

まずはじめに考えることってすごくシンプルなんだなと感じました。
ここまで考えたらコマンドをたたいていくのですが、流れはこんなかんじです。

* データをjson形式で準備する
* Groongaをインストールする
* データベースを作成する
* データを入れるテーブルを作成する
* データを入れるテーブルのカラムを作成する
* データをロードする
* 試しに検索してみる（転置インデックス作成前）
* 転置インデックス用テーブルを作成する
* 転置インデックス用テーブルのカラムを作成する
* もっかい検索してみる（転置インデックス作成後）

では次からいっこずつみていきましょう。

データをjson形式で準備する
--
* [公開されているデータ](http://www.ndl.go.jp/jp/data/data_service/jnb/ebej_tsv.html)（tsv形式）を取得

* tsvデータの内容はこんな感じです。
    `NDLサーチ書誌詳細画面URL	ISBN	セットISBN	ISSN	ISSN-L	DOI	NDLJP	タイトル	巻次・部編番号	部編名	別タイトル	シリーズタイトル	版	著者	出版者	出版地	出版年月日（W3CDTF形式）	受理日（W3CDTF形式）	目次	注記	言語（ISO639-2形式）	大きさ、容量等	記録形式（IMT形式）	掲載誌名	掲載巻	掲載号	掲載通号	提供元書誌詳細画面のURL	一次資料へのリンクURL	原資料へのリンク	関連資料	異版を持つ	掲載誌情報	目次・記事	別の記録形式である	別の記録形式を持つ`

*  参加者 [@yamamaijp](http://qiita.com/yamamaijp)さんはtsvをjsonに変換するスクリプトをPHPで自作！
* かっこいい！
* と思いつつ、私は自作せず、、、先輩がgoでかいたスクリプト([ukitiyan/tsvToJson](https://github.com/ukitiyan/tsvToJson/tree/master))を使わせてもらいました(＾▽＾)
* とりあえず準備OK。

Groongaをインストールする
--
ここに関しては、自分の環境で実行したコマンドをのせておきます。
今回は使わないけど、とりあえず`--with-mecab`つけてます。環境はMacOSXです。

`brew install groonga --with-mecab`

あれ、でも動かない。brewが入っていませんでした。
そこで先輩が教えてくれた[Homebrew](http://brew.sh/index_ja.html)を入れて再度実行。無事成功しました。
ついにGroonga入りました！これだけでちょっとうれしい。。

データベースを作成する
--
```
$ mkdir /tmp/groonga-databases/
$ groonga -n /tmp/groonga-databases/introduction.db
```
ここに関しても自分でやったこと書いてます。ドキュメントを参考にデータベースを作成しました。
このコマンド入力後対話モードになります。以下対話モードでコマンド入力していきます。


データを入れるテーブルを作成する
--
`table_create --name Books --flags TABLE_HASH_KEY --key_type ShortText`

* [`table_create`](http://groonga.org/ja/docs/ reference/commands/table_create.html#table-create)：テーブル作成コマンドです。
* `--name`：テーブル名を指定します。これは必須です。
* `--flags`：テーブルの種類を指定します。
* `--key_type`：キーの[型](http://groonga.org/ja/docs/reference/types.html)を指定します。 

`--flags`に何を指定するかですが、
Groongaには[4種類テーブル](http://groonga.org/ja/docs/reference/tables.html)が提供されており、用途によって使い分けます。
ここでは`TABLE_HASH_KEY`を指定してますが、キーでレコードを検索しないのであれば、
キーをサポートしない`TABLE_NO_KEY`の方が適切ということでした。
こちらの方が小さくて早いテーブルだそうです。ちなみにキーをサポートしないのは`TABLE_NO_KEY`だけです。

`--key_type`に関してですが、今回はURLをキーを選択しており、
通常だと4Kを超えないだろうということで適切であるという話でした。

データを入れるテーブルのカラムを作成する
--
`column_create Books title --flags COLUMN_SCALAR --type ShortText`
`column_create Books author --flags COLUMN_SCALAR --type ShortText`

* `column_create`コマンドで作成します。直後にテーブル名、カラム名を入力しています。
* `--flags`にはカラムの種類を指定します。
* `--type`にはカラムの型を指定します。

カラムの種類は「scalar」、「vecter」、「index」の3つです。

```text:カラムの種類
・scalar：データを格納するカラム。値を1つのみ格納できる。
・vecter：データを格納するカラム。値を複数格納できる。（配列みたいなイメージ）
・index：転置インデックステーブルのカラム等に指定使用する。
```
1つの本に対して本のタイトルは1つなので`title`に`scalar`を指定するのは適切です。
一方著者は複数いる場合もあるので`author`は`vecter`でもいいと考えられますが、
今回のデータでは著者が複数のケースはないようなのでOKでした。


データをロードする
--
`load --table Books [json]`

対話モードだと[json]にjsonにしたデータを貼付ければloadできますが、
今回はjsonファイルを読み込みたいので準備しておいたjsonファイルの1行目に
`load --table Books`を追記し、いったん対話モードを終了して以下コマンドを実行しました。

`groonga /tmp/groonga-databases2/introduction.db < /tmp/groonga-databases2/data2.json `

無事loadできました！
ちなみにjsonの中身はこんなかんじです。

```
load --table Books3
[
{"_key":"http://iss.ndl.go.jp/books/R100000040-I000322695-00", "title":"次世代高
速通信用分散補償ファイバグレーティング", "author":"フジクラ"},...
]
```

試しに検索してみる（転置インデックス作成前）
--
`select --table Books --query title:@日本酒`

* [`select`](http://groonga.org/ja/docs/reference/commands/select.html?highlight=select)コマンドで検索します。このコマンドはどうやら重要らしいのでおいおい勉強していきたいです。
* `--table`にテーブルを指定します。
* `--query`には検索条件を指定します。
* 上記コマンドの場合、Booksテーブルのtitleカラムを「日本酒」で全文検索するという意味になります。
* @があると全文検索をするという宣言になるそうです。ないと完全一致になります。
* 全レコード数は34.9万件の状態です。
* 転置インデックス作成前なのでフルスキャンとなりますが0.6秒という結果。
* なくても早いんですよ〜(ニコニコ)と須藤さん。すてきです！

転置インデックス用テーブルを作成
--
`table_create --name Indexes --flags TABLE_PAT_KEY --key_type ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto`

* `--flags`：テーブルの種類を指定します。
* `--key_type`：キーの型を指定します。
* `--default_tokenizer`：トークナイズ方法を指定します。
* `--normalizer`：ノーマライズ方法を指定します。

**default_tokenizer**
`--default_tokenizer`には検索時の検索キーワードとデータロード時のトークナイズ方法を指定します。何を指定すればいいかというのは用途によります。今回は`TokenBigram`を指定おり、以下の理由で適切と考えられます。

* 本のタイトルというのは普通の文章と比べてインパクト重視で考えられていたりするので、単語に分けるのが難しい
* 検索漏れがない方がユーザーにとってはいいと思われる  　

あれ、ちょっとまて。1文字で検索したくなることがありそう。。（「酒」で検索したい。。）それはできるのか？という疑問もでました。

**1文字で検索したいけどバイグラムでできるのか？**
　`--flags`に`TABLE_PAT_KEY`を指定すれば前方一致ができるのでそれで実現可能というこです。（`TABLE_HASH_KEY`を選択した場合は不可。）今回は`--flags`に`TABLE_PAT_KEY`を指定しているのでOKでした。ちなみに、1文字で検索したいというニーズは後方一致でも実現できますが、後方一致はコストが高く難しいため、Groongaでは前方一致を採用しているそうです。

転置インデックス用テーブルのカラムを作成　
--
`column_create --table Indexes --name book_title --flags COLUMN_INDEX|WITH_POSITION --type Bookes --source title`

* `--table`：どのインデックステーブルのカラムであるかを指定します。
* `--name`：作成するカラムの名前を指定します。
* `--flags`：格納方法を指定します。
* `--type`:どのテーブルのインデックスかを指定します。
* `--source`：どのカラムのデータであるかを指定します。

**COLUMN_INDEX**
インデックスカラムの作成を指示します。

**WITH_POSITION**
 インデックスを使用する際、検索する文字が隣あっているかどうかをみるためのpositonを保持することができます。これによってフレーズ検索が可能になります。つけないと意図しない検索結果がたくさん混じってしまうのでつけるべしです。
 　
**source**
どのカラムのデータのことかを指定します。複数指定することも可能です。複数指定したい場合は`WITH_SECTION`もつける必要があります。

ちなみにデータをロードするときもインデックスの`source`を使っている。ロードするときにこのカラムを`source`に使っているやつはないかな？と見に行って、あった場合にインデックスにトークナイズして入れているそうです。だからただデータをロードしただけなのに早くなったぞ！ってなったりします。

**WITH_SECTION** 
対象のカラムが複数だとデータがどのカラムに格納されているものかわからないので、カラムにセクションIDを持たせます。`WITH_SECTION`を指定することでセクションIDを保持する領域が確保されます。検索時は対象外のセクションIDのデータを無視します。

もっかい検索してみる（転置インデックス作成後）
--
`select --table Books --query title:@日本酒`

* 検索結果は同じだったので正しく検索できているといえる。
* 速度は20倍に！！転置インデックスの威力を感じました。
 
 
まとめ
--
* 実践をもとに学ぶのはいざ使うときのイメージをつかめるし、すごくいい勉強法だと感じました。
* 須藤さんが教えてくれたドキュメント[「グルンガの紹介」](http://slide.rabbit-shocker.org/authors/daijiro/groonga-night-3)をみて自分がGroongaのことを全然知らないことがわかりました。これをもっとちゃんと理解したい。。
* 盛りだくさんでまとめられないよう。。と思っていたのですが先輩に「コマンドたたけばいいですよ」と言われたので、実際にたたいてみました。
* @yamamaijpさんのをまねてコマンド打っただけですが、それなりに躓いたり、あわせてドキュメント読んだりしたので身になりました！
* 今回は3時間コースでしたがあっという間でした！たのしかった〜。今回もありがとうございました！