【Google Cloud】Enterprise Searchで社内ドキュメントを検索してみた

2023.08.29

はじめに

新規事業部 山本です。

ChatGPT(OpenAI API)をはじめとしたAIの言語モデル(Large Language Model:以下、LLM)を使用して、チャットボットを構築するケースが増えています。通常、LLMが学習したときのデータに含まれている内容以外に関する質問には回答ができません。そのため、例えばある社内システムに関するチャットボットを作成しようとしても、素のLLMでは質問に対してわからないという回答や異なる知識に基づいた回答が(当然ながら)得られてしまいます。

この問題を解決する方法として、Retrieval Augmented Generation(以下、RAG)という手法がよく使用されます。RAGでは、ユーザからの質問に回答するために必要そうな内容が書かれた文章を検索し、その文章をLLMへの入力(プロンプト)に付け加えて渡すことで、ユーザが欲しい情報に関して回答させることができます。

以前の記事では、RAGを試しに作ってみた内容や、文章を検索するためのベクトル検索の問題点や改良方法について記載しました。

OpenAIのAPIを使って営業資料をベクトル検索するボットをつくってみた | DevelopersIO

ベクトル検索で欲しい情報が得られないときの問題点と改良方法を考えてみた | DevelopersIO

今回は、(上記2つ目の記事の「対策」「方針4:ほかの検索方法も併用する」で述べた)クラウドの文章検索サービス(エンタープライズ検索)を試してみた内容について記載します。具体的には、Google Cloud(GCP)のGen App Builder(Enterprise Search)を使って、社内ドキュメントを検索する方法について説明し、使用感や機能・性能について簡単に記載します。

※ 本記事で紹介しているGoogle Cloud(GCP)のGen App Builder(Enterprise Search)は、Google Cloud’s AI Trusted Tester Programとして使用・評価した内容です。

注意事項

Gen App Builderは登録が必要です。すぐに使用することができず、Google Cloud側の許可を待つ必要があります(2023/08/24現在)。

サービスがGAされたことに伴い、料金について記載を追記し、Trusted Testerに関する記載を削除しました。 Enterprise SearchからVertex AI Searchに変更になる、また、Gen App Builderという名称がなくなる、とアナウンスされましたが、まだ元の表記が残っているドキュメントが多く、分かりにくくなってしまうため、本記事での呼び方は変更していません。

また、現状では日本語未対応となっているので、一部機能が想定外の挙動をする可能性があります(2023/09/06)

概要・用語の整理

本記事では、以下のような用語で説明します。

エンタープライズ検索

検索の種類の一つ。自分が検索した中では、厳密な定義は使う人によって異なっており、正確な定義は見つけられませんでした。本記事では、以下のような特徴を持つ検索として使用しています。

  • さまざまな種類のファイルをインポートできる
    • PDFファイル、オフィス系ドキュメントファイル、テキストファイルなど
  • 複数のデータソースを持つことができ、一括で検索できる
  • 大量のデータをインポートできる

Enterprise Search

Google Cloud(GCP)におけるエンタープライズ検索、またはそれを使用したアプリを指す言葉。Enterprise Searchというサービスはなく、Gen App Builderというサービスでドキュメントを検索する際に自動的に使用されます。

Gen App Builder

Google Cloud(GCP)のサービスの一つ。検索技術や基盤モデル・会話型AIを組み合わせて生成AIのアプリケーションを簡単に作成することができます。

Gen App Builderのコンソールから作成できるEnterprise Searchのアプリケーションとしては、以下の2種類があります

  • 検索:クエリとなるテキストを入力として、インデックス内の文章を検索する
  • レコメンデーション:インデックス内のドキュメントを1つ指定し、似たようなドキュメントを検索する

(この記事では「検索」について記載しています)

(2023/10/17 追記) 名称が変更になり、Gen App BuilderのEnterprise Search(検索)はVertex AI Searchと呼ばれるようになりました。

補足

Conversational AIという機能も、Gen App Builderの中に含まれる形で解説されている場合もありますが、現在、Gen App Builderのコンソールページからは利用できず、Dialogflow CXのコンソールページから利用できます。

