前々回の記事で作成したオウム返しチャットボットにQ&Aエンジンを実装する

#前々回の記事で作成したオウム返しチャットボットにQ&Aエンジンを実装する

　
※本記事は最終的にQ&Aチャットボットを構築するための一部分となります。
[本編はこちら](https://qiita.com/abemaki/items/8c611cc63ec109ab3835)




###仕組み

①Q&A情報を保持したエクセルブックをナレッジベースとして活用します
②ユーザーの質問文を検索キーワードにしナレッジベースを全文検索します。質問文にヒットしたナレッジのマッチスコア次第でBOTが応答を返すといった仕組みです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/c6004043-5b4e-03fb-35b0-683099be3552.png)

言葉のベクトルの近さから類義語の吸収をしたりなどの特殊な構造を除けば一般的なチャットボットの仕組みと同じ仕組みです。
一般的なチャットボットシステムではこういった仕組みの上にナレッジベースを効率的に賢くする機能が付属されていたりします。
こうしたAIの教育機能等は今回は作成する想定にありません。



###Q&Aナレッジを格納するベースファイルを作成します

テストの為に気象庁のQ&Aを元にQ&Aエクセルを作成します
QANDA.xlsxというファイル名で保存します

出典：https://www.jma.go.jp/jma/kishou/know/faq/faq10.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/463e3d05-4e25-144f-92f7-fb0b8ffc4bc1.png)

　

　
　
knowledgeBaseというフォルダを作成し、QANDA.xlsxを配置します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/28ad108f-1225-df5d-d59b-1d669349ce05.png)




libというフォルダを作成し、knowledgeBase.jsというファイルを作成します
これはエクセルのQA一覧をナレッジベースとして読み込むためのリソースです。

参考用ソースは以下
<details><summary>knowlegeBasex.js ※新規作成（クリックして展開）</summary><div>

```javascript
/***********************************
 エクセルからナレッジベースを取得
***********************************/
exports.getKnowledgeBase = function() {

	var docList = [];
	const XLSX = require("xlsx");
	const Utils = XLSX.utils;

	// Workbookの読み込み
	const workbook = XLSX.readFile("knowledgeBase/QANDA.xlsx");

	// シート読み込み
	let worksheet = workbook.Sheets['Sheet1'];
	// 有効なセルを取得
	let range = worksheet['!ref'];
	const decodeRange = Utils.decode_range(range);

	// シートからQ＆A情報を取得する
	var count = 0;
	for (let rowIndex = decodeRange.s.r; rowIndex <= decodeRange.e.r; rowIndex++) {
		const q = Utils.encode_cell({ r: rowIndex, c:0 });
		const a = Utils.encode_cell({ r: rowIndex, c:1 });
		const cellq = sheet1[q];
		const cella = sheet1[a];
		if (typeof cellq !== 'undefined' && typeof cellq.v !== 'undefined' && cellq.v !== 'Question'
		  && typeof cella !== 'undefined' && typeof cella.v !== 'undefined' && cella.v !== 'Answer') {
			docList.push({ id: count,'title': cellq.v, 'body': cella.v});
			count ++;
		}
	}
	console.log('Knowledge base');
	console.log(docList);

	return docList;
}            

```
</div></details>
　　
　
　

　
検索エンジンを日本語に対応するのに必要なリソースを手に入れてlib配下にコピーします
https://github.com/MihaiValentin/lunr-languages
上記ページから以下のファイルを取得してlib配下にコピー
lunr.stemmer.support.js
lunr.jp.js

日本語形態素解析に必要なリソースもlib配下にコピーします
http://chasen.org/~taku/software/TinySegmenter/tiny_segmenter-0.2.js

　　
　
同じくlibフォルダ配下に、elasticlunrsearch.jsというファイルを作成します
これはナレッジベースとして読み込んだナレッジからQ&A抽出を行う為の検索エンジンコントローラです。

参考用ソースは以下
<details><summary>elasticlunrsearch.js ※新規作成（クリックして展開）</summary><div>

