Groongaの新機能「slice」の使い方

[Groonga Advent Calendar 2016](http://qiita.com/advent-calendar/2016/groonga) 13日目は、最近のバージョンのGroongaで実装された機能「slice」を取り上げます。

## sliceとは？

プログラミング経験のある方は、配列や文字列を操作するメソッドで`slice()`という物を聞いたことがあるかもしれません。これは対象の一部だけを取り出す機能で、例えばJavaScriptでは以下のようにして配列から部分配列を取り出すことができます。

~~~javascript
[0, 1, 2, 3, 4, 5].slice(3)
// 3番目以降の要素からなる配列を取り出す
// => [3, 4, 5]
~~~

Groongaのslice機能もこれに似ています。一言で言えば、*検索結果の中からさらに一部分を取り出す機能*ということになります。SQLでは`SELECT * FROM (SELECT * FROM table WHERE ...)`のように、テーブル名の代わりに別のSELECTの結果を指定することでその結果をさらに絞り込んだり加工したりすることができますが、これに似たことがGroongaの`select`コマンドでもできるようになりました。

## 準備

実際のsliceの使い方を見てみましょう。実験用に、[チュートリアル](http://groonga.org/ja/docs/tutorial/introduction.html)で作成したテーブルにインデックスを追加したものを用意します。

~~~bash
$ cat dump.grn
table_create Site TABLE_HASH_KEY ShortText
column_create Site title COLUMN_SCALAR ShortText
table_create Terms TABLE_PAT_KEY ShortText --default_tokenizer TokenBigram --normalizer NormalizerAuto
load --table Site
[
["_key","title"],
["http://example.org/","This is test record 1!"],
["http://example.net/","test record 2."],
["http://example.com/","test test record three."],
["http://example.net/afr","test record four."],
["http://example.org/aba","test test test record five."],
["http://example.com/rab","test test test test record six."],
["http://example.net/atv","test test test record seven."],
["http://example.org/gat","test test record eight."],
["http://example.com/vdw","test test record nine."]
]
column_create Terms blog_title COLUMN_INDEX|WITH_POSITION Site title
column_create Terms blog_url COLUMN_INDEX|WITH_POSITION Site _key
$ rm -rf /tmp/groonga-databases
$ mkdir -p /tmp/groonga-databases
$ groonga -n /tmp/groonga-databases/introduction.db quit
$ cat dump.grn | groonga /tmp/groonga-databases/introduction.db
~~~

## sliceの基本的な使い方

sliceは、`select`コマンドのオプションとして指定します。以下は`Site`テーブルのすべてのレコードを検索して、そこからさらに`org`と`com`という2つのsliceを取り出しています。

~~~bash:
$ groonga /tmp/groonga-databases/introduction.db \
    select --table Site --output_columns '_key,title' \
           --slices\[org\].filter '_key@\"org\"' \
           --slices\[com\].filter '_key@\"com\"' |
    jq .
[
  [
    0,
    1481796854.345919,
    0.001838922500610352
  ],
  [
    [
      [
        9
      ],
      [
        [
          "_key",
          "ShortText"
        ],
        [
          "title",
          "ShortText"
        ]
      ],
      [
        "http://example.org/",
        "This is test record 1!"
      ],
      [
        "http://example.net/",
        "test record 2."
      ],
      [
        "http://example.com/",
        "test test record three."
      ],
      [
        "http://example.net/afr",
        "test record four."
      ],
      [
        "http://example.org/aba",
        "test test test record five."
      ],
      [
        "http://example.com/rab",
        "test test test test record six."
      ],
      [
        "http://example.net/atv",
        "test test test record seven."
      ],
      [
        "http://example.org/gat",
        "test test record eight."
      ],
      [
        "http://example.com/vdw",
        "test test record nine."
      ]
    ],
    {
      "org": [
        [
          3
        ],
        [
          [
            "_id",
            "UInt32"
          ],
          [
            "_key",
            "ShortText"
          ],
          [
            "title",
            "ShortText"
          ]
        ],
        [
          1,
          "http://example.org/",
          "This is test record 1!"
        ],
        [
          5,
          "http://example.org/aba",
          "test test test record five."
        ],
        [
          8,
          "http://example.org/gat",
          "test test record eight."
        ]
      ],
      "com": [
        [
          3
        ],
        [
          [
            "_id",
            "UInt32"
          ],
          [
            "_key",
            "ShortText"
          ],
          [
            "title",
            "ShortText"
          ]
        ],
        [
          3,
          "http://example.com/",
          "test test record three."
        ],
        [
          6,
          "http://example.com/rab",
          "test test test test record six."
        ],
        [
          9,
          "http://example.com/vdw",
          "test test record nine."
        ]
      ]
    }
  ]
]
~~~

sliceは、`--slices[slice名].filter`または`--slices[slice名].query`として元の検索結果に対する追加の絞り込み条件を指定すると出力されます。取り出されたsliceはこの例のように、元々の`select`の検索結果の後に続けてそれぞれのslice名のキーの下に出力されます。ここでは、`--slices[org].filter '_key@\"org\"'`という指定でURLに`org`を含むレコードを`org` slice、`--slices[com].filter '_key@\"com\"'`という指定でURLに`com`を含むレコードをそれぞれ`com` sliceとして取り出しています（シェル上では`[`と`]`はエスケープする必要があることに注意して下さい）。

sliceは元の検索結果の部分集合なので、検索結果が変わるとsliceも影響を受けます。元の検索の`filter`に`query("_key","org OR net")`という検索条件を指定して、結果を絞り込んでみましょう。

~~~bash:
$ groonga /tmp/groonga-databases/introduction.db \
    select --table Site --output_columns '_key,title' --filter '"query(\"_key\",\"org OR net\")"' \
           --slices\[org\].filter '_key@\"org\"' \
           --slices\[com\].filter '_key@\"com\"' |
    jq .
[
  [
    0,
    1481797371.340685,
    0.001978874206542969
  ],
  [
    [
      [
        6
      ],
      [
        [
          "_key",
          "ShortText"
        ],
        [
          "title",
          "ShortText"
        ]
      ],
      [
        "http://example.org/",
        "This is test record 1!"
      ],
      [
        "http://example.org/aba",
        "test test test record five."
      ],
      [
        "http://example.org/gat",
        "test test record eight."
      ],
      [
        "http://example.net/",
        "test record 2."
      ],
      [
        "http://example.net/afr",
        "test record four."
      ],
      [
        "http://example.net/atv",
        "test test test record seven."
      ]
    ],
    {
      "org": [
        [
          3
        ],
        [
          [
            "_id",
            "UInt32"
          ],
          [
            "_key",
            "ShortText"
          ],
          [
            "title",
            "ShortText"
          ]
        ],
        [
          1,
          "http://example.org/",
          "This is test record 1!"
        ],
        [
          5,
          "http://example.org/aba",
          "test test test record five."
        ],
        [
          8,
          "http://example.org/gat",
          "test test record eight."
        ]
      ],
      "com": [
        [
          0
        ],
        [
          [
            "_id",
            "UInt32"
          ],
          [
            "_key",
            "ShortText"
          ],
          [
            "title",
            "ShortText"
          ]
        ]
      ]
    }
  ]
]
~~~

`org`のsliceには結果が含まれているのに、`com`のsliceは空になりました。これは、元の検索結果の中に`com`のsliceの絞り込み条件にマッチする結果が存在しないためです。

## sliceごとのオプション指定

sliceには、それぞれに異なった出力オプションを指定できるという特長があります。試しに、`org`のsliceは`title`カラムの値だけを取り出して、`com`のsliceは内容を逆順でソートしてみます。

~~~bash:
$ groonga /tmp/groonga-databases/introduction.db \
    select --table Site --output_columns '_key,title' --limit 0 \
           --slices\[org\].filter '_key@\"org\"' --slices\[org\].output_columns 'title' \
           --slices\[com\].filter '_key@\"com\"' --slices\[com\].sort_keys '-_key'  |
    jq .
[
  [
    0,
    1481797796.667199,
    0.05050277709960938
  ],
  [
    [
      [
        9
      ],
      [
        [
          "_key",
          "ShortText"
        ],
        [
          "title",
          "ShortText"
        ]
      ]
    ],
    {
      "org": [
        [
          3
        ],
        [
          [
            "title",
            "ShortText"
          ]
        ],
        [
          "This is test record 1!"
        ],
        [
          "test test test record five."
        ],
        [
          "test test record eight."
        ]
      ],
      "com": [
        [
          3
        ],
        [
          [
            "_id",
            "UInt32"
          ],
          [
            "_key",
            "ShortText"
          ],
          [
            "title",
            "ShortText"
          ]
        ],
        [
          9,
          "http://example.com/vdw",
          "test test record nine."
        ],
        [
          6,
          "http://example.com/rab",
          "test test test test record six."
        ],
        [
          3,
          "http://example.com/",
          "test test record three."
        ]
      ]
    }
  ]
]
~~~

`--limit 0`は元の検索結果に対する出力オプションなので、元々の`select`の結果のレコードは出力が省略されています。sliceは`--limit`や`--offset`で取り出される前の検索結果に対する絞り込みなので、`--limit`の影響は受けません（この点はdrilldownと同じです）。見ての通り、それぞれのsliceで出力のフォーマットが異なっていることが分かります。

sliceではこの他にも、同様の書式で`--slices[slice名].limit`や`--slices[slice名].offset`といったオプションも指定できます。

## サポートから本体の改良へ

これまでslice相当のことをするためには、複数回の`select`を実行する必要がありました。sliceを利用することにより、複雑な検索結果を一回の検索で得られるようになったため、Groongaをバックエンドに使用したサービスをより開発・メンテナンスしやすくなりました。

実は、slice機能はGroongaを実際のサービスでお使いのお客様から寄せられたご要望に基づいて追加した機能です。[Groongaサポートを提供している各社](http://groonga.org/ja/support/ "Groonga - サポート")ではお客様のお問い合わせへの対応や導入支援を行っていますが、一般的に有用と思われる機能についてはこのようにGroonga本体の改良として実現することもあります。自社サービスの改良に役立つようなこんな機能がGroongaに欲しい！というニーズをお持ちの企業様がいらっしゃいましたら、ぜひご相談下さい。

以上、[Groonga Advent Calendar 2016](http://qiita.com/advent-calendar/2016/groonga) 13日目としてsliceの使い方を簡単に紹介しました。引き続きGroonga Advent Calendarをお楽しみ下さい！