パワーワードが作れる？ランダム単語合成アプリを作ってみた

# はじめに
クリックするだけ！ランダムに2つの単語を「反応」させて、パワーワードを作りましょう！

![rand.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/df038918-b418-b274-9be1-20f82fcad614.gif)

モードは

- 形容詞＋名詞
- 副詞＋動詞
- 名詞＋助詞＋動詞

の3種類です！

![phrases.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/90a32f70-1533-99a7-3fec-3f38f772eb53.gif)

# ソースコード

[GitHub - Syuparn/LiteralReaction: ランダムに単語を「反応」させて、パワーワードを作ろう](https://github.com/Syuparn/LiteralReaction)

（DockerやVue.jsをはじめて使ったので、汚いところもあると思います…ツッコミ、修正歓迎です）

Webアプリの形で作りましたが、web上では公開していません。
（単語が本当にランダムなので、暴言や不謹慎な表現も生成されるかもしれないからです…）

試したい方は上記リンクからダウンロードしてください。docker-composeを使用しているので、

```
$ docker-compose -f docker-compose.prod.yml up -d
```

でコンテナを起動すれば`localhost:8080`からアクセスできます。

# webアプリケーションの構成
WebサーバとAPIサーバの2段構成になっています。

<img width="630" alt="network.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/334884f3-dbed-78ad-6b8b-95ce2d9fedc1.png">

WebサーバはUI（html）、APIサーバは単語情報(JSON)をやりとりします。

コンテナもWebサーバとAPIサーバの2つに分けています。
（UIと単語生成を疎結合にして修正しやすくするためです）

<img width="560" alt="containers.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/4934a8d7-d993-dd52-9fb6-393d31cbe3a3.png">

```yml:docker-compose.prod.yml
version: '3'
services:
  apiserver:
    build:
      context: ./apiserver/
      dockerfile: Dockerfile
    ports:
      - "127.0.0.1:5050:5050" # host:container
    expose:
      - "5050" # port for other containers (not host)
  vue-frontend:
    build:
      dockerfile: Dockerfile
      context: ./vue-frontend
    ports:
      - "127.0.0.1:8080:80" # host:container
```

ただし、ユーザーから見ても別々のURLになってしまうと不格好なので、Webサーバの`/api/`ではじまるURLにリクエストされた場合はAPIサーバに転送するようにしています。

```nginx:vue-frontend/nginx_config/default.conf
server {
    listen 80;
    server_name localhost;
    server_tokens off;

    location / {
    	# デフォルトではhtmlを返す
        root /usr/share/nginx/html;
        try_files $uri $uri/ @dynamic;
    }

    location /api/ {
    	# URLが"/api/"ではじまる場合のみ、APIサーバに転送
        # URLのうち"/api/"に続く部分のみを取り出す
        rewrite /api/(.*) /$1 break;
        # docker-composeの機能によって、exposeされた他コンテナのポートはアプリケーション名でアクセス可能
        proxy_pass http://apiserver:5050;
        # redirectを無効化（既にrewriteでリダイレクトを設定しているため）
        proxy_redirect off;
        proxy_set_header Host $host;
    }
}
```

単語情報GETの流れは以下の通りです。（例は「形容詞＋名詞」の場合）

1. クライアントは、単語生成（「反応」）ページを開いたときに、形容詞を`localhost:8080/api/rand/adjective`へGETリクエスト
2. WebサーバがリクエストをAPIサーバ`http://apiserver:5050/rand/adjective`へ転送
3. APIサーバがリクエストに基づきランダムな形容詞1つをJSON形式で（Webサーバに）返す
4. Webサーバはレスポンスをクライアントへ転送
5. クライアントに形容詞データのJSONが届く
6. 名詞についても同様

## 単語データ生成
「MeCab IPADIC」を使用しました。

MeCabは、文章を単語（正確には形態素、ことばの最小単位）ごとに分かち書きするソフトウェアです。

そして、MeCabが参照する形態素の辞書の1つがIPADICです。

品詞ごとにcsvファイルで書かれていて、各行に各形態素の表記、読み、品詞、活用などが格納されています。

```:Adj.csv
あらっぽい,19,19,6956,形容詞,自立,*,*,形容詞・アウオ段,基本形,あらっぽい,アラッポイ,アラッポイ
あらっぽし,23,23,6956,形容詞,自立,*,*,形容詞・アウオ段,文語基本形,あらっぽい,アラッポシ,アラッポシ
あらっぽから,27,27,6956,形容詞,自立,*,*,形容詞・アウオ段,未然ヌ接続,あらっぽい,アラッポカラ,アラッポカラ
...
```

このアプリの単語合成モードは

- 形容詞（終止形）＋名詞
- 副詞＋動詞（終止形）
- 名詞＋（助詞）＋動詞（終止形）

の3種類です。
この形式で最大限単語を利用するため、（文法的用語には少し不正確ですが）以下のルールで単語を抽出しました。

```bash:apiserver/db/format-ipadic-csv.bash
# 形容詞として利用

# 形容詞終止形
cat $PATH_FROM/Adj.csv       | awk -F"," '$10~/^基本形$/ {print $1}' >  $PATH_TO/adj.txt
# (「な」を付けると形容詞になる名詞)+「な」
cat $PATH_FROM/Noun.adjv.csv | awk -F"," '{print $1 "な"}'         >> $PATH_TO/adj.txt
# (「ない」を付けると形容詞になる名詞)+「ない」
cat $PATH_FROM/Noun.nai.csv  | awk -F"," '{print $1 "ない"}'       >> $PATH_TO/adj.txt

# 名詞として利用
# 「ナンセンスさ」を出したいので人名、地名、専門用語は除外し、一般名詞だけ使用

# 名詞
cat $PATH_FROM/Noun.csv          | awk -F"," '{print $1}' >  $PATH_TO/noun.txt
# 「する」を付けると動詞になる名詞
cat $PATH_FROM/Noun.verbal.csv   | awk -F"," '{print $1}' >> $PATH_TO/noun.txt
# 「な」を付けると形容詞になる名詞
cat $PATH_FROM/Noun.adverbal.csv | awk -F"," '{print $1}' >> $PATH_TO/noun.txt
# 「ない」を付けると形容詞になる名詞は単体で使うと不自然なので未使用)

# 副詞として使用

# 副詞
cat $PATH_FROM/Adverb.csv        | awk -F"," '{print $1}'                    >  $PATH_TO/adverb.txt
# 副詞としても使える名詞（「毎日」等）
cat $PATH_FROM/Noun.adverbal.csv | awk -F"," '{print $1}'                    >> $PATH_TO/adverb.txt
## 形容詞連用形（小さい「っ」で終わるものは動詞が後続できないので削除）
cat $PATH_FROM/Adj.csv           | awk -F"," '$10~/^連用テ接続$/ {print $1}' \
                                 | awk       '!/っ$/'                        >> $PATH_TO/adverb.txt

# 動詞として利用

# 動詞
cat $PATH_FROM/Verb.csv        | awk -F"," '$10~/^基本形$/ {print $1}'        >  $PATH_TO/verb.txt
# (「する」を付けると動詞になる名詞)+「する」
cat $PATH_FROM/Noun.verbal.csv | awk -F"," '{print $1 "する"}'                >> $PATH_TO/verb.txt
```

上記で生成したcsvをSQLiteに流し込んだものを、単語データベースとして使用しました。

```bash:apiserver/db/csv2sqlite.bash
sqlite3 $SQL_PATH/$SQL_NAME << EOS

/* create tables */
.read ./db/init.sql

/* let col separator "," */
.separator ','

/* import word data csvs */
.mode csv
.import $CSV_PATH/adj.csv adjectives
.import $CSV_PATH/adverb.csv adverbs
.import $CSV_PATH/noun.csv nouns
.import $CSV_PATH/verb.csv verbs
EOS
```

```sql:apiserver/db/init.sql
drop table if exists adjectives;
create table adjectives (
    id integer primary key,
    word text
);

drop table if exists adverbs;
create table adverbs (
    id integer primary key,
    word text
);

drop table if exists nouns;
create table nouns (
    id integer primary key,
    word text
);

drop table if exists verbs;
create table verbs (
    id integer primary key,
    word text
);
```

# （個人的）面白かった生成結果

<img width="925" alt="Screenshot from 2020-03-14 23-17-22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/5f2ff952-1355-028f-9317-4a6f7de9af4a.png">
私たちがコードを書けるのは、数学様のおかげでございます。

<img width="924" alt="Screenshot from 2020-03-14 22-43-34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/587b96e8-b2b2-60b1-fbac-7cf403929149.png">
ウケ狙いちゃうわ！

<img width="923" alt="Screenshot from 2020-03-14 22-08-33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/30a7ea31-4f0c-00e5-4eb3-beaa99de72e0.png">
こんなアプリ作っちゃうくらいだからしょうがないね

![Screenshot from 2020-03-14 22-42-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/391a8728-ef29-d1de-6b14-72ad7b414284.png)
文章が成立している！

意外とフレーズが成立する割合が高く（体感100回に1回くらい？）、他にも以下のような「まともな」フレーズが生成されました。

```
居場所を失う
酷い蛇行
自我を失う
泥棒を訴える
移り気なロマンチスト
やさしいインターフェース
```

# はまったところ
## docker-compose buildできない
`docker-compose`は`apt install`でインストールすると古いバージョンが入ってしまいます…

そして、古いバージョンのdocker-composeでバージョン3の`docker-compose.yml`を使うとビルドに失敗します。

`apt install`ではなく`curl`で最新バージョンをダウンロードしましょう

[Install Docker Compose \| Docker Documentation](https://docs.docker.com/compose/install/)

（公式ドキュメントはちゃんと読まねば…反省）

## go buildできない
Go1.13からはライブラリ管理の方法が変わったため、最初に`go mod init`をする必要があります。

このコマンドを打つと、モジュールの依存関係ファイル`go.mod`やモジュールが本物か確かめるためのチェックサム`go.sum`が生成されます。

[Go Modules - Qiita](https://qiita.com/propella/items/e49bccc88f3cc2407745)

[Go 1.13 に向けて知っておきたい Go Modules とそれを取り巻くエコシステム - blog.syfm](https://syfm.hatenablog.com/entry/2019/08/10/170730)

その代わり、`go.mod`のおかげでライブラリを手動で`go get`する必要がなくなりました！
（`go build`時に自動でダウンロードされる）

## コンテナ作成中に生成した実行ファイルが消える
`volumes`と`COPY`を混同していました。

`Dockerfile`中で`COPY`すると、コンテナの**ビルド時**にホストのディレクトリをコピーします。

`docker-compose.yml`中で`volumes`を指定すると、コンテナの**起動時**にコンテナのディレクトリにホストのディレクトリをマウントします。

そのため、ホストのソースを`COPY`してせっかく実行ファイルをビルドしても、同じディレクトリを`volumes`に指定するとホストのディレクトリ（もちろん実行ファイルは無い）に隠されてしまいます…

[docker-composeのvolumesで指定したホストのディレクトリがマウントされずハマった](https://kuroeveryday.blogspot.com/2016/11/docker-compose-volumes.html)

## conic-gradientが使えない
単語生成画面の集中線はCSSの`conic-gradient`関数を使用する予定でした

…が、この関数はFirefoxでは非対応です。

[conic-gradient() - CSS: カスケーディングスタイルシート \| MDN](https://developer.mozilla.org/ja/docs/Web/CSS/conic-gradient)

そこで、同名のnpmパッケージを使ってjs側で模様を生成しました。

[conic-gradient - npm](https://www.npmjs.com/package/conic-gradient)

しかし、このパッケージはES5で書かれているのでVue CLI内部で`import`できません。

結局、

1. `index.html`で直接グローバルに読み込む
2. Vue側では`window.ConicGradient`の形で呼び出す
3. `npm run build`ではビルドされないので、別途モジュールをstaticにコピーする

という手順を取りました。

```html:vue-frontend/index.html
<!DOCTYPE html>
<html>
...
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <!-- ここでモジュールを読み込む(prefixfreeはconic-gradientの依存モジュール) -->
    <script src="./static/prefixfree.min.js"></script>
    <script src="./static/conic-gradient.js"></script>
  </body>
</html>
```

```vue:vue-frontend/src/components/bangBackground.vue
<script>
export default {
  name: 'bangBackground',
  data: function () {
    return {
      bangSVG: new window.ConicGradient({
        repeating: true,
        stops: `#ffffff 0,
                #ffffff 2.0%,
                #AAAAAA 2.125%,
                #AAAAAA 2.375%,
                #ffffff 2.5%`
      })
    }
  }
}
</script>
```

```docker:vue-frontend/Dockerfile
FROM node:lts-alpine as builder

RUN mkdir -p /app
WORKDIR /app

RUN npm install -g http-server

COPY ./package*.json ./
RUN npm install \
    # conic-gradientモジュールをstaticにコピー
    && mkdir -p static/ \
    && cp node_modules/conic-gradient/conic-gradient.js static/ \
    && cp node_modules/prefixfree/prefixfree.min.js static/

COPY . .
RUN npm run build

FROM nginx:stable-alpine as product

COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx_config/default.conf /etc/nginx/conf.d/default.conf

CMD ["nginx", "-g", "daemon off;"]
```

# 参考文献
## Go言語
APIサーバ
[golangでREST APIをやってみた① - Qiita](https://qiita.com/katekichi/items/d94e078b376151858ca4)

SQLiteとの接続
[golangでSQLite3を使ってデータベースを操作する方法まとめ \| Black Everyday Company](https://kuroeveryday.blogspot.com/2017/08/sqlite3-with-golang.html)

## MeCab
IPADICダウンロード方法
[Alpine LinuxでMeCab with NEologd - Qiita](https://qiita.com/nownabe/items/4171776aec1f05de9f28)

## Vue.js
SPA(シングルページアプリケーション)のつくり方
[【Vue.js】爆速でSPAを作る - Qiita](https://qiita.com/nagimaruxxx/items/8fc59a5ca05bb91bfe1f)

axiosを使ったJSONのGET/POST
[Vue-CLIのプロジェクトでaxiosを使ってAPIや外部リソースからのデータを取得する \| 大阪市天王寺区SOHOホームページ制作 | デザインサプライ-DesignSupply.-](https://designsupply-web.com/knowledgeside/5183/)

コンポーネント内部に要素を入れる(公式)
[スロット — Vue.js](https://jp.vuejs.org/v2/guide/components-slots.html#%E5%90%8D%E5%89%8D%E4%BB%98%E3%81%8D%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88)

## nginx
URLの一部分だけ取り出してポートフォワーディング(`/api/hoge -> apiserver:5050/hoge`)
[Nginx reverse proxy + URL rewrite - Server Fault](https://serverfault.com/questions/379675/nginx-reverse-proxy-url-rewrite)

## Docker,docker-compose
docker-compose基本操作
[docker-compose コマンドまとめ - Qiita](https://qiita.com/wasanx25/items/d47caf37b79e855af95f)

複数サーバ（複数コンテナ）を協調させる方法
[マイクロサービスほどじゃないけどウェブサービスを分割開発したい人向けDocker設定を集めるスレ - Qiita](https://qiita.com/shibukawa/items/fd49f98736045789ffc3)

vue-cliビルド方法(公式)
[Vue.js アプリケーションを Docker 化する — Vue.js](https://jp.vuejs.org/v2/cookbook/dockerize-vuejs-app.html)

Golangマルチステージビルド方法

（マルチステージビルドのメリット）
[Dockerのマルチステージビルドを使う - Qiita](https://qiita.com/carimatics/items/01663d32bf9983cfbcfe)
（Go1.7の記事ですが、Go1.13以上の場合Dockerfileで`go get`をする必要はありません）

（go-sqlite3ライブラリを使う場合）
[go-sqlite3 が入った状態での Docker のマルチステージビルドを行う - Pistatium note](https://scrapbox.io/kimihiro-n/go-sqlite3_%E3%81%8C%E5%85%A5%E3%81%A3%E3%81%9F%E7%8A%B6%E6%85%8B%E3%81%A7%E3%81%AE_Docker_%E3%81%AE%E3%83%9E%E3%83%AB%E3%83%81%E3%82%B9%E3%83%86%E3%83%BC%E3%82%B8%E3%83%93%E3%83%AB%E3%83%89%E3%82%92%E8%A1%8C%E3%81%86)