TECH BLOG

エルカミーの技術ブログです

Notionの情報をDifyのナレッジに取り込んでみた

はじめに


今回は「Notionに貯めた情報をDifyに自動で登録できたら便利じゃない?」ということで、GAS(Google Apps Script)を使ってそれを実現してみました。

結論から言うと、思ったより簡単にできました!
この記事では、その手順とポイントをわかりやすくまとめています。

環境(使ったもの)


  • Notion(社内ナレッジ用)
  • Google Apps Script(GAS)
  • Dify(生成AIアプリ基盤)

事前準備


  • Difyのナレッジベース
    • 今回は「GAS_テスト」という名前で作成しています。
      image block
  • Difyのナレッジベースに登録したいコンテンツをまとめたNotionページ
    • 今回は「GAS接続先」というページに3つの子ページを作成しています。
      image block

本編


やりたいこと

以下のような流れで、Notionページの情報をDifyのナレッジベースに登録していきます。

graph LR
	1.Notionインテグレーションの設定
	-->2.NotionページIDをCSVに保存
	-->3.Notionページからコンテンツを取得
	-->4.Difyナレッジに登録

手順

  1. Notionインテグレーションの設定
  2. NotionページIDをCSVに保存
  3. Notionページからコンテンツを取得
  4. Difyナレッジに登録
Step 1:Notionインテグレーションの設定

まず、GASからNotionにアクセスするための準備をします。

GASがNotionに接続するためには、Notionのインテグレーションを設定する必要があります。

⚠️
この設定は、Notionのワークスペースのメンバーが行ってください。

  1. インテグレーションの管理ページに移動します。
    image block
    image block
  2. 次に、インテグレーションの設定を行います。

    今回はNotionページの「コンテンツを読み取る」機能だけで問題ありません。
    「内部インテグレーションシークレット」はGASから接続する際に必要となります。

    image block
    image block
  3. 設定が完了すると、作成したインテグレーションが「Active connections」に表示されます。
    image block

Step 2:Notion 子ページのIDをCSVに保存

続いて、Notionの親ページの下にある子ページのIDを取得して、CSV形式でGoogle Driveに保存します。
今回は下記3つのGASのコードに分けています。

ファイル名 内容
common.gs store_notion_id.gsとregister_dify_knowledge.gsに共通する設定や関数
store_notion_id.gs Notion APIを使ってNotionページIDを取得し、CSVファイルに保存する
register_dify_knowledge.gs Notion APIを使ってNotionページのコンテンツを取得し、DifyナレッジAPIを使ってナレッジベースに登録する

  1. まず、スプレッドシートからApps Scriptを開きます。
    image block
  2. 次に、common.gsにNotionの「内部インテグレーションシークレット」を記載します。

    common.gs

    const NOTION_TOKEN = "*****"; // このケースではGAS Connectionからコピペ

  3. 続いて、 store_notion_id.gs に必要な設定を記載します。

    store_notion_id.gs

    // store_notion_id.gsだけで使う変数
    const PAGE_ID = "*****" // Notionページ「GAS接続先」のページID
    const FOLDER_ID = "*****" // CSVファイルを保存するフォルダのID
    
    // Notion子ページのIDを取得してCSVに保存する関数
    // 1列目: NotionページID
    // 2列目: ページタイトル(Difyナレッジ登録に必要ではないが、IDとタイトルの対応を確認するために使用)
    function getChildPageIds() {
      const url = `https://api.notion.com/v1/blocks/${PAGE_ID}/children?page_size=100`;
    
      const options = {
        method: "get",
        headers: {
          "Authorization": `Bearer ${NOTION_TOKEN}`,
          "Notion-Version": "2022-06-28",
          "Content-Type": "application/json"
        }
      };
    
      const response = UrlFetchApp.fetch(url, options);
      const json = JSON.parse(response.getContentText());
    
      const pageIds = [];
    
      json.results.forEach(block => {
        if (block.type === "child_page") {
          pageIds.push({
            id: block.id,
            title: block.child_page.title
          });
        }
      });
    
      saveAsCsv(pageIds);
    }
    
    // CSVに保存する関数
    function saveAsCsv(data) {
      const csv = data.map(row => `"${row.id}","${row.title}"`).join("\n");
      const folder = DriveApp.getFolderById(`${FOLDER_ID}`);
      const fileName = `notion_child_pages_${new Date().toISOString()}.csv`;
    
      folder.createFile(fileName, csv, MimeType.CSV);
    }

    PAGE_IDは、NotionページIDはURLに記載されている32桁の英数字です。
    FOLDER_IDは、CSVファイルを保存するGoogle DriveフォルダのURLの drive.google.com/drive/folders/ に続く英数字です。

    • NotionページID
      image block
      https://www.notion.so/GAS-{NotionページID(32)}?pvs=4
    • Google DriveフォルダのID
      https://drive.google.com/drive/folders/{Google DriveフォルダのID}

