JavaScriptとJSONでチャットボット風の検索機能を作る

更新日:

当ブログの管理者であるきょうみくんがマスコットキャラクターを務めている、パソコン修理のPCホスピタルでは症状・お困りごとを検索する機能を提供しています。

今回は症状・お困りごとを検索する機能を元に、JavaScriptとJSONでチャットボット風の検索機能を作る方法を解説します。

※当記事で紹介しているコードは解説のために改変しており、実際に使用しているコードとは異なります。

完成イメージ

完成イメージ

検索結果として表示する内容を決める

まずはキーワードを入力された時に表示する内容を決めます。ここでは例として「ウイルス感染」のキーワードを使用します。

下記のようにそれぞれの項目の内容を考えます。

名前 ウイルスに感染した
文章 ウイルスに感染した場合は、まずは二次被害を防ぐため必ずインターネットから切断してください。もし契約が有効なウイルス対策ソフトをインストールしている場合は、ウイルス定義の更新を行なってから、パソコン全体のスキャンを行なってみてください。スキャンを行なっても改善されない場合は、手作業での駆除が必要です。ウイルス対策ソフトをインストールしていなかった場合は、後からインストールするとかえって状態が悪化することがあります。
リンク
タグ “ウイルス”, “感染”, “スパイウェア”, “マルウェア”, “ランサムウェア”, “Emotet”, “エモテット”, “トロイの木馬”, “ワーム”, “不正”, “請求”, “架空”, “不当”, “アダルト”, “流出”, “漏えい”, “漏洩”, “セキュリティ”, “security”, “駆除”, “身代金”, “要求”, “乗っ取り”, “踏み台”, “踏台”, “遠隔操作”, “リモート操作”, “暗号化”, “ハッキング”, “クラッキング”, “ポップアップ”

「名前」はセレクトメニューで選択された際の検索で使用される文字列、「文章」は2つ目の吹き出しに表示する文章、「リンク」は2つ目の吹き出しに表示するリンク、「タグ」は検索時に使用するキーワードです。

「タグ」に、いかに検索に使用されるキーワードを設定できるかが重要です。ここにキーワードが設定されていなければ検索でヒットしません。

表示する内容をJSON形式で作成する

表示する内容が決まったら、JSON形式で作成してJSONファイルとして保存します。JSONでは末尾のカンマは許容されないので付けないように注意してください。付いているとSyntax errorが発生します。

[
    {
        "name": "ウイルスに感染した",
        "text": "ウイルスに感染した場合は、まずは二次被害を防ぐため必ずインターネットから切断してください。もし契約が有効なウイルス対策ソフトをインストールしている場合は、ウイルス定義の更新を行なってから、パソコン全体のスキャンを行なってみてください。スキャンを行なっても改善されない場合は、手作業での駆除が必要です。ウイルス対策ソフトをインストールしていなかった場合は、後からインストールするとかえって状態が悪化することがあります。",
        "link": [
            {"ウイルス・マルウェアの駆除": "https://www.4900.co.jp/service/virus.php"},
            {"セキュリティー対策": "https://www.4900.co.jp/service/security.php"},
            {"パソコンのウイルス対策をしないとどうなる?セキュリティソフトの必要性を解説": "https://www.4900.co.jp/smarticle/11801/"},
            {"パソコンがウイルスに感染したらどうなる?画面に現れる症状と予防策": "https://www.4900.co.jp/smarticle/7202/"},
            {"有名なコンピュータウイルスの種類と事例、対策と感染後の対処法": "https://www.4900.co.jp/smarticle/7642/"}
        ],
        "tag": ["ウイルス", "感染", "スパイウェア", "マルウェア", "ランサムウェア", "Emotet", "エモテット", "トロイの木馬", "ワーム", "不正", "請求", "架空", "不当", "アダルト", "流出", "漏えい", "漏洩", "セキュリティ", "security", "駆除", "身代金", "要求", "乗っ取り", "踏み台", "踏台", "遠隔操作", "リモート操作", "暗号化", "ハッキング", "クラッキング", "ポップアップ"],
        "excluded_tag": [],
        "example": []
    }
]

excluded_tagとexampleという項目がありますが、のちほど説明するのでここでは気にしないでください。

参考:JSON – JavaScript | MDN

検索フォームを作成する

次にHTMLで検索フォームを作成します。formタグは不要です。

<p>キーワードを選択して「検索する」、または検索キーワードを入力して「検索する」を押してください。</p>

<div id="trouble-search-condition-wrapper" class="trouble-search-condition-wrapper">
    <div class="trouble-search-condition">
        <section class="trouble-search-condition-box">
            <h3>調べたい内容に合うキーワードを選択してください</h3>
            <select id="trouble-search-condition-select">
                <option value="">選択してください</option>
                <option value="ウイルスに感染した">ウイルスに感染した</option>
            </select>
        </section>

        <section class="trouble-search-condition-box">
            <h3>選択肢にキーワードがない場合は入力してください</h3>
            <input type="text" id="trouble-search-condition-keyword">
            <p><strong class="red">※「パソコンにジュースをこぼした」など具体的に入力してください。</strong></p>
        </section>
    </div><!-- /.trouble-search-condition -->

    <div class="trouble-search-condition-submit-wrapper">
        <input type="submit" value="検索する" id="trouble-search-condition-submit" class="trouble-search-condition-submit">
    </div><!-- /.trouble-search-condition-submit-wrapper -->

    <div id="trouble-search-result" class="trouble-search-result">
        <div id="trouble-search-result-keyword"></div>
        <div id="trouble-search-result-text"></div>
        <div id="trouble-search-result-endtext"></div>
    </div><!-- /.trouble-search-result -->
