Elixir/Phoenixで、Amazon Elasticsearch Serviceを使って全文検索機能を実装してみる(AWS署名バージョン4&多言語対応)

メディア系Webサービスにおいて全文検索機能を実装する場合、全文検索エンジンとして[Elasticsearch](https://www.elastic.co/products/elasticsearch)を用いる方式が最近はトレンドになっています。

さらに昨年、AWSから[Amazon Elasticsearch Service](https://aws.amazon.com/jp/elasticsearch-service/)というマネージドサービスがリリースされました。

私が以前開発を担当したメディア系のWebサービスでは、EC2上に独自にElasticsearchのクラスタを構築していましたが、マネージドサービスがあるならばそちらを利用した方が管理やスケールの手間が省けるだろうということで、今回はAmazon Elasticsearch Serviceを使用して、Elixir/Phoenixに全文検索機能を実装してみたいと思います。

# 問題点1

Amazon Elasticsearch Service(以降「Amazon ES」と略)に関してご調査されたことがある方はご存知だと思いますが、Amazon ESはVPC上に構築されるわけではないので、アクセス制限として「セキュリティグループ」を使用することが出来ません。

Amazon ESのアクセス制限として利用できる方式は、[こちら](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html#es-createdomain-configure-access-policies)で説明されているように、


| 方式 | 
|:-----------|
| リソースベースのアクセスポリシー |
| IPベースのアクセスポリシー |
| IAMユーザーとRoleベースのアクセスポリシー |

のいずれかとなります。

  * 「リソースベースのアクセスポリシー」と「IAMユーザーとRoleベースのアクセスポリシー」の違いは、前者は「Amazon ES(リソース)に対してポリシーを設定する」のに対して、後者は「IAMユーザーやRoleに対してポリシーを設定する」という点にあります。詳しくは[こちら](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_permissions.html#TypesPermissions)の説明をご参照ください。

IPベースのアクセスポリシーは、要するにIPアドレスでアクセス元を制限するということになりますが、この手法は簡易ではあるものの保守性/柔軟性に欠けています。(アクセス元のサーバーが増えた場合にいちいち設定を変更する必要がある)

ということで「リソースベースのアクセスポリシー」もしくは「IAMユーザーとRoleベースのアクセスポリシー」のいずれかを使用することになりますが、今回の実装では「分かりやすさ」を重視して「リソースベースのアクセスポリシー」を使用してアクセス制限を行いました。

# 問題点2

アクセス制限の方法として「リソースベースのアクセスポリシー」を使用する場合、Amazon ESのAPIを実行するためには、[AWS署名バージョン4](https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4_signing.html)を使用して、HTTPリクエストに署名を付加する必要があります。

Elixirにおける、AWS署名バージョン4用のパッケージとしては[ex_aws](https://github.com/CargoSense/ex_aws)もしくは[aws_auth](https://github.com/bryanjos/aws_auth)というパッケージが使用可能ですが、私が調査した時点では、どちらも「HTTPヘッダー用の署名」に関しては正常に動作していたものの、「クエリ文字列用の署名」に関しては、S3以外だと正常に動作しないという不具合が存在していました。(おそらく[S3用の署名に関する仕様](http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html)だけを見て実装してしまっていることが原因)

今回のケースでは、Amazon ESのアクセスには[tirexs](https://github.com/Zatvobor/tirexs)を使用することを前提としており、このパッケージに「HTTPヘッダーをパッケージ外部から渡す」という機能が用意されておらず、「クエリ文字列用の署名」機能を使用せざるを得なかったため、比較的コード量の少ないaws_authの方に[プルリク](https://github.com/bryanjos/aws_auth/pull/19)を送信して該当箇所を改修してもらいました。

また、tirexs側にも一部不具合がありましたので、こちらも[プルリク](https://github.com/Zatvobor/tirexs/pull/223)を送信して該当箇所を直してもらいました。

# 実装

ということで、「Elixir/Phoenixでメディア系のWebサービスを開発するための技術サンプル」として、以前からオープンソースで開発している[media_sample](https://github.com/kenta-aktsk/media_sample)の方に、ElasticSearch用の機能を追加実装してみましたが、その中心のソースが[こちら](https://github.com/kenta-aktsk/media_sample/blob/master/lib/media_sample/search.ex)になります。

インデックスの作成/削除、データのロード機能等の使用方法はREADME.mdに追記してあります。また、記事(Entry)モデルの新規作成/更新/削除の際に、Amazon ESのドキュメントも新規作成/更新/削除されるようになっています。(ここら辺はRailsの[elasticsearch-rails](https://github.com/elastic/elasticsearch-rails)の挙動を参考にしています)

settingsやmappingsに関しては多言語対応が行われています。現状では日本語と英語に対応しており、日本語用に`media_sample_ja`、英語用に`media_sample_en`というインデックスが生成されます。(記事モデル更新時のlocaleに応じて、更新するインデックスは自動判定されます)

Amazon ESへの、リソースベースのアクセスポリシーに関しては[こちら](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-createupdatedomains.html)の情報等を参考にして設定してみてください。

# 例

上記サンプルは、Amazon ESではない普通のElasticseachでも使用可能です。ローカルMac上にElasticsearchのノードを構築して、上記サンプルを使用してインデックスを作成し、例えばkuromojiプラグインが正常に動作しているかを判断するためのコマンドは下記のようになります。

```bash
curl -XGET 'http://localhost:9200/media_sample_ja/_analyze?pretty=true&analyzer=kuromoji_analyzer' \
-d '舛添都知事がヤフオクでいろいろ購入'
```

上記コマンドの結果は下記のようになります。

```bash
{
  "tokens" : [ {
    "token" : "舛添",
    "start_offset" : 0,
    "end_offset" : 2,
    "type" : "word",
    "position" : 0
  }, {
    "token" : "都知事",
    "start_offset" : 2,
    "end_offset" : 5,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "ヤフオク",
    "start_offset" : 6,
    "end_offset" : 10,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "いろいろ",
    "start_offset" : 11,
    "end_offset" : 15,
    "type" : "word",
    "position" : 5
  }, {
    "token" : "購入",
    "start_offset" : 15,
    "end_offset" : 17,
    "type" : "word",
    "position" : 6
  } ]
}
```

# まとめ

Amazon ESが「セキュリティグループによるアクセスポリシーの設定に対応していない」のは結構困るというか署名が面倒なので何とかして欲しいところです。なぜRDSのようにVPC上に構築出来ないのかちょっと理解出来ませんが、早いとこ対応してほしいなと。

また、Elixirのパッケージは、上記のように初歩的な不具合が残っている場合が結構多い＆作者の皆さんもお忙しいのでIssueを立ててもなかなか対応して頂けない場合が多いので、Issue立てるよりも自分でがんがんプルリクを送ってしまう方が良さそうだなあという印象です。