まとめると、以下のような関係です(ドキュメントを読んでいてここが少し分かりにくかったので、注意が必要です)。

  • Gen App Builder(生成AIを使ったアプリ全般を指す言葉。概念)
    • Enterprise Search(エンタープライズ検索と生成AIを組み合わせたアプリを指す言葉)

      Gen App Builderのページから利用可能)

      • 検索
      • レコメンデーション
    • Conversational AI(生成AIを使用した会話・チャット形式のアプリを指す言葉)

      (現在はDialogflow CXのページから利用可能。今後Gen App Builderから利用可能になることが想定されている)

高度なLLM機能

Gen App Builderサービスで設定できる項目の一つ。これをオンにすることで、以下の2つの機能を使用することができます。

  • 要約
    • 検索の際に、検索結果をまとめて入力された質問に対する回答を生成する機能
  • マルチターン検索
    • 連続して検索クエリを実行できる機能(新しいクエリを検索するとき、コンテキスト(前のクエリや検索結果)を考慮しながら検索を実行してくれます)

この機能は、Gen App Builderのコンソールページでの「構成」の「ADVANCED」から、ON/OFFすることができます。

1回限りのクエリであり、かつ、要約が不要な(ドキュメントの検索のみをする)場合は、どちらの機能も不要なので、「高度なLLM機能」をオフにしても問題ありません。「高度なLLM機能」をオンしたままだと、2つの機能を使用しない場合でもクエリに追加料金がかかるため、不要な場合はオフにしておいた方が良いです。数分待つ必要がありますが、すぐにオンに戻すことができます。

Google Cloudのドキュメント内の「LLMアドオン(Search LLM Add-On)」と同じものを指します。

Discovery Engine API

Gen App BuilderのEnterprise Searchの「検索」を使用する際、Google Cloudライブラリでは、Discovery Engine APIが該当します。

使い方

ここでは、企業内のドキュメントを検索することを想定して、PDFファイルの情報を検索するアプリケーションを作成する方法について説明します。

同じ新規事業部の和田さん(waddy_u)が記事を書かれているので、基本的な使い方についてはこちらもご覧ください。スクリーンショットなどもあってわかりやすいです。

Zennを例に Gen App Builder の Enterprise Search 活用方法を考察する

上の記事ではWebサイトをインポートしていましたが、本記事ではPDFファイルなどのドキュメントをインポートするため、異なる点があります。具体的には以下のような手順です。

YouTubeのGoogle Cloud Techチャンネルで配信している動画もあります。操作画面の説明が動画でされているので、わかりやすいです。

https://www.youtube.com/watch?v=fY8aOe6H2nw

手順0:データを準備する

以下はローカルのファイルをアップロードする場合の手順です(他の場合でも最終的にCloud Storageにドキュメントデータがある状態になればOKです)。

  • ドキュメントファイルを用意する
    • 今回はNotionのページをエクスポートしたものを用意しました
    • 内部にはPDFファイルが含まれていて、ページの階層構造と同様のディレクトリ構造になっています
    • 全部でPDFファイルが1714件あり、ファイルサイズは合計1.6GBでした。
  • Cloud Storageにバケットを作成する
    • Google CloudコンソールでCloud Storageのページを開き、バケットを作成します
    • (画面の案内に沿って操作すれば作成されます。詳しい手順は他の記事などをご覧ください)
  • Cloud Storageにデータをアップロードする
    • (※1、後で詳しく説明します)

手順1:Gen App Builderを準備する

  • Google CloudコンソールでGen App Builderを開く
    • 上の記事と同様です
  • 新規アプリを作成する
    • 上の記事と同様です
  • 「アプリの作成」
    • 上の記事と同様です
    • 後述のextractive contentが必要な場合は、Enterprise edition featuresをオンにしておきます
  • 「データストアの作成」
    • 「非構造化データ」を選択する
  • Cloud Storageからインデックスにドキュメントをインポートする
    • (※2、後で詳しく説明します)

手順2:検索する

  • クエリを入力してドキュメントを検索する
    • コンソールから検索する場合は、上記の(和田さんの)記事と同様です
    • (※3、Pythonを使ってSDK経由で検索する場合については、後で詳しく説明します)