</div><!-- /.trouble-search-condition-wrapper -->

jQueryを読み込む

JavaScriptはjQueryを使用してコーディングするため、jQueryを読み込みます。バージョンは1.8.3以上でも構いません。

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>

JavaScriptで定数を宣言する

ここからはJavaScriptでコーディングしていきます。まずは必要な定数を宣言します。

/**
 * JSONデータを取得
 *
 * @param {string} url
 * @return object
 */
const getJsonData = function(url) {
    return $.ajax({
        type: 'GET', 
        url: url,
        dataType: 'json',
        timeout: 5000,
        cache: false
    });
};

/**
 * @type {object} トラブル内容一覧
 */
const LIST = getJsonData('表示する内容を記載したJSONファイル.json');

/**
 * @type {object} セレクトメニューのjQueryオブジェクト
 */
const $select = $('#trouble-search-condition-select');

/**
 * @type {object} キーワードのjQueryオブジェクト
 */
const $input = $('#trouble-search-condition-keyword');

/**
 * @type {object} 入力キーワードを表示するdivのjQueryオブジェクト
 */
const $resultKeyword = $('#trouble-search-result-keyword');

/**
 * @type {object} 検索結果の文章を表示するdivのjQueryオブジェクト
 */
const $resultText = $('#trouble-search-result-text');

/**
 * @type {object} 検索結果の締めの文章を表示するdivのjQueryオブジェクト
 */
const $resultEndText = $('#trouble-search-result-endtext');

/**
 * @type {object} 検索結果を表示するdivのjQueryオブジェクト
 */
const $output = $('#trouble-search-result');

/**
 * @type {object} 検索ボタンのjQueryオブジェクト
 */
const $submit = $('#trouble-search-condition-submit');

/**
 * @type {string} ローディングアイコン
 */
const loadingHtml = '<div class="loading"></div>';

JavaScriptでメインの関数を作成する

次に検索を実行する際に使用するメインの関数を作成します。

/**
 * 検索
 */
