公式LINEでボットを作ってみました!是非お友達追加してお試しください!
当ブログ内の記事にはプロモーションを含む場合がございます。

\ 記事検索LINEボット /
友だち追加

【無料】LINE公式アカウントにキーワード反応の記事レコメンドBOTを自作した話

現在運用している「ぼくのノートブック」と「TRAVeeeNET」のブログ記事が2,000本を超えたあたりから、何か面白いこと出来ないかなと考えていました。

「せっかく書いた記事をLINE公式アカウントで検索出来るようにできれば面白いのではないか?」

公式LINEのAIチャット機能にFAQを投げて作ろうかと思ったのですが、登録出来るFAQが100個のみと記事紹介BOTには少なすぎました。

そこで、LINEでキーワードを送るだけで、関連記事を最大5件までカード形式でレコメンドしてくれるBOTを、Google Apps Script(GAS)だけを使って無料で作ってみました。

この記事では、実際に動かすまでの手順とハマったポイントを、これから同じものを作りたい人向けに全部書き残しておきます。

\ お友達追加してチェック /

この記事でわかること

  • Google Apps ScriptだけでLINE公式アカウントの自動応答BOTを作る全体像
  • キーワードに応じて記事をレコメンドする「カルーセルメッセージ」の実装方法
  • 「はい/いいえ」で分岐する会話フロー(クイックリプライ)の作り方
  • 記事の追加・更新をスプレッドシートだけで完結させる仕組み
  • 実際にハマったエラーと解決方法(302エラー・スプレッドシートID・応答メッセージ設定など)

完成イメージ

やりたかったことはシンプルです。

  1. ユーザーが「タイ eSIM」のようにキーワードを送る
  2. BOTが関連記事を検索し、カード型のメッセージ(カルーセル)で最大5件表示
  3. 「気になる記事は見つかった?」とはい/いいえボタン付きで質問
  4. 「はい」→ お礼を言って終了
  5. 「いいえ」→ 「こんな記事が欲しい」をリクエストできるフォームのURLを送信

読者が求めている情報にすぐたどり着けて、なおかつ見つからなかった場合のフィードバックも自動で集められる、という設計です。

なぜGoogle Apps Scriptを選んだのか

LINE BotのバックエンドというとNode.jsやPythonでサーバーを立てるイメージが強いかもしれませんが、今回は次の理由からGASを選びました。

  • サーバー代がかからない(GASは無料枠で十分動く)
  • 常時稼働のサーバー管理が不要(Webhookが届いた時だけ動く)
  • データベースの代わりにスプレッドシートが使える(SQLを書かなくていい)
  • ブログ記事の管理をすでにスプレッドシートでやっている人なら、学習コストがほぼゼロ

特に最後のポイントが大きくて、「記事タイトルと回答文とURLを一覧にしたスプレッドシート」さえあれば、それがそのままBOTのデータベースになります。

記事を追加したいときは、コードを一切触らずスプレッドシートに1行足すだけでBOTの回答が増えるのは、運用面でかなり楽です。

必要なもの

  • LINE Developersアカウント(無料)
  • Googleアカウント(スプレッドシート・Apps Script用)
  • 紹介したい記事の「質問文・回答文・URL・カテゴリ」を整理したデータ

全体構成

[ユーザー] --メッセージ送信--> [LINE Messaging API]
                                     |
                                     | Webhook (POST)
                                     v
                        [Google Apps Script (doPost)]
                                     |
                    +----------------+----------------+
                    |                                 |
           [スプレッドシートを検索]          [ユーザーの状態を保存/取得]
                    |                         (PropertiesService)
                    v
        [カルーセルメッセージを生成]
                    |
                    v
        [LINE Messaging API へ返信]

ポイントは、GASのdoPost関数がWebhookの受け口になるという点です。

LINEからメッセージが届くたびにこの関数が呼ばれ、スプレッドシートを検索してカードを組み立て、返信する、という流れです。

Step1: 記事データをスプレッドシートに用意する

まず、以下の5列を持つスプレッドシートを作ります。シート名は記事一覧にしました。

