こんにちは。WEBサービス開発グループの中野です。
業務ではWEBサービスの検索基盤を担当しています。
ニフティのWEBサービスでは、一部サービスのサイト内検索やログ可視化などで Elasticsearch を利用しています。
Elasticsearch は Elastic社の提供する分散型の検索エンジンで、検索・集計・分析はもちろん、可視化の Kibana を初めとした強力な周辺ツールが魅力的です。
また、Apache Solr などと同じく Apache Lucene をベースとしています。
今回は、Elasticsearch 5.3 から追加された Field Collapsing 機能を使ってみたいと思います。
Field Collapsing はその名のとおり検索結果を「折りたたんで」くれる機能で、検索結果をグルーピングして SQL の GROUP BY のような操作をすることができます。
まずはサンプルをみてみましょう。
サンプル
データ
サンプルデータとして、青空文庫さんの作品リストcsvを使用させて頂いています。
1 2 3 |
$ head -2 list_person_all_utf8.csv 人物ID,著者名,作品ID,作品名,仮名遣い種別,翻訳者名等,入力者名,校正者名,状態,状態の開始日,底本名,出版社名,入力に使用した版,校正に使用した版 001257,"アーヴィング ワシントン",056078,"駅伝馬車",旧字旧仮名,"高垣 松雄","雀","小林繁雄",公開,2013-09-20,"スケッチ・ブック","岩波文庫、岩波書店","2010(平成22)年2月23日第31刷","1992(平成4)年2月26日第30刷" |
このデータを Elasticsearch に投入し、通常の検索結果と Field Collapsing でグルーピングした検索結果を比較してみます。
通常の検索結果
JSONの hits の中に、作品のオブジェクトが並んでいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
{ "took": 3, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 47, "max_score": 8.686901, "hits": [ { "_index": "aozora", "_type": "books", "_id": "AVztwxPuTls5uo3IuXcw", "_score": 8.686901, "_source": { "id": "001562", "title": "朝", "author": "太宰 治", "publisher": "新潮文庫、新潮社" } }, { "_index": "aozora", "_type": "books", "_id": "AVztwxPuTls5uo3IuXcO", "_score": 8.39296, "_source": { "id": "046438", "title": "朝", "author": "竹久 夢二", "publisher": "小学館文庫、小学館" } }, { "_index": "aozora", "_type": "books", "_id": "AVztw100Tls5uo3IuZBh", "_score": 8.39296, "_source": { "id": "052885", "title": "朝", "author": "牧野 信一", "publisher": "筑摩書房" } }, { "_index": "aozora", "_type": "books", "_id": "AVztwym_Tls5uo3IuXnl", "_score": 7.9438734, "_source": { "id": "004598", "title": "朝", "author": "田山 花袋", "publisher": "現代企画室" } }, { "_index": "aozora", "_type": "books", "_id": "AVztww6JTls5uo3IuWB6", "_score": 6.876827, "_source": { "id": "052036", "title": "朝の公園", "author": "小川 未明", "publisher": "講談社" } } ] } } |
Field Collapsing の検索結果
JSONの hits の中に並んでいるのは作品ではなく著者で、それぞれの著者の下に inner_hits として作品が並んでいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
{ "took": 4, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 47, "max_score": null, "hits": [ { "_index": "aozora", "_type": "books", "_id": "AVztwxPuTls5uo3IuXcw", "_score": 8.686901, "_source": {}, "fields": { "author": [ "太宰 治" ] }, "inner_hits": { "books": { "hits": { "total": 3, "max_score": 8.686901, "hits": [ { "_index": "aozora", "_type": "books", "_id": "AVztwxPuTls5uo3IuXcw", "_score": 8.686901, "_source": { "id": "001562", "title": "朝", "author": "太宰 治", "publisher": "新潮文庫、新潮社" } }, { "_index": "aozora", "_type": "books", "_id": "AVztwym_Tls5uo3IuXdB", "_score": 5.2545714, "_source": { "id": "002255", "title": "右大臣実朝", "author": "太宰 治", "publisher": "筑摩書房" } } ] } } } }, { "_index": "aozora", "_type": "books", "_id": "AVztwxPuTls5uo3IuXcO", "_score": 8.39296, "_source": {}, "fields": { "author": [ "竹久 夢二" ] }, "inner_hits": { "books": { "hits": { "total": 1, "max_score": 8.39296, "hits": [ { "_index": "aozora", "_type": "books", "_id": "AVztwxPuTls5uo3IuXcO", "_score": 8.39296, "_source": { "id": "046438", "title": "朝", "author": "竹久 夢二", "publisher": "小学館文庫、小学館" } } ] } } } }, { "_index": "aozora", "_type": "books", "_id": "AVztw100Tls5uo3IuZBh", "_score": 8.39296, "_source": {}, "fields": { "author": [ "牧野 信一" ] }, "inner_hits": { "books": { "hits": { "total": 7, "max_score": 8.39296, "hits": [ { "_index": "aozora", "_type": "books", "_id": "AVztw100Tls5uo3IuZBh", "_score": 8.39296, "_source": { "id": "052885", "title": "朝", "author": "牧野 信一", "publisher": "筑摩書房" } }, { "_index": "aozora", "_type": "books", "_id": "AVztw100Tls5uo3IuZCO", "_score": 6.0719876, "_source": { "id": "045282", "title": "駆ける朝", "author": "牧野 信一", "publisher": "筑摩書房" } } ] } } } }, { "_index": "aozora", "_type": "books", "_id": "AVztwym_Tls5uo3IuXnl", "_score": 7.9438734, "_source": {}, "fields": { "author": [ "田山 花袋" ] }, "inner_hits": { "books": { "hits": { "total": 1, "max_score": 7.9438734, "hits": [ { "_index": "aozora", "_type": "books", "_id": "AVztwym_Tls5uo3IuXnl", "_score": 7.9438734, "_source": { "id": "004598", "title": "朝", "author": "田山 花袋", "publisher": "現代企画室" } } ] } } } }, { "_index": "aozora", "_type": "books", "_id": "AVztww6JTls5uo3IuWB6", "_score": 6.876827, "_source": {}, "fields": { "author": [ "小川 未明" ] }, "inner_hits": { "books": { "hits": { "total": 3, "max_score": 6.876827, "hits": [ { "_index": "aozora", "_type": "books", "_id": "AVztww6JTls5uo3IuWB6", "_score": 6.876827, "_source": { "id": "052036", "title": "朝の公園", "author": "小川 未明", "publisher": "講談社" } }, { "_index": "aozora", "_type": "books", "_id": "AVztww6JTls5uo3IuWGV", "_score": 6.0719876, "_source": { "id": "051659", "title": "春さきの朝のこと", "author": "小川 未明", "publisher": "講談社" } } ] } } } } ] } } |
Field Collapsing でグルーピングした方は、著者ごとにまとまった検索結果になっていることが確認できました。
使い方
次に、ドキュメント に従って Field Collapsing の使い方を確認したいと思います。
余談ですが、Elasticsearch のクエリ動作確認には Kibana の Dev Tools が便利です。
ブラウザから簡単にクエリ実行ができ、シンタックスの補完もしてくれます。
グルーピング
グルーピングをするためには、クエリに collapse 句を追加します。
collapse 句の中では、グルーピングのキーとなるフィールド名を filed に指定します。
キーに指定するフィールドは、keyword 型か numeric 型である必要があります。
下記のクエリでは、aozora というインデックスを検索条件なしで取得して、検索結果を author というフィールドの値ごとにグルーピングしています。
1 2 3 4 5 6 7 8 9 |
GET /aozora/_search { "query": { "match_all": {} }, "collapse": { "field": "author" } } |
グループの展開
collapse 句の中に inner_hits 句を追加することで、各グループに含まれるレコードを検索結果に含めることができます。
inner_hits 句では、name 句で検索結果内のJSONオブジェクト名、size 句で各グループごとに何件取得するか、sort 句で各グループ内のソート方法、を指定可能です。
下記のクエリでは、検索結果を author というフィールドでグルーピングし、グループごとに pudDate の降順にソートした上位3件を books_recent という名前で取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
GET /aozora/_search { "query": { "match_all": {} }, "collapse": { "field": "author", "inner_hits": { "name": "books_recent", "size": 3, "sort": { "pubDate": "desc" } } } } |
参考までに、今後のアップデート では multiple inner_hits 機能が追加され、各グループごとに複数の inner_hits を取得することが可能になるようです。
ページング・ソート
通常のクエリと同様に、ページングは from 句と size 句、ソートは sort 句で指定することが可能です。
下記のクエリでは、グルーピング結果を author フィールドの昇順にソートして50グループ目から10グループぶん取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
GET /aozora/_search { "from": 50, "size": 10, "sort": { "author": { "order": "asc" } }, "query": { "match_all": {} }, "collapse": { "field": "author", "inner_hits": { "name": "books", "size": 3 } } } |
Field Collapsing の基本的な使い方については、以上となります。
注意事項
Field Collapsing を利用していると、検索結果に含まれる took(検索実行の所要時間)より実際のレスポンスタイムが大幅に遅くなることがあります。
特に、inner_hits 句を指定して各グループに含まれる情報を展開している場合、グループ数が多くなればなるほどこの差は大きくなります。
この理由は、Field Collapsing の仕組み上 inner_hits を生成するために各グループごとに内部クエリを発行しているのですが、took にはこの内部クエリの所要時間が含まれていないためです。
本問題については既に Pull Request で取り上げられているため、今後のバージョンアップで改善されると思います。
ちなみに上記の内部クエリをどの程度並列に実行するかは max_concurrent_group_searches パラメータで調節することも可能です。
この値はデフォルトではノード数とスレッドプールサイズによって自動的に決定されるようです。
その他、Elasticsearch がどのように検索を実行しているかについては、ドキュメントのガイドが非常に参考になりますので、ご一読をおすすめします。
新しい機能を利用する際には色々な問題に出くわすこともありますが、ドキュメントだけでなくその機能が追加された Pull Request や関連する Issue を確認してみると参考になる情報が得られるかもしれません。
まとめ
Field Collapsing はいかがでしたでしょうか。
WEBサイトやECサービスで検索結果などのコンテンツを表示する際に、重複したコンテンツを一つにまとめたり、属性ごとにグルーピングして提供できれば一覧性が向上しそうですね。
ニフティのWEBサービスでも Field Collapsing などの便利な機能を活用していきたいです。
Elasticsearch は5系になってからリリース頻度がとても高いので、今後のアップデートにも期待したいと思います。