function troubleSearch() {
    /**
     * @type {string} セレクトメニューのキーワード
     */
    const selectKeyword = $select.val();

    /**
     * @type {string} 入力キーワード
     */
    const inputKeyword = $input.val();

    /**
     * @type {object} 検索結果の表示開始時間
     */
    let START_TIME = {};
    START_TIME['keyword']  = 500;
    START_TIME['text']     = START_TIME['keyword'] + 1000;
    START_TIME['end_text'] = START_TIME['text'] + 4000;

    START_TIME['notfound_keyword']  = START_TIME['keyword'];
    START_TIME['notfound_text']     = START_TIME['text'];
    START_TIME['notfound_end_text'] = START_TIME['notfound_text'] + 4000;

    /**
     * 吹き出し
     *
     * @param {string} content 内容
     * @param {string} type アイコンの種類
     * @param {boolean} loading ローディングアイコン
     * @return string
     */
    const balloon = function(content, type, loading) {
        /**
         * @type {string} HTML
         */
        let html = '<div class="trouble-search-result-baloon-wrapper ' + type + '"><div class="trouble-search-result-baloon">' + content + '</div></div>';

        if (loading) {
            html += loadingHtml;
        }

        return html;
    };

    /**
     * トラブル内容を格納
     *
     * @param {object} data 検索結果
     * @return array
     */
    const troubleSearchResult = function(data) {
        /**
         * @type {object} HTML
         */
        let HTML = {};

        $.each(data, function(key, val) {
            HTML['text'] = '<p>' + val.text + '</p>';
            HTML['text'] += '<p>まずは下記ページをご覧ください。</p>';
            HTML['text'] += '<ul class="trouble-search-result-links">';

            $.each(val.link, function(k, v) {
                $.each(v, function(text, url) {
                    HTML['text'] += '<li><a href="' + url + '" target="_blank">' + text + '</a></li>';
                });
            });

            HTML['text'] += '</ul>';
        });

        HTML['text'] = balloon(HTML['text'], 'staff', true);

        return HTML;
    };

    /**
     * 検索結果を表示
     *
     * @param {string} result 検索結果
     * @param {object} output 出力場所
     * @param {number} startTime 表示開始時間
     */
    const showResult = function(result, output, startTime) {
        setTimeout(function() {
            /**
             * @type {object} 検索結果
             */
            const $appendElement = $(result).appendTo(output);

            $('.loading').hide();
            $appendElement.hide().fadeIn(500);

            if ($('#trouble-search-result-endtext').find('*')[0]) {
                $input.prop('disabled', false);
                $submit.prop('disabled', false);
            }

            if ($appendElement.parent() && output !== $resultEndText) {
                /**
                 * @type {number} スクロール差分
                 */
                let scrollDifference = 0;

                /**
                 * @type {number} スクロール位置
                 */
                const scrollTopPosition = $appendElement.parent().offset().top - scrollDifference;

                $('body, html').animate({scrollTop: scrollTopPosition}, 400);
            }
        }, startTime);
    };

    /**
     * @type {string} 検索キーワード
     */
    let keyword = '';

    /**
     * @type {array} 検索結果
     */
    let RESULT = [];

    /**
     * キーワード検索
     *
     * @param {string} tag 登録されているタグ
     * @param {string} keyword 検索キーワード
     * @return boolean
     */
    const searchKeyword = function(tag, keyword) {
        /**
         * @type {object} 入力キーワードの正規表現
         */
        const regexKeyword = new RegExp(keyword, 'i');

        /**
         * @type {object} タグの正規表現
         */
        const regexTag = new RegExp(tag, 'i');

        if (
            tag.match(regexKeyword)
            || keyword.match(regexTag)
        ) {
            return true;
        } else {
            return false;
        }
    };

    /**
     * HTMLをエスケープ
     *
     * @param {string} str HTML
     * @return string
     */
    function htmlEscape(str) {
        if (!str) return;
        return str.replace(/[<>&"'`]/g, function(match) {
            const escape = {
                '<': '&lt;',
                '>': '&gt;',
                '&': '&amp;',
                '"': '&quot;',
                "'": '&#39;',
                '`': '&#x60;'
            };
            return escape[match];
        });
    }

    LIST.done(function(data) {
        if (selectKeyword !== '') {
            keyword = selectKeyword;
        } else if (inputKeyword !== '') {
            keyword = inputKeyword;
        }

        if (keyword !== '') {
            $input.prop('disabled', true);
            $submit.prop('disabled', true);

            $.each(data, function(key, val) {
                if (selectKeyword !== '') {
                    if (val.name === selectKeyword) {
                        RESULT.push(val);
                    }
                } else {
                    /**
                     * @type {boolean} 除外判定のフラグ
                     */
                    let skipFlag = false;

                    $.each(val.excluded_tag, function(k, v) {
                        if (searchKeyword(v, keyword)) {
                            skipFlag = true;
                        }
                    });

                    if (!skipFlag) {
                        $.each(val.tag, function(k, v) {
                            if (
                                searchKeyword(v, keyword)
                                && RESULT.indexOf(val) === -1
                            ) {
                                RESULT.push(val);
                            }
                        });
                    }
                }

                if (RESULT.length) {
                    return false;
                }
            });

            /**
             * @type {object} 検索結果のHTML
             */
            let HTML = troubleSearchResult(RESULT);

            HTML['keyword'] = balloon('<p>「' + htmlEscape(keyword) + '」</p>', 'customer', true);
            HTML['end_text'] = balloon('<p>PCホスピタルでは有料でパソコンや周辺機器などのデジタル機器の設定・修理・トラブル解決サポートを行っております。上記ページの情報で解決されない場合は、下記のサポートご予約専用ダイヤルまでお電話ください。</p>', 'staff');

            if (!RESULT.length) {
                START_TIME['keyword']  = START_TIME['notfound_keyword'];
                START_TIME['text']     = START_TIME['notfound_text'];
                START_TIME['end_text'] = START_TIME['notfound_end_text'];

                HTML['text'] = '<p>「' + htmlEscape(keyword) + '」に関する情報は見つかりませんでした。</p>';
                HTML['text'] += '<p>検索キーワードを変更する、または下記ページをご覧いただくと解決する可能性があります。</p>';
                HTML['text'] += '<ul class="trouble-search-result-links">';
                HTML['text'] += '<li><a href="https://www.4900.co.jp/service/anything.php" target="_blank">パソコンの困った!なんでもトラブル解決!</a></li>';
                HTML['text'] += '</ul>';
                HTML['text'] = balloon(HTML['text'], 'staff', true);
            }

            $resultKeyword.empty();
            $resultText.empty();
            $resultEndText.empty();

            showResult(loadingHtml, $output, 0);
            showResult(HTML['keyword'], $resultKeyword, START_TIME['keyword']);
            showResult(HTML['text'], $resultText, START_TIME['text']);
            showResult(HTML['end_text'], $resultEndText, START_TIME['end_text']);
        }
    });
}

上記のコードを1つずつ解説します。

下記コードは、検索フォームのselectタグとinputタグの入力値を定数に格納しています。

/**
 * @type {string} セレクトメニューのキーワード
 */
const selectKeyword = $select.val();

/**
 * @type {string} 入力キーワード
 */
const inputKeyword = $input.val();

下記のコードは、検索結果をチャットボット風に表示させる際の、吹き出しの表示開始時間を設定しています。

/**
 * @type {object} 検索結果の表示開始時間
 */
let START_TIME = {};
START_TIME['keyword']  = 500;
START_TIME['text']     = START_TIME['keyword'] + 1000;
START_TIME['end_text'] = START_TIME['text'] + 4000;

START_TIME['notfound_keyword']  = START_TIME['keyword'];
START_TIME['notfound_text']     = START_TIME['text'];
START_TIME['notfound_end_text'] = START_TIME['notfound_text'] + 4000;

notfoundとついている方は、検索結果が見つからなかった時の吹き出しの表示開始時間です。どちらも同じ時間が設定されていますが、これは後からそれぞれの時間を変更できるようにするために分けています。

下記のコードは、検索結果をチャットボット風に表示させる際の吹き出しを返す関数です。お客さまとスタッフの2種類の吹き出しを設定できるように、引数でアイコンの種類を設定できるようになっています。アイコンを表示させるCSSはのちほどご紹介します。