※1:Cloud Storageにデータをアップロードする

  • gcloud CLIをインストールする
  • シェル(コマンドプロンプト・ターミナルなど)で以下のコマンドを実行してください

    コマンドは、以下のように変更して実行してください

    • OBJECT_LOCATION:ローカルにあるフォルダへのパス
    • DESTINATION_BUCKET_NAME:手順1で作成したCloud Storageのバケットのバケット名
    gcloud storage cp --recursive OBJECT_LOCATION gs://DESTINATION_BUCKET_NAME/

※2:Cloud Storageからインデックスにドキュメントをインポートする

  • Pythonをインストールする
    • 適宜、仮想環境などを作成してください
  • ライブラリをインストールする
    pip install google-cloud-discoveryengine google-cloud-storage
  • 認証情報を取得し設定する

    ※ このJSONファイルは共有しないように注意してください。

  • プログラムを実行する

    • 今回は以下のコードを使用しました。サポートしてくださったGoogleのSho Onumaさんにサンプルを作成していただき、一部を山本が修正したものです。
    • サンプルコード
      from __future__ import annotations
      
      from google.cloud import discoveryengine, storage
      
      # バケット名とprefix を指定すると、ディレクトリ一覧を取得できます
      def list_directories(bucket_name, prefix=""):
          client = storage.Client()
          blobs = client.list_blobs(bucket_name, prefix=prefix)
          files = []
          for b in blobs:
              files.append(b.name)
          dirs = set()
          for file in files:
              tmp = "/".join(file.split("/")[0:-1]) + "/**"
              dirs.add("gs://{}/{}".format(bucket_name, tmp))
          return list(dirs)
      
      # 上記はファイルからディレクトリを特定してワイルドカード指定にしていますが、ファイルパス一覧の取得でもOKです
      def get_all_file_paths(bucket_name: str) -> list:
          bucket = storage.Bucket(client=storage.Client(), name=bucket_name)
          return [file.name for file in bucket.list_blobs() if not file.name.endswith("/")]
      
      def import_documents_sample(
          project_id: str,
          location: str,
          search_engine_id: str,
          data_schema: str = "custom",
          gcs_bucket_name: str | None = None,
          gcs_prefix: str = "",
          bigquery_dataset: str | None = None,
          bigquery_table: str | None = None,
      ) -> str:
          # Create a client
          client = discoveryengine.DocumentServiceClient()
          # The full resource name of the search engine branch.
          # e.g. projects/{project}/locations/{location}/dataStores/{data_store}/branches/{branch}
          parent = client.branch_path(
              project=project_id,
              location=location,
              data_store=search_engine_id,
              branch="default_branch",
          )
      
          if gcs_bucket_name:
              uris = list_directories(gcs_bucket_name, gcs_prefix)
              uris_from_root_splitted = [uri[5:].split("/")[1:] for uri in uris]  # remove "gs://bucket_name/"
              max_depth_from_root = max([len(uri_from_root) for uri_from_root in uris_from_root_splitted])
      
              depth_prefix = len([folder for folder in gcs_prefix.split("/") if folder != ""])
              max_depth_from_prefix = max_depth_from_root - depth_prefix
      
              uris_wildcard = []
              for i in range(max_depth_from_prefix - 1):
                  uri_depth_i_wildcard = f"gs://{gcs_bucket_name}/{gcs_prefix}" + "/**" * (i + 1)
                  uri_depth_i_wildcard = uri_depth_i_wildcard[:-1]
                  uris_wildcard.append(uri_depth_i_wildcard)
              request = discoveryengine.ImportDocumentsRequest(
                  parent=parent,
                  gcs_source=discoveryengine.GcsSource(
                      input_uris=uris_wildcard,
                      data_schema=data_schema,
                  ),
                  # Options: `FULL`, `INCREMENTAL`
                  reconciliation_mode=discoveryengine.ImportDocumentsRequest.ReconciliationMode.INCREMENTAL,
              )
          else:
              request = discoveryengine.ImportDocumentsRequest(
                  parent=parent,
                  bigquery_source=discoveryengine.BigQuerySource(
                      project_id=project_id,
                      dataset_id=bigquery_dataset,
                      table_id=bigquery_table,
                      data_schema=data_schema,
                  ),
                  # Options: `FULL`, `INCREMENTAL`
                  reconciliation_mode=discoveryengine.ImportDocumentsRequest.ReconciliationMode.INCREMENTAL,
              )
      
          # Make the request
          operation = client.import_documents(request=request)
          print(f"Waiting for operation to complete: {operation.operation.name}")
          response = operation.result()
          # Once the operation is complete,
          # get information from operation metadata
          metadata = discoveryengine.ImportDocumentsMetadata(operation.metadata)
          # Handle the response
          print(response)
          print(metadata)
          return operation.operation.name
      
      # 実際の関数実行部分
      import_documents_sample(
          # 書き換えてください。Google CloudのプロジェクトID。
          "sample-project",
          # global固定でOKです。
          "global",
          # 書き換えてください。サーチエンジンのID。「Gen App Builder」のトップページに表示されるアプリの一覧の中の、「ID」です。「名前」ではありません。
          "yamahiro-test_00000000000",
          # pdf などの非構造化データは content を指定します。
          data_schema="content",
          # 書き換えてください。gcs のバケット名。gs:// は不要です
          gcs_bucket_name="yamahiro-test-bucket",
          # prefix. 特定のディレクトリ以下を対象としたい場合、指定できます。
          # gs://example/aaa 以下の場合、 "aaa" と指定します。バケット内のすべてのファイルをインポートする場合、""を指定してください。
          gcs_prefix="folder1/folder2",
      )

    ※ 自分の環境では実行できましたが、動作検証などは行っていないため、使用する際は内容をチェックして使用してください。