Step 3:Notionページからコンテンツを取得

次に、保存されたCSVからNotionページIDを読み込み、それぞれのページの中身を取得します。

先ほどのステップで取得したNotionページIDからコンテンツを取得する処理を register_dify_knowledge.gs に記載します。

  1. まず、CSVファイルからNotionページのIDを取得します。
    // (1) CSVファイルからIDリストを取得
    function getPageIdsFromCsv() {
      const fileId = '*****'; // Googleドライブ上のCSVファイルID
      const file = DriveApp.getFileById(fileId);
      const csv = Utilities.parseCsv(file.getBlob().getDataAsString());
      
      const ids = [];
      for (let i = 0; i < csv.length; i++) { // ヘッダなし
        ids.push(csv[i][0]); // 1列目にNotionのページIDがある前提
      }
      return ids;
    }

    fileIdには、対象のCSVファイルのIDをベタ書きしています。
    このIDはCSVファイルのURLの drive.google.com/file/d/ に続く英数字です。

    • CSVファイルのID
      image block
      https://drive.google.com/file/d/{CSVファイルのID}/view?usp=drive_link
  2. NotionページIDに対応するコンテンツを取得する処理を記載しています。

    今回は以下のブロックタイプのコンテンツを対象とします。

    • paragraph
    • heading_1, heading_2, heading_3
    • bulleted_list_item, numbered_list_item
    // (2) Notion APIで本文取得
    function fetchNotionPageContent(pageId) {
      const url = `https://api.notion.com/v1/blocks/${pageId}/children`;
      const options = {
        method: 'get',
        headers: {
          'Authorization': `Bearer ${NOTION_TOKEN}`,
          'Notion-Version': '2022-06-28'
        }
      };
    
      const response = UrlFetchApp.fetch(url, options);
      const data = JSON.parse(response.getContentText());
    
      const target_blocktypes = [
        "paragraph", 
        "heading_1", 
        "heading_2", 
        "heading_3", 
        "bulleted_list_item", 
        "numbered_list_item"
        ]
      let textContent = '';
      for (const block of data.results) {
        if (!target_blocktypes.includes(block.type)) {
          continue;  // 対象外はスキップ
        }
    
        // 各タイプごとにrich_text配列を取り出す
        let richTexts = [];
    
        switch(block.type) {
          case "paragraph":
            if (block.paragraph && block.paragraph.rich_text) {
              richTexts = block.paragraph.rich_text;
            }
            break;
    
          case "heading_1":
          case "heading_2":
          case "heading_3":
            if (block[block.type] && block[block.type].rich_text) {
              richTexts = block[block.type].rich_text;
            }
            break;
    
          case "bulleted_list_item":
          case "numbered_list_item":
            if (block[block.type] && block[block.type].rich_text) {
              richTexts = block[block.type].rich_text;
            }
            break;
        }
    
        // richTextsが空でなければテキストを抽出
        if (richTexts.length > 0) {
          textContent += richTexts.map(t => t.plain_text).join('') + '\n';
        }
      }
      Logger.log(textContent);
      return textContent;
    }

Step 4:Difyナレッジに登録

最後は、DifyのAPIを使って、Notionのテキストをナレッジベースに登録します。