/**
 * 吹き出し
 *
 * @param {string} content 内容
 * @param {string} type アイコンの種類
 * @param {boolean} loading ローディングアイコン
 * @return string
 */
const balloon = function(content, type, loading) {
    /**
     * @type {string} HTML
     */
    let html = '<div class="trouble-search-result-baloon-wrapper ' + type + '"><div class="trouble-search-result-baloon">' + content + '</div></div>';

    if (loading) {
        html += loadingHtml;
    }

    return html;
};

下記コードは、JSONファイルから取り出した、検索内容に一致した検索結果を表示させる吹き出しを構築する関数です。完成イメージの2つ目の吹き出しにあたります。

/**
 * トラブル内容を格納
 *
 * @param {object} data 検索結果
 * @return array
 */
const troubleSearchResult = function(data) {
    /**
     * @type {object} HTML
     */
    let HTML = {};

    $.each(data, function(key, val) {
        HTML['text'] = '<p>' + val.text + '</p>';
        HTML['text'] += '<p>まずは下記ページをご覧ください。</p>';
        HTML['text'] += '<ul class="trouble-search-result-links">';

        $.each(val.link, function(k, v) {
            $.each(v, function(text, url) {
                HTML['text'] += '<li><a href="' + url + '" target="_blank">' + text + '</a></li>';
            });
        });

        HTML['text'] += '</ul>';
    });

    HTML['text'] = balloon(HTML['text'], 'staff', true);

    return HTML;
};

下記コードは、検索結果を表示する関数です。ローディングアイコンの表示や入力フィールドの無効化解除、検索ボタンの無効化解除、次の吹き出しへの自動スクロールなどを行っています。

/**
 * 検索結果を表示
 *
 * @param {string} result 検索結果
 * @param {object} output 出力場所
 * @param {number} startTime 表示開始時間
 */
const showResult = function(result, output, startTime) {
    setTimeout(function() {
        /**
         * @type {object} 検索結果
         */
        const $appendElement = $(result).appendTo(output);

        $('.loading').hide();
        $appendElement.hide().fadeIn(500);

        if ($('#trouble-search-result-endtext').find('*')[0]) {
            $input.prop('disabled', false);
            $submit.prop('disabled', false);
        }

        if ($appendElement.parent() && output !== $resultEndText) {
            /**
             * @type {number} スクロール差分
             */
            let scrollDifference = 0;

            /**
             * @type {number} スクロール位置
             */
            const scrollTopPosition = $appendElement.parent().offset().top - scrollDifference;

            $('body, html').animate({scrollTop: scrollTopPosition}, 400);
        }
    }, startTime);
};

サイトのヘッダーなどを固定している場合に、scrollDifferenceという変数にヘッダーなどの高さを指定することで、自動スクロールの位置を調整できるようにしています。必要な場合は28行目にscrollDifferenceの値を上書きする処理を追加します。

スムーススクロールのアニメーション対象にbodyとhtmlを指定していますが、もしコールバック関数を指定する場合はコールバック関数が2回呼ばれてしまうので、その辺りは調整してください。

下記コードは、検索キーワードと検索結果を格納するための変数を宣言しています。これらはのちほど使用します。

/**
 * @type {string} 検索キーワード
 */
let keyword = '';

/**
 * @type {array} 検索結果
 */
let RESULT = [];

下記コードは、入力されたキーワードが登録されているタグに含まれているか、または登録されているタグが入力されたキーワードに含まれているかを判定する関数です。

/**
 * キーワード検索
 *
 * @param {string} tag 登録されているタグ
 * @param {string} keyword 検索キーワード
 * @return boolean
 */
const searchKeyword = function(tag, keyword) {
    /**
     * @type {object} 入力キーワードの正規表現
     */
    const regexKeyword = new RegExp(keyword, 'i');

    /**
     * @type {object} タグの正規表現
     */
    const regexTag = new RegExp(tag, 'i');

    if (
        tag.match(regexKeyword)
        || keyword.match(regexTag)
    ) {
        return true;
    } else {
        return false;
    }
};

下記のコードは、入力された検索キーワードを出力する際にエスケープするための関数です。掲載時の都合によりアンパサンドを全角にしていますが、実際は半角です。

/**
 * HTMLをエスケープ
 *
 * @param {string} str HTML
 * @return string
 */
function htmlEscape(str) {
    if (!str) return;
    return str.replace(/[<>&"'`]/g, function(match) {
        const escape = {
            '<': '&lt;',
            '>': '&gt;',
            '&': '&amp;',
            '"': '&quot;',
            "'": '&#39;',
            '`': '&#x60;'
        };
        return escape[match];
    });
}

下記コードは、表示する内容を記載したJSONファイルの読み込みが完了した時に実行する処理を書いています。JSONファイルをAjaxで読み込むためこのようになっています。

