google-gemini-file-search by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill google-gemini-file-searchGoogle Gemini 文件搜索是一个完全托管的 RAG 系统。上传文档(支持 100 多种格式:PDF、Word、Excel、代码等)并使用自然语言进行查询——自动分块、嵌入、语义搜索和引用。
此技能提供的内容:
在 https://aistudio.google.com/apikey 创建 API 密钥
免费层限制:
付费层定价:
最低版本: Node.js 18+(推荐 v20+)
node --version # 应 >=18.0.0
npm install @google/genai
# 或
pnpm add @google/genai
# 或
yarn add @google/genai
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
当前稳定版本: 1.30.0+(使用 npm view @google/genai version 验证)
⚠️ 重要提示: 文件搜索 API 需要 @google/genai v1.29.0 或更高版本。早期版本不支持文件搜索。该 API 在 v1.29.0 版本(2025 年 11 月 5 日)中添加。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
此技能可预防实现文件搜索时遇到的 12 个常见错误:
症状:
Error: Documents cannot be modified after indexing
原因: 文档一旦被索引就不可变。没有 PATCH 或 UPDATE 操作。
预防: 使用删除+重新上传模式进行更新:
// ❌ 错误:尝试更新文档(无此 API)
await ai.fileSearchStores.documents.update({
name: documentName,
customMetadata: { version: '2.0' }
})
// ✅ 正确:先删除再重新上传
const docs = await ai.fileSearchStores.documents.list({
parent: fileStore.name
})
const oldDoc = docs.documents.find(d => d.displayName === 'manual.pdf')
if (oldDoc) {
await ai.fileSearchStores.documents.delete({
name: oldDoc.name,
force: true
})
}
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('manual-v2.pdf'),
config: { displayName: 'manual.pdf' }
})
症状:
Error: Quota exceeded. Expected 1GB limit, but 3.2GB used.
原因: 存储计算包括输入文件 + 嵌入 + 元数据。总存储空间 ≈ 3 倍输入大小。
预防: 上传前计算存储空间:
// ❌ 错误:假设存储空间 = 文件大小
const fileSize = fs.statSync('data.pdf').size // 500 MB
// 预期使用 500 MB → 错误
// ✅ 正确:考虑 3 倍乘数
const fileSize = fs.statSync('data.pdf').size // 500 MB
const estimatedStorage = fileSize * 3 // 1.5 GB(嵌入 + 元数据)
console.log(`预计存储空间:${estimatedStorage / 1e9} GB`)
// 上传前检查是否在配额内
if (estimatedStorage > 1e9) {
console.warn('⚠️ 文件可能超出免费层 1 GB 限制')
}
症状: 检索质量差、不相关的结果,或上下文在句子中间被截断。
原因: 默认分块可能不适合您的内容类型。
预防: 使用推荐的分块策略:
// ❌ 错误:未经测试就使用默认值
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('docs.pdf')
// 默认分块可能太大或太小
})
// ✅ 正确:为精确度配置分块
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('docs.pdf'),
config: {
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500, // 更小的块 = 更精确的检索
maxOverlapTokens: 50 // 10% 重叠防止上下文丢失
}
}
}
})
分块指南:
症状:
Error: Maximum 20 custom metadata key-value pairs allowed
原因: 每个文档最多只能有 20 个元数据字段。
预防: 设计紧凑的元数据模式:
// ❌ 错误:元数据字段太多
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('doc.pdf'),
config: {
customMetadata: {
doc_type: 'manual',
version: '1.0',
author: 'John Doe',
department: 'Engineering',
created_date: '2025-01-01',
// ... 还有 18 个字段 → 错误!
}
}
})
// ✅ 正确:使用分层键或 JSON 字符串
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('doc.pdf'),
config: {
customMetadata: {
doc_type: 'manual',
version: '1.0',
author_dept: 'John Doe|Engineering', // 合并相关字段
dates: JSON.stringify({ // 或对复杂数据使用 JSON
created: '2025-01-01',
updated: '2025-01-15'
})
}
}
})
症状: 上传 10 GB 文档后意外收到 $375 账单。
原因: 索引成本是一次性的,但按输入令牌计算($0.15/100 万令牌)。
预防: 索引前估算成本:
// ❌ 错误:没有成本估算
await uploadAllDocuments(fileStore.name, './data') // 上传了 10 GB → $375 意外
// ✅ 正确:提前计算成本
const totalSize = getTotalDirectorySize('./data') // 10 GB
const estimatedTokens = (totalSize / 4) // 粗略估算:1 令牌 ≈ 4 字节
const indexingCost = (estimatedTokens / 1e6) * 0.15
console.log(`预计索引成本:$${indexingCost.toFixed(2)}`)
console.log(`预计存储空间:${(totalSize * 3) / 1e9} GB`)
// 继续前确认
const proceed = await confirm(`继续索引?成本:$${indexingCost.toFixed(2)}`)
if (proceed) {
await uploadAllDocuments(fileStore.name, './data')
}
成本示例:
症状: 上传后立即查询无结果,或索引不完整。
原因: 文件上传是异步处理的。必须轮询操作直到 done: true。
预防: 始终使用超时和回退轮询操作状态:
// ❌ 错误:假设上传是即时的
const operation = await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('large.pdf')
})
// 立即查询 → 无结果!
// ✅ 正确:轮询直到索引完成,带超时
const operation = await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('large.pdf')
})
// 带超时和回退的轮询
const MAX_POLL_TIME = 60000 // 60 秒
const POLL_INTERVAL = 1000
let elapsed = 0
while (!operation.done && elapsed < MAX_POLL_TIME) {
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL))
elapsed += POLL_INTERVAL
try {
operation = await ai.operations.get({ name: operation.name })
console.log(`索引进度:${operation.metadata?.progress || 'processing...'}`)
} catch (error) {
console.warn('轮询失败,假设完成:', error)
break
}
}
if (operation.error) {
throw new Error(`索引失败:${operation.error.message}`)
}
// ⚠️ 警告:对于大文件,operations.get() 可能不可靠
// 如果达到超时,手动验证文档是否存在
if (elapsed >= MAX_POLL_TIME) {
console.warn('轮询超时 - 手动验证文档')
const docs = await ai.fileSearchStores.documents.list({ parent: fileStore.name })
const uploaded = docs.documents?.find(d => d.displayName === 'large.pdf')
if (uploaded) {
console.log('✅ 尽管轮询超时,但找到了文档')
} else {
throw new Error('上传失败 - 未找到文档')
}
}
console.log('✅ 索引完成:', operation.response?.displayName)
来源: https://ai.google.dev/api/file-search/file-search-stores#uploadtofilesearchstore, GitHub Issue #1211
症状:
Error: Cannot delete store with documents. Set force=true.
原因: 包含文档的存储需要 force: true 才能删除(防止意外删除)。
预防: 删除非空存储时始终使用 force: true:
// ❌ 错误:尝试删除包含文档的存储
await ai.fileSearchStores.delete({
name: fileStore.name
})
// 错误:无法删除包含文档的存储
// ✅ 正确:使用强制删除
await ai.fileSearchStores.delete({
name: fileStore.name,
force: true // 删除存储和所有文档
})
// 替代方案:先删除文档
const docs = await ai.fileSearchStores.documents.list({ parent: fileStore.name })
for (const doc of docs.documents || []) {
await ai.fileSearchStores.documents.delete({
name: doc.name,
force: true
})
}
await ai.fileSearchStores.delete({ name: fileStore.name })
症状:
Error: File Search is only supported for Gemini 3 Pro and Flash models
原因: 文件搜索需要 Gemini 3 Pro 或 Gemini 3 Flash。不支持 Gemini 2.x 和 1.5 模型。
预防: 始终使用 Gemini 3 模型:
// ❌ 错误:使用 Gemini 1.5 模型
const response = await ai.models.generateContent({
model: 'gemini-1.5-pro', // 不支持!
contents: 'What is the installation procedure?',
config: {
tools: [{
fileSearch: { fileSearchStoreNames: [fileStore.name] }
}]
}
})
// ✅ 正确:使用 Gemini 3 模型
const response = await ai.models.generateContent({
model: 'gemini-3-flash', // ✅ 支持(快速,成本效益高)
// 或
// model: 'gemini-3-pro', // ✅ 支持(更高质量)
contents: 'What is the installation procedure?',
config: {
tools: [{
fileSearch: { fileSearchStoreNames: [fileStore.name] }
}]
}
})
症状:
groundingChunks[0].title === null // 未显示文档来源
原因: 在 @google/genai v1.34.0 之前的版本中,当以 Blob 对象(而非文件路径)上传文件时,SDK 会丢弃 displayName 和 customMetadata 配置字段。
预防:
// ✅ 正确:升级到 v1.34.0+ 以自动修复
npm install @google/genai@latest // v1.34.0+
await ai.fileSearchStores.uploadToFileSearchStore({
name: storeName,
file: new Blob([arrayBuffer], { type: 'application/pdf' }),
config: {
displayName: 'Safety Manual.pdf', // ✅ 现在已保留
customMetadata: { version: '1.0' } // ✅ 现在已保留
}
})
// ⚠️ v1.33.0 及更早版本的解决方法:使用可恢复上传
const uploadUrl = `https://generativelanguage.googleapis.com/upload/v1beta/${storeName}:uploadToFileSearchStore?key=${API_KEY}`
// 步骤 1:在请求体中初始化并包含 displayName
const initResponse = await fetch(uploadUrl, {
method: 'POST',
headers: {
'X-Goog-Upload-Protocol': 'resumable',
'X-Goog-Upload-Command': 'start',
'X-Goog-Upload-Header-Content-Length': numBytes.toString(),
'X-Goog-Upload-Header-Content-Type': 'application/pdf',
'Content-Type': 'application/json'
},
body: JSON.stringify({
displayName: 'Safety Manual.pdf' // ✅ 适用于可恢复上传
})
})
// 步骤 2:上传文件字节
const uploadUrl2 = initResponse.headers.get('X-Goog-Upload-URL')
await fetch(uploadUrl2, {
method: 'PUT',
headers: {
'Content-Length': numBytes.toString(),
'X-Goog-Upload-Offset': '0',
'X-Goog-Upload-Command': 'upload, finalize',
'Content-Type': 'application/pdf'
},
body: fileBytes
})
症状:
response.candidates[0].groundingMetadata === undefined
// 即使配置了 fileSearch 工具
原因: 当使用 responseMimeType: 'application/json' 进行结构化输出时,API 会忽略 fileSearch 工具,并且不返回任何引用元数据,即使使用 Gemini 3 模型也是如此。
预防:
// ❌ 错误:结构化输出覆盖了引用
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Summarize guidelines',
config: {
responseMimeType: 'application/json', // 丢失引用
tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }]
}
})
// ✅ 正确:两步法
// 步骤 1:获取有引用的文本响应
const textResponse = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Summarize guidelines',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }]
}
})
const grounding = textResponse.candidates[0].groundingMetadata
// 步骤 2:在提示词中转换为结构化格式
const jsonResponse = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: `Convert to JSON: ${textResponse.text}
Format:
{
"summary": "...",
"key_points": ["..."]
}`,
config: {
responseMimeType: 'application/json',
responseSchema: {
type: 'object',
properties: {
summary: { type: 'string' },
key_points: { type: 'array', items: { type: 'string' } }
}
}
}
})
// 合并结果
const result = {
data: JSON.parse(jsonResponse.text),
sources: grounding.groundingChunks
}
症状:
Error: "Search as a tool and file search tool are not supported together"
Status: INVALID_ARGUMENT
原因: Gemini API 不允许在同一请求中同时使用 googleSearch 和 fileSearch 工具。
预防:
// ❌ 错误:组合搜索工具
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'What are the latest industry guidelines?',
config: {
tools: [
{ googleSearch: {} },
{ fileSearch: { fileSearchStoreNames: [storeName] } }
]
}
})
// ✅ 正确:使用独立的专业代理
async function searchWeb(query: string) {
return ai.models.generateContent({
model: 'gemini-3-flash',
contents: query,
config: { tools: [{ googleSearch: {} }] }
})
}
async function searchDocuments(query: string) {
return ai.models.generateContent({
model: 'gemini-3-flash',
contents: query,
config: { tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }] }
})
}
// 根据查询类型编排
const needsWeb = query.includes('latest') || query.includes('current')
const response = needsWeb
? await searchWeb(query)
: await searchDocuments(query)
症状: 使用元数据字段时,无法将批处理响应与请求关联。
原因: 当使用包含 metadata 字段的 InlinedRequest 的批处理 API 时,相应的 InlinedResponse 不会返回元数据。
预防:
// ❌ 错误:期望响应中包含元数据
const batchRequest = {
metadata: { key: 'my-request-id' },
contents: [{ parts: [{ text: 'Question?' }], role: 'user' }],
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }]
}
}
const batchResponse = await ai.batch.create({ requests: [batchRequest] })
console.log(batchResponse.responses[0].metadata) // ❌ undefined
// ✅ 正确:使用数组索引进行关联
const requests = [
{ metadata: { id: 'req-1' }, contents: [...] },
{ metadata: { id: 'req-2' }, contents: [...] }
]
const responses = await ai.batch.create({ requests })
// 按索引映射(不理想但有效)
responses.responses.forEach((response, i) => {
const requestMetadata = requests[i].metadata
console.log(`Response for ${requestMetadata.id}:`, response)
})
社区验证: 维护者已确认,内部 bug 已提交。
import { GoogleGenAI } from '@google/genai'
import fs from 'fs'
// 使用 API 密钥初始化客户端
const ai = new GoogleGenAI({
apiKey: process.env.GOOGLE_API_KEY
})
// 验证 API 密钥是否已设置
if (!process.env.GOOGLE_API_KEY) {
throw new Error('GOOGLE_API_KEY 环境变量是必需的')
}
// 创建一个存储(文档的容器)
const fileStore = await ai.fileSearchStores.create({
config: {
displayName: 'my-knowledge-base', // 人类可读的名称
// 可选:添加存储级别的元数据
customMetadata: {
project: 'customer-support',
environment: 'production'
}
}
})
console.log('创建的存储:', fileStore.name)
// 输出:fileSearchStores/abc123xyz...
查找现有存储:
// 列出所有存储(分页)
const stores = await ai.fileSearchStores.list({
pageSize: 20 // 每页最多 20 个
})
// 按显示名称查找
let targetStore = null
let pageToken = null
do {
const page = await ai.fileSearchStores.list({ pageToken })
targetStore = page.fileSearchStores.find(
s => s.displayName === 'my-knowledge-base'
)
pageToken = page.nextPageToken
} while (!targetStore && pageToken)
if (targetStore) {
console.log('找到现有存储:', targetStore.name)
} else {
console.log('未找到存储,正在创建新的...')
}
单文件上传:
const operation = await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('./docs/manual.pdf'),
config: {
displayName: 'Installation Manual',
customMetadata: {
doc_type: 'manual',
version: '1.0',
language: 'en'
},
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500,
maxOverlapTokens: 50
}
}
}
})
// 轮询直到索引完成
while (!operation.done) {
await new Promise(resolve => setTimeout(resolve, 1000))
operation = await ai.operations.get({ name: operation.name })
}
console.log('✅ 已索引:', operation.response.displayName)
批量上传(并发):
const filePaths = [
'./docs/manual.pdf',
'./docs/faq.md',
'./docs/troubleshooting.docx'
]
// 并发上传所有文件
const uploadPromises = filePaths.map(filePath =>
ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream(filePath),
config: {
displayName: filePath.split('/').pop(),
customMetadata: {
doc_type: 'support',
source_path: filePath
},
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500,
maxOverlapTokens: 50
}
}
}
})
)
const operations = await Promise.all(uploadPromises)
// 轮询所有操作
for (const operation of operations) {
let op = operation
while (!op.done) {
await new Promise(resolve => setTimeout(resolve, 1000))
op = await ai.operations.get({ name: op.name })
}
console.log('✅ 已索引:', op.response.displayName)
}
基本查询:
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'What are the safety precautions for installation?',
config: {
tools: [{
fileSearch: {
fileSearchStoreNames: [fileStore.name]
}
}]
}
})
console.log('答案:', response.text)
// 访问引用
const grounding = response.candidates[0].groundingMetadata
if (grounding?.groundingChunks) {
console.log('\n来源:')
grounding.groundingChunks.forEach((chunk, i) => {
console.log(`${i + 1}. ${chunk.retrievedContext?.title || 'Unknown'}`)
console.log(` URI: ${chunk.retrievedContext?.uri || 'N/A'}`)
})
}
带元数据过滤的查询:
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'How do I reset the device?',
config: {
tools: [{
fileSearch: {
fileSearchStoreNames: [fileStore.name],
// 仅搜索英文、版本 1.0 的故障排除文档
metadataFilter: 'doc_type="troubleshooting" AND language="en" AND version="1.0"'
}
}]
}
})
console.log('答案:', response.text)
元数据过滤语法:
key1="value1" AND key2="value2"key1="value1" OR key1="value2"(key1="a" OR key1="b") AND key2="c"// 列出存储中的所有文档
const docs = await ai.fileSearchStores.documents.list({
parent: fileStore.name,
pageSize: 20
})
console.log(`总文档数:${docs.documents?.length || 0}`)
docs.documents?.forEach(doc => {
console.log(`- ${doc.displayName} (${doc.name})`)
console.log(` 元数据:`, doc.customMetadata)
})
// 获取特定文档详情
const docDetails = await ai.fileSearchStores.documents.get({
name: docs.documents[0].name
})
console.log('文档详情:', docDetails)
// 删除文档
await ai.fileSearchStores.documents.delete({
name: docs.documents[0].name,
force: true
})
// 删除整个存储(强制删除所有文档)
await ai.fileSearchStores.delete({
name: fileStore.name,
force: true
})
console.log('✅ 存储已删除')
分块配置显著影响检索质量。根据内容类型进行调整:
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500, // 更小的块用于精确的代码/API 查找
maxOverlapTokens: 50 // 10% 重叠
}
}
最适合: API 文档、SDK 参考、代码示例、配置指南
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 800, // 更大的块保留叙述流程
maxOverlapTokens: 80 // 10% 重叠
}
}
最适合: 博客文章、新闻文章、产品描述、营销材料
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 300, // 非常小的块用于高精度
maxOverlapTokens: 30 // 10% 重叠
}
}
最适合: 法律文件、合同、法规、合规文档
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 400, // 中等块(1-2 个问答对)
maxOverlapTokens: 40 // 10% 重叠
}
}
最适合: 常见问题解答、故障排除指南、操作指南文章
通用规则: 保持 10% 的重叠(重叠 = 块大小 / 10)以防止块边界处的上下文丢失。
设计用于过滤和组织的元数据模式:
customMetadata: {
doc_type: 'faq' | 'manual' | 'troubleshooting' | 'guide',
product: 'widget-pro' | 'widget-lite',
version: '1.0' | '2.0',
language: 'en' | 'es' | 'fr',
category: 'installation' | 'configuration' | 'maintenance',
priority: 'critical' | 'normal' | 'low',
last_updated: '2025-01-15',
author: 'support-team'
}
查询示例:
metadataFilter: 'product="widget-pro" AND (doc_type="troubleshooting" OR doc_type="faq") AND language="en"'
customMetadata: {
doc_type: 'contract' | 'regulation' | 'case-law' | 'policy',
jurisdiction: 'US' | 'EU' | 'UK',
practice_area: 'employment' | 'corporate' | 'ip' | 'tax',
effective_date: '2025-01-01',
status: 'active' | 'archived',
confidentiality: 'public' | 'internal' | 'privileged'
}
customMetadata: {
doc_type: 'api-reference' | 'tutorial' | 'example' | 'changelog',
language: 'javascript' | 'python' | 'java' | 'go',
framework: 'react' | 'nextjs' | 'express' | 'fastapi',
version: '1.2.0',
difficulty: 'beginner' | 'intermediate' | 'advanced'
}
提示:
snake_case 或 camelCase)// 跟踪已上传文件的哈希值以避免重复
const uploadedHashes = new Set<string>()
async function uploadWithDeduplication(filePath: string) {
const fileHash = await getFileHash(filePath)
if (uploadedHashes.has(fileHash)) {
console.log(`跳过重复文件:${filePath}`)
return
}
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream(filePath)
})
uploadedHashes.add(fileHash)
}
// 索引前将图像转换为文本(OCR)
// 压缩 PDF(移除图像,使用纯文本)
// 使用 markdown 而非 Word 文档(更小)
// ❌ 昂贵:搜索所有 10GB 文档
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Reset procedure?',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [fileStore.name] } }]
}
})
// ✅ 更便宜:仅过滤到故障排除文档(子集)
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Reset procedure?',
config: {
tools: [{
fileSearch: {
fileSearchStoreNames: [fileStore.name],
metadataFilter: 'doc_type="troubleshooting"' // 减少搜索范围
}
}]
}
})
// Gemini 3 Flash 的查询成本比 Pro 便宜 10 倍
// 除非需要 Pro 的高级推理能力,否则使用 Flash
// 开发/测试:使用 Flash
model: 'gemini-3-flash'
// 生产(高风险答案):使用 Pro
model: 'gemini-3-pro'
// 列出存储并估算存储空间
const stores = await ai.fileSearchStores.list()
for (const store of stores.fileSearchStores || []) {
const docs = await ai.fileSearchStores.documents.list({
parent: store.name
})
console.log(`存储:${store.displayName}`)
console.log(`文档数:${docs.documents?.length || 0}`)
// 估算存储空间(3 倍输入大小)
console.log(`预计存储空间:~${(docs.documents?.length || 0) * 10} MB`)
}
const store = await ai.fileSearchStores.get({
name: fileStore.name
})
console.assert(store.displayName === 'my-knowledge-base', '存储名称不匹配')
console.log('✅ 存储创建成功')
const docs = await ai.fileSearchStores.documents.list({
parent: fileStore.name
})
console.assert(docs.documents?.length > 0, '没有文档被索引')
console.log(`✅ ${docs.documents?.length} 个文档已索引`)
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'What is this knowledge base about?',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [fileStore.name] } }]
}
Google Gemini File Search is a fully managed RAG system. Upload documents (100+ formats: PDF, Word, Excel, code) and query with natural language—automatic chunking, embeddings, semantic search, and citations.
What This Skill Provides:
Create an API key at https://aistudio.google.com/apikey
Free Tier Limits:
Paid Tier Pricing:
Minimum Version: Node.js 18+ (v20+ recommended)
node --version # Should be >=18.0.0
npm install @google/genai
# or
pnpm add @google/genai
# or
yarn add @google/genai
Current Stable Version: 1.30.0+ (verify with npm view @google/genai version)
⚠️ Important: File Search API requires @google/genai v1.29.0 or later. Earlier versions do not support File Search. The API was added in v1.29.0 (November 5, 2025).
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}
This skill prevents 12 common errors encountered when implementing File Search:
Symptom:
Error: Documents cannot be modified after indexing
Cause: Documents are immutable once indexed. There is no PATCH or UPDATE operation.
Prevention: Use the delete+re-upload pattern for updates:
// ❌ WRONG: Trying to update document (no such API)
await ai.fileSearchStores.documents.update({
name: documentName,
customMetadata: { version: '2.0' }
})
// ✅ CORRECT: Delete then re-upload
const docs = await ai.fileSearchStores.documents.list({
parent: fileStore.name
})
const oldDoc = docs.documents.find(d => d.displayName === 'manual.pdf')
if (oldDoc) {
await ai.fileSearchStores.documents.delete({
name: oldDoc.name,
force: true
})
}
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('manual-v2.pdf'),
config: { displayName: 'manual.pdf' }
})
Source: https://ai.google.dev/api/file-search/documents
Symptom:
Error: Quota exceeded. Expected 1GB limit, but 3.2GB used.
Cause: Storage calculation includes input files + embeddings + metadata. Total storage ≈ 3x input size.
Prevention: Calculate storage before upload:
// ❌ WRONG: Assuming storage = file size
const fileSize = fs.statSync('data.pdf').size // 500 MB
// Expect 500 MB usage → WRONG
// ✅ CORRECT: Account for 3x multiplier
const fileSize = fs.statSync('data.pdf').size // 500 MB
const estimatedStorage = fileSize * 3 // 1.5 GB (embeddings + metadata)
console.log(`Estimated storage: ${estimatedStorage / 1e9} GB`)
// Check if within quota before upload
if (estimatedStorage > 1e9) {
console.warn('⚠️ File may exceed free tier 1 GB limit')
}
Source: https://blog.google/technology/developers/file-search-gemini-api/
Symptom: Poor retrieval quality, irrelevant results, or context cutoff mid-sentence.
Cause: Default chunking may not be optimal for your content type.
Prevention: Use recommended chunking strategy:
// ❌ WRONG: Using defaults without testing
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('docs.pdf')
// Default chunking may be too large or too small
})
// ✅ CORRECT: Configure chunking for precision
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('docs.pdf'),
config: {
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500, // Smaller chunks = more precise retrieval
maxOverlapTokens: 50 // 10% overlap prevents context loss
}
}
}
})
Chunking Guidelines:
Source: https://www.philschmid.de/gemini-file-search-javascript
Symptom:
Error: Maximum 20 custom metadata key-value pairs allowed
Cause: Each document can have at most 20 metadata fields.
Prevention: Design compact metadata schema:
// ❌ WRONG: Too many metadata fields
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('doc.pdf'),
config: {
customMetadata: {
doc_type: 'manual',
version: '1.0',
author: 'John Doe',
department: 'Engineering',
created_date: '2025-01-01',
// ... 18 more fields → Error!
}
}
})
// ✅ CORRECT: Use hierarchical keys or JSON strings
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('doc.pdf'),
config: {
customMetadata: {
doc_type: 'manual',
version: '1.0',
author_dept: 'John Doe|Engineering', // Combine related fields
dates: JSON.stringify({ // Or use JSON for complex data
created: '2025-01-01',
updated: '2025-01-15'
})
}
}
})
Source: https://ai.google.dev/api/file-search/documents
Symptom: Unexpected bill for $375 after uploading 10 GB of documents.
Cause: Indexing costs are one-time but calculated per input token ($0.15/1M tokens).
Prevention: Estimate costs before indexing:
// ❌ WRONG: No cost estimation
await uploadAllDocuments(fileStore.name, './data') // 10 GB uploaded → $375 surprise
// ✅ CORRECT: Calculate costs upfront
const totalSize = getTotalDirectorySize('./data') // 10 GB
const estimatedTokens = (totalSize / 4) // Rough estimate: 1 token ≈ 4 bytes
const indexingCost = (estimatedTokens / 1e6) * 0.15
console.log(`Estimated indexing cost: $${indexingCost.toFixed(2)}`)
console.log(`Estimated storage: ${(totalSize * 3) / 1e9} GB`)
// Confirm before proceeding
const proceed = await confirm(`Proceed with indexing? Cost: $${indexingCost.toFixed(2)}`)
if (proceed) {
await uploadAllDocuments(fileStore.name, './data')
}
Cost Examples:
Source: https://ai.google.dev/pricing
Symptom: Query returns no results immediately after upload, or incomplete indexing.
Cause: File uploads are processed asynchronously. Must poll operation until done: true.
Prevention: Always poll operation status with timeout and fallback:
// ❌ WRONG: Assuming upload is instant
const operation = await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('large.pdf')
})
// Immediately query → No results!
// ✅ CORRECT: Poll until indexing complete with timeout
const operation = await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('large.pdf')
})
// Poll with timeout and fallback
const MAX_POLL_TIME = 60000 // 60 seconds
const POLL_INTERVAL = 1000
let elapsed = 0
while (!operation.done && elapsed < MAX_POLL_TIME) {
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL))
elapsed += POLL_INTERVAL
try {
operation = await ai.operations.get({ name: operation.name })
console.log(`Indexing progress: ${operation.metadata?.progress || 'processing...'}`)
} catch (error) {
console.warn('Polling failed, assuming complete:', error)
break
}
}
if (operation.error) {
throw new Error(`Indexing failed: ${operation.error.message}`)
}
// ⚠️ Warning: operations.get() can be unreliable for large files
// If timeout reached, verify document exists manually
if (elapsed >= MAX_POLL_TIME) {
console.warn('Polling timeout - verifying document manually')
const docs = await ai.fileSearchStores.documents.list({ parent: fileStore.name })
const uploaded = docs.documents?.find(d => d.displayName === 'large.pdf')
if (uploaded) {
console.log('✅ Document found despite polling timeout')
} else {
throw new Error('Upload failed - document not found')
}
}
console.log('✅ Indexing complete:', operation.response?.displayName)
Source: https://ai.google.dev/api/file-search/file-search-stores#uploadtofilesearchstore, GitHub Issue #1211
Symptom:
Error: Cannot delete store with documents. Set force=true.
Cause: Stores with documents require force: true to delete (prevents accidental deletion).
Prevention: Always use force: true when deleting non-empty stores:
// ❌ WRONG: Trying to delete store with documents
await ai.fileSearchStores.delete({
name: fileStore.name
})
// Error: Cannot delete store with documents
// ✅ CORRECT: Use force delete
await ai.fileSearchStores.delete({
name: fileStore.name,
force: true // Deletes store AND all documents
})
// Alternative: Delete documents first
const docs = await ai.fileSearchStores.documents.list({ parent: fileStore.name })
for (const doc of docs.documents || []) {
await ai.fileSearchStores.documents.delete({
name: doc.name,
force: true
})
}
await ai.fileSearchStores.delete({ name: fileStore.name })
Source: https://ai.google.dev/api/file-search/file-search-stores#delete
Symptom:
Error: File Search is only supported for Gemini 3 Pro and Flash models
Cause: File Search requires Gemini 3 Pro or Gemini 3 Flash. Gemini 2.x and 1.5 models are not supported.
Prevention: Always use Gemini 3 models:
// ❌ WRONG: Using Gemini 1.5 model
const response = await ai.models.generateContent({
model: 'gemini-1.5-pro', // Not supported!
contents: 'What is the installation procedure?',
config: {
tools: [{
fileSearch: { fileSearchStoreNames: [fileStore.name] }
}]
}
})
// ✅ CORRECT: Use Gemini 3 models
const response = await ai.models.generateContent({
model: 'gemini-3-flash', // ✅ Supported (fast, cost-effective)
// OR
// model: 'gemini-3-pro', // ✅ Supported (higher quality)
contents: 'What is the installation procedure?',
config: {
tools: [{
fileSearch: { fileSearchStoreNames: [fileStore.name] }
}]
}
})
Source: https://ai.google.dev/gemini-api/docs/file-search
Symptom:
groundingChunks[0].title === null // No document source shown
Cause: In @google/genai versions prior to v1.34.0, when uploading files as Blob objects (not file paths), the SDK dropped the displayName and customMetadata configuration fields.
Prevention:
// ✅ CORRECT: Upgrade to v1.34.0+ for automatic fix
npm install @google/genai@latest // v1.34.0+
await ai.fileSearchStores.uploadToFileSearchStore({
name: storeName,
file: new Blob([arrayBuffer], { type: 'application/pdf' }),
config: {
displayName: 'Safety Manual.pdf', // ✅ Now preserved
customMetadata: { version: '1.0' } // ✅ Now preserved
}
})
// ⚠️ WORKAROUND for v1.33.0 and earlier: Use resumable upload
const uploadUrl = `https://generativelanguage.googleapis.com/upload/v1beta/${storeName}:uploadToFileSearchStore?key=${API_KEY}`
// Step 1: Initiate with displayName in body
const initResponse = await fetch(uploadUrl, {
method: 'POST',
headers: {
'X-Goog-Upload-Protocol': 'resumable',
'X-Goog-Upload-Command': 'start',
'X-Goog-Upload-Header-Content-Length': numBytes.toString(),
'X-Goog-Upload-Header-Content-Type': 'application/pdf',
'Content-Type': 'application/json'
},
body: JSON.stringify({
displayName: 'Safety Manual.pdf' // ✅ Works with resumable upload
})
})
// Step 2: Upload file bytes
const uploadUrl2 = initResponse.headers.get('X-Goog-Upload-URL')
await fetch(uploadUrl2, {
method: 'PUT',
headers: {
'Content-Length': numBytes.toString(),
'X-Goog-Upload-Offset': '0',
'X-Goog-Upload-Command': 'upload, finalize',
'Content-Type': 'application/pdf'
},
body: fileBytes
})
Source: GitHub Issue #1078
Symptom:
response.candidates[0].groundingMetadata === undefined
// Even though fileSearch tool is configured
Cause: When using responseMimeType: 'application/json' for structured output, the API ignores the fileSearch tool and returns no grounding metadata, even with Gemini 3 models.
Prevention:
// ❌ WRONG: Structured output overrides grounding
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Summarize guidelines',
config: {
responseMimeType: 'application/json', // Loses grounding
tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }]
}
})
// ✅ CORRECT: Two-step approach
// Step 1: Get grounded text response
const textResponse = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Summarize guidelines',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }]
}
})
const grounding = textResponse.candidates[0].groundingMetadata
// Step 2: Convert to structured format in prompt
const jsonResponse = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: `Convert to JSON: ${textResponse.text}
Format:
{
"summary": "...",
"key_points": ["..."]
}`,
config: {
responseMimeType: 'application/json',
responseSchema: {
type: 'object',
properties: {
summary: { type: 'string' },
key_points: { type: 'array', items: { type: 'string' } }
}
}
}
})
// Combine results
const result = {
data: JSON.parse(jsonResponse.text),
sources: grounding.groundingChunks
}
Source: GitHub Issue #829
Symptom:
Error: "Search as a tool and file search tool are not supported together"
Status: INVALID_ARGUMENT
Cause: The Gemini API does not allow using googleSearch and fileSearch tools in the same request.
Prevention:
// ❌ WRONG: Combining search tools
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'What are the latest industry guidelines?',
config: {
tools: [
{ googleSearch: {} },
{ fileSearch: { fileSearchStoreNames: [storeName] } }
]
}
})
// ✅ CORRECT: Use separate specialist agents
async function searchWeb(query: string) {
return ai.models.generateContent({
model: 'gemini-3-flash',
contents: query,
config: { tools: [{ googleSearch: {} }] }
})
}
async function searchDocuments(query: string) {
return ai.models.generateContent({
model: 'gemini-3-flash',
contents: query,
config: { tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }] }
})
}
// Orchestrate based on query type
const needsWeb = query.includes('latest') || query.includes('current')
const response = needsWeb
? await searchWeb(query)
: await searchDocuments(query)
Source: GitHub Issue #435, Google Codelabs
Symptom: Cannot correlate batch responses with requests when using metadata field.
Cause: When using Batch API with InlinedRequest that includes a metadata field, the corresponding InlinedResponse does not return the metadata.
Prevention:
// ❌ WRONG: Expecting metadata in response
const batchRequest = {
metadata: { key: 'my-request-id' },
contents: [{ parts: [{ text: 'Question?' }], role: 'user' }],
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }]
}
}
const batchResponse = await ai.batch.create({ requests: [batchRequest] })
console.log(batchResponse.responses[0].metadata) // ❌ undefined
// ✅ CORRECT: Use array index to correlate
const requests = [
{ metadata: { id: 'req-1' }, contents: [...] },
{ metadata: { id: 'req-2' }, contents: [...] }
]
const responses = await ai.batch.create({ requests })
// Map by index (not ideal but works)
responses.responses.forEach((response, i) => {
const requestMetadata = requests[i].metadata
console.log(`Response for ${requestMetadata.id}:`, response)
})
Community Verification: Maintainer confirmed, internal bug filed.
Source: GitHub Issue #1191
import { GoogleGenAI } from '@google/genai'
import fs from 'fs'
// Initialize client with API key
const ai = new GoogleGenAI({
apiKey: process.env.GOOGLE_API_KEY
})
// Verify API key is set
if (!process.env.GOOGLE_API_KEY) {
throw new Error('GOOGLE_API_KEY environment variable is required')
}
// Create a store (container for documents)
const fileStore = await ai.fileSearchStores.create({
config: {
displayName: 'my-knowledge-base', // Human-readable name
// Optional: Add store-level metadata
customMetadata: {
project: 'customer-support',
environment: 'production'
}
}
})
console.log('Created store:', fileStore.name)
// Output: fileSearchStores/abc123xyz...
Finding Existing Stores:
// List all stores (paginated)
const stores = await ai.fileSearchStores.list({
pageSize: 20 // Max 20 per page
})
// Find by display name
let targetStore = null
let pageToken = null
do {
const page = await ai.fileSearchStores.list({ pageToken })
targetStore = page.fileSearchStores.find(
s => s.displayName === 'my-knowledge-base'
)
pageToken = page.nextPageToken
} while (!targetStore && pageToken)
if (targetStore) {
console.log('Found existing store:', targetStore.name)
} else {
console.log('Store not found, creating new one...')
}
Single File Upload:
const operation = await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream('./docs/manual.pdf'),
config: {
displayName: 'Installation Manual',
customMetadata: {
doc_type: 'manual',
version: '1.0',
language: 'en'
},
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500,
maxOverlapTokens: 50
}
}
}
})
// Poll until indexing complete
while (!operation.done) {
await new Promise(resolve => setTimeout(resolve, 1000))
operation = await ai.operations.get({ name: operation.name })
}
console.log('✅ Indexed:', operation.response.displayName)
Batch Upload (Concurrent):
const filePaths = [
'./docs/manual.pdf',
'./docs/faq.md',
'./docs/troubleshooting.docx'
]
// Upload all files concurrently
const uploadPromises = filePaths.map(filePath =>
ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream(filePath),
config: {
displayName: filePath.split('/').pop(),
customMetadata: {
doc_type: 'support',
source_path: filePath
},
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500,
maxOverlapTokens: 50
}
}
}
})
)
const operations = await Promise.all(uploadPromises)
// Poll all operations
for (const operation of operations) {
let op = operation
while (!op.done) {
await new Promise(resolve => setTimeout(resolve, 1000))
op = await ai.operations.get({ name: op.name })
}
console.log('✅ Indexed:', op.response.displayName)
}
Basic Query:
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'What are the safety precautions for installation?',
config: {
tools: [{
fileSearch: {
fileSearchStoreNames: [fileStore.name]
}
}]
}
})
console.log('Answer:', response.text)
// Access citations
const grounding = response.candidates[0].groundingMetadata
if (grounding?.groundingChunks) {
console.log('\nSources:')
grounding.groundingChunks.forEach((chunk, i) => {
console.log(`${i + 1}. ${chunk.retrievedContext?.title || 'Unknown'}`)
console.log(` URI: ${chunk.retrievedContext?.uri || 'N/A'}`)
})
}
Query with Metadata Filtering:
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'How do I reset the device?',
config: {
tools: [{
fileSearch: {
fileSearchStoreNames: [fileStore.name],
// Filter to only search troubleshooting docs in English, version 1.0
metadataFilter: 'doc_type="troubleshooting" AND language="en" AND version="1.0"'
}
}]
}
})
console.log('Answer:', response.text)
Metadata Filter Syntax:
key1="value1" AND key2="value2"key1="value1" OR key1="value2"(key1="a" OR key1="b") AND key2="c"// List all documents in store
const docs = await ai.fileSearchStores.documents.list({
parent: fileStore.name,
pageSize: 20
})
console.log(`Total documents: ${docs.documents?.length || 0}`)
docs.documents?.forEach(doc => {
console.log(`- ${doc.displayName} (${doc.name})`)
console.log(` Metadata:`, doc.customMetadata)
})
// Get specific document details
const docDetails = await ai.fileSearchStores.documents.get({
name: docs.documents[0].name
})
console.log('Document details:', docDetails)
// Delete document
await ai.fileSearchStores.documents.delete({
name: docs.documents[0].name,
force: true
})
// Delete entire store (force deletes all documents)
await ai.fileSearchStores.delete({
name: fileStore.name,
force: true
})
console.log('✅ Store deleted')
Chunking configuration significantly impacts retrieval quality. Adjust based on content type:
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 500, // Smaller chunks for precise code/API lookup
maxOverlapTokens: 50 // 10% overlap
}
}
Best for: API docs, SDK references, code examples, configuration guides
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 800, // Larger chunks preserve narrative flow
maxOverlapTokens: 80 // 10% overlap
}
}
Best for: Blog posts, news articles, product descriptions, marketing materials
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 300, // Very small chunks for high precision
maxOverlapTokens: 30 // 10% overlap
}
}
Best for: Legal documents, contracts, regulations, compliance docs
chunkingConfig: {
whiteSpaceConfig: {
maxTokensPerChunk: 400, // Medium chunks (1-2 Q&A pairs)
maxOverlapTokens: 40 // 10% overlap
}
}
Best for: FAQs, troubleshooting guides, how-to articles
General Rule: Maintain 10% overlap (overlap = chunk size / 10) to prevent context loss at chunk boundaries.
Design metadata schema for filtering and organization:
customMetadata: {
doc_type: 'faq' | 'manual' | 'troubleshooting' | 'guide',
product: 'widget-pro' | 'widget-lite',
version: '1.0' | '2.0',
language: 'en' | 'es' | 'fr',
category: 'installation' | 'configuration' | 'maintenance',
priority: 'critical' | 'normal' | 'low',
last_updated: '2025-01-15',
author: 'support-team'
}
Query Example:
metadataFilter: 'product="widget-pro" AND (doc_type="troubleshooting" OR doc_type="faq") AND language="en"'
customMetadata: {
doc_type: 'contract' | 'regulation' | 'case-law' | 'policy',
jurisdiction: 'US' | 'EU' | 'UK',
practice_area: 'employment' | 'corporate' | 'ip' | 'tax',
effective_date: '2025-01-01',
status: 'active' | 'archived',
confidentiality: 'public' | 'internal' | 'privileged'
}
customMetadata: {
doc_type: 'api-reference' | 'tutorial' | 'example' | 'changelog',
language: 'javascript' | 'python' | 'java' | 'go',
framework: 'react' | 'nextjs' | 'express' | 'fastapi',
version: '1.2.0',
difficulty: 'beginner' | 'intermediate' | 'advanced'
}
Tips:
snake_case or camelCase)// Track uploaded file hashes to avoid duplicates
const uploadedHashes = new Set<string>()
async function uploadWithDeduplication(filePath: string) {
const fileHash = await getFileHash(filePath)
if (uploadedHashes.has(fileHash)) {
console.log(`Skipping duplicate: ${filePath}`)
return
}
await ai.fileSearchStores.uploadToFileSearchStore({
name: fileStore.name,
file: fs.createReadStream(filePath)
})
uploadedHashes.add(fileHash)
}
// Convert images to text before indexing (OCR)
// Compress PDFs (remove images, use text-only)
// Use markdown instead of Word docs (smaller size)
// ❌ EXPENSIVE: Search all 10GB of documents
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Reset procedure?',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [fileStore.name] } }]
}
})
// ✅ CHEAPER: Filter to only troubleshooting docs (subset)
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Reset procedure?',
config: {
tools: [{
fileSearch: {
fileSearchStoreNames: [fileStore.name],
metadataFilter: 'doc_type="troubleshooting"' // Reduces search scope
}
}]
}
})
// Gemini 3 Flash is 10x cheaper than Pro for queries
// Use Flash unless you need Pro's advanced reasoning
// Development/testing: Use Flash
model: 'gemini-3-flash'
// Production (high-stakes answers): Use Pro
model: 'gemini-3-pro'
// List stores and estimate storage
const stores = await ai.fileSearchStores.list()
for (const store of stores.fileSearchStores || []) {
const docs = await ai.fileSearchStores.documents.list({
parent: store.name
})
console.log(`Store: ${store.displayName}`)
console.log(`Documents: ${docs.documents?.length || 0}`)
// Estimate storage (3x input size)
console.log(`Estimated storage: ~${(docs.documents?.length || 0) * 10} MB`)
}
const store = await ai.fileSearchStores.get({
name: fileStore.name
})
console.assert(store.displayName === 'my-knowledge-base', 'Store name mismatch')
console.log('✅ Store created successfully')
const docs = await ai.fileSearchStores.documents.list({
parent: fileStore.name
})
console.assert(docs.documents?.length > 0, 'No documents indexed')
console.log(`✅ ${docs.documents?.length} documents indexed`)
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'What is this knowledge base about?',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [fileStore.name] } }]
}
})
console.assert(response.text.length > 0, 'Empty response')
console.log('✅ Query successful:', response.text.substring(0, 100) + '...')
const response = await ai.models.generateContent({
model: 'gemini-3-flash',
contents: 'Provide a specific answer with citations.',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [fileStore.name] } }]
}
})
const grounding = response.candidates[0].groundingMetadata
console.assert(
grounding?.groundingChunks?.length > 0,
'No grounding/citations returned'
)
console.log(`✅ ${grounding?.groundingChunks?.length} citations returned`)
File Search supports streaming responses with generateContentStream():
// ✅ Streaming works with File Search (v1.34.0+)
const stream = await ai.models.generateContentStream({
model: 'gemini-3-flash',
contents: 'Summarize the document',
config: {
tools: [{ fileSearch: { fileSearchStoreNames: [storeName] } }]
}
})
for await (const chunk of stream) {
process.stdout.write(chunk.text)
}
// Access grounding after stream completes
const grounding = stream.candidates[0].groundingMetadata
Note: Early SDK versions (pre-v1.34.0) may have had streaming issues. Use v1.34.0+ for reliable streaming support.
Source: GitHub Issue #1221
This skill includes 3 working templates in the templates/ directory:
Minimal Node.js/TypeScript example demonstrating:
Use when: Learning File Search, prototyping, simple CLI tools
Run:
cd templates/basic-node-rag
npm install
npm run dev
Cloudflare Workers integration showing:
Use when: Building global edge applications, integrating with Cloudflare stack
Deploy:
cd templates/cloudflare-worker-rag
npm install
npx wrangler deploy
Full-stack Next.js application featuring:
Use when: Building production documentation sites, knowledge bases
Run:
cd templates/nextjs-docs-search
npm install
npm run dev
Official Documentation:
Tutorials:
Bundled Resources in This Skill:
references/api-reference.md - Complete API documentationreferences/chunking-best-practices.md - Detailed chunking strategiesreferences/pricing-calculator.md - Cost estimation guidereferences/migration-from-openai.md - Migration guide from OpenAI Files APIscripts/create-store.ts - CLI tool to create storesscripts/upload-batch.ts - Batch upload scriptscripts/query-store.ts - Interactive query toolscripts/cleanup.ts - Cleanup scriptWorking Templates:
templates/basic-node-rag/ - Minimal Node.js exampletemplates/cloudflare-worker-rag/ - Edge deployment exampletemplates/nextjs-docs-search/ - Full-stack Next.js appSkill Version: 1.1.0 Last Verified: 2026-01-21 Package Version: @google/genai ^1.38.0 (minimum 1.29.0 required) Token Savings: ~67% Errors Prevented: 12 Changes: Added 4 new errors from community research (displayName Blob issue, grounding with JSON mode, tool conflicts, batch API metadata), enhanced polling timeout pattern with fallback verification, added streaming support note
Weekly Installs
328
Repository
GitHub Stars
652
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code270
gemini-cli220
opencode215
antigravity203
cursor202
codex186
Azure 配额管理指南:服务限制、容量验证与配额增加方法
77,500 周安装
save-thread技能:会话保存与交接摘要工具(兼容性说明)
320 周安装
Canvas Design - AI设计哲学与视觉表达工具,自动生成专业级视觉艺术作品
320 周安装
Codex Subagent 子代理技能:创建自主代理卸载上下文密集型任务,优化AI工作流
320 周安装
Snowflake原生Streamlit应用开发指南:部署、运行时与安全模型详解
320 周安装
agent-browser:专为AI智能体设计的Rust无头浏览器自动化CLI工具
321 周安装
Preline主题生成器 - 一键生成美观UI主题CSS文件,支持深色模式
321 周安装