BotUIでチャットボット開発（DB編）

## はじめに
[前回投稿した記事](https://qiita.com/t_power/items/e966b4a5f35340ed9a41)の続きで、BotUIを使ったチャットボットを、DB化した話になります。
BotUIって何？どんなチャットボットを作ったの？という方は前回の記事をご覧ください。

## DB化の背景と目的
前回はJavaScript、CSS、HTMLで簡単なシナリオ型のチャットボットを作成しました。
そこでは質問文がまとめてあるjsファイルと、回答文がまとめてあるjsファイルを用意しました。

チャットボットをリリースしたところ、質問文と回答文（以降、シナリオと表記）に、度々修正が加わることが分かりました。

前回のままだと、担当者が直接jsファイルを編集する必要がありました。
また、シナリオが増減すると、分岐ロジックを修正する必要もありました。

シナリオ変更の度にソースコードを修正するのはよくないので、シナリオをDBへ切り分けることにしました。
（前回記事でいうところの、「answer.js」と「bot.js」を廃止しました）

## 作業環境

OS: Windows10
テキストエディタ: Visual Studio Code
ブラウザ: Microsoft Edge
DB: MariaDB 10.5.12
サーバOS: CentOS 7
Webサーバ: Apache 2.4
言語: PHP 7.4、JavaScript、HTML、CSS

## 工夫した点

### 動的にデータを扱う

![スクリーンショット (190).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1998725/bd70595c-a31c-d99a-0c6e-796b184256eb.png)

|value  |第一カテゴリ  |第二カテゴリ  |question  |answer  |
|---|---|---|---|---|
|1  |中央システムについて  |経営方針  |会社経営の最終的な目標はなんですか  |事業と社会貢献の一致です。  |

|Field  |Type  |
|---|---|
|value  |int(11)  |
|category  |varchar(100)  |
|question  |text  |
|answer  |text  |

上記の画像は、実際のチャットボットの画面です。
便宜上、「中央システムについて」を**第一カテゴリ**、「経営方針」を**第二カテゴリ**と呼びます。テーブルには**category**カラム一つしか設定していないため、PHPの関数、**シリアライズ**を使用して、**category**カラムにまとめています。
「会社経営の最終的な目標はなんですか」、「事業と会社貢献の一致です。」はそれぞれ、**question**カラム、**answer**カラムに入る値になります。
また、登録された順にSELECTできるよう、**value**カラムではauto_incrementを設定しています。（今回のSQLではソートをかけていないですが）

参考記事
[PHPのシリアライズを知ろう！便利な使い方徹底解説](https://www.sejuku.net/blog/80079)

```index.html
<?php
//■SQL文
$sql = "SELECT * FROM `{$TableName}`";

//■SQL実行
if ($result = @mysqli_query($con, $sql)){
  while ($row = mysqli_fetch_assoc($result)) {
    //■カラムごとに値を取得
    $val_array[] = $row["value"];
    $cate_array[] = unserialize($row["category"])[0];
    if(count(unserialize($row["category"])) > 1){
      $cate2_array[] = unserialize($row["category"])[1];
    }else{
      $cate2_array[] = "";
    }
    $que_array[] = $row["question"];
    $ans_array[] = $row["answer"];
  }
}
?>

//省略

    <script>
      //PHPで取得したカラムごとの値をjavascriptに渡している
      var val_array = <?php echo json_encode($val_array); ?>;
      var cate_array = <?php echo json_encode($cate_array); ?>;
      var cate2_array = <?php echo json_encode($cate2_array); ?>;
      var que_array = <?php echo json_encode($que_array); ?>;
      var ans_array = <?php echo json_encode($ans_array); ?>;
    </script>
```

データを全件取得し、カラムごとに配列に代入しています。
categoryカラムはアンシリアライズして取り出しています。
また、第二カテゴリがないものは、空白で配列に追加しています。
最終的に、取り出した配列をJavaScriptに渡しています。

このように取り出すことで、
val_array = [1,・・・]
cate_array = ["中央システムについて",・・・]
cate2_array = ["経営方針",・・・]
que_array = ["会社経営の最終的な目標はなんですか",・・・]
ans_array = ["事業と社会貢献の一致です。",・・・]
というように、あるDBレコードのデータを扱いたい場合、それぞれの配列で同じ添え字を指定すれば取り出せるようになりました。

メリットとしてfor文だけで分岐させることができるので、とてもわかりやすいです。


#### DB化前

```bot.js
//質問の選択肢を表示する関数
function showQuestions(){
  botui.message.add({
    photo: Photo,
    content: '質問をお選びください',
    delay: 1000
  }).then(function(){

  return botui.action.button({
    autoHide: false,
    delay: 1000,
    action: [
      {icon: 'sticky-note-o', text: '採用', value: 'recruitment'},
      {icon: 'user', text: '新入社員', value: 'newEmployee'},
      {icon: 'ellipsis-h', text: 'その他', value: 'other'}]
  });
  }).then(function(res){
    botui.action.hide();
      switch(res.value){
        case 'recruitment': showRecruitment(); break;
        case 'newEmployee': showNewEmployee(); break;
        case 'other': showOther(); break;
        default: end();
      }
  });
}
```

#### DB化後

```index.html
    <script>
      //第一カテゴリ重複削除し取得
      var category_array = cate_array.filter(function (x, i, self) {
        return self.indexOf(x) === i;
      });

      //第一カテゴリを表示する関数
      function showQuestions(){
        //第一カテゴリのボタンを作成するためのリスト(q_action)を取得
        var q_action = []
        for(i = 0; i < category_array.length; i++){
          q_action.push({icon: 'circle', text: category_array[i], value: category_array[i]});
        }

        botui.message.add({
          photo: gaPhoto,
          content: '当てはまる項目をお選びください',
          delay: 1000
        }).then(function(){
        //ボタンを表示
        return botui.action.button({
          autoHide: false,
          delay: 1000,
          action: q_action
        });
        }).then(function(res){
          botui.action.hide();
            for (i = 0; i < category_array.length; i++){
              switch(res.value){
                case category_array[i] : category1(i); break;
              }
            }
        });
      }
    </script>
```

新しいカテゴリ、シナリオを追加したい！という場合に、DB化前だと、actionの中身や、swtich文を書き加える必要がありました。
しかし、DB化後は書き加える必要なく、自動的にチャットボットに反映されるようになりました。

<details><summary>今回作成したBotUIを使用したスクリプトの全文はこちら</summary><div>

```index.html
    <script>

      var botui = new BotUI('botui-app');

      var gaPhoto = '../images/sample.png'

      //PHPで取得したカラムごとの値をjavascriptに渡している
      var val_array = <?php echo json_encode($val_array); ?>;
      var cate_array = <?php echo json_encode($cate_array); ?>;
      var cate2_array = <?php echo json_encode($cate2_array); ?>;
      var que_array = <?php echo json_encode($que_array); ?>;
      var ans_array = <?php echo json_encode($ans_array); ?>;

      //第一カテゴリ重複削除し取得
      var category_array = cate_array.filter(function (x, i, self) {
        return self.indexOf(x) === i;
      });


      //初期メッセージ
      botui.message.add({
        photo: gaPhoto,
        content: '採用チャットボットです',
        delay: 1000
      }).then(showQuestions);


      //第一カテゴリを表示する関数
      function showQuestions(){

        //第一カテゴリのボタンを作成するためのリスト(q_action)を取得
        var q_action = []
        for(i = 0; i < category_array.length; i++){
          q_action.push({icon: 'circle', text: category_array[i], value: category_array[i]});
        }

        botui.message.add({
          photo: gaPhoto,
          content: '当てはまる項目をお選びください',
          delay: 1000
        }).then(function(){
        //ボタンを表示
        return botui.action.button({
          autoHide: false,
          delay: 1000,
          action: q_action
        });
        }).then(function(res){
          botui.action.hide();
            for (i = 0; i < category_array.length; i++){
              switch(res.value){
                case category_array[i] : category1(i); break;
              }
            }
        });
      }


      //第二カテゴリ及び第二カテゴリがないquestionを表示する関数
      function category1(i){

        //第一カテゴリの値を取得
        var category_value = category_array[i];

        //第二カテゴリ及び第二カテゴリがないquestionを取得
        var category2_array = [];
        for(x = 0; x < cate_array.length; x++){
          if(category_array[i] == cate_array[x] && cate2_array[x] != ""){
            category2_array.push(cate2_array[x]);
          }else if(category_array[i] == cate_array[x] && cate2_array[x] == ""){
            category2_array.push(que_array[x]);
          }
        }

        //第二カテゴリの重複削除し取得
        var category2_array = category2_array.filter(function (x, i, self) {
          return self.indexOf(x) === i;
        });

        //第二カテゴリ及び第二カテゴリがないquestionボタンを作成するためのリスト(q2_action)を取得
        var q2_action = []
        for(i = 0; i < category2_array.length; i++){
          q2_action.push({icon: 'circle', text: category2_array[i], value: category2_array[i]});
        }

        //戻るボタンの追加
        q2_action.push({icon: 'long-arrow-left', text: '1つ戻る', value: 'return'});

        botui.message.add({
          photo: gaPhoto,
          delay: 1000,
          content: '当てはまる項目をお選びください'
        }).then(function(){

          //ボタンを表示
          return botui.action.button({
            autoHide: false,
            delay: 1000,
            action: q2_action
          });
          }).then(function(res){
            botui.action.hide();
              //questionカラムに含まれているか、そうでないかで条件分岐
              if(que_array.includes(res.value)){
                for(i = 0; i < que_array.length; i++){
                  if(res.value == que_array[i] && cate2_array[i] == ""){
                    answer(i);
                    break;
                  }else if(res.value == que_array[i] && cate2_array[i] != ""){
                    var category2_value = cate2_array[i];
                    switch(res.value){
                      case category2_value: category2(category_value, category2_value); break;
                    }
                  }
                }
              }else{
                for (i = 0; i < category2_array.length; i++){
                  var category2_value = category2_array[i];
                  switch(res.value){
                    case category2_value: category2(category_value, category2_value); break;
                  }
                }
              }
              switch(res.value){
                case 'return': showQuestions(); break;
              }
          });
      }


      //第二カテゴリがあるquestionを表示する関数
      function category2(category_value, category2_value){

        //第二カテゴリがあるquestionボタンを作成するためのリスト(q3_action)を取得
        var q3_action = [];
        for(i = 0; i < cate2_array.length; i++){
          if(category_value == cate_array[i] && category2_value == cate2_array[i]){
            q3_action.push({icon: 'circle', text: que_array[i], value: que_array[i]});
          }
        }

        //戻るボタンの追加
        q3_action.push({icon: 'long-arrow-left', text: '1つ戻る', value: 'return'});

        botui.message.add({
          photo: gaPhoto,
          delay: 1000,
          content: '当てはまる項目をお選びください'
        }).then(function(){

        //ボタンを表示
        return botui.action.button({
          autoHide: false,
          delay: 1000,
          action: q3_action
        });
        }).then(function(res){
          botui.action.hide();
            //正しい回答を表示させるための条件分岐
            for(i = 0; i < que_array.length; i++){
              if(category_value == cate_array[i] && category2_value == cate2_array[i]){
                switch(res.value){
                  case que_array[i]: answer(i, category_value, category2_value); break;
                }
              }
            }
            //選んでいたカテゴリに戻るための条件分岐
            for(i = 0; i < category_array.length; i++){
              if(category_array[i] == category_value){
                switch(res.value){
                case 'return': category1(i); break;
                }
              }
            }
        });
      }


      //回答を表示する関数
      function answer(i, category_value, category2_value){
        //回答に含まれているダブルクォーテーションの削除
        var answer = ans_array[i].replace(/"/g, '');
        botui.message.add({
          photo: gaPhoto,
          delay: 1000,
          content: answer
        }).then(function(){
          //category1に戻るための第一カテゴリの要素番号を取得
          for(x = 0; x < category_array.length; x++){
            if(cate_array[i] == category_array[x]){
              var return_cate1 = x;
            }
          }
          //第二カテゴリが空欄かそうでないかで飛ばす関数を変更
          if(cate2_array[i] == ""){
            var nextFunction = 'c1';
          }else{
            var nextFunction = 'c2';
          }
          switch(nextFunction){
            case 'c1': category1(return_cate1); break;
            case 'c2': category2(category_value, category2_value); break;
          }
        });
      }

    </script>
```

</div></details>

## おわりに

今回DB化させたことで、シナリオの管理がとても楽になったのでよかったです。
本記事では紹介していませんが、CSVファイルを読み込んでDBに登録させる機能も追加しました。
そちらに関しての記事は後日投稿するので、興味がありましたら見ていただけると嬉しいです。

最後まで読んでいただき、ありがとうございました:bow_tone1: