Vertex AI RAG Engine を使って Notion の RAG を実装する

音声による概要

この音声概要は AI によって生成されており、誤りを含む可能性があります。

社内の Slack で誰かが質問しているのを見て、それ Notion に書いてあるのにな、 chatbot が答えてくれたらいいのにな、と思ったことはありませんか? 僕はあります。

そんな動機から社内の Slack chatbot に Google Cloud の Vertex AI RAG Engine を使って、Notion の情報を活用した RAG(Retrieval-Augmented Generation)機能を追加してみたので、その際の経験を共有します。

チュートリアル実行以上、プロダクション投入未満な事例の一つとして参考になれば幸いです。

背景:Notion の情報を活用したい

HQ では日々の業務や知識の共有のために Notion を活用しており、さまざまな会社固有の情報が蓄積されています。 しかし、情報量が増えるにつれて目的の情報を見つけるのが難しくなってきました。

2025 年ですから AI がいい感じにやってくれることを期待したいところです。 AI 活用という観点でいうと Notion AI と Notion 公式 MCP サーバーがまず思い浮かびますが、以下のような課題がありイマイチやりたいことに合いませんでした。

  • Notion AI
    • Notion の情報しか使えない。
    • Notion の web サイト上での利用に限られるため社員同士で回答を簡単に共有できない。また API が存在しないため、外部から利用することもできない。
  • MCP サーバー
    • 内部的には Notion API を利用しているため、Notion API の制約を受ける。Notion API の検索 API はタイトルにしかマッチしないため、必然的に MCP サーバーで本文について問い合わせても関連するタイトルのページに書かれた情報しか使われない。

こうなったら自前で RAG を実装するしかありませんね。

RAG の経験・感じていた課題感

HQ のプロダクトでは実験的な機能として一部 RAG を利用しているものがあり、全くの未経験というわけではありませんでした。 その際の経験から、 Notion の RAG を実装するにはいくつかの考慮すべきポイントがあると感じていました。

  • コーパスの単位
    • プロダクトで扱ったファイルは、それぞれが一つのトピックについて書かれた程よい長さだったため、ファイルをそのままベクトル化しても問題がなかった。
    • 一方、社内の Notion のページはさまざまな粒度で書かれており、中には複数のトピックにまたがるような巨大なページも存在する。そのため、ページをそのままベクトル化しても精度が出ない可能性がある。
  • コードの陳腐化
    • AI 関連のツールの進化はめざましく、何かのライブラリを使っていてもあっという間にバージョンが変わってしまう。以前まだ v0 の頃に LangChain を使って RAG を実装したことがありますが、バージョンアップとそれに伴うコードの修正が頻繁に必要で苦労した。
    • RAG そのものは自前では実装せず、できるだけマネージドサービスを呼び出すだけで済ませたい。
  • 根拠の提示
    • 単に RAG で知識を補完してプロンプトを膨らませるだけでなく、回答と情報の結びつきを根拠として示したい。プロンプトに情報を入れること自体は容易だが、この結びつきを示すのは簡単ではない。そのためサービスの側で何らかの形で結びつきを示してくれると助かる。

まとめると「楽してそれなりにちゃんとした RAG を実装したい」ということです。 そんな折に、Google Cloud の Vertex AI RAG Engine の存在を知りました。 HQ では Google Cloud を利用しているため、その観点からも都合が良かったため使ってみることにしました。

Vetex AI RAG Engine の概要

Vertex AI RAG Engine (以後 RAG Engine)は、Google Cloud が提供する RAG を簡単に実現できるマネージドサービスです。 前述の課題に対し、RAG Engine は以下のような形で寄与してくれます。

  • コーパスの単位・長大な記事への対応: RAG Engine では、与えた文書を自動的にチャンク(適切な長さの断片)に分割し、それぞれをベクトル化してインデックス化します。これにより長大なページや複数トピックが混在する文書でも、必要な情報だけを高精度に検索・抽出できます。
  • コードの陳腐化・運用負荷の低減: ベクトルストアやエンベディング、検索、LLM 連携など、RAG 構成に必要な要素を Google Cloud 側でマネージドに提供してくれるため、インフラ運用に悩まされることがありません。API ベースでシンプルに利用できます。
  • 根拠の提示(グラウンディング): RAG Engine はグラウンディングに対応しており、生成した回答の根拠となる情報源や引用元を自動的に紐付けて返してくれます。これにより、回答の信頼性や透明性を担保しやすくなります。