NotionページのコンテンツをページごとにDifyナレッジベースに登録する処理をregister_dify_knowledge.gs に記載します。

今後、Notionページごとに更新や削除などができるよう、Difyナレッジのタイトル(ファイル名)はNotionページIDとします。

  1. まず、GASからDifyナレッジベースに接続するために必要な情報をregister_dify_knowledge.gs に記載します。
    • DifyナレッジAPIキー
      image block

      image block
    • DifyナレッジベースID

      ナレッジベースのURLの cloud.dify.ai/datasets/ に続く英数字です。

      https://cloud.dify.ai/datasets/{ナレッジベースID}/documents
    • register_dify_knowledge.gs
      // register_dify_knowledge.gsだけで使う変数
      const DIFY_KNOWLEDGE_TOKEN = 'dataset-*****'; // DifyナレッジAPIキー 
      const DIFY_KNOWLEDGE_ID = '*****'; // DifyナレッジベースID
  2. 次に、DifyナレッジにNotionページのコンテンツを登録する処理を記載します。
    // (3) Difyナレッジに登録
    function uploadToDifyKnowledge(title, content) {
      const url = `https://api.dify.ai/v1/datasets/${DIFY_KNOWLEDGE_ID}/document/create_by_text`;
      const options = {
        method: 'post',
        contentType: 'application/json',
        headers: {
          'Authorization': `Bearer ${DIFY_KNOWLEDGE_TOKEN}`
        },
        payload: JSON.stringify({
          name: title,
          text: content,
          indexing_technique: 'high_quality',
          process_rule: {"mode": "automatic"}
        })
      };
      
      const response = UrlFetchApp.fetch(url, options);
    }

GASの全体像

common.gs

const NOTION_TOKEN = "*****"; // Notionの内部インテグレーションシークレット

// メイン(スプレッドシートで実行ボタンに割り当てる関数)
function main() {
  getChildPageIds();
  syncNotionPagesToDify();
}

store_notion_id.gs

// store_notion_id.gsだけで使う変数
const PAGE_ID = "*****" // Notionページ「GAS接続先」のページID
const FOLDER_ID = "*****" // CSVファイルを保存するフォルダのID

// Notion子ページのIDを取得してCSVに保存する関数
// 1列目: NotionページID
// 2列目: ページタイトル(Difyナレッジ登録に必要ではないが、IDとタイトルの対応を確認するために使用)
function getChildPageIds() {
  const url = `https://api.notion.com/v1/blocks/${PAGE_ID}/children?page_size=100`;

  const options = {
    method: "get",
    headers: {
      "Authorization": `Bearer ${NOTION_TOKEN}`,
      "Notion-Version": "2022-06-28",
      "Content-Type": "application/json"
    }
  };

  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());

  const pageIds = [];

  json.results.forEach(block => {
    if (block.type === "child_page") {
      pageIds.push({
        id: block.id,
        title: block.child_page.title
      });
    }
  });

  saveAsCsv(pageIds);
}

// CSVに保存する関数
function saveAsCsv(data) {
  const csv = data.map(row => `"${row.id}","${row.title}"`).join("\n");
  const folder = DriveApp.getFolderById(`${FOLDER_ID}`);
  const fileName = `notion_child_pages_${new Date().toISOString()}.csv`;

  folder.createFile(fileName, csv, MimeType.CSV);
}

register_dify_knowledge.gs

// register_dify_knowledge.gsだけで使う変数
const DIFY_KNOWLEDGE_TOKEN = 'dataset-*****'; // DifyナレッジAPIキー 
const DIFY_KNOWLEDGE_ID = '*****'; // DifyナレッジベースID

// (1) CSVファイルからIDリストを取得
function getPageIdsFromCsv() {
  const fileId = '*****'; // Googleドライブ上のCSVファイルID
  const file = DriveApp.getFileById(fileId);
  const csv = Utilities.parseCsv(file.getBlob().getDataAsString());
  
  const ids = [];
  for (let i = 0; i < csv.length; i++) { // ヘッダなし
    ids.push(csv[i][0]); // 1列目にNotionのページIDがある前提
  }
  return ids;
}

