AWS Lambda で MeCab + NEologd 辞書を Node.js で動かす

この記事は[Nextremer Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nextremer)の2日目の記事です。

表題の通り、[mecab-ipadic-NEologd](https://github.com/neologd/mecab-ipadic-neologd) を AWS Lambda で使えるようにしたという内容です。

# 困難
- NEologd 辞書が標準で 900 MB 程度ある
- Lambda にはデプロイパッケージを zip で上げることが可能であるが、unzip 後、250 MB 以下でなければいけないという制限がある
- Lambda には実行時、一時ファイル領域(/tmp)が用意されているが、容量 512 MB までである
- NEologd を縮小版(`--eliminate-redundant-entry` オプション)で生成しても 400 MB 程度になる

# 既存のソリューション: s3 に辞書を置いておく
こちらに、s3 使った方法がまとまっています。
https://speakerdeck.com/satorukadowaki/aws-apigateway-plus-python-lambda-plus-neologddezuo-rusabaresuri-ben-yu-xing-tai-su-jie-xi-api

が、この記事では違う方法を取ります。

# この記事でのやりかた
縮小版 neologd 辞書を zip で固めると 100MB 以下になるため、デプロイパッケージに含めることができると考えました。

## 手順
デプロイパッケージは[Amazon が公開している Lambda 用の AMI](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/current-supported-versions.html)上で作ります。
(この記事執筆時点で `amzn-ami-hvm-2017.03.1.20170812-x86_64-gp2` を使いました。)
EC2 インスタンスを上記の AMI で立ち上げます。
以下は EC2 インスタンス上での作業になります。

いろいろインストール。

``` sh
$ sudo yum install gcc gcc-c++ git patch
$ sudo rpm -ivh http://packages.groonga.org/centos/groonga-release-1.1.0-1.noarch.rpm
# 以下は NEologd インストール時に必要
$ sudo yum install perl-App-cpanminus
$ sudo cpanm autodie
```

MeCab をビルド。

``` sh
$ mkdir ~/mecab-service
$ curl -L "https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7cENtOXlicTFaRUE" -o mecab-0.996.tar.gz
$ tar zxvf mecab-0.996.tar.gz
$ cd mecab-0.996
$ ./configure --prefix=$HOME/mecab-service/local --enable-utf8-only
$ make
$ make install
```

mecab-ipadic もビルド。

``` sh
$ cd ~
$ curl -L "https://drive.google.com/uc?export=download&id=0B4y35FiV1wh7MWVlSDBCSXZMTXM" -o mecab-ipadic-2.7.0-20070801.tar.gz
$ tar zxvf mecab-ipadic-2.7.0-20070801.tar.gz
$ cd mecab-ipadic-2.7.0-20070801
$ export PATH=$HOME/mecab-service/local/bin:$PATH
$ export LD_LIBRARY_PATH=$HOME/mecab-service/local/lib:$LD_LIBRARY_PATH
$ ./configure --prefix=$HOME/mecab-service/local --enable-utf8-only --with-charset=utf8
$ make
$ make install
```

mecab-ipadic-NEologd をビルド。

``` sh
$ cd ~
$ git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git
$ cd mecab-ipadic-neologd
$ ./bin/install-mecab-ipadic-neologd -y -p $HOME/neologd -n --eliminate-redundant-entry
```

ここで、正しくビルドできたかチェック。

```
$ echo 東京スカイツリー | ~/mecab-service/local/bin/mecab -d ~/neologd
東京スカイツリー	名詞,固有名詞,一般,*,*,*,東京スカイツリー,トウキョウスカイツリー,トーキョースカイツリー
EOS
```

必要なファイルを zip 化します。

``` sh
$ cd ..
$ zip -r mecab-service/neologd.zip neologd
$ zip -r ~/mecab-service.zip mecab-service
```

ここからローカルでの作業になります。
`mecab-service.zip` を scp でローカルに落としてきてください。
これで EC2 は不要になります。

`mecab-service.zip` を解凍してください。

解凍した `mecab-service` フォルダに `index.js` を追加します。内容は以下の通り。

``` javascript
const exec = require('child_process').exec;
const MeCab = new require('mecab-async');
const fs = require('fs');

const mecab = new MeCab();
mecab.command = '/var/task/lambda_neologd/local/bin/mecab -d /tmp/neologd/';

exports.handler = async (event) => {
  try {
    fs.accessSync('/tmp/neologd');
  } catch(e) {
    await new Promise((resolve, reject) => {
      exec('unzip -d /tmp/ /var/task/lambda_neologd/neologd.zip', (err, stdout, stderr) => {
        if (err) {
          console.error(err);
          reject(err);
        }
        console.log(stdout);
        resolve(stdout);
      });
    });
  }
  const res = mecab.parseSync(normalize("僕の名前はラルセイです。"));
    const response = {
        statusCode: 200,
        body: JSON.stringify(['Hello', res])
    };
    return response;
};

function normalize(text) {
  return text.normalize("NFKC").toLowerCase();
}
```

そして再び `mecab-service` を `mecab-service.zip` に圧縮してください。
zip を s3 へ上げます。
AWS Lambda コンソールで新しい関数を追加します。ランタイムは `Node.js 8.10` を指定。
関数コードとして s3 上に上げた `mecab-service.zip` を指定して保存します。

環境変数を以下のように設定。

```
LD_LIBRARY_PATH=/var/task/local/lib
MECABRC=/var/task/local/etc/mecabrc
```

タイムアウトを10秒以上に設定してください。

保存してテストすると、5秒くらい経って成功すると思います。
一度実行すれば、`/tmp/` に辞書が展開されるので、2度目からは早いです。

# おわりに
これで、s3 に辞書置かずに NEologd を使うことができました。
s3 からの転送は8秒程かかり、unzip は5秒かかるので若干のアドバンテージがあります。といっても、時間がかかるのは初回呼び出しだけですが…。

あと、`--eliminate-redundant-entry` オプションで辞書を縮小しているので、表記ゆれが吸収されません。MeCab に掛ける前にテキストの正規化が必要なことに注意してください。
このように NFKC 正規化と小文字化が必要です。

``` js
text.normalize("NFKC").toLowerCase();
```
Lambda で動かす以上、こればかりはどうしようも無いですね…。