実装

ここでは、RAG Engine を利用して Notion の情報を活用した RAG を実装する際に面倒だったり工夫した点などを紹介します。

Notion からページをとってくる

RAG Engine は主に Cloud Storage に保存されたファイルをコーパスとして利用します。 そのため、Notion のページを Cloud Storage に保存する必要があります。 Notion API を通じてページを取得し、それを Markdown に変換して Cloud Storage に保存する、というのが基本的な流れですが、 Notion API の割り当て制限が厳しく、また API のレスポンス速度の問題もあるため、毎回全てのページを取得することは現実的ではありません。

そこで notion-event-publisher という OSS ツールを開発して Notion の更新・削除を検知し API の呼び出しを最小限に抑えています。 このツールは Notion の検索 API を用いてポーリングし Cloud Pub/Sub にイベントを発行するもので、このイベントをトリガーに Cloud Function を実行し、Cloud Storage を同期します。

NOTE

2024 年 12 月に Notion API に Webhook が追加されましたが、integration の追加・削除を通知するイベントが存在しないためいずれにせよポーリングが必要です。 そのため、現時点では Webhook は使用していません。

一括でファイルをインポートする

RAG Engine では Corpus と呼ばれるリソースを作成し、そこにファイルを追加していくことでコーパスを構築します。 その際に Cloud Storage の URI で blob を指定することで一括でインポートすることができて非常に便利です。

URI が同じであれば、同じファイルを何度インポートしても問題ありません。

TIP

一括インポートする際にチャンクの大きさやオーバラップの大きさを指定することができます。 ただ、適切な値は実験をしないと分からない一方でそこまでやってられないというのが正直なところです。 そのためドキュメントに書かれていたデフォルト値をそのまま使っています。

一方でファイルが無くなったことを検知してはくれないため、ファイルの削除は明示的に行う必要があります。 notion-event-publisher が発行した page-deleted イベントを受け取った際に、RAG Engine の API を呼び出して削除すればいいのですが、ここでちょっとした面倒なポイントがあります。

URI を指定して Corpus からファイルを削除したい

現状 RAG Engine が提供している list_files API では URI による絞り込みができないため、素朴に実装すると毎回全てのファイルを取得して URI に対応するファイルを探す必要があります。 当初はこの方法で実装していましたが、ファイル数が増えるにつれて時間がかかるようになってきました。 今後ますます Corpus に入れるファイルは増えていくはずなので、 URI から RAG ファイルへの転置インデックスを作成することにしました。

また会社のツールセット的な都合もあり Cloud Function 自体は Node.js で実装している一方、 RAG Engine の SDK は Python のものしか存在しません。

というわけで Cloud Storage の URI を受け取って対応する RAG ファイルを削除する delete-rag-corpus-file を Python で実装しました。 こちらも OSS として公開しています。

グラウンディングは Gemini API が全部やってくれる

Vertex AI の Node.js SDK に関しては本家のドキュメントを読んでもらうのが一番良いと思いますが、RAG Engine を使う上で最も重要な部分を抜粋すると以下のようになります。 tools として RAG Corpus を指定して作成したモデルに generateContent を呼び出すと、内部で自動的に RAG Corpus を必要に応じて利用し、そして利用した場合はそれを含めて生成したコンテンツを返してくれます。

const model = vertexAI.getGenerativeModel({
  model: "gemini-2.0-flash",
  tools: [
    {
      retrieval: {
        vertexRagStore: {
          ragResources: [{ ragCorpus }],
          similarityTopK: 10,
        },
      },
    },
  ],
});

await model.generateContent({ contents });

このようにグラウンディングを簡単に実現できるのが便利です。 ただ一方で Cloud Storage の URI を使ってインポートしたファイルを使っている場合、グラウンディングを意味あるものにするためにはもう一工夫必要になってきます。

URI から Notion ページを復元する

先ほどの generateContent の呼び出し結果は例えばこのような JSON で返ってきます。

