全文検索エンジンgroongaでサジェスト機能(補完)を使ってみる


[gihyo.jp](http://gihyo.jp/)さんでgroongaの隔週連載が最終回を迎えました。groongaの最新情報と今後に興味があれば一読をおすすめします。該当記事は[「最終回　groongaの今と未来」](http://gihyo.jp/dev/clip/01/groonga/0011) です。

第1回から第10回までの過去記事については、[隔週連載groonga](http://gihyo.jp/dev/clip/01/groonga)のページを参照してください。

# はじめに

オープンソースの[カラムストア機能付き全文検索エンジンgroonga](http://groonga.org/ja/)を公開しています。この記事を書いた時点の最新のバージョンは2013年9月29日にリリースした[3.0.8](http://groonga.org/ja/docs/news.html#release-3-0-8)です。

今回は、groongaでサジェスト機能を利用するときの具体例を紹介します。

# groongaのサジェスト機能とは

全文検索エンジンgroongaの特徴のひとつに、サジェスト機能があります。
このサジェスト機能では、補完、補正、提案ができるようになっています。

* [補完](http://groonga.org/ja/docs/reference/suggest/introduction.html#completion)
* [補正](http://groonga.org/ja/docs/reference/suggest/introduction.html#correction)
* [提案](http://groonga.org/ja/docs/reference/suggest/introduction.html#suggestion)

補完は一部の入力をもとにして、登録済みの語の中から補完候補の語を返します。補正は誤入力をもとにして、登録済みの補正ペア(誤入力に対応した正しい入力のペア)から補正された語を返します。提案は入力をもとにして、登録済みの関連クエリペア(キーワードとそれに関連するクエリのペア)から追加のキーワードを含む新しいクエリを返します。

サジェスト機能の個別の機能についてはドキュメントに簡単な例がありますが、一連の流れをまとめて説明しているわけではないのでちょっとわかりにくいかもしれません。

[チュートリアル](http://groonga.org/ja/docs/suggest/tutorial.html)のセクションも用意していますが、まだTODOという状態で空っぽです。

提案機能を使った具体例としては[groonga 3.0.3のリリースアナウンス](http://sourceforge.jp/projects/groonga/lists/archive/dev/2013-April/001337.html)で「学習時の重み付けのカスタマイズをサポート」というトピックがあるので、それが参考になるかもしれません。

上記の状況なので、実際に使ってみようとしてどうしたらいいの、となってしまうかもしれません。
というわけで、もう少し具体的な例を上げて使い方を紹介してみます。

丁度、テストデータにサジェスト機能を使ったものがあるので実際に試してみましょう。

今回の例では次のようにして検索データを準備します。

1. groonga-sugguest-create-datasetでデータセットの準備
2. 参照用データの読み込み
3. 学習用データの読み込み

上記をすべて実行すると検索する準備ができます。

## groonga-sugguest-create-datasetでデータセットの準備

サジェスト機能を利用するには、あらかじめテーブルやカラム定義が必要です。

groongaではサジェスト機能を利用するのに必要なテーブルやカラムなどのスキーマ定義をgroonga-suggest-create-dataseetを使うことで、まとめて行うことができます。

このコマンドの詳細については、[groonga-sugguest-create-dataset](http://groonga.org/ja/docs/reference/executables/groonga-suggest-create-dataset.html)を参照してください。

では、実際にデータセットを作成してみましょう。
作成するデータセットは、ruremaとします。

```
% mkdir -p testdb
% groonga-suggest-create-dataset testdb/db rurema
```

上記コマンドを実行すると、データセットを作成できます。

作成途中のログも次のように表示されます。

```
> register suggest/suggest
true
> table_create event_type TABLE_HASH_KEY ShortText
true
> table_create bigram TABLE_PAT_KEY|KEY_NORMALIZE ShortText --default_tokenizer TokenBigram
true
> table_create kana TABLE_PAT_KEY|KEY_NORMALIZE ShortText
true
> table_create item_rurema TABLE_PAT_KEY|KEY_NORMALIZE ShortText --default_tokenizer TokenDelimit
true
> column_create bigram item_rurema_key COLUMN_INDEX|WITH_POSITION item_rurema _key
true
> column_create item_rurema kana COLUMN_VECTOR kana
true
> column_create kana item_rurema_kana COLUMN_INDEX item_rurema kana
true
> column_create item_rurema freq COLUMN_SCALAR Int32
true
> column_create item_rurema last COLUMN_SCALAR Time
true
> column_create item_rurema boost COLUMN_SCALAR Int32
true
> column_create item_rurema freq2 COLUMN_SCALAR Int32
true
> column_create item_rurema buzz COLUMN_SCALAR Int32
true
> table_create pair_rurema TABLE_HASH_KEY UInt64
true
> column_create pair_rurema pre COLUMN_SCALAR item_rurema
true
> column_create pair_rurema post COLUMN_SCALAR item_rurema
true
> column_create pair_rurema freq0 COLUMN_SCALAR Int32
true
> column_create pair_rurema freq1 COLUMN_SCALAR Int32
true
> column_create pair_rurema freq2 COLUMN_SCALAR Int32
true
> column_create item_rurema co COLUMN_INDEX pair_rurema pre
true
> table_create sequence_rurema TABLE_HASH_KEY ShortText
true
> table_create event_rurema TABLE_NO_KEY
true
> column_create sequence_rurema events COLUMN_VECTOR|RING_BUFFER event_rurema
true
> column_create event_rurema type COLUMN_SCALAR event_type
true
> column_create event_rurema time COLUMN_SCALAR Time
true
> column_create event_rurema item COLUMN_SCALAR item_rurema
true
> column_create event_rurema sequence COLUMN_SCALAR sequence_rurema
true
> table_create configuration TABLE_HASH_KEY ShortText
true
> column_create configuration weight COLUMN_SCALAR UInt32
true
> load --table configuration
> [
> {"_key": "rurema", "weight": 1}
> ]
1
```

次のようにたくさんのデーブルが作成されていることがわかります。

* event_type
* bigram
* kana
* item_rurema
* pair_rurema
* sequence_rurema
* event_rurema
* configuration

これで、データを格納する入れ物ができました。
次の参照用データの読み込みに進みます。

## 参照用データの読み込み

スキーマの定義ができたので、参照用データを登録します。

参照用のデータを登録するloadコマンドが記載された[items.grn](https://raw.github.com/groonga/groonga/master/test/command/fixture/suggest/rurema/items.grn)を次のようにしてtestdbに反映します。

```
% groonga testdb/db < items.grn
[[0,1381979195.03576,0.00296139717102051],12]
```

参照用のデータを登録するためのitems.grnの内容は次の通りです。

```
load --table item_rurema
[
["_key", "kana", "boost"],
["π","パイ",0],
["短縮","タンシュク",0],
["ディレクトリ","ディレクトリ",0],
["削除","サクジョ",0],
["エイリアス","エイリアス",0],
["変数","ヘンスウ",100],
["配列","ハイレツ",0],
["彼女ほしい","カノジョホシイ",0],
["どれどれ","ドレドレ",0],
["置換","チカン",0],
["値","ネ",0],
["SSH","エスエスエイチ",100]
]
```

groonga-suggest-create-datasetではデータセット名としてruremaを指定しました。
なので、参照用のデータを登録する先は、item_ruremaとなっています。

item_ruremaのkanaカラムに読みを登録しておくことで、あとで実行する補完が効くようになります。

実際にgroongaコマンドを対話的に実行すると、次のようにデータがきちんと登録されていることがわかります。

```
% groonga testdb/db
> select item_rurema
[[0,1381985924.21672,0.0016169548034668],[[[12],[["_id","UInt32"],["_key","ShortText"],["boost","Int32"],["buzz","Int32"],["co","pair_rurema"],["freq","Int32"],["freq2","Int32"],["kana","kana"],["last","Time"]],[12,"ssh",100,0,0,0,0,["エスエスエイチ"],0.0],[1,"π",0,0,0,0,0,["パイ"],0.0],[9,"どれどれ",0,0,0,0,0,["ドレドレ"],0.0],[5,"エイリアス",0,0,0,0,0,["エイリアス"],0.0],[3,"ディレクトリ",0,0,0,0,0,["ディレクトリ"],0.0],[11,"値",0,0,0,0,0,["ネ"],0.0],[4,"削除",0,0,0,0,0,["サクジョ"],0.0],[6,"変数",100,0,0,0,0,["ヘンスウ"],0.0],[8,"彼女ほしい",0,0,0,0,0,["カノジョホシイ"],0.0],[2,"短縮",0,0,0,0,0,["タンシュク"],0.0]]]]
```

これで、参照用データの準備ができました。
次の学習用データの読み込みに進みます。

## 学習用データの読み込み

学習用のデータも登録しておきましょう。

学習用のデータを登録するloadコマンドが記載されたlearn.grnを次のようにしてtestdbに反映します。

```
% groonga testdb/db < learn.grn
[[0,1381979722.02598,0.0464754104614258],786]
```

[learn.grn](https://raw.github.com/groonga/groonga/master/test/command/fixture/suggest/rurema/learn.grn)は次のようになっています。

登録件数が多いので、内容を省略しています。

学習用データはevent_ruremaテーブルに登録します。(タイムスタンプと入力、typeがsubmitなら確定したという意味になります。)

```
load --table event_rurema --each 'suggest_preparer(_id, type, item, sequence, time, pair_rurema)'
[
["sequence", "time", "item", "type"],
["e61684e407fc1ed00cd982dd821618a1fc71e35f",1312130117.0,"awesome_print",null],
["e61684e407fc1ed00cd982dd821618a1fc71e35f",1312130126.0,"awesome_print","submit"],
["1bb27c64e1d11dd3962a5c928256f4e55ce60a40",1312132795.0,"File.join",null],
["1bb27c64e1d11dd3962a5c928256f4e55ce60a40",1312132795.0,"File.join","submit"],
["1bb27c64e1d11dd3962a5c928256f4e55ce60a40",1312133262.0,"String","submit"],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312134098.0,"f",null],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312134105.0,"er",null],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312134106.0,"erb",null],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312134107.0,"erb","submit"],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312135222.0,"b",null],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312135223.0,"binding",null],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312135224.0,"binding","submit"],
["21e80fd5e5bc2126469db1c927b7a48fb3353dd9",1312135306.0,"proc","submit"],
["74fcea933590d94dfd8f7ad99d31fd5528365e64",1312135592.0,"yaml",null],
["74fcea933590d94dfd8f7ad99d31fd5528365e64",1312135592.0,"yaml","submit"],
["62b3b7c99ed1912c1057d593867219e4ea98f4b1",1312144053.0,"Net",null],
["62b3b7c99ed1912c1057d593867219e4ea98f4b1",1312144053.0,"Net:",null],
["62b3b7c99ed1912c1057d593867219e4ea98f4b1",1312144054.0,"Net::",null],
["62b3b7c99ed1912c1057d593867219e4ea98f4b1",1312144055.0,"Net:",null],
["62b3b7c99ed1912c1057d593867219e4ea98f4b1",1312144057.0,"net::http","submit"],
["d674ea0b818d6069a98f33921fc649e87191dce4",1312144190.0,"dele",null],
["d674ea0b818d6069a98f33921fc649e87191dce4",1312144194.0,"delete","submit"],
["1bb27c64e1d11dd3962a5c928256f4e55ce60a40",1312145103.0,"compact",null],
["1bb27c64e1d11dd3962a5c928256f4e55ce60a40",1312145103.0,"compact","submit"],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312145193.0,"しゃ",null],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312145195.0,"sha",null],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312145195.0,"sha","submit"],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312145244.0,"sha25",null],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312145244.0,"sha256",null],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312145244.0,"sha256","submit"],
["2e16e39201b8e04a49c8d96703627bddf879b704",1312146056.0,"Array#zip",null],
["2e16e39201b8e04a49c8d96703627bddf879b704",1312146057.0,"Array#zip","submit"],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312146156.0,"d",null],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312146157.0,"dige",null],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312146158.0,"digest",null],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312146158.0,"digest","submit"],
["ae69d285c8e40a66f461b0b34016771fa1acc732",1312146169.0,"digest",null],
...
["2fbeee8d51392746ca08ec18779fdc802186c1be",1312662508.0,"ss",null],
["2fbeee8d51392746ca08ec18779fdc802186c1be",1312662509.0,"s",null],
["2fbeee8d51392746ca08ec18779fdc802186c1be",1312662509.0,"st",null],
["2fbeee8d51392746ca08ec18779fdc802186c1be",1312662510.0,"str",null],
["2fbeee8d51392746ca08ec18779fdc802186c1be",1312662511.0,"strin",null],
["2fbeee8d51392746ca08ec18779fdc802186c1be",1312662512.0,"string",null],
["2fbeee8d51392746ca08ec18779fdc802186c1be",1312662515.0,"string each","submit"]
```

これで、学習用のデータを登録し、検索用のデータの準備ができました。

## groongaで補完してみよう

では、groongaを対話的に起動して補完を試してみましょう。

補完したいときには、 suggestコマンドの ``--types`` に``complete`` を指定します。

「しゃ」を指定してみるとどうなるでしょうか。

```
> suggest --table item_rurema --column kana --types complete --frequency_threshold 1 --query しゃ
[[0,1381993066.89974,0.000294685363769531],{"complete":[[1],[["_key","ShortText"],["_score","Int32"]],["sha",1]]}]
```

結果として「sha」が返ってきました。

では「配」一文字だけ指定するとどうなるでしょうか。

```
> suggest --table item_rurema --column kana --types complete --frequency_threshold 1 --query 配
[[0,1381992548.53464,0.000301361083984375],{"complete":[[1],[["_key","ShortText"],["_score","Int32"]],["配列",5]]}]
```

補完結果の「配列」を前方一致検索の結果として返します。

もう少し、他の例を試してみましょう。

```
> suggest --table item_rurema --column kana --types complete --frequency_threshold 1 --query hairetu
[[0,1381992769.64145,0.000309944152832031],{"complete":[[1],[["_key","ShortText"],["_score","Int32"]],["配列",6]]}]
> suggest --table item_rurema --column kana --types complete --frequency_threshold 1 --query hairetsu
[[0,1381992771.35906,0.00031733512878418],{"complete":[[1],[["_key","ShortText"],["_score","Int32"]],["配列",5]]}]
> suggest --table item_rurema --column kana --types complete --frequency_threshold 1 --query はい
[[0,1381992787.4715,0.000309228897094727],{"complete":[[1],[["_key","ShortText"],["_score","Int32"]],["配列",5]]}]
> suggest --table item_rurema --column kana --types complete --frequency_threshold 1 --query ハイ
[[0,1381992791.70961,0.000283002853393555],{"complete":[[1],[["_key","ShortText"],["_score","Int32"]],["配列",5]]}]
```

入力は「hairetu」でも「hairetsu」でも結果を返しています。
この学習データの場合には「はい」でも「ハイ」でも補完結果の配列を返していますね。

前方一致RK(Romaji Katakana)検索では、ユーザーの入力はローマ字でもカタカナでもひらがなでも良いようになっています。
これは日本語を検索するときに便利です。

# まとめ

今回は、全文検索エンジンgroongaのサジェスト機能のうち補完について紹介しました。
まだチュートリアル等が充実していないので、とっつきにくいかったサジェスト機能の参考になったでしょうか。

groongaに興味を持ったなら、まずは[インストール](http://groonga.org/ja/docs/install.html)して試してみてください。

groongaの基本的な動作を知るための[チュートリアル](http://groonga.org/ja/docs/tutorial.html)もあります。インストールしたら試してみてください。