息子の可愛さを普及するために、AWS + LINEでBotを作った話〜形態素解析導入編〜

我が家の息子が可愛すぎる。

可愛すぎるので、LINE Botを作成し布教したが、少し問題が・・・

# はじめに

[息子の可愛さを普及するために、AWS + LINEでBotを作った話](https://qiita.com/yamachita0109/items/04ecc29504391a332729)の続編となります。元ソースや環境はこちらに記載してあります。

# 問題
こちらをご覧ください。
![Bef.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/3c7b4b38-5ace-8fff-b222-d43ddb50dedc.gif)

「かわいい」と送信した際には、会話ができているが、「かわいいね」や「超！！かわいい！！！」など、「かわいい」という文字列にアドオンされた場合、該当するメッセージがないと判断され、「〜ってなんでちゅか？」と分からないフリをするかわいい。（親バカ）

# 原因

原因となる部分はこちら

```.js
const body = JSON.parse(event.body)
const events = body.events[0]
const message = events.message.text

const param = {
  TableName: 'ReplyMapping',
  Key: {
    type: message ← ここ！！
  }
}
const result = await dynamodb.get(param).promise()
let msg, img
if (result.Item) {
  const getRandomList = (list) => list[Math.floor(Math.random() * list.length)]
  msg = getRandomList(result.Item.msg)
  img = getRandomList(result.Item.img)
} else {
  msg = `${message}ってなんでちゅか？`
  img = ''
}
```

メッセージとtypeが完全一致した時に、DynamoDBからレコードを取得している。
今回の事象は、「かわいい」というレコードは登録されているが、「かわいいね」や「超！！かわいい！！！」といったレコードが登録されていないため発生している。

# 対策

じゃあ、メッセージを形態素解析して、どういった種類のメッセージか。解析するようにしたら良いのでは？

# 実装

[kuromoji.js](https://github.com/takuyaa/kuromoji.js/blob/master/example/load-node.js)を使って実現してみよう。
※ kuromoji.jsとは、javaのオープンソース日本語形態素解析エンジン[kuromoji](https://www.atilika.com/ja/kuromoji/)のjs移植版である。

ラッパークラスを作ってみた。

```KuromojiWrapper.js
'use strict'
const path = require('path')
const DIR = './node_modules/kuromoji/dict'
const kuromoji = require('kuromoji')

module.exports = class KuromojiWrapper {
  constructor() {
  }

  async init() {
    this.tokenizer = await new Promise((resolve, reject) => {
      kuromoji
        .builder({ dicPath: DIR })
        .build((err, tokenizer) => {
          if (err) reject(err)
          resolve(tokenizer)
        })
    })
  }

  exec(text) {
    return this.tokenizer.tokenize(text)
  }

  get(text) {
    const res = this.exec(text)
    // 第一優先: 感動詞
    const prop1 = res.find((o) => o.pos === '感動詞')
    if (prop1) return prop1.basic_form
    // 第二優先: 形容詞
    const prop2 = res.find((o) => o.pos === '形容詞')
    if (prop2) return prop2.basic_form
    return text
  }
}
```

`this.tokenizer.tokenize(text)`で形態素解析を行う。
例えば、`超！かわいいね！`という文字列の場合、kuromojiからのレスポンスは下記となる。

```.json
[
  {
    word_id: 70940,
    word_type: 'KNOWN',
    word_position: 1,
    surface_form: '超',
    pos: '接頭詞',
    pos_detail_1: '名詞接続',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '超',
    reading: 'チョウ',
    pronunciation: 'チョー'
  },
  {
    word_id: 91640,
    word_type: 'KNOWN',
    word_position: 2,
    surface_form: '！',
    pos: '記号',
    pos_detail_1: '一般',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '！',
    reading: '！',
    pronunciation: '！'
  },
  {
    word_id: 1717990,
    word_type: 'KNOWN',
    word_position: 3,
    surface_form: 'かわいい',
    pos: '形容詞',
    pos_detail_1: '自立',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '形容詞・イ段',
    conjugated_form: '基本形',
    basic_form: 'かわいい',
    reading: 'カワイイ',
    pronunciation: 'カワイイ'
  },
  {
    word_id: 92590,
    word_type: 'KNOWN',
    word_position: 7,
    surface_form: 'ね',
    pos: '助詞',
    pos_detail_1: '終助詞',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: 'ね',
    reading: 'ネ',
    pronunciation: 'ネ'
  },
  {
    word_id: 91640,
    word_type: 'KNOWN',
    word_position: 9,
    surface_form: '！',
    pos: '記号',
    pos_detail_1: '一般',
    pos_detail_2: '*',
    pos_detail_3: '*',
    conjugated_type: '*',
    conjugated_form: '*',
    basic_form: '！',
    reading: '！',
    pronunciation: '！'
  }
]
```
この中から、`pos`が感動詞となっているものを第一優先。形容詞となっているものを第二優先として、抜き出す。
※ ここは正直適当。優先順位を持たせることができたらとりあえずOK

呼び出し方としてはこう。
※ node.jsってconstructorでawaitできないのね・・・ちょっと強引に外部メソッドで実現。

```sample.js
const KuromojiWrapper = require('./KuromojiWrapper')

const main = async () => {
  // イニシャライズ
  const analysis = new KuromojiWrapper()
  // 外部init
  await analysis.init()
  const res = analysis.get('超！かわいいね！！')
  console.log(res)
}

main()

// 実行結果
// かわいい
```

# 既存の処理に組み込む

ソース全文

```index.js
'use strict'

const line = require('@line/bot-sdk')
const client = new line.Client({ channelAccessToken: process.env.ACCESSTOKEN })
const crypto = require('crypto')
const AWS = require('aws-sdk')
const dynamodb = new AWS.DynamoDB.DocumentClient({
  region: 'ap-northeast-1'
})
const KuromojiWrapper = require('./KuromojiWrapper')

exports.handler = async (event) => {
  return new Promise(async (resolve, reject) => {
    const signature = crypto.createHmac('sha256', process.env.CHANNELSECRET).update(event.body).digest('base64')
    const checkHeader = (event.headers || {})['X-Line-Signature']
    const body = JSON.parse(event.body)

    if (signature !== checkHeader) {
      reject('Authentication error')
    }
    const events = body.events[0]
    const message = events.message.text

    // ここから 追加
    const analysis = new KuromojiWrapper()
    await analysis.init()
    const key = analysis.get(message)
    // ここまで 追加

    const param = {
      TableName: 'ReplyMapping',
      Key: {
        type: key // ここもいじった
      }
    }
    const result = await dynamodb.get(param).promise()
    let msg, img
    if (result.Item) {
      const getRandomList = (list) => list[Math.floor(Math.random() * list.length)]
      msg = getRandomList(result.Item.msg)
      img = getRandomList(result.Item.img)
    } else {
      msg = `${message}ってなんでちゅか？`
      img = ''
    }

    const replyText = {
      type: 'text',
      text: msg
    }
    const replyImage = {
      type: 'image',
      originalContentUrl: img,
      previewImageUrl: img
    }

    client.replyMessage(events.replyToken, replyText)
      .then((r) => {
        return client.pushMessage(events.source.userId, replyImage)
      })
      .then((r) => {
        resolve({
          statusCode: 200,
          headers: { 'X-Line-Status': 'OK' },
          body: '{"result":"completed"}'
        })
      }).catch(reject)
  })
}
```

# 結果
![Aft.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300430/894df9f0-5594-6a2b-8c74-0320479989cb.gif)

固定文言じゃなくても、単文レベルならそれっぽくできた！！！

ただ・・レスポンス遅くね？

# まとめ

## それっぽいロジックは組み込めたけど・・・
それっぽいロジックを組み込むことができたが、考えれば考えるほど様々な処理を入れたくなる。（例えば年齢ロジックとか、天気とか）
これらを詰め合わせると、とんでもないことになりそうな予感。

## パフォーマンス劣化
パフォーマンスがかなり劣化。
ローカルで動かした時より圧倒的に劣化しているので、NW周りとか、Lambdaのメモリーとかが関係しているのかな。