Luminus で GCE 上の API を叩いてみる

## シリーズバックナンバー
1. [何も考えずに Emacs を使って Clojure の Luminus webframework を使う](https://qiita.com/MeguruMokke/items/fbd1e7ff753702195895)
2. [Luminus のサーバ側で手っ取り早くAPIを試したいメモ書き](https://qiita.com/MeguruMokke/items/cda60091ca09fa481450)
3. [Luminus で re-frame 、ping-pong ボタンを追加してみるメモ書き](https://qiita.com/MeguruMokke/items/0ae124ab4d507667fbab)
4. [Luminus で GCE 上の API を叩いてみる](https://qiita.com/MeguruMokke/items/da6948eafac6f99d539c)

# GCEで良い感じにAPIが建った！取り敢えず curl しようぜ！
GCE (Google Cloud Engine) でGPU付きの仮想マシンを借り、cuda 9.0 なんかの設定を(~~~当時手っ取り早く Tensorflow を動かすためにはなんとかして 9.0 をインストールする必要があった。おのれTensorflow~~~)済ませる下処理1、良い感じに Nginx や gunicorn、 supervisor やらを設定する下処理２をし、良い感じにAPIが立ったものとします（中身の公開はもう少し待ってね）。

ちなみに借りた環境はこんな感じの構成になっています。よわよわですが、学習をさせたいわけじゃないので、まあいいかなという感じです。


- マシンタイプ : n1-highmem-8（vCPU x 8、メモリ 52 GB）
- GPU : 1 x NVIDIA Tesla P100
- zone : us-east1-b
- ファイアウォール : http トラフィックを許可（これがないと多分HTTPリクエスト通りません） 
- ストレージ : SSD 50GB (外部ストレージは速度的にどうなのか不安だった（~~~あとよくわかんなかった~~~）ので、モデルファイルはここに入れました。) 
- 外部IP : 34.73.35.132 (固定していないので再起動するごとに変わります。)

するとAPIはこんな感じで curl でアクセスする事ができるようになります。(jq を通しているのは UTF-8 文字を可視化するためと、JSONを見やすくするためです。)

### 一つ目のAPI
docomo の雑談対話APIみたいなことをやります。入力文に対応する出力文を作って返してきます。雑に作っただけあって適当な答えが返ってきます。

```shell
curl -H "Content-type: application/json" -X GET -d '{"input": "付き合って下さい"}' http://34.73.35.132/translate | jq
# =>
# {
#   "Content-Type": "application/json",
#   "input": "付き合って下さい",
#   "request-date": "None",
#   "turn": "どうしようかな。",
#   "user-id": "None"
# }
```


### 二つ目のAPI
文を良い感じにくだけさせます。特に語尾変化なんかをよく学習しています（多分）。

```shell
curl -H "Content-type: application/json" -X GET -d '{"input": "付き合って下さい"}' http://34.73.35.132/transform | jq

# =>
# {
#  "Content-Type": "application/json",
#  "input": "付き合って下さい",
#  "request-date": "None",
#  "turn": "付き合って",
#  "user-id": "None"
#}
```

### 三つ目のAPI
任意の入力文に対してそれがいくつかのイベントトリガーになる文であるかを判定し、その確率を返します。
イベントトリガーとなる文として今回は30文用意し、その類似文を幾つか用意してそれの要約統計を取ることで確率としています。
<img src="https://qiita-image-store.s3.amazonaws.com/0/147520/c58c5a6d-3f7b-7514-43d1-e93deb55c33b.png" width=50%>

```shell
curl -H "Content-type: application/json" -X GET -d '{"input": "注文お願いします。"}' http://34.73.35.132/detect-class | jq

# {
#   "Content-Type": "application/json",
#   "input": "注文お願いします。",
#   "request-date": "None",
#   "turn": {
#     "label": 26,
#     "percentage": 71.32364147109911,
#     "represent": "注文して良いですか？",
#     "sort": [
#       [
#         20,
#         0.0017914147465489805
#       ],
#       [
#         7,
#         0.0017973725334741175
#       ],
#      ...
#       [
#         29,
#         0.15609956318179943
#       ],
#       [
#         26,
#         0.7132364147109911
#       ]
#      ]
#   },
#   "user-id": "None"
# }
```

# Web サーバで curl の再現をしよう！
curl で出来たことを Luminus からやってみようということです。

```clojure:{project-name}/src/clj/{project-name}/routes/services.cljに追記
(ns demo-app.routes.services
  (:require [reitit.swagger :as swagger]
            [reitit.swagger-ui :as swagger-ui]
            ;; ...
            [ring.util.http-response :refer :all]
            [clojure.java.io :as io]
            ;;;
            [clj-http.client :as client]
            ;;;
            ))
;;;
(def gce-global-ip "http://34.73.35.132")


(def my-api-routes
  ["/sample-api"
   {:swagger {:tags ["sample-api"]}}
   ["/get-translate"
    {:post {:summary "get a translated (1-by-1 communication) sentence"
            :parameters {:body {:uttr string?}}
            :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                       (let [response (client/get
                                       (str gce-global-ip "/translate")
                                       {:content-type :json
                                        :as :json
                                        :form-params {:input uttr}
                                        })]
                         {:status 200
                          :body (:body response)}))
            }}]
   ["/get-transform"
    {:post {:summary "get a transformed sentence"
            :parameters {:body {:uttr string?}}
            :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                       (let [response (client/get
                                       (str gce-global-ip "/transform")
                                       {:content-type :json
                                        :as :json
                                        :form-params {:input uttr}})]
                         {:status 200
                          :body (:body response)}))}}]
   ["/get-class"
    {:post {:summary "get a transformed sentence"
            :parameters {:body {:uttr string?}}
            :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                       (let [response (client/get
                                       (str gce-global-ip "/detect-class")
                                       {:content-type :json
                                        :as :json
                                        :socket-timeout 10000
                                        :form-params {:input uttr}})]
                         {:status 200
                          :body (-> response
                                    :body
                                    (update-in  [:turn] dissoc :sort))}))}}]
   ])
;;;

(defn service-routes []
  ["/api"
   {:coercion spec-coercion/coercion
    :muuntaja formats/instance
    :swagger {:id ::api}
    :middleware [;; query-params & form-params
                 parameters/parameters-middleware
                 ;; ...
                 ;; multipart
                 multipart/multipart-middleware]}

   ;; swagger documentation
   ["" {:no-doc true
        :swagger {:info {:title "my-api"
                         :description "https://cljdoc.org/d/metosin/reitit"}}}

    ["/swagger.json"
     {:get (swagger/create-swagger-handler)}]

    ["/api-docs/*"
     {:get (swagger-ui/create-swagger-ui-handler
             {:url "/api/swagger.json"
              :config {:validator-url nil}})}]]
   ;;;
   my-api-routes
   ;;;

   ["/ping"
    {:get (constantly (ok {:message "pong"}))}]
   

   ["/math"
    {:swagger {:tags ["math"]}}

    ["/plus"
     ;; ...
    ]]

   ["/files"
    {:swagger {:tags ["files"]}}

    ["/upload" 
     ;; ...
    ]

    ["/download"
     ;; ...
     ]]])
```

## 説明
```clojure
(ns demo-app.routes.services
  (:require [reitit.swagger :as swagger]
            [reitit.swagger-ui :as swagger-ui]
            ;; ...
            [ring.util.http-response :refer :all]
            [clojure.java.io :as io]
            ;;;
            [clj-http.client :as client]
            ;;;
            ))
```
依存関係に ```clj-http.client``` を追加しています。これは clojure で http 通信を行うためのライブラリ ```clj-http``` の ```client``` クラスです。今回は後述するように、このクラスの ```get``` 関数を使っています。

```clojure
(def gce-global-ip "http://34.73.35.132")
```
GCE から与えられるグローバルIPアドレスです。静的なものではないので、サーバを再起動するたびに書き換える必要があります。（~~そして恐らくGCEの運営側はそれなりの料金を払って静的グローバルアドレスを取得してこの部分を書き換えることを期待しています。~~）

```clojure
(def my-api-routes
  ["/sample-api"
   {:swagger {:tags ["sample-api"]}}
   ["/get-translate"
    {:post {:summary "get a translated (1-by-1 communication) sentence"
            :parameters {:body {:uttr string?}}
            :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                       (let [response (client/get
                                       (str gce-global-ip "/translate")
                                       {:content-type :json
                                        :as :json
                                        :form-params {:input uttr}})]
                         {:status 200
                          :body (:body response)}))
            }}]
   ["/get-transform"
    {:post {:summary "get a transformed sentence"
            :parameters {:body {:uttr string?}}
            :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                       (let [response (client/get
                                       (str gce-global-ip "/transform")
                                       {:content-type :json
                                        :as :json
                                        :form-params {:input uttr}})]
                         {:status 200
                          :body (:body response)}))}}]
   ["/get-class"
    {:post {:summary "get a transformed sentence"
            :parameters {:body {:uttr string?}}
            :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                       (let [response (client/get
                                       (str gce-global-ip "/detect-class")
                                       {:content-type :json
                                        :as :json
                                        :socket-timeout 10000
                                        :form-params {:input uttr}})]
                         {:status 200
                          :body (-> response
                                    :body
                                    (update-in  [:turn] dissoc :sort))}))}}]
   ])
```
ちょっと長いですね。しかしやっていることは対して難しいものではないです。
```[]``` のネストを考えると、```/sample-api``` の下に ```/get-translate``` などが含まれていることがわかると思います。これはそのままURLの階層構造と見なすことができます。つまり、get-translate にアクセスしたいならば ```(localhost:3000/api)/sample-api/get-translate``` を指定すれば良いということです。
さて、まず ```get-translate``` を見てみましょう。

```clojure
["/get-translate"
 {:post {:summary "get a translated (1-by-1 communication) sentence"
         :parameters {:body {:uttr string?}}
         :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                    (let [response (client/get
                                    (str gce-global-ip "/translate")
                                    {:content-type :json
                                     :as :json
                                     :form-params {:input uttr}})]
                      {:status 200
                       :body (:body response)}))
         }}]
```
```:post``` とは即ちpost method を示しています。この値としては ```:summary``` ```:parameters``` ```:handler``` のキーを持ったマップ構造になっています。
```:summary``` はこの method の説明を記述します。 
```:parameters``` は引き取る値のヒントを記述します。ここでは、body 要素の中の uttr に 文字列型の値を期待しています。
```:handler``` はこの method が呼ばれた時に処理されるハンドラを記述します。ここでは、クライアントから送られてきたデータから ```:parameter``` 要素を取り出し、その中の ```:body``` 要素にある ```:uttr``` 要素を取り出します。
簡単に言うと、```{:parameters {:body {:uttr data}}}``` の ```data``` を取り出していることになります。
そしてその ```data``` を用いて機械学習サーバに問い合わせを行います。この部分は後述します。
最後に得られたレスポンスの body を取り出し、```:status 200``` を付けてクライアントへ送るデータとしてまとめます。

```clojure
(client/get
  (str gce-global-ip "/translate")
  {:content-type :json
   :as :json
   :form-params {:input uttr}})
```

これが curl を書き換えている部分です。ここで注目するのは ```:as :json``` の部分と ```:form-params``` の部分です。
 ```:as :json``` は json データがサーバからかえってくる事を補足し、受け取った際にこれを clojure のマップ構造に置換できるようにしています。
```:form-params``` は ```:content-type :json``` が指定されているとき、値である マップ構造を json 形式にエンコードし、リクエストの body 要素とします。(```:content-type :json``` が指定されていなければこれは url エンコードされて body 要素になるようです。)

他の２つも同様ですが、最後の ```get-class``` のハンドラがちょっと特殊なのでその部分だけ触れます。

```clojure
["/get-class"
    {:post {:summary "get a transformed sentence"
            :parameters {:body {:uttr string?}}
            :handler (fn [{{{:keys [uttr]} :body} :parameters}]
                       (let [response (client/get
                                       (str gce-global-ip "/detect-class")
                                       {:content-type :json
                                        :as :json
                                        :socket-timeout 10000
                                        :form-params {:input uttr}})]
                         {:status 200
                          :body (-> response
                                    :body
                                    (update-in  [:turn] dissoc :sort))}))}}]
```

```:socket-timeout``` を指定しているのはこの機械学習APIが結構時間がかかってしまうためで、10秒経っても結果が返ってこなければその旨を示すエラー出すようにしています。
最後の行周辺の Threading macro は、```:turn``` 要素の ```:sort``` 要素を削除する処理を行っています。これは ```:sort``` 部がちょっと量が多いのでクライアントに見せる必要がないかな、と思いまして省略する意図で書いています。 

# Swagger で動いているのを見せれば十分やろ
```localhost:3000/swagger-ui``` にアクセスすると良い感じの画面が見えます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/147520/d8f52c38-0acd-d1d2-e824-8228b6ca6584.png" width=50%>

適当にぽちぽちと入力して ```Try it out!``` を押すとサーバから出力を得ることができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/147520/ffa3d460-c8b3-bd0e-b98a-e5ec9b964112.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/147520/d343cc48-9c2c-4004-0ddd-f3c7717f3d71.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/147520/ad2fc38f-7ff5-b376-7950-dfac9bc8bf0d.png)

## ちょっとした愚痴
~~ところでこのAPIの中身、卒業研究で行ったものなんですが、すこぶる受けが悪くて私悲しい...
やっぱり画像認識の方が学術的に受けるんですかね？~~