補足

コンソール画面でデータをインポートする処理を実行できますが、その際には指定した階層のみのファイルのみがインポートされ、それより深い階層のファイルはインポートされません。これに対処するためには、コンソール画面から階層の深さの回数分、ワイルドカードを指定してインポートを実行するか、今回のようにAPIで複数階層を指定してAPIを1回実行する必要があります。

※3:Pythonでドキュメントを検索する

  • プログラムを実行する
    • サンプルコード
      from google.cloud import discoveryengine
      from pydantic import BaseModel
      
      class SearchParams(BaseModel):
          project_id: str  # ex) "sample-project"
          search_engine_id: str  # ex) "yamahiro-test_00000000000"
          location: str = "global"
          serving_config_id: str = "default_config"
      
      def extract_path_from_link(link: str):
          return "/".join(link[5:].split("/")[1:])  # remove "gs://bucket_name/"
      
      def search(params: SearchParams, query: str):
          client = discoveryengine.SearchServiceClient()
      
          serving_config = client.serving_config_path(
              project=params.project_id,
              location=params.location,
              data_store=params.search_engine_id,
              serving_config=params.serving_config_id,
          )
      
          request = discoveryengine.SearchRequest(
              serving_config=serving_config,
              query=query,
              page_size=10,
              content_search_spec=discoveryengine.SearchRequest.ContentSearchSpec(
                  extractive_content_spec=discoveryengine.SearchRequest.ContentSearchSpec.ExtractiveContentSpec(
                      max_extractive_segment_count=1,
                      max_extractive_answer_count=1,
                  ),
                  snippet_spec=discoveryengine.SearchRequest.ContentSearchSpec.SnippetSpec(
                      return_snippet=True,
                  ),
                  summary_spec=discoveryengine.SearchRequest.ContentSearchSpec.SummarySpec(
                      summary_result_count=5,  # NOTE: maximum 5
                      include_citations=True,
                  ),
              ),
          )
      
          response = client.search(request=request)
      
          search_result = {
              "answer": response.summary.summary_text,
              "retrieved_texts": [
                  {
                      "document_path": extract_path_from_link(result.document.derived_struct_data["link"]),
                      "text": result.document.derived_struct_data["extractive_segments"][0]["content"],
                      # "text": result.document.derived_struct_data["snippets"][0]["snippet"],
                      # "text": result.document.derived_struct_data["extractive_answers"][0]["content"],
                  }
                  for result in response.results
              ],
          }
      
          return search_result
      
      def main():
          search(
              SearchParams(
                  project_id="sample-project",  # 書き換えてください。
                  search_engine_id="yamahiro-test_00000000000",  # 書き換えてください。
              ),
              query="Slackでアンケートを取る方法があると聞いてます。方法を教えてください。",
          )
      
      if __name__ == "__main__":
          main()

    ※ 返答に含まれるパスはprefix起点のものではなく、バケット名起点なのでご注意ください。

    ※ 自分の環境では実行できましたが、動作検証などは行っていないため、使用する際は内容をチェックして使用してください。