```javascript
// ******************************************************************//
// ** 全文検索エンジン関連 ここから                            　     **//
// ******************************************************************//
const elasticlunr = require('elasticlunr');
require('./lunr.stemmer.support.js')(elasticlunr);
require('./lunr.jp.js')(elasticlunr);

// ドキュメントライブラリを取得
var libdocs = require('./knowledgeBase.js');
var docs = libdocs.getKnowledgeBase();

// 質問のブースト値
const BOT_qes_boost=process.env.BOT_qes_boost;

// 回答のブースト値
const BOT_ans_boost=process.env.BOT_ans_boost;

// インデックス構築
const index = elasticlunr(function () {
    this.use(elasticlunr.jp);
    this.addField('body');
    this.addField('title');
    this.setRef('id');
    for(var i = 0; i < docs.length; i++){
        this.addDoc(docs[i]);
    }
});

/***********************************
 エクセルからナレッジベースを取得
***********************************/
exports.indedxsearch = async function(keyword) {
    const result = await   index.search(keyword, {
        fields: {
            title: {boost: BOT_qes_boost},
            body: {boost:  BOT_ans_boost},
        },
        expand: true,
    });
    return result;
}

/***********************************
 ナレッジIDをキーにナレッジを取得
***********************************/
exports.searchById = function(id) {
    for(var i = 0; i < docs.length; i++){
        if(id == docs[i].id){
            return docs[i];
        }
    }
    return null;
}

```

</div></details>



　


同じくlibフォルダ配下に、bot.jsというファイルを作成します
これは現在のindex.jsのBOT関連リソースを集約したファイルになります。
これに合わせてindex.jsのソースも修正します

参考用ソースは以下
<details><summary>bot.js ※新規作成（クリックして展開）</summary><div>

```javascript
/***********************************
 BOTが回答できない場合のソーリーメッセージ
***********************************/
exports.BOT_sorrymsg =  process.env.BOT_sorrymsg;
/***********************************
 BOTが理解不能と判断する際の閾値（下回り）
***********************************/
const BOT_Sorry_threshold = process.env.BOT_Sorry_threshold;
/***********************************
 LINEのエンドポイント設定
***********************************/
const Channel_endpoint = process.env.Channel_endpoint;
/***********************************
 LINEのチャネルアクセストークン設定
***********************************/
const Channel_access_token = process.env.Channel_access_token;

/***********************************
 BOTの回答が有効なものである場合の定数値
***********************************/
const BOT_ANSWER_TRUE = "BOT_ANSWER_TRUE";
exports.BOT_ANSWER_TRUE = BOT_ANSWER_TRUE;

/***********************************
 BOTの回答が有効だが一定の確信度に満たない場合の低数値
***********************************/
const BOT_ANSWER_TRUE_ANY = "BOT_ANSWER_TRUE_ANY";
exports.BOT_ANSWER_TRUE_ANY = BOT_ANSWER_TRUE_ANY;

/***********************************
 BOTの回答が無効なものである場合の定数値
***********************************/
const BOT_ANSWER_FALSE = "BOT_ANSWER_FALSE";
exports.BOT_ANSWER_FALSE = BOT_ANSWER_FALSE;

// ---- ************************************************** -------
// ---- BOTの回答が有効であるか判断した結果を返す             -------
// ---- ************************************************** -------
exports.isAnswerEnabled = function(result) {
    if(isNullOrUndefined(result) 
        || result.lentgh == 0
        || isNullOrUndefined(result[0])){
        // 回答がない場合
        return BOT_ANSWER_FALSE;
    } else if(isNullOrUndefined(result[0].ref) 
            || result[0].score < BOT_Sorry_threshold){
        // 回答はあるが最低限の確信度に満たない場合
        console.log("first score(score ng)");
        console.log(result[0].score);
        return BOT_ANSWER_FALSE;
    } else {
        // 最低限の確信度を満たした回答が存在する場合
        console.log("first score");
        console.log(result[0].score);
        return BOT_ANSWER_TRUE;
    }
}

// ---- ************************ -------
// ---- 一答形式の回答を作成する   -------
// ---- ************************ -------
exports.createAnsMessage = function(req,message) {
    var options = {
        method: "POST",
        uri: Channel_endpoint,
        body: {
            replyToken: req.body.events[0].replyToken,
            messageNotified: 0,
            messages: [
                // 基本情報
                {
                    contentType: 1,
                    type: "text",
                    text: message,
                }
            ]
        },
        auth: {
            bearer : Channel_access_token
        },
        json: true
    };
    return options;
}

// ---- ************************************************** -------
// ---- 空チェック  -------
// ---- ************************************************** -------
function isNullOrUndefined(o){
    return (o === undefined || o === null);
}
```

