📚

Gemini File Search Tool触ってみた

に公開

こんにちは、KozokaAIの前田です。

先日部内の雑談で「Geminiのファイル検索Toolが爆速でRAGを構築するのに良さそう」と盛り上がりました。
そこで実際にToolを触り、その挙動を検証してみた内容をまとめます。

Gemini APIはFunction Callingに対応しており、Google提供の組み込みToolやユーザーが作成するカスタムTool呼び出しが可能です。
今回はその中でも、File Search Toolを触ってチャンキングの設定やフィルタリングの柔軟度を試してみた結果をまとめた記事です。
https://ai.google.dev/gemini-api/docs/file-search

検証環境

レスポンス形式をjsonでサクッと見てみたかったのでjs版のSDKを利用しています。

  • gemini-2.5-flash
  • Node.js 20 + @google/genai 1.30.0
  • テストデータ: IPA公開のPDF4種類(10大脅威2024/2023、DX白書、ソフトウェア開発分析データ集)
検証用コード

結論

項目 できる/できない
チャンクサイズの指定
取得チャンク数の指定(top_k)
メタデータでのフィルタリング
複数ストアの横断検索
チャンクの分割位置の確認 ×
類似度スコアの取得 ×
チャンクIDやページ番号の取得 ×

検証項目

チャンキングの設定

アップロード時のchunkingConfigを指定することで、操作ができました。

async function uploadFile(storeName, filePath, config = {}) {
  const operation = await ai.fileSearchStores.uploadToFileSearchStore({
    fileSearchStoreName: storeName,
    file: filePath,
    config,
  });

  console.log(`Uploaded: ${path.basename(filePath)}`);
  return operation;
}

await uploadFile(store.name, path.join(DATA_DIR, "ipa_10threats_2024.pdf"), {
displayName: "10大脅威2024",
chunkingConfig: {
    whiteSpaceConfig: {
    maxTokensPerChunk: 200, // min: 21, max: 512
    maxOverlapTokens: 40, // min: 0, max: maxTokens - 1
    },
},
});

気になるのはmaxTokensPerChunkとmaxOverlapTokensの境界値ですが、二分探索でAPIを実際に叩いて調査したところ、下記のようになっていました。
投入するドキュメントの種類によるとは思いますが、一般的な資料等であれば問題ない値だと思います。

パラメータ 説明 min max
maxTokensPerChunk チャンクの最大トークン数 21 512
maxOverlapTokens チャンク間の重複トークン数 0 maxTokens - 1

ファイルのメタデータの管理

ファイルアップロード時ファイルにKey-Value形式のメタデータを追加することで、サーチクエリ実行時にフィルタリングをかけることができます。
SDK型定義曰く最大20個のメタデータが付与できるそうです。

A Document can have a maximum of 20 CustomMetadata.

SDK型定義

詳しくは公式のドキュメントを参考にするのが良いとは思うのですが、下記のようにメタデータの付与とフィルタリングをかけたクエリがかけられるそうです。

全然本題とは逸れますが、検索時のフィルタリングの構文はAIP-160形式を用いるそうで、Googleが提唱するAPI設計指針のFilteringの構文だそう。
API設計時の指針として勉強になりますね。
https://google.aip.dev/160

アップロード

await uploadFile(store.name, path.join(DATA_DIR, "ipa_10threats_2024.pdf"), {
  displayName: "10大脅威2024",
  customMetadata: [
    { key: "category", stringValue: "security" },
    { key: "year", numericValue: 2024 },
  ],
});
await uploadFile(store.name, path.join(DATA_DIR, "ipa_dx_whitepaper.pdf"), {
  displayName: "DX白書",
  customMetadata: [
    { key: "category", stringValue: "dx" },
    { key: "year", numericValue: 2023 },
  ],
});

クエリ

async function search(storeNames, query, options = {}) {
  const response = await ai.models.generateContent({
    model: "gemini-2.5-flash",
    contents: query,
    config: {
      tools: [
        {
          fileSearch: {
            fileSearchStoreNames: storeNames,
            ...options,
          },
        },
      ],
    },
  });
  return response;
}

// フィルタなし
let response = await search([store.name], "組織の課題は?");

// category="security" でフィルタ
response = await search([store.name], "組織の課題は?", {
  metadataFilter: 'category = "security"',
});

// year > 2023 でフィルタ
response = await search([store.name], "最新の脅威は?", {
  metadataFilter: "year > 2023",
});

複数ストアの横断検索

アップロードするファイルを永続化させるためには、ストアに対してアップロードする必要があります。このストアに対して横断的に検索をかけることも可能でした。

具体的にはfileSearchStoreNames に複数のストア名を渡して検索させるだけです。

// 2種類のストアを作る(セキュリティストア・ビジネスストア)
const storeSecurity = await createStore("security_docs");
const storeBusiness = await createStore("business_docs");

// ストアを分けてアップロード
await uploadFile(
  storeSecurity.name,
  path.join(DATA_DIR, "ipa_10threats_2024.pdf"),
  { displayName: "10大脅威2024" }
);

await uploadFile(
  storeBusiness.name,
  path.join(DATA_DIR, "ipa_dx_whitepaper.pdf"),
  { displayName: "DX白書" }
);

// セキュリティストアのみ
let response = await search([storeSecurity.name], "組織の課題は?");

// 両方のストアを横断検索
response = await search(
  [storeSecurity.name, storeBusiness.name],
  "組織の課題は?"
);

所感

ストアを作ることやフィルタリングを行うことで簡易なRAGシステムを作ることはできましたが、いわゆる親子関係のチャンキングの作成等の高度なことをやりたい場合は自前でRAGを作成する必要がありそう。
「とりあえずPDFを突っ込んで質問に答えさせたい」という用途には十分使える。メタデータフィルタと複数ストアを組み合わせれば、それなりに柔軟な検索システムが作れそう。
一方で、検索精度のチューニングは難しい。類似度スコアが見えないので「なぜこのチャンクが選ばれたか」が分からない。検索結果がイマイチな時に打てる手が限られる。
本番で使うならメタデータ設計を事前にしっかりやっておくことが重要。

株式会社kozokaAI 開発チーム

Discussion