補足1

リクエストの設定(26~43行目)によって、レスポンスに含まれるデータが変わります。データは以下の種類があります。詳細はこちらのGoogle Cloudのドキュメントにかかれています。下のサンプル結果はドキュメントからの引用です。

質問例:"生成 AI アプリ ビルダーとは何ですか?"

  • テキストの該当箇所(オリジナルの文章の一部・そのまま)
    • Snippet

      クエリのキーワードなどに該当した部分のテキストです

      例:To enable this, we are announcing our new Generative AI App Builder, the fastest way for developers to jumpstart the creation of gen apps such as bots, ...

    • Extractive content

      該当箇所の近辺のテキスト。これを利用するには、Appを作成する際に「Enterprise edition features」をオンにしておく必要があります

      • Extractive answer

        回答として使用できうる部分のテキスト。

        例:Generative AI App Builder を使用すると、開発者はボット、チャット インターフェイス、カスタム検索エンジン、デジタル アシスタントなどの新しいエクスペリエンスを迅速に提供できます。開発者は Google の基盤モデルに API アクセスでき、すぐに使用できるテンプレートを使用して数分から数時間で Gen アプリの作成を開始できます。

      • Extractive segment

        質問の回答として使用できる情報が含まれている部分のテキスト。

        RAGの用途としては、この部分を使うのが良さそうです

        例:企業や政府も、この新しい AI テクノロジーを使用して、顧客、パートナー、従業員のやり取りをより効果的かつ有益なものにしたいと考えています。これを可能にするために、新しい Generative AI App Builder を発表します。

        Generative AI App Builder を使用すると、開発者はボット、チャット インターフェイス、カスタム検索エンジン、デジタル アシスタントなどの新しいエクスペリエンスを迅速に提供できます。開発者は Google の基盤モデルに API アクセスでき、すぐに使用できるテンプレートを使用して数分から数時間で Gen アプリの作成を開始できます。Generative AI App Builder を使用すると、開発者は次のことも行うことができます。

        ・組織データと情報検索技術を組み合わせて、適切な答えを提供します。 ・テキストだけでなく検索して応答します。 ・自然な会話と構造化されたフローを組み合わせます。 ・単に通知するだけではなく、取引を行ってください。

  • 要約(検索結果を元に、生成AIがクエリの質問に対して考えた回答)

    これを利用するには「高度なLLM機能」をオンにしておく必要があります。

    • summary

プログラムでは、以下のように設定することでレスポンスに含めることがdけいます。

  • extractive_content_spec(31~34行目)
    • max_extractive_segment_count(32行目)

      値の上限は1なので、必要である場合は1を設定してください。

    • max_extractive_answer_count(33行目)

  • snippet_spec(35~37行目)

    • return_snippet(36行目)

      これをTrueにするとレスポンスにsnippetが含まれるようになります。

  • summary_spec(38~41行目)

    • summary_result_count(39行目)

      検索結果の上位から、この数の結果を利用して回答を生成します。値の上限は5です。

    • include_citations(40行目)

      これをTrueにすると、summaryのテキスト内で引用(例:”[0]”)を含めた回答になります。

APIのリファレンスなどは以下のリンクなどにありますが、記載されていない内容も多いので、直接コード(パラメータのクラス定義など)を見り、実行したときのエラーを読んだ方が速く解決することが多かったです。

https://cloud.google.com/python/docs/reference/discoveryengine/latest

https://cloud.google.com/python/docs/reference/discoveryengine/latest/google.cloud.discoveryengine_v1.services.search_service.pagers.SearchPager

補足2

リクエストにsummaryのパラメータを設定しても、クエリのテキストによっては、response.summary.summary_textが空の文字列になっている場合があります。おそらくクエリの内容に該当するドキュメントを検索できなかったケースだと思われます。

補足3

デフォルトの設定では、レスポンス内のドキュメントはクエリに関連度が高い順に並んでいます。別の順で返してほしい場合は、SearchRequestのorder_byというパラメータを設定すれば良いようです。詳細についてはドキュメントなどをご覧ください。