Noカテゴリ質問回答URL
1通信(eSIM/SIM)タイ旅行で使えるおすすめのeSIMを知りたいタイ旅行で使えるおすすめのeSIMを比較してご紹介しています。https://example.com/xxx
2観光バンコクのおすすめ観光スポットを知りたいバンコクの見どころ・観光スポットについて詳しくご紹介しています。https://example.com/yyy

検索は「カテゴリ」「質問」「回答」の文字列に対する部分一致で行うので、読者が打ちそうなキーワード(国名・地名・サービス名など)が「質問」や「回答」の文章に自然に含まれているかがヒット率を左右します。

Step2: LINE Developersでチャネルを作成する

  1. LINE Developersにログインし、Messaging APIのチャネルを作成
  2. 「Messaging API設定」タブでチャネルアクセストークン(長期)を発行してコピー
  3. この段階では「Webhook URL」はまだ空欄でOK(GAS側の準備が終わってから設定します)

Step3: Google Apps Scriptでコードを書く

スプレッドシートのメニューから「拡張機能」→「Apps Script」を開き、以下のようなコードを書きます(全文はこの記事末尾にまとめて掲載しています)。

Webhookの受け口

js

function doPost(e) {
  try {
    const body = JSON.parse(e.postData.contents);
    const events = body.events || [];
    events.forEach(function (event) {
      handleEvent(event);
    });
  } catch (err) {
    Logger.log('doPost error: ' + err);
  }
  return ContentService.createTextOutput(JSON.stringify({ status: 'ok' }))
    .setMimeType(ContentService.MimeType.JSON);
}

doPostは「LINEから実際にPOSTリクエストが届いた時」だけ動く特別な関数です。エディタの「実行」ボタンで直接動かそうとすると、必要な引数eが渡されずエラーになります(私も最初これでハマりました)。テストする時は、ダミーのeを作って渡す専用のテスト関数を用意すると安全です。

キーワード検索ロジック

js

function searchArticles(userText) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME);
  const data = sheet.getDataRange().getValues();
  const rows = data.slice(1);

  const keywords = userText.replace(/ /g, ' ').split(' ')
    .map(function (k) { return k.trim(); })
    .filter(function (k) { return k.length > 0; });

  const scored = rows.map(function (row) {
    const category = String(row[1] || '');
    const question = String(row[2] || '');
    const answer = String(row[3] || '');
    const url = String(row[4] || '');
    const searchTarget = (category + ' ' + question + ' ' + answer).toLowerCase();

    let score = 0;
    keywords.forEach(function (kw) {
      const kwLower = kw.toLowerCase();
      if (searchTarget.indexOf(kwLower) !== -1) score += 1;
      if (category.toLowerCase().indexOf(kwLower) !== -1) score += 1;
    });

    return { category, question, answer, url, score };
  });

  return scored
    .filter(function (r) { return r.score >= CONFIG.MIN_SCORE; })
    .sort(function (a, b) { return b.score - a.score; })
    .slice(0, CONFIG.MAX_RESULTS);
}

スペース区切りで複数キーワード(例:「タイ 観光」)にも対応させています。スコアが高い順に並び替えて上位5件を返すだけのシンプルなロジックですが、これで十分実用的に動きました。

カルーセル(カード型)メッセージの生成

js

function buildCarouselMessage(results) {
  const columns = results.map(function (r) {
    return {
      title: truncate(r.question, 40),
      text: truncate(r.answer, 60),
      actions: [{ type: 'uri', label: '記事を見る', uri: r.url }],
    };
  });

  return {
    type: 'template',
    altText: 'おすすめ記事のご紹介',
    template: { type: 'carousel', columns: columns },
  };
}

LINEのカルーセルテンプレートは、タイトル40文字・本文60文字までという制限があるので、truncate関数で自動的に切り詰めています。

「はい/いいえ」の会話フロー

js

function buildFeedbackQuickReply() {
  return {
    type: 'text',
    text: '気になる記事は見つかった?',
    quickReply: {
      items: [
        { type: 'action', action: { type: 'message', label: 'はい', text: 'はい' } },
        { type: 'action', action: { type: 'message', label: 'いいえ', text: 'いいえ' } },
      ],
    },
  };
}

