elasticsearchでダウンタイムなしでインデックスのマッピングを切り替える

#### TL;DR

elasticsearch公式ブログの手順を試したらできたのでメモ。index aliasesを使うと実現できる。

[Elasticsearch.org Changing Mapping With Zero Downtime | Blog | Elasticsearch](http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/)
### 背景

最近、elasticsearch v1.4系 + kibana4 をログの可視化用途として本番環境で利用し始めてみたものの、ほとんどデフォルトのままの設定ではkibana上でtermsによるデータのカウントがうまくとれないという問題に行き当たりました。

原因としては、string型として扱われているfieldがデフォルトでanalyzed属性を持っているため、fieldの値がtoknizeされてインデックスされていたためでした。stringのfieldをnot_analyzedにするためには以下の様なindex templatesという機能を利用すれば良さそうです。

* [Kibana+Elasticsearchで文字列の完全一致と部分一致検索の両方を実現する - Qiita](http://qiita.com/harukasan/items/4ec517d8d96f557367e1)
* [Index Templates](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html)

ただし、index templatesを指定したからといって既存のindexがnot_analyzedになるわけではないので、既存のindexまでnot_analyzedに変更した場合は再度indexを構築し直す必要がありそうです。

### 調査

elasticsearchのドキュメントを調べてみるとインデックスを作り直すための一番シンプルな方法はドキュメントを古いindexから新しいものへコピーしなおすことと書いてあり、scan-and-scrollで取得してbluk APIでぶち込んで行けとの事だった。

* [Reindexing Your Data](http://www.elasticsearch.org/guide/en/elasticsearch/guide/current/reindex.html)

このままだと、古いインデックスから新しいインデックスへの切り替えをするためには一旦elasticsearchを利用しているサービスを止めないと古いインデックスに対して更新が新しいインデックスへ反映されなかったりという問題が起きそう。

などと思って、さらに調べていたら、まさにやりたいと思っていたことが公式ブログの記事になっていた。

* [Elasticsearch.org Changing Mapping With Zero Downtime | Blog | Elasticsearch](http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/)

###　手順

今回試したのは以下の手順。Index Aliasesという仕組みを利用する。 [Index Aliases](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html)

##### 0.（準備としてテスト用のインデックスを用意）

```
$ curl -X POST  localhost:9200/shibathon 2> /dev/null | jq -C '.' 2> /dev/null
{
  "acknowledged": true
}
```

```
$ curl -X GET -w -  localhost:9200/shibathon 2> /dev/null | jq -C '.' 2> /dev/null
{
  "shibathon": {
    "settings": {
      "index": {
        "version": {
          "created": "1040299"
        },
        "number_of_shards": "5",
        "number_of_replicas": "1",
        "uuid": "tyTLZf2iS6iedDlrLvgDIw",
        "creation_date": "1419313945001"
      }
    },
    "mappings": {}
  }
}
```

```
$ curl -X POST  -d '{ "body" : "シバッ" }'  localhost:9200/shibathon/shiba/1 2> /dev/null | jq -C '.' 2> /dev/null
{
  "created": false,
  "_version": 1,
  "_id": "1",
  "_type": "shiba",
  "_index": "shibathon"
}
```


##### 1. 既存インデックスを指し示すエイリアスを作成する

```
$ curl -X POST  -d '{
    "actions" : [{
        "add" : {
            "alias": "shibathon_alias",
            "index": "shibathon"
        }
    }]
}'  localhost:9200/_aliases 2> /dev/null | jq -C '.' 2> /dev/null
{
  "acknowledged": true
}
```

これで、`shibathon_alias` が `shibathon` を指す事になるので、アプリケーション側で使っている箇所や、fluent-plugin-elasticsearchの向き先などをこのエイリアスに変更する。

##### 2. 新しいインデックスを作る

今後もmappingを変更するたびにインデックスを作りなおしたりするかもしれないのでバージョン番号などをインデックス名につけとくと良さそう。今回はshibaのbodyをnot_analyzedにしてみた。

```
$ curl -X POST  -d '{
     "mappings" : {
          "shiba": {
              "properties" : {
                   "body": {
                       "type": "string",
                       "index" : "not_analyzed"
                   }
               }
          }
      }
}'  localhost:9200/shibathon_v2 2> /dev/null | jq -C '.' 2> /dev/null
{
  "acknowledged": true
}
```

##### 3. インデックスを作り直す

インデックスを作り直すためにはscan & scrollで全件を検索し、各種elasticsearchクライアントのライブラリで新しいインデックスにドキュメントを追加していく。今回は、Rubyのtireというgemを利用した。reindexするメソッドは各種クライアントライブラリに代替生えているとのこと。tireの場合、[ここ](https://github.com/karmi/retire/blob/master/lib/tire/index.rb#L308-L324)だった。

使い方は超簡単で、古いインデックス名のインスタンス作ってreindexメソッドに新しいインデックス名を与えて呼び出すだけ。多分データ量多いとそれなりに時間かかるのだろうけど、ドキュメントが1個しかなかったので一瞬で終わった。

```
$ bundle exec rails console
[1] pry(main)> idx = Tire::Index.new('shibathon') # 古いインデックス名
[2] pry(main)> idx.reindex('shibathon_v2')        # 新しいインデックス名
```

##### 4. 新しいインデックスに切り替える

エイリアスが指している古いインデックス(shibathon)を新しいインデックス(shibathon_v2)に切り替える。

```
$ curl -X POST  -d '{
     "actions" : [{
         "remove" : {
             "alias" : "shibathon_alias",
             "index" : "shibathon"
         }
     }, {
         "add" : {
             "alias": "shibathon_alias",
             "index": "shibathon_v2"
         }
     }
 ]}'  localhost:9200/_aliases 2> /dev/null | jq -C '.' 2> /dev/null
{
  "acknowledged": true
}
```

すると、エイリアスが新しいインデックスを指すようになる。これでダウンタイムなしでマッピングしなおしたインデックスに切り替えられる！

```
$ curl -X GET  localhost:9200/shibathon_alias 2> /dev/null | jq -C '.' 2> /dev/null
{
  "shibathon_v2": {
    "settings": {
      "index": {
        "version": {
          "created": "1040299"
        },
        "number_of_shards": "5",
        "number_of_replicas": "1",
        "uuid": "eEN8mYw0TN-z7Obd5XXAmA",
        "creation_date": "1419316414675"
      }
    },
    "mappings": {
      "shiba": {
        "properties": {
          "id": {
            "type": "string"
          },
          "body": {
            "index": "not_analyzed",  
            "type": "string",
          },
          "_type": {
            "type": "string"
          }
        }
      }
    },
    "aliases": {
      "shibathon_alias": {}
    }
  }
}
```

##### 5. 最後に古いインデックスを消す

```
$ curl -X DELETE  localhost:9200/shibathon 2> /dev/null | jq -C '.' 2> /dev/null
{
  "acknowledged": true
}
```


### まとめ

ブログに書いてあるとおり、aliasを使うとインデックスが滑らかに移行できそうということがわかった。（実運用中のElasticsearchではまだ試してないので、試してうまくいったら追記しておきたい。）

今回の手順、ひと通りをドキュメントに書いてあるとおりcurlで試した（reindexの部分はcurlだけではちょっとだるいのでRubyのESクライアント使った）が、実際にはすべて、Elasticsearchクライアントのライブラリを利用してコードで書いておいて、すべての手順が滑らかに実行できるスクリプトを作っておくと良さそう。

#### おまけ

今回、この記事を書くためにcurlとjq組み合わせてelasticsearchのREST APIを叩いていたけど、毎回コマンド書き続けるのめんどかったので以下の様なラッパーを作った。コマンドの履歴とjqで綺麗にしたレスポンス結果を適当なファイルに残しておくのそこそこ便利。

```rb:es.rb
#!/usr/bin/env ruby

method  = ARGV.shift
url     = ARGV.shift
data    = ARGV.shift
data_options = " -d '#{data}' " if data

puts command = "curl -X #{method} #{data_options} #{url} 2> /dev/null | jq -C '.' 2> /dev/null"
print response = `#{command}`

File.open('es_history.log', 'a+') do |f|
  f.puts("$ " + command)
  f.puts(response)
  f.puts("\n")
end
```

こんなかんじで使っていた。
```
$ ./es.rb POST localhost:9200/shibathon/shiba/1 '{ "body" : "シバッ" }'
```