Groongaでの複数カラムとインデックスと--match_columnsパラメータの複雑な関係

[前日の記事](http://qiita.com/groonga/items/dfc1e170a9cb53de53cc)では、Groongaのインデックスの構造とインデックスを使った検索の仕方の詳細について述べました。[Groonga Advent Calendar 2016](http://qiita.com/advent-calendar/2016/groonga) 16日目のこの記事では、その続きとして、`select`コマンドの`--match_columns`パラメータの指定の仕方による検索の動作の違いを取り上げます。

同じカラム対して複数のインデックスが存在する場合や、同じテーブルの複数のカラムにまたがる検索を行う場合、`--match_columns`の指定の仕方によって行われる検索処理の実態が大きく変わります。適切に指定しないと、せっかくのインデックスを活かせず無駄に時間やリソースを消費してしまうことにもなります。この解説を見て、目的に応じたより適切な`--match_columns`の指定の仕方を把握しましょう。

なお、以下の説明は`--filter`で使用できる[スクリプト構文の`query()`関数](http://groonga.org/ja/docs/reference/functions/query.html)の第1引数の指定にも当てはまります。`--filter`の方を使っている場合、適宜説明を読み替えて下さい。

## 準備

実験用に、インデックスを複数持つテーブルを用意します。

~~~bash:
$ cat dump.grn
table_create Entries TABLE_HASH_KEY ShortText
column_create Entries title COLUMN_SCALAR ShortText
column_create Entries body COLUMN_SCALAR ShortText
load --table Entries
[
["_key","title","body"],
["http://example.com/entry1","Hello Groonga.", "This is my first entry."],
["http://example.com/entry2","Hello world.","I love Groonga!"],
["http://example.com/entry3","Hello Mroonga, bye Groonga.","I use Mroonga."],
["http://example.com/entry4","Say, Hello Groonga!","I'm back."]
]
table_create Terms TABLE_PAT_KEY ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto
column_create Terms entries_title COLUMN_INDEX|WITH_POSITION Entries title
column_create Terms entries_body COLUMN_INDEX|WITH_POSITION Entries body
column_create Terms entries_whole COLUMN_INDEX|WITH_POSITION|WITH_SECTION Entries title,body
$ rm -rf /tmp/groonga-databases
$ mkdir -p /tmp/groonga-databases
$ groonga -n /tmp/groonga-databases/database.db quit
$ cat dump.grn | groonga /tmp/groonga-databases/database.db
~~~

このデータベース内には、以下のような内容を持つ`Entries`というテーブルがあります。

| _id | _key | title (sid=1) | body (sid=2) |
|:-:|:-:|:-:|:-:|
| 1 | http://example.com/entry1 | Hello Groonga. | This is my first entry. |
| 2 | http://example.com/entry2 | Hello world. | I love Groonga! |
| 3 | http://example.com/entry3 | Hello Mroonga, bye Groonga. | I use Mroonga. |
| 4 | http://example.com/entry4 | Say, Hello Groonga! | I'm back. |

また、この`Entries`テーブルの`title`と`body`の各カラムに対して、語彙表の`Terms`テーブルに以下のような3つのインデックスが作成されています。

| _key | entries_title | entries_body | entries_whole |
|:-:|:-:|:-:|:-:|
| Hello | [id=1,pos=1], [id=2,pos=1], [id=3,pos=1], [id=4,pos=2] |  | [id=1,pos=1,sid=1], [id=2,pos=1,sid=1], [id=3,pos=1,sid=1], [id=4,pos=2,sid=1] |
| Groonga | [id=1,pos=2], [id=3,pos=4], [id=4,pos=3] | [id=2,pos=3] | [id=1,pos=2,sid=1], [id=3,pos=4,sid=1], [id=4,pos=3,sid=1], [id=2,pos=3,sid=2] |
| world | [id=2,pos=2] |  | [id=2,pos=2,sid=1] |
| bye | [id=3,pos=3] |  | [id=3,pos=3,sid=1] |
| Mroonga | [id=3,pos=2] | [id=3,pos=3] | [id=3,pos=2,sid=1], [id=3,pos=3,sid=2] |
| Say | [id=4,pos=1] | | [id=4,pos=1,sid=1] |

ここでは簡単のため、`title`カラム由来のトークンに基づくインデックスの情報だけを示しています。複数のカラムにまたがるインデックスって何？という方は、先に[前日の記事](http://qiita.com/groonga/items/dfc1e170a9cb53de53cc)の方をご覧下さい。

以下、これを材料にして`--match_columns`の指定の仕方による検索の動作の違いを解説していきます。

## CASE1：単独のデータカラムを指定した場合

`--match_columns`の使い方で最も一般的なのは、`select --table Entries --match_columns title --query "Hello Groonga"`というような、*データカラムの名前を1つだけ指定する*書き方でしょう。

この時Groongaは、*そのデータカラムに対応するインデックスカラムを自動的に探し出して*、そのインデックスを使って[前日の記事](http://qiita.com/groonga/items/dfc1e170a9cb53de53cc)で述べたような検索処理を行います。

ここで1つ疑問が浮かびます。「自動的に」と言いましたが、この記事の例のように*1つのデータカラムに対して複数のインデックスがある場合*には、どのインデックスが使われるのでしょうか？

結論から言うと、*その場合の動作は不定です*。

実際の所、Groonga 6.1.1の場合は「最後に作られたインデックスが自動選択される」という結果になります。が、これはたまたまそのような結果になる実装がなされているというだけで、*特に仕様としてそのような動作が保証されているわけではありません*。なので、将来的には動作が変わる可能性があります。

以上を踏まえると、安定した検索結果を返すためには、*1つのデータカラムに対応するインデックスが複数ある場合は`--match_columns`でそのデータカラム名を指定するのは避けた方がよい*ということが言えます。このようなケースでは、後述する*インデックスカラム名を指定する方法*を取るのがお勧めです。


## CASE2：複数のデータカラムを指定した場合

`--match_columns`には、`select --table Entries --match_columns title||body --query "Hello Groonga"`のように、*`||`で区切って複数のデータカラム名を列挙する書き方*があります。

この時Groongaは、*それぞれのデータカラムに対応するインデックスカラムを自動的に探し出して*、そのインデックスを使って個別に検索を行い、それらの結果の論理和を最終的な検索結果として返します。つまり、*複数カラムにまたがった検索を行います*。

この時も、データカラムの名前からインデックスを一意に特定できない場合にどのインデックスが使われるかは*不定*です。安定した検索結果を返すためには、こちらも後述する*インデックスカラム名を指定する方法*を取る事をお勧めします。


## CASE3：インデックスカラムを指定した場合

### CASE3-1：単独カラムの指定

今の所まだチュートリアル等に詳しい説明を用意できていないのですが、実は`--match_columns`には*インデックスカラムも指定できます*。この指定の仕方であれば、同じカラムに複数のインデックスがある場合でも、どのインデックスを使うか明示することができます。

特定のカラムに結び付いたインデックスを指定すれば、そのインデックスを使って、暗黙的にそのカラムだけを対象に検索することになります。

 * `select --table Entries --match_columns Terms.entries_title --query "Hello Groonga"`
   →`Terms.entries_title`をインデックスとして使い、暗黙的に`title`カラムだけを対象に検索する。
 * `select --table Entries --match_columns Terms.entries_body --query "Hello Groonga"`
   →`Terms.entries_body`をインデックスとして使い、暗黙的に`body`カラムだけを対象に検索する。

複数のカラムにまたがるインデックスでは、インデックスカラム名の後にデータカラム名を指定して「どのインデックスを使い、どのデータカラムを対象に検索するか」を明示できます。

 * `select --table Entries --match_columns Terms.entries_whole.title --query "Hello Groonga"`
   →`Terms.entries_whole`をインデックスとして使い、明示的に`title`カラムだけを対象に検索する。
 * `select --table Entries --match_columns Terms.entries_whole.body --query "Hello Groonga"`
   →`Terms.entries_whole`をインデックスとして使い、明示的に`body`カラムだけを対象に検索する。

この指定の仕方は、BigramやMeCabや[JMAT](https://github.com/groonga/groonga-jmat "groonga/groonga-jmat: Groonga plugins to integrate with JMAT")という風に複数のトークナイザーを使い分けたい場合に有用です。`select`の時点でのカラム指定で簡単に使用インデックスを切り替えられるため、それぞれのトークナイザーの性能評価などが容易になります。

### CASE3-2：複数カラムの指定

インデックスカラムを指定する場合も、CASE2と同様に`||`でカラム名を列挙できます。例えば`select --table Entries --match_columns Terms.entries_title||Terms.entries_body --query "Hello Groonga"`とすれば、複数のインデックスカラムを使った検索結果の論理和が検索結果として返されます。

この場合も、CASE2に比べてどのインデックスを使うかをはっきりと明示できるのが利点となります。

### CASE3-3：複数カラムにまたがる検索

複数のカラムにまたがるインデックスに対して、`select --table Entries --match_columns Terms.entries_whole --query "Hello Groonga"`のように*データカラム名を明示せずにインデックスカラム名だけを指定*した場合、*そのインデックスを共有しているすべてのカラムにまたがった検索*が行われます。

この記事の例だと、複数のインデックスを`||`で列挙して`select --table Entries --match_columns Terms.entries_title||Terms.entries_body --query "Hello Groonga"`と指定した場合と検索結果としては変わりません。しかし、*検索処理の効率*の点では違いがあります。

 * `--match_columns Terms.entries_title||Terms.entries_body`
   →それぞれのインデックスで個別に検索を行い、その論理和を返す。2回検索するので時間がかかる。
 * `--match_columns Terms.entries_whole`
   →インデックスのセクションIDを無視してマッチングを行い、検索結果を返す。検索が1回で済むので高速。

ただ、複数カラムにまたがるインデックスカラム名を単独で指定した場合、[データカラムごとの重み付け](http://groonga.org/ja/docs/reference/commands/select.html#select-match-columns)はできなくなります。重み付けが必要な場合にはこちらの指定ではなく、CASE2やCASE3-2のように各データカラムやインデックスカラム名を列挙する必要があります。


## まとめ

`--match_columns`の指定の仕方について、いくつかのパターンのメリット・デメリットをそれぞれ解説しました。

ちなみに、`--match_columns`にはこの他に、[同一テーブルの複数カラムでは無く、複数のテーブルのカラムをまたいだインデックスを指定する使い方](http://groonga.org/ja/docs/tutorial/match_columns.html#nested-index-search-among-related-table-by-column-index)もあります。長くなるので説明は省略しますが、こちらの方法では「ブログのコメント欄の内容を対象に検索して、その親エントリーを検索結果として得る」というようなことができます。興味のある方は、リンク先のチュートリアルもご覧下さい。

`--match_columns`の指定の仕方を工夫すると、Groongaのインデックスをより効果的に活用できます。Groongaはこのようにインデックスの詳細を理解した上でのきめ細やかな指定によって、エンジンの性能を限界まで引き出すことができるのが特長です。「なんかだかよくわからないけど、インデックスを使うと検索が早くなる」というレベルの理解から一歩踏み込んで、皆さんもぜひ計算機資源を有効に利用して下さい。

以上、[Groonga Advent Calendar 2016](http://qiita.com/advent-calendar/2016/groonga) 16日目として`--match_columns`の指定の仕方による検索動作の変化を解説しました。引き続きGroonga Advent Calendarをお楽しみ下さい！