結果(レスポンス)

クラスメソッドのNotionにある社内公式情報というページ以下をPDFファイルとしてエクスポートし、上記の手順でインポートしました。

「Slackでアンケートを取る方法があると聞いてます。方法を教えてください。」というクエリで検索し、上位※3のsearch_resultを表示すると、以下のようになりました(document_pathや、他の項目に社内の情報が含まれていそうだったので、省略しています)。この質問はFAQに含まれているもので、そのFAQに該当するページが一番上に表示されました。retrieved_textsは10件含まれていました(page_sizeに指定した値=10と同じです)。

{
  "answer": "はい、SlackにはPollyというアプリがあり、このアプリを使ってアンケートを取ることができます。Pollyはチャネルに招待して、質問と選択肢を入力するだけで簡単にアンケートを作成することができます。アンケートの結果は、チャネルに公開することも、Pollyのダッシュボードで確認することもできます 。",
  "retrieved_texts": [
    {
      "document_path": "社内公式情報/【FAQ】.../.../...pdf",
      "text": "Slackでアンケートを取る⽅法があると聞いてます。⽅法を教えてください。\n\n1\n\nSlackでアンケートを取る⽅法が\nあると聞いてます。⽅法を教えて\nください。\n\nタグ\n\nSlack アンケート\n\n最終更新⽇時\n\n【回答】\n\n以下 ⼿順書ご参照ください。\n\nはじめに\n\nSlack には⾊々な拡張機能があり、アンケートを取る⽅法はいろいろとあると思い\nますが、このページは Polly を⽤いた⽅法を紹介します。\n\n準備\n\n利⽤するにはアプリ「Polly」を チャネルに招待 する必要があります。既にPollyが\n参加しているかどうかはメンバーリストから確認できます。\n参加させる場合は以下を⼊⼒し招待します。\n\n/invite @Polly\n\n招待に成功すると、以下のように表⽰されます。\n\n使い⽅\nアンケートを作成\nPollyに質問⽂と選択肢を伝えるだけです。\n\n@2023年2⽉25⽇ 20:36"
    },
    {
       ...
    },
    ...
    {
       ...
    }
  ]
}

FAQにも社内情報にも無い質問として、「ChatGPTのFunctionCallingの実装方法を教えてください」で検索すると、該当するドキュメントがないようで、以下のように結果になりました。

{"answer": "", "retrieved_texts": []}

性能・コストなど

検索精度

他にも、FAQに登録されている内容を質問をしたら、FAQの該当ページがほとんどのケースで一番上に含まれレスポンスが返され、他のケースでも上位に含まれるレスポンスが返されました。

また、少し言葉を変えても(例:「Notionで社外の人がページ編集・閲覧をすることはできるのでしょうか?」→「Notionでクラスメソッド以外の人が、ページの内容を変更したり見たりをすることはできるのでしょうか?」)同様のレスポンスになりました。

処理時間(レイテンシー)

searchメソッドを100回連続で実行した際の時間は(※3のプログラムの45行目をfor文章で100実行)、35.3秒でした。1回あたり353[ms]という計算になり、ユーザの質問を検索するケースにおいては十分短い時間と言えそうです。

処理性能(スループット)

Gen App Builder(Enterprise Search)のQuotaはこちらのページにかかれています。

https://cloud.google.com/generative-ai-app-builder/quotas

使用できるドキュメント数など全体的に十分な値で、ほとんどのユースケースにおいて気にならない印象です。

一番気になる検索のクエリのレートも300回/分であり、十分なレベルだと思います。(この値は、他社のエンタープライズ検索サービスよりも1桁~2桁ほど高いです。他社サービスの中ではクエリレートが公表されていないケースもありました。)

Search requests per minute per project:300

また、このレートはサポート経由で quota 引き上げ申請を行うことが可能です。

https://cloud.google.com/generative-ai-app-builder/quotas#request_a_quota_increase

対応ファイル

今回利用した非構造化データ(unstructured data)のインポートでは、HTML・PDFのテキスト部分・TXTがサポートされており、PPTX・DOCXはプレビューとして利用可能です。

Unstructured apps support documents in HTML, PDF with embedded text, and TXT format. PPTX and DOCX formats are available in Preview.

