PhoenixでMicrosoft Translator テキスト APIを利用してみる

（この記事は、[fukuoka.ex(その2) Elixir Advent Calendar 2017 - Adventar](https://adventar.org/calendars/2871)の19日目、[自然言語処理 Advent Calendar 2017 - Qiita](https://qiita.com/advent-calendar/2017/nlp) の16日目です)

昨日は@zacky1972さんの「[ZEAM開発ログv0.1.6.1 Elixir / Rustler 小ネタ集〜 Rustler でリストからバイナリに変換](https://qiita.com/zacky1972/items/cab329e03f9fae6c7404)」でした！


## 本題

前回の記事「[Phoenix + Vue.js入門](https://qiita.com/koga1020/items/c02a0fd5ae11fb5da1e0)」ではPhoenix + Vueの構成で、DBに保存したレコードを画面表示するところまでを書きました。

今回は、Phoenixから[Microsoft Translator テキスト API](https://www.microsoft.com/ja-jp/translator/translatorapi.aspx)を叩いて結果を取得するまでを書いてみます。環境は前回記事と同様です。


## 事前準備

事前準備として、azureにサインアップ後、APIを有効にしてkeyを取得する必要があります。

keyの発行については以下の記事が参考になりました。
[Microsoft Translator テキスト API で、日本語を英語に翻訳するサンプル](https://qiita.com/TakeshiNickOsanai/items/a8039ba8d558f7c8a05e)

keyの発行が済んだら、次に進みましょう。


## ゴール

「日本語のタイトル・本文と、それを英語に翻訳したタイトルと本文が画面に表示される」をゴールとします :checkered_flag:	

## httpoisonの導入

PhoenixからAPIを叩くために、Elixir製のHTTP clientであるhttpoisonを導入します。json parserのpoisonも合わせて入れておきましょう。

[GitHub - edgurgel/httpoison: Yet Another HTTP client for Elixir powered by hackney](https://github.com/edgurgel/httpoison)
[GitHub - devinus/poison: An incredibly fast, pure Elixir JSON library](https://github.com/devinus/poison)

mix.exsのapplications, depsに追記します。

```elixir
  def application do
    [
      extra_applications: [:logger, :runtime_tools, :httpoison, :poison]
    ]
  end

  defp deps do
    [
     ...
      {:poison, "~> 3.1"},
      {:httpoison, "~> 1.0"}
    ]
  end
```

変更後、お決まりの`mix deps.get`を実行しておきます。

```bash

$ mix deps.get
```

## APIを叩くmoduleを用意する
APIを叩くコードを書きます。実際にAPIを叩く部分を、`lib/translate_api.ex`モジュールとして切り出してみました。

```elixir
defmodule TranslateApi do
  @moduledoc """
  Microsoft Translate Api Client written by Elixir.
  """
  @translate_api_endpoint "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0"
  @issue_token_endpoint "https://api.cognitive.microsoft.com/sts/v1.0/issueToken"

  @doc """
  get authorization token from the token service.
  """
  def get_token() do
    {:ok, response} =
      HTTPoison.post(@issue_token_endpoint, "{}", [
        {"Ocp-Apim-Subscription-Key", "<API key>"}
      ])

    response.body()
  end

  @doc """
  translate texts by microsoft translate api.

  ## Examples
  translate(["hello", "world"], %{"from" => "en", "to" => "ja"})
  """
  def translate(texts \\ [], params \\ %{"to" => "en", "from" => "ja"}) do
    data = Enum.map(texts, fn text -> %{"Text" => text} end)

    {:ok, %HTTPoison.Response{body: body}} =
      HTTPoison.post(
        @translate_api_endpoint,
        Poison.encode!(data),
        [
          {"Content-Type", "application/json"},
          {"Authorization", "Bearer " <> get_token()}
        ],
        params: params
      )

    Poison.decode!(body)
    |> Enum.map(fn %{"translations" => [%{"text" => text}]} -> text end)
  end
end
```

translate()にリストを渡せば、日本語→英語へ翻訳した結果が取得できるようにしました。

```bash
TranslateApi.translate(["こんにちは", "世界"])
→ ["Hello", "World"]
```

APIの実行にtokenが必要なので、token取得用の関数を用意しました。10分で有効期限が切れるようなのですが、都度叩く必要はないので、そこは今後の課題です(汗)

post値は以下のようなJSONフォーマットにする必要があるので、`data = Enum.map(texts, fn text -> %{"Text" => text} end)`の部分でpost用のデータを作ってます。

```json
[
  {
    "Text" => "翻訳したい文字"
  },
  {
    "Text" => "翻訳したい文字"
  },
  {
    "Text" => "翻訳したい文字"
  },
  ...
]
```

## APIを叩いてレコードの値を翻訳→Viewに渡す

`article_controller.ex`のindex()を修正します。

```elixir
  def index(conn, _params) do
    articles = Blog.list_articles()
    |> Enum.map(fn article ->
      # 翻訳の実行
      [en_title, en_body] = TranslateApi.translate([article.title, article.body])
      
      # 結果をmapに追加
      Map.merge(article, %{en_title: en_title, en_body: en_body})
    end)

    render(conn, "index.json", articles: articles)
  end
```

DBから取得したレコードの各行に対して、タイトルと本文に対して翻訳を実行し、articleのマップに追加しています。
レコード数分リクエストが走っちゃうので、ちゃんと作るなら実装に工夫が必要そうです。

実はこれだけではviewで値は利用できません。`article_view.ex`の`render("article.json")`部分に、翻訳結果のフィールドを追記します。

```elixir
  def render("article.json", %{article: article}) do
    %{id: article.id, title: article.title, body: article.body, en_title: article.en_title, en_body: article.en_body}
  end
```

これで、view側に値が渡るようになりました。渡ってきた値を表示してみましょう。

```vue
<div v-for="article in articles">
  <h2>{{ article.title }}（{{ article.en_title }}）</h2>
  <pre>{{ article.body }}</pre>
  <pre>{{ article.en_body }}</pre>
  <hr>
</div>
```

翻訳した結果が表示できました！

<img width="1422" alt="Hello PhoenixVueExample! 2018-06-07 22-30-29.png" src="https://qiita-image-store.s3.amazonaws.com/0/99116/992d1988-16c3-18f3-8303-824bda27bea5.png">

## まとめ

Microsoft Translator テキスト APIを利用して文字列の翻訳ができました。「外部APIで取得したJSONを、独自のフォーマットに変形する」といった処理はパターンマッチのおかげで書きやすいです♪


次回は、@kobatako さんの「[Slack botで通知したい投稿日のものを通知する with Qiita API](https://qiita.com/kobatako/items/f4c3cb1b21c499f7614e#_reference-4b94dd67514d6b9d7fbd)
」です！お楽しみに！

---------------------------
:stars::stars::stars::stars::stars:<b>　満員御礼！Elixir MeetUpを6月末に開催します　</b>:stars::stars::stars::stars::stars:
<font color="red"><b>※応募多数により、増枠しました</b></font>
[<b>「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」</b>](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します。特別ゲストも迎え、非常に濃い2時間になること間違いなしです！ご興味のある方はぜひご参加ください！



[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)