(この記事は、fukuoka.ex(その2) Elixir Advent Calendar 2017 - Adventarの19日目、自然言語処理 Advent Calendar 2017 - Qiita の16日目です)

昨日は@zacky1972さんの「ZEAM開発ログv0.1.6.1 Elixir / Rustler 小ネタ集〜 Rustler でリストからバイナリに変換」でした!

本題

前回の記事「Phoenix + Vue.js入門」ではPhoenix + Vueの構成で、DBに保存したレコードを画面表示するところまでを書きました。

今回は、PhoenixからMicrosoft Translator テキスト APIを叩いて結果を取得するまでを書いてみます。環境は前回記事と同様です。

事前準備

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

keyの発行については以下の記事が参考になりました。

Microsoft Translator テキスト API で、日本語を英語に翻訳するサンプル

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

GitHub - devinus/poison: An incredibly fast, pure Elixir JSON library

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

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

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

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


$ mix deps.get

APIを叩くmoduleを用意する

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

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()にリストを渡せば、日本語→英語へ翻訳した結果が取得できるようにしました。

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

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

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

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

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

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

  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.exrender("article.json")部分に、翻訳結果のフィールドを追記します。

  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側に値が渡るようになりました。渡ってきた値を表示してみましょう。

<div v-for="article in articles">
  <h2>{{ article.title }}({{ article.en_title }})</h2>
  <pre>{{ article.body }}</pre>
  <pre>{{ article.en_body }}</pre>
  <hr>
</div>

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

Hello PhoenixVueExample! 2018-06-07 22-30-29.png

まとめ

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

次回は、@kobatako さんの「Slack botで通知したい投稿日のものを通知する with Qiita API 」です!お楽しみに!