LIST.done(function(data) {
    ~
}

下記コードは、セレクトメニューが選択されていればそのキーワードを変数に格納し、逆にキーワードが入力されていればそのキーワードを変数に格納しています。

if (selectKeyword !== '') {
    keyword = selectKeyword;
} else if (inputKeyword !== '') {
    keyword = inputKeyword;
}

下記コードは、上記の変数にキーワードが格納されているかを判定しています。

if (keyword !== '') {
    ~
}

下記コードは、検索キーワードを入力するinputタグと検索するボタンのinputタグにdisabled属性を追加することで無効化し、チャットボット風の吹き出しが表示されている最中に別の検索を実行されないようにしています。

$input.prop('disabled', true);
$submit.prop('disabled', true);

下記コードは、表示する内容を記載したJSONファイルの中身を反復処理しています。

$.each(data, function(key, val) {
    ~
}

下記コードは、セレクトメニューでキーワードが選択されていれば、そのキーワードに合う項目をRESULTという配列に追加します。

逆に検索キーワードが入力されていれば、登録されているタグを反復処理してキーワードが含まれる場合にRESULTに追加します。

if (selectKeyword !== '') {
    if (val.name === selectKeyword) {
        RESULT.push(val);
    }
} else {
    /**
     * @type {boolean} 除外判定のフラグ
     */
    let skipFlag = false;

    $.each(val.excluded_tag, function(k, v) {
        if (searchKeyword(v, keyword)) {
            skipFlag = true;
        }
    });

    if (!skipFlag) {
        $.each(val.tag, function(k, v) {
            if (
                searchKeyword(v, keyword)
                && RESULT.indexOf(val) === -1
            ) {
                RESULT.push(val);
            }
        });
    }
}

間に除外判定のフラグというものがありますが、これはJSONファイルを作成する時に追加したexcluded_tagを使用します。項目が1つしかない場合は使用しませんが、検索キーワードの一部が重複する場合に有用です。

例えば「ウイルスに感染した」と「液晶が割れた」という2つの項目を設定する場合、「表示」というキーワードがどちらにも含まれる可能性があります。

ウイルスに感染した場合の検索例
  • 画面にウイルスが表示される
  • ポップアップ請求画面の表示を削除したい
液晶が割れた場合の検索例
  • 画面の表示がおかしい
  • 液晶が割れて表示されなくなった

こういった場合に「液晶が割れた」のexcluded_tagに「ウイルス」「請求」などのタグを登録すれば、skipFlagという変数の値がtrueとなり、RESULTへの項目の追加がスキップされます。その結果、「画面にウイルスが表示される」と検索された時に、「液晶が割れた」の項目に「表示」というタグが登録されていても「ウイルスに感染した」の項目が検索結果として選ばれます。

[
    {
        "name": "液晶が割れた",
        "text": "液晶が割れた、真っ黒で表示されない、チカチカする、縦線または横線が入るなどの液晶トラブルにはメーカー、年式は問わずに対応いたします。WindowsだけではなくMacにも対応しています。外付け液晶ディスプレイ(パソコン本体とディスプレイが分離しているタイプ)の修理には対応していません。",
        "link": [
            {"液晶パネル 修理・交換": "https://www.4900.co.jp/service/lcd.php"},
            {"パソコンのディスプレイが映らない!本体の故障?原因と対処法 修理・交換": "https://www.4900.co.jp/smarticle/7222/"},
            {"スマホの画面割れ・液晶割れの修理方法と事前対策を解説!": "https://www.4900.co.jp/smarticle/7759/"},
            {"パソコンの画面に線が入ってしまう7つの原因と6つの対処法を解説": "https://www.4900.co.jp/smarticle/25116/"},
            {"パソコンの液晶に不具合が出る原因と対応策を紹介": "https://www.4900.co.jp/smarticle/12057/"},
            {"画面が真っ黒でカーソルだけ表示された時の原因と対処法": "https://www.4900.co.jp/smarticle/25139/"}
        ],
        "tag": ["液晶", "パネル", "割れ", "表示", "モニタ", "ディスプレイ", "ガラス", "暗い", "線", "映ら", "真っ黒", "真黒", "真っ暗", "真暗", "画面", "バックライト", "映像"],
        "excluded_tag": ["ウイルス", "請求"],
        "example": []
    },
    {
        "name": "ウイルスに感染した",
        "text": "ウイルスに感染した場合は、まずは二次被害を防ぐため必ずインターネットから切断してください。もし契約が有効なウイルス対策ソフトをインストールしている場合は、ウイルス定義の更新を行なってから、パソコン全体のスキャンを行なってみてください。スキャンを行なっても改善されない場合は、手作業での駆除が必要です。ウイルス対策ソフトをインストールしていなかった場合は、後からインストールするとかえって状態が悪化することがあります。",
        "link": [
            {"ウイルス・マルウェアの駆除": "https://www.4900.co.jp/service/virus.php"},
            {"セキュリティー対策": "https://www.4900.co.jp/service/security.php"},
            {"パソコンのウイルス対策をしないとどうなる?セキュリティソフトの必要性を解説": "https://www.4900.co.jp/smarticle/11801/"},
            {"パソコンがウイルスに感染したらどうなる?画面に現れる症状と予防策": "https://www.4900.co.jp/smarticle/7202/"},
            {"有名なコンピュータウイルスの種類と事例、対策と感染後の対処法": "https://www.4900.co.jp/smarticle/7642/"}
        ],
        "tag": ["ウイルス", "感染", "スパイウェア", "マルウェア", "ランサムウェア", "Emotet", "エモテット", "トロイの木馬", "ワーム", "不正", "請求", "架空", "不当", "アダルト", "流出", "漏えい", "漏洩", "セキュリティ", "security", "駆除", "身代金", "要求", "乗っ取り", "踏み台", "踏台", "遠隔操作", "リモート操作", "暗号化", "ハッキング", "クラッキング", "ポップアップ"],
        "excluded_tag": [],
        "example": []
    }
]

もしexcluded_tagの内容が多くなる場合は、項目自体の順番を入れ替えた方が良いです。入れ替えた場合は「ウイルスに感染した」の項目が優先されるため、「画面にウイルスが表示される」や「ポップアップ請求画面の表示を削除したい」といったキーワードが入力されても、「液晶が割れた」の項目は検索結果として選ばれません。

下記コードは、一致する検索結果が見つかった場合にfalseを返して反復処理を抜けています。

if (RESULT.length) {
    return false;
}

下記コードは、出力するHTMLを格納するための変数を宣言しています。troubleSearchResult(RESULT)で、完成イメージの2つ目の吹き出しにあたる、JSONファイルから取り出した文章とリンクを格納しています。

/**
 * @type {object} 検索結果のHTML
 */
let HTML = troubleSearchResult(RESULT);

下記コードは、上記の変数に、入力されたキーワードと完成イメージの3つ目の吹き出しに表示させる文章を追加しています。

HTML['keyword'] = balloon('<p>「' + htmlEscape(keyword) + '」</p>', 'customer', true);
HTML['end_text'] = balloon('<p>PCホスピタルでは有料でパソコンや周辺機器などのデジタル機器の設定・修理・トラブル解決サポートを行っております。上記ページの情報で解決されない場合は、下記のサポートご予約専用ダイヤルまでお電話ください。</p>', 'staff');

下記コードは、検索結果が見つからなかった場合の吹き出しの表示タイミングの設定と、完成イメージの3つ目の吹き出しに表示させる文章を追加しています。

if (!RESULT.length) {
    START_TIME['keyword']  = START_TIME['notfound_keyword'];
    START_TIME['text']     = START_TIME['notfound_text'];
    START_TIME['end_text'] = START_TIME['notfound_end_text'];

    HTML['text'] = '<p>「' + htmlEscape(keyword) + '」に関する情報は見つかりませんでした。</p>';
    HTML['text'] += '<p>検索キーワードを変更する、または下記ページをご覧いただくと解決する可能性があります。</p>';
    HTML['text'] += '<ul class="trouble-search-result-links">';
    HTML['text'] += '<li><a href="https://www.4900.co.jp/service/anything.php" target="_blank">パソコンの困った!なんでもトラブル解決!</a></li>';
    HTML['text'] += '</ul>';
    HTML['text'] = balloon(HTML['text'], 'staff', true);
}

下記コードは、HTMLを出力するdivタグの中身を削除して空にしています。2回目以降の検索時に前の検索結果が表示されないようにするためです。

$resultKeyword.empty();
$resultText.empty();
$resultEndText.empty();

下記コードは、検索結果を表示させるための関数を実行して、ローディングアイコンと吹き出しのHTMLを表示しています。

showResult(loadingHtml, $output, 0);
showResult(HTML['keyword'], $resultKeyword, START_TIME['keyword']);
showResult(HTML['text'], $resultText, START_TIME['text']);
showResult(HTML['end_text'], $resultEndText, START_TIME['end_text']);

セレクトメニューが変更された時に入力フィールドの値を削除する

ここからは検索フォームを操作された時の処理です。

セレクトメニューまたは入力フィールドのどちらかだけ使用するので、セレクトメニューが変更された時(セレクトメニューが使用された時)に入力フィールドに入力されたキーワードを削除します。

// セレクトメニューを選択時に入力キーワードを削除
$select.on('change', function() {
    $input.val('');
});

参考:.change() | jQuery API Documentation

入力フィールドにキーワードが入力された時にセレクトメニューの選択を解除する

逆に入力フィールドにキーワードが入力された時(入力フィールドが使用された時)にセレクトメニューの選択を解除します。

// キーワードの入力時にセレクトメニューを初期化
$input.on('keyup', function() {
    $select.prop('selectedIndex', 0);
});

セレクトメニューの選択解除はvalメソッドで行えないため、selectedIndexプロパティの値を操作します。

セレクトメニューの一番上にvalueの値を空にしているoptionタグがあれば0、なければ-1を指定することで未選択状態にすることができます。

参考:.keyup() | jQuery API Documentation

検索が実行された時にメインの関数を実行する

エンターキーが押された時、または検索するボタンが押された時にメインの関数を実行します。

// エンターで検索を実行
$input.on('keydown', function(e) {
    if (e.key === 'Enter') {
        troubleSearch();
    }
});

// 検索するボタンで検索を実行
$submit.on('click', function() {
    troubleSearch();
});

参考:.keydown() | jQuery API Documentation
参考:.click() | jQuery API Documentation

CSS(SCSS)を設定する

最後に見た目を整えるためにCSS(SCSS)を設定します。

※他のCSSの影響を受けるため、このCSSを適用させても完成イメージと同じ表示にならない可能性があります。

.trouble-search-condition {
    display: flex;
    justify-content: space-between;

    &-box {
        width: calc((100% - 20px) / 2);

        select,
        input {
            box-sizing: border-box;
            padding: 5px;
            width: 100%;
        }
    }

    &-submit-wrapper {
        margin-bottom: 20px;
        text-align: center;
    }

    &-submit {
        -webkit-appearance: none;
        background: linear-gradient(to bottom, #4999f0 0%, #06c 100%);
        border: 1px solid #0066cc;
        color: #fff;
        cursor: pointer;
        display: inline-block;
        padding: 10px;
        text-align: center;
        text-decoration: none;

        &:hover {
            background: #4999f0;
        }
    }
}

.trouble-search-result-baloon-wrapper {
    background: url(吹き出しの横に表示するスタッフの画像) left top/50px auto no-repeat;
    margin-bottom: 10px;
    min-height: 40px;
    padding: 10px 0 0 70px;

    @media print,screen and (min-width: 768px) {
        background-size: auto 100px;
        min-height: 80px;
        padding: 20px 0 0 120px;
    }

    .trouble-search-result-baloon {
        background: #fff8dc;
        padding: 10px;
        position: relative;
        font-size: 14px;

        @media print,screen and (min-width: 768px) {
            font-size: inherit;
            padding: 20px;
        }

        p {
            margin-top: 0;
        }

        p:last-child,
        ul:last-child {
            margin-bottom: 0;
        }

        &::before {
            border-bottom: 15px solid transparent;
            border-right: 15px solid #fff8dc;
            border-top: 15px solid transparent;
            content: '';
            display: block;
            height: 0;
            position: absolute;
            left: -15px;
            top: 10px;
            width: 0;
        }
    }

    &.customer {
        background-image: url(吹き出しの横に表示するユーザーの画像);
        background-position: right top;
        padding: 10px 70px 0 0;

        @media print,screen and (min-width: 768px) {
            padding: 20px 120px 0 0;
        }

        .trouble-search-result-baloon::before {
            border-left: 15px solid #fff8dc;
            border-right: 0;
            left: auto;
            right: -15px;
        }
    }
}

.trouble-search-result {
    .trouble-search-result-links {
        list-style: none;
        margin: 0;

        @media print,screen and (min-width: 768px) {
            display: flex;
            flex-wrap: wrap;
            justify-content: space-between;
        }

        li {
            background: #fff;
            border: 1px dashed #ccc;
            margin-bottom: 5px;

            @media print,screen and (min-width: 768px) {
                width: calc(50% - 5px);
            }

            a {
                box-sizing: border-box;
                display: block;
                padding: 10px 10px 10px 27px;
                background: url(矢印のアイコン) 10px center no-repeat;

                @media print,screen and (min-width: 768px) {
                    align-items: center;
                    display: flex;
                    height: 100%;
                }
            }
        }
    }
}

おまけ

ここまでできたら検索機能が動作する状態になっていると思います。ここからはおまけとしてサジェスト機能の追加方法を解説します。

サジェスト機能を追加する

表示する内容を記載したJSONファイルにexampleという項目を含めていますが、これはサジェスト機能で使用する項目です。

サジェスト機能にはjQuery UIというライブラリのAutocompleteウィジェットを使用します。

まずは必要なCSSファイルとライブラリを読み込みます。

<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/themes/smoothness/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js"></script>

次にサジェスト機能を実装するためのJavaScriptのコードを追加します。

/**
 * @type {object} サジェスト
 */
let SUGGEST_KEYWORDS = [];

// サジェスト
LIST.done(function(data) {
    $.each(data, function(key, val) {
        if ('example' in val) {
            $.each(val.example, function(k, v) {
                SUGGEST_KEYWORDS.push(v);
            });
        }
    });

    if (SUGGEST_KEYWORDS.length) {
        $('#trouble-search-condition-keyword').autocomplete({
            delay: 0,
            source: SUGGEST_KEYWORDS
        });
    }
});

上記コードを、下記コードの上辺りに追加します。

/**
 * 検索
 */
function troubleSearch() {
    ~
}

ここからはサジェスト機能のコードを解説します。

下記コードは、JSONファイルのexampleの項目を格納するための変数を宣言しています。

/**
 * @type {object} サジェスト
 */
let SUGGEST_KEYWORDS = [];

下記コードは、JSONファイルの読み込みが完了した時に実行する処理を書いています。サジェスト機能で使用する情報がJSONファイルに記載されているためこのようになっています。

// サジェスト
LIST.done(function(data) {
    ~
}

下記コードは、JSONファイル内のexampleの項目を反復処理して、内容が登録されていたらSUGGEST_KEYWORDSという配列に追加しています。

$.each(data, function(key, val) {
    if ('example' in val) {
        $.each(val.example, function(k, v) {
            SUGGEST_KEYWORDS.push(v);
        });
    }
});

下記コードは、検索キーワードの入力フィールドにサジェストを表示する設定です。jQuery UIのAutocompleteウィジェットの詳細は参考ページをご覧ください。

if (SUGGEST_KEYWORDS.length) {
    $('#trouble-search-condition-keyword').autocomplete({
        delay: 0,
        source: SUGGEST_KEYWORDS
    });
}

参考:jQuery UI
参考:Autocomplete | jQuery UI

ここまでできたら、JSONファイルのexampleの項目にサジェストで表示する内容を登録します。

[
    {
        "name": "液晶が割れた",
        "text": "液晶が割れた、真っ黒で表示されない、チカチカする、縦線または横線が入るなどの液晶トラブルにはメーカー、年式は問わずに対応いたします。WindowsだけではなくMacにも対応しています。外付け液晶ディスプレイ(パソコン本体とディスプレイが分離しているタイプ)の修理には対応していません。",
        "link": [
            {"液晶パネル 修理・交換": "https://www.4900.co.jp/service/lcd.php"},
            {"パソコンのディスプレイが映らない!本体の故障?原因と対処法 修理・交換": "https://www.4900.co.jp/smarticle/7222/"},
            {"スマホの画面割れ・液晶割れの修理方法と事前対策を解説!": "https://www.4900.co.jp/smarticle/7759/"},
            {"パソコンの画面に線が入ってしまう7つの原因と6つの対処法を解説": "https://www.4900.co.jp/smarticle/25116/"},
            {"パソコンの液晶に不具合が出る原因と対応策を紹介": "https://www.4900.co.jp/smarticle/12057/"},
            {"画面が真っ黒でカーソルだけ表示された時の原因と対処法": "https://www.4900.co.jp/smarticle/25139/"}
        ],
        "tag": ["液晶", "パネル", "割れ", "表示", "モニタ", "ディスプレイ", "ガラス", "暗い", "線", "映ら", "真っ黒", "真黒", "真っ暗", "真暗", "画面", "バックライト", "映像"],
        "excluded_tag": ["ウイルス", "請求"],
        "example": [
            "ノートパソコンの液晶が割れた",
            "パソコンの画面が映らない"
        ]
    }
]

上記のように登録することで、検索キーワードの入力フィールドに「パソコン」「液晶」「割れた」「画面」「映らない」などが入力された時にサジェストとして表示されます。

サジェスト機能

具体的に入力してもらいたいキーワードや、曖昧なキーワードに対してサジェストを登録してサジェストから選んでもらうことで、適切な検索結果にたどり着いてもらえる可能性が高まります。

以上が、JavaScriptとJSONでチャットボット風の検索機能を作る解説です。

トラブルシューティング

検索機能が動作しないなど、考えられるトラブルの一覧です。上手くいかない場合はご確認ください。

検索を実行しても動かない
  • JSONの末尾にカンマがついている
  • JSONファイルが読み込めていない
  • jQueryが読み込めていない
  • jQueryの読み込み位置が下すぎる
  • JavaScriptのコードが間違っている
  • Internet Explorer 11未満のブラウザーを使用している
CSSが適用されない
  • SCSSのコードをCSSファイルに記載している
  • SCSSファイルをCSSファイルにコンパイルしていない
  • id名やclass名が間違っている
自動スクロールの位置がおかしい
  • scrollDifferenceの値が調整できていない
セレクトメニューで選択していても見つかりませんでしたになる
  • セレクトメニューのoptionの値と、JSONファイルのnameの値が一致していない
検索キーワードを確認したい

tooolsのTech Blogではこれからも役に立つ情報を発信していきますので、定期的に閲覧していただけると幸いです。

学校授業の「IT化」で、こんなお悩みありませんか?【e-おうち】
出張/持込/宅配でパソコン修理・設定 24時間365日対応
消費税計算

税率を設定して税込/税抜金額の消費税計算ができます。

文字数カウント

文字数をカウントできます。

和暦西暦変換

和暦と西暦を相互変換できます。

年齢計算

和暦または西暦から年齢を計算できます。

入学年・卒業年計算

履歴書に必要な学校の入学年・卒業年を生年月日から計算できます。

単位変換(換算)

キロ、マイル、グラム、華氏などの様々な単位を相互変換(換算)できます。

カラーコード変換

カラーコード(16進数)とRGB値(10進数)を相互変換できます。

Webタイマー(カウントダウン)

Webタイマー(カウントダウン)です。ストップウォッチ機能もあります。

生活に便利な電話番号一覧

警察や消防などの緊急連絡先や電話番号案内などの電話番号を確認できます。

プロバイダーのカスタマーサポートの電話番号一覧

主なプロバイダーのカスタマーサポートの電話番号を確認できます。

タスク管理(ToDo)

自分のWebブラウザーだけでタスク管理(ToDo)ができます。

エクセル関数

エクセル関数を検索できます。

麻雀の点数計算

麻雀の和了時の点数(符数/翻数/役)を計算することができます。

便利なショートカット一覧

Windows 10やExcelなどで使用できる便利なショートカットを確認できます。

電気料金計算

消費電力、使用時間、使用日数、1kWh単価から電気料金を計算できます。

パスワード生成(作成)

大文字・小文字・数字・記号を含むランダムなパスワードを生成できます。

自分のグローバルIPアドレスを確認

自分がインターネットに接続する時のグローバルIPアドレスを確認できます。

学校授業の「IT化」で、こんなお悩みありませんか?【e-おうち】
出張/持込/宅配でパソコン修理・設定 24時間365日対応
出張/持込/宅配でパソコン修理・設定 24時間365日対応
きょうみくん
このサイトの管理者
名前 きょうみくん
身長 181.1cm
誕生日 1月21日
所属 日本PCサービス株式会社
コメント

パソコン、インターネット、サーモン、ミルクティーが好きです。
猫ではありません。

エクセル家計簿の作り方など、技術的なコラムを書いているTech Blogも運営しています。