JavaScriptでElasticsearchのクエリを作る

この記事は、[Elastic stack Advent Calendar 2016](http://qiita.com/advent-calendar/2016/elastic)の１３日目の記事です。

作ったツールをこの記事で紹介しようと思います。
GitHub: [es-builder](https://github.com/toniov/es-builder)

このツールを使ってJavaScriptでElasticsearchのクエリを簡単に作成できます。
特に作成できるのが、`Query DSL`経由で作る部分です （bodyの`query`項目）。

# Motivation

弊社で開発しているサービスでElasticsearchを導入することになったことで、クエリ作成も含めてNode.jsで[公式のクライアント](https://github.com/elastic/elasticsearch-js)を使って、検索ロジックを実装しました。

簡単なクエリなら公式クライアントで十分かもしれませんが、複雑なクエリを作るならツールが必要となると思います。直接JSONでかくと、間違えやすいし、かなり冗長になります。

[elastic.js](https://github.com/fullscale/elastic.js/)を使おうと思いましたが、２年ぐらいメンテされなくて、Query DSL 2.xに互換性がありません。他に良さそうなツールもありましたが、ネストクエリができなかったりだったので、結局自分で作ることにしました。

# 基本の使い方

下記のJSONクエリを

```json
 {
   "bool": {
     "must": [{
       "term": {
         "name": "承太郎"
       }
     }, {
       "match": {
         "description": {
           "query": "やれやれだぜ"
         }
       }
     }],
     "must_not": {
       "term": {
         "location": "杜王町"
       }
     }
   }
 }
```

`es-builder`で作りましょう。

```js
const eb = require('es-builder');
const elasticsearch = require('elasticsearch');

const client = new elasticsearch.Client({
  host: 'localhost:9200'
});

const query = eb.QueryBuilder()
  .query(eb.TermQuery('name', '承太郎'))
  .query(eb.MatchQuery('description', 'やれやれだぜ'))
  .queryMustNot(eb.TermQuery('location', '杜王町'));
```

作られたqueryインスタンスをstringifyすると、上記と同じJSONが作られます。

```js
JSON.stringify(query);
// {
//   "bool": {
//     "must": [{
//       "term": {
//             ...
//             ...
//     }
//    }
```

内部的にElasticsearchのライブラリに渡される`JSのオブジェクトのbody`を`JSON`にコンバートしてくれるので、 `JSON.stringify`せずにそのまま作った`query`インスタンスを渡してもいいです！

```js
client.search({
  index: 'user',
  type: 'stand_user',
  body: {
    query: query
  }
}, function(err, resp) {
  // result of the search here
  ...
});
```

上記に作ったクエリは固定していましたが、インプット等で可変なクエリのJSONを作るのは複雑になると思います。その場合も`es-builder`を使うと楽になります。

## ネストクエリ

ネストクエリも作成可能です。

```js
const eb = require('es-builder');

const query = eb.QueryBuilder();

// フィルターを追加する
query
  .filter(eb.TermsQuery('name', ['ジョセフ', 'ジョナサン']))
  .filter(eb.ExistsQuery('age'));

// bool queryを作る
const boolQuery = eb.BoolQuery()
  .should(eb.RangeQuery('age').gt(20).lt(40))
  .should(eb.PrefixQuery('surname', 'ジョ'));

// ネストする
query.filter(boolQuery);

// queryインスタンスはstringifyすると
// JSON.stringify(query)
// {
//   "bool": {
//     "filter": {
//       bool: {
//         "must": [{
//           "terms": {
//             "name": {
//               "value": ["ジョセフ", "ジョナサン"]
//             } 
//           }
//         }, {
//           "exists": {
//             "field": "age"
//           }
//         }, {
//           "bool": {
//             "should": [{
//               "range": {
//                 "age": { "gt": 20, "lt": 40 }
//               }
//             }, {
//               "prefix": {
//                 "surname": {
//                   "value": "ジョ"
//                 }
//               }
//             }]
//           }
//         }]
//       }
//     }
//   }
// }
```

# REPL

global moduleとしてインストールすれば、REPL経由でクエリを作成できます。
REPLの場合だと全クエリはexposeされたので、`eb.`なしでそのまま使えます。

```bash
$ npm install -g es-builder
...

$ es-builder

es-builder> query = QueryBuilder().query(TermQuery('name', 'カービィ')).query(MatchQuery('description', 'まるくて、ふわふわしてる')).queryMustNot(TermQuery('name', 'ワドルディ'));

es-builder> query.stringified
'{"bool":{"must":[{"term":{"name":{"value":"カービィ"}}},{"match":{"description":{"query":"まるくて、ふわふわしてる"}}}],"must_not":{"term":{"name":{"value":"ワドルディ"}}}}}'

es-builder> .exit
```

作られたクエリをコピーして、`curl`でもで使えます。

```bash
$ curl -XGET 'http://localhost:9200/games/dreamland/_search' -d'
{
    "query" : {"bool":{"must":[{"term":{"name":{"value":"カービィ"}}},{"match":{"description":{"query":"まるくて、ふわふわしてる"}}}],"must_not":{"term":{"name":{"value":"ワドルディ"}}}}}
}'
```

# おわりに

[es-builder](https://github.com/toniov/es-builder)を使って、JavascriptでElasticsearchのクエリを作れるツールの紹介でした。
提案や意見等あれば是非イシューもしくはプルリクエストを送ってください。お待ちしております！