「はい/いいえ」の状態管理には、GASのPropertiesServiceを使いました。データベースを立てなくても、ユーザーIDごとに「今、はい/いいえの返事待ちかどうか」を簡単に記憶できます。

js

function setUserState(userId, state) {
  PropertiesService.getScriptProperties().setProperty('state_' + userId, state);
}
function getUserState(userId) {
  return PropertiesService.getScriptProperties().getProperty('state_' + userId);
}
function clearUserState(userId) {
  PropertiesService.getScriptProperties().deleteProperty('state_' + userId);
}

Step4: Webアプリとしてデプロイする

  1. 右上の「デプロイ」→「新しいデプロイ」
  2. 種類は「ウェブアプリ」を選択
  3. 実行するユーザー: 自分
  4. アクセスできるユーザー: 全員 ← ここを間違えると詰みます(後述)
  5. デプロイすると、https://script.google.com/macros/s/xxxxx/exec というURLが発行される

Step5: LINE DevelopersにWebhook URLを設定する

  1. LINE Developersの「Messaging API設定」→「Webhook URL」に、Step4で発行されたURLを貼り付け
  2. 「Webhookの利用」をON
  3. 「検証」ボタンを押して、200が返ってくることを確認
  4. 「応答メッセージ」はOFFにする(ここがONだと、LINE公式アカウントマネージャーのデフォルト応答とBOTの返信が二重に動いてしまいます)

実際にハマったエラーと解決策

ここからは実際に構築中にぶつかった問題です。同じ症状で困っている人の参考になれば幸いです。

① 検証ボタンで「302 Found」が返ってくる

原因は2つ考えられます。

  • デプロイURLがテスト用の /dev になっていた(本番用の /exec を使う必要がある)
  • デプロイ時の「アクセスできるユーザー」が「全員」になっておらず、Googleのログイン画面にリダイレクトされていた

「デプロイを管理」から設定を見直し、「全員」+ /exec のURLに変更したところ解決しました。

doPost error: TypeError: Cannot read properties of undefined (reading 'postData')

これはdoPostをエディタの「実行」ボタンで直接動かした時に必ず起きるエラーでした。eという引数はLINEからのリクエスト時にしか渡されないため、単体実行では想定通りエラーになります。テスト専用にダミーのeを渡す関数を別途用意することで解決しました。

js

function testDoPost() {
  const dummyEvent = {
    postData: {
      contents: JSON.stringify({
        events: [{
          type: 'message',
          message: { type: 'text', text: 'タイ eSIM' },
          source: { type: 'user', userId: 'test-user' },
          replyToken: 'dummy-reply-token',
        }],
      }),
    },
  };
  const result = doPost(dummyEvent);
  Logger.log(result.getContent());
}
Illegal spreadsheet id or key

スプレッドシートのURLからIDをコピーする際、余分な文字まで含めてコピーしてしまっていたのが原因でした。正しいIDは/d//editの間の文字列だけです。

https://docs.google.com/spreadsheets/d/【ここだけ】/edit

スプレッドシートに紐づいた形でApps Scriptを作成している場合は、SpreadsheetApp.getActiveSpreadsheet()を使えばID指定自体が不要になり、このミスを根本的に防げるのでおすすめです。

運用してみて感じたメリット

実際に運用を始めて感じたのは、記事の追加・修正がスプレッドシートを触るだけで完結するという手軽さです。

エンジニアでなくても、行を1つ追加するだけでBOTの「知識」が増えていきます。デプロイし直す必要もありません。

今後の改善予定

  • スプレッドシートに「キーワード」列を追加して、検索の精度を手動でチューニングできるようにする
  • カードにサムネイル画像を追加してタップ率を上げる
  • 「いいえ」を選んだ回数が多いキーワードを集計して、記事ネタの発掘に活用する

まとめ

LINE公式アカウントの自動応答は、専門のツールを契約しなくても、Google Apps Scriptとスプレッドシートだけで十分実用的なものが作れます。

特に「記事をレコメンドしたいけど、検索の手間はかけさせたくない」というブログ・メディア運営者には、今回のようなキーワード反応型のカルーセルBOTはかなり相性が良いと感じました。

同じように自作を検討している方の参考になれば嬉しいです。