{
  "content": {
    "role": "model",
    "parts": [
      {
        "text": "株式会社 HQ のミッションは「テクノロジーの力で、自分らしい生き方を支える社会インフラをつくる」です"
      }
    ]
  },
  "groundingMetadata": {
    "groundingChunks": [
      {
        "retrievedContext": {
          "uri": "gs://your-bucket/rag-corpus/1382d3c5-ee95-8040-8688-e38b883550ef.md",
          "title": "1382d3c5-ee95-8040-8688-e38b883550ef.md",
          "text": "【Mission】\nテクノロジーの力で、\n自分らしい生き方を支える社会インフラをつくる\n"
        }
      }
    ],
    "groundingSupports": [
      {
        "segment": {
          "startIndex": 265,
          "endIndex": 357,
          "text": "「テクノロジーの力で、自分らしい生き方を支える社会インフラをつくる」"
        },
        "groundingChunkIndices": [0],
        "confidenceScores": [0.9169535]
      }
    ]
  }
}

この JSON をもとに、一般的な Chat UI のグラウンディングのように以下のような Slack メッセージを組み立てたいと考えた場合、乗り越えなければならない幾つかの問題があることがわかります。

  • 本文中にグラウンディングの結果を挿入する(↓ の [1] の部分)
  • グラウンディングの結果に Notion ページのタイトルとリンクを付与する
期待する出力。グラウンディングの結果が本文中にいい感じに挿入されている
  • LLM の生成したコンテンツである content とグラウンディングの結果である groundingMetadata を自分で結びつけてやらないといけない
  • groundingMetadata の中にある retrievedContext の URI は Cloud Storage の URI であり、この URI からどの Notion ページなのかを復元する必要がある

前者については、最近の LLM は十分に賢いので JSON を渡してにお願いすればいい感じに実装してくれるでしょう。

後者については、 URI から Notion ページを復元するためのマッピングを用意しておく必要があります。 このマッピングは、RAG Engine にファイルをインポートする際に一緒に作成して Cloud Storage の別の場所に保存していて、その都度読み込んで解決するようにしています。

最後の最後、ユーザー体験をより良いものにするための詰めの作業にかかる労力が意外と必要でした。

今後の課題と展望

RAG ファイルの管理

今回は Notion API の制約上、 integration で検索可能な全てのページを RAG Engine の Corpus にインポートしました。 Notion ではページやデータベースごとに integration の接続を個別に管理することができますが、一方で接続したページの子孫となる全てのページが自動的に検索可能となるため、意図しないページが検索対象となる可能性があります。 RAG を用いた AI 機能では与える情報の選定が重要であり、不要な情報を含めると回答の精度や信頼性に悪影響を及ぼす可能性があります。

この問題への対処として、重要なのが、 AI が認識している Notion ページを可視化することだと考えています。 現状 RAG Engine は web UI を提供していないため、可視化は自前で行う必要があります。 まず可視化を行うことで、問題をより明確にし、不要なページを特定することができます。

その上で対象となる Notion ページを無視させる機能を Chatbot 側で実装することを検討しています。

NOTE

Notion 上のページを整理して検索対象をコントロールすることが考えられますが、ページ構造を人力で整理し続けるのは現実的ではないですし、人間にとっての分かりやすさと AI にとって最適な構造が必ずしも一致するわけではありません。

コンテキストを考慮する

質問を投げたユーザーのコンテキストを考慮して、利用する情報を選択することで精度を向上させることを検討しています。 例えばセールスメンバーがやりとりする Slack チャンネルからの質問に対しては、セールス関連の情報を優先的に利用するなどです。

generateContent API は使用する RAG ファイルを指定することができます。 この機能を利用して、ユーザーのコンテキストに応じて RAG ファイルをフィルタリングする事前処理を行うことで、より精度の高い回答を得ることができるのではないかと考えています。

Notion 以外のデータソース

今後の展開として RAG Engine の Corpus に Notion 以外の情報を追加していくことを考えています。 RAG Engine は Google Drive をサポートしているため、まずは Google Drive の情報を追加してみたいと考えています。

まとめ

本記事では、Notion API の制約を乗り越え、RAG Engine を活用した Slack Chatbot のナレッジ検索機能の実装事例を紹介しました。 社内向けの機能としてはひとまず実用的なものができたと思います。

しかし、今後情報量が増えていく中で、より良いユーザー体験を提供するためにはまだまだ改善の余地もあります。

今後も Google Cloud の新機能や各種 AI ツールを活用し、社内の業務効率化やナレッジ共有の向上に努めていきます。