// (2) Notion APIで本文取得
function fetchNotionPageContent(pageId) {
  const url = `https://api.notion.com/v1/blocks/${pageId}/children`;
  const options = {
    method: 'get',
    headers: {
      'Authorization': `Bearer ${NOTION_TOKEN}`,
      'Notion-Version': '2022-06-28'
    }
  };

  const response = UrlFetchApp.fetch(url, options);
  const data = JSON.parse(response.getContentText());

  const target_blocktypes = [
    "paragraph", 
    "heading_1", 
    "heading_2", 
    "heading_3", 
    "bulleted_list_item", 
    "numbered_list_item"
    ]
  let textContent = '';
  for (const block of data.results) {
    if (!target_blocktypes.includes(block.type)) {
      continue;  // 対象外はスキップ
    }

    // 各タイプごとにrich_text配列を取り出す
    let richTexts = [];

    switch(block.type) {
      case "paragraph":
        if (block.paragraph && block.paragraph.rich_text) {
          richTexts = block.paragraph.rich_text;
        }
        break;

      case "heading_1":
      case "heading_2":
      case "heading_3":
        if (block[block.type] && block[block.type].rich_text) {
          richTexts = block[block.type].rich_text;
        }
        break;

      case "bulleted_list_item":
      case "numbered_list_item":
        if (block[block.type] && block[block.type].rich_text) {
          richTexts = block[block.type].rich_text;
        }
        break;
    }

    // richTextsが空でなければテキストを抽出
    if (richTexts.length > 0) {
      textContent += richTexts.map(t => t.plain_text).join('') + '\n';
    }
  }
  Logger.log(textContent);
  return textContent;
}


// (3) Difyナレッジに登録
function uploadToDifyKnowledge(title, content) {
  const url = `https://api.dify.ai/v1/datasets/${DIFY_KNOWLEDGE_ID}/document/create_by_text`;
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': `Bearer ${DIFY_KNOWLEDGE_TOKEN}`
    },
    payload: JSON.stringify({
      name: title,
      text: content,
      indexing_technique: 'high_quality',
      process_rule: {"mode": "automatic"}
    })
  };
  
  const response = UrlFetchApp.fetch(url, options);
}

// 全体処理
function syncNotionPagesToDify() {
  const ids = getPageIdsFromCsv();
  for (const id of ids) {
    try {
      const content = fetchNotionPageContent(id);
      const title = `${id}`; // タイトルはNotionIDと一致させる
      uploadToDifyKnowledge(title, content);
    } catch (e) {
      Logger.log(`Error with ID ${id}: ${e}`);
    }
  }
}

実行結果

準備

  1. スプレッドシートに実行ボタンを作成して、実行する関数としてmainを割り当てます。
    image block
    image block

  2. 実行ボタンを押すと、Notion子ページIDを取得してからNotionコンテンツをDifyナレッジに登録するまでの一連の流れが実行されます。

    その結果として、以下が作成されます。

    • Notion子ページIDを保存したCSVファイル(Google Driveフォルダ)
      image block
    • Difyナレッジベース
      image block

      image block

おわりに


この記事では、Notionを使ってNotionページのコンテンツを、GASを使ってDifyのナレッジに登録する方法を紹介しました。

今回は、NotionページのコンテンツをDifyナレッジベースに登録するだけの処理を扱いましたが、次のようなカスタマイズが可能です。

  • 「毎日●時」や「毎週▲曜日●時」など定期実行する
  • Notionの子ページ・子データベースを指定の階層までコンテンツとして取得する
  • CSVファイルにNotionページの最終更新日時を追加して、前回登録時から更新があったNotionページに対応するDifyナレッジベースだけを更新する
  • 不要なCSVファイルを削除する

「Notionを使っているが、LLMとの連携が上手くできない」「Difyのナレッジを更新する手間を省きたい」などお困りごとがあれば、ぜひ1度ご相談を頂ければと思います。

参考