https://cloud.google.com/generative-ai-app-builder/docs/enterprise-search-introduction#unstructured-data

料金

pricingのページに記載されています。

https://cloud.google.com/generative-ai-app-builder/pricing

Enterprise Searchを利用する上で、かかる料金は以下の2種類です。(2023/09/06現在)

  • インデックス料金(Data Index pricing)
    • 料金:毎月 5$ / GB

    • インデックスに保持されているデータの容量に対する料金

    • クエリを使用しなくても固定でかかる料金

    • (実際にインデックスのデータ量どれくらいかを、Gen App Builderのコンソールから確認する方法は見つけられませんでした)

    • 10GBまでは無料(Free quota of 10 GiB per month provided)

  • クエリ料金(Enterprise Search pricing)

    • クエリに対する従量課金

    • Search Standard EditionとEnterprise Editionの2種類がある

    • 料金:2$ / 1000 query(Standard)

    • 料金:4$ / 1000 query(Enterprise)

    • 前述の「高度なLLM機能」をオンにすると、追加の料金がかかる(その機能を使わないクエリ・APIに対しても)

    • 料金:+4$ / 1000 query

今回使用したデータの例で、仮に月2000回クエリを利用した場合、毎月の料金は以下のように計算されます。

  • インデックス料金
    • 1.6GB * 5$ / GB = 8$

    • (実際に課金対象となるデータサイズがちょっとわからなかったため、元のデータサイズで仮の計算をしました。正確な料金は異なる可能性があります)

  • クエリ料金(Enterpriseの場合、高度なLLM機能を使用しない場合)

    • 2000 query * 4$ / 1000 query = 8$
  • 合計
    • 16$ = 2320円(月額)(1$ = 145円の場合)

正確には、インデックス料金は10GBまでは無料なので、他のインデックスとの合計で10GBを超えていなければ、月額8$ = 1160円になる計算です。

他のエンタープライズ検索サービスでは月10万円するケースもあり、比較すると2桁ほど安くかなり使いやすい印象です。

考察

自作でベクトル検索するインデックスやサーバを作成する場合と比べて、以下の点にメリットがありそうです。

  • 検索部分をマネージドなサービスに置き換えることができる
    • インデックス化機能・検索機能を自分で実装する手間がなくなる
    • インスタンス・インフラ周りの管理が不要
    • データ更新の手間がすくない(SDKでデータを削除・再インポートするだけで、インデックスを更新できる)
  • 検索精度が(おそらく)高い
    • Google CloudのEnterprise Searchでは、セマンティックな検索だけなく他の検索も組み合わせている、とされているので、単純なベクトル検索のインデックスより精度が高いことが予想されます
    • (厳密な比較はしていません)

こうした構築・運用や精度面から、検索部分をエンタープライズ検索に置き換えることで、RAGを改良できることが期待されます。

まとめ

クラウドのエンタープライズ検索サービスとして、Google CloudのGen App Builderを通してEnterprise Searchを試してみました。

注意点としては、現状GA前のサービスであること、登録・許可が必要ですぐに使用できないことを考慮した方が良さそうです。

使用感としては、インポート時のパスを工夫する必要があるのが少し手間ですが、大量のドキュメントを一括でインポートでき、短時間で処理が終了し、使い勝手が良かったです。

検索としては、精度が良い・処理性能が高いというメリットがあり、利用しやすい印象をもちました。

料金が大変安いので、プロトタイプで試しやすい点、実際の運用コストも低く抑えられる点が、大きなメリットです。

RAGの検索の部分をエンタープライズ検索に置き換えることで、回答生成の精度が向上することを見込めそうです。今後、試してみたいと思います。

謝辞

今回の作業において、Google Cloud エンジニアのSho Onumaさん(https://zenn.dev/sonuma)に多くのサポートしていただきました。大変ありがとうございました。

参考にさせていただいたサイト・ページ

https://zenn.dev/google_cloud_jp/articles/google-cloud-generative-ai

https://cloud.google.com/generative-ai-app-builder/docs/enterprise-search-introduction#unstructured-data

https://io.google/2023/program/27cce05f-df4c-4ab2-9a59-5b466bdae0f9/intl/ja/