</div></details>

　

　　

　
　
index.jsを以下のように修正します
lib配下に移動した処理を削除
BOTの回答を制御するための処理を追加

参考用ソースは以下
<details><summary>index.js ※修正（クリックして展開）</summary><div>

```javascript
// ******************************************************************//
// ** 初期設定関連 ここから                                          **//
// ******************************************************************//
var express = require("express");
var app = express();
var cfenv = require("cfenv");
require('dotenv').config();
var request = require("request");
var bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({
  extended: true
}));
app.use(bodyParser.json());

// 検索エンジンコントローラ読み込み
var elasticlunrsearch = require('./lib/elasticlunrsearch.js');

// BOTコントローラ読み込み
var bot = require('./lib/bot.js');

// ******************************************************************//
// ** メッセージ処理 ここから                                      **//
// ******************************************************************//
const asyncwrap = fn => (req, res, next) => fn(req, res, next).catch(next);
app.post("/api", asyncwrap(async (req, res) => {

    // 受信テキスト
    var userMessage = req.body["events"][0]["message"]["text"];
    console.log("user -> " + userMessage);
       
    // 問い合わせ内容をキーにQAデータを検索
    var result = await elasticlunrsearch.indedxsearch(userMessage);

    // 取得したQAデータを元に応答メッセージを作成
    var options = null;

    // 回答が有効化どうかを判断した結果を取得する
    switch(bot.isAnswerEnabled(result)) {

        // BOTが質問を理解できない場合はソーリーメッセージ
        case bot.BOT_ANSWER_FALSE:
             options = bot.createAnsMessage(req , bot.BOT_sorrymsg);
             break;
     
　　    // BOTが質問を理解し、回答が可能な場合
        case bot.BOT_ANSWER_TRUE:
             // ナレッジIDをキーにナレッジを抽出する
             const knowledge = elasticlunrsearch.searchById(result[0].ref);
             options = bot.createAnsMessage(req , knowledge.body);
             break;
    }

    // メッセージを返す
    request(options, function(err, res, body) {
    });
    res.send("OK");
}));

// サーバ起動
var appEnv = cfenv.getAppEnv();
app.listen(appEnv.port, "0.0.0.0", function() {
  console.log("server starting on " + appEnv.url);
});

```

</div></details>







　





　








.envファイルに以下の設定を追記します


```config

# ---------------------#
# BOTのチューニング設定         
# ---------------------#
# BOTが理解不能と判断する際の閾値（下回り）
BOT_Sorry_threshold=0.25

# BOTが1問1答する為の確信度の閾値（上回り）
BOT_Confidence=0.9

# 質問のブースト値
BOT_qes_boost=0.3

# 回答のブースト値
BOT_ans_boost=0.1

# 複数回答を返す際に最大いくつの候補を提示するか
BOT_any_ans_count=5

# BOTが質問を理解できないときのソーリーメッセージ
BOT_sorrymsg=申し訳ありません。質問の意味が理解できませんでした。

# BOTが複数回答を投げかけるときのメッセージ
BOT_anyansmsg=この中にお役に立てる情報はございますでしょうか。
```

　
　
　
現時点でソースコードは以下のような構成になっているはずです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/74ad82c7-6e15-2c6e-f563-18bcead6c0a0.png)

　　

　

### 動作確認

BOTを起動して動作確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/57bff0dc-f51b-842f-14c9-dcc1e80ed5c5.png)


　
　

シュミレータ上で期待通りの動きをすることを確認したら、Gitにコミット＆プッシュします

```console
git add .
git commit -am "Q&Aエンジンの追加"
git push heroku master
```

　
　
LINEで動きを確認してみましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524081/c8ae26db-e49b-88cd-e972-c24657eeaeb1.png)