{"slug": "building-production-grade-rag-with-go-and-gemini-file-search-a-zero-approach", "title": "Building Production-Grade RAG with Go and Gemini File Search: A Zero-Infrastructure Approach", "summary": "A developer has outlined a zero-infrastructure approach to production-grade Retrieval-Augmented Generation (RAG) by pairing the Go programming language with Google's Gemini File Search tool, eliminating the need for a dedicated vector database. The design relies on Gemini's internal chunking, embedding, and retrieval to handle unstructured documents, while Go manages the HTTP layer and data transformations. The developer cautions that File Search suits medium-to-large unstructured text corpora but is not a substitute for structured relational databases or complex numeric filtering.", "body_md": "*Originally published on [tamiz.pro](https://tamiz.pro/insights/production-rag-go-gemini-file-search-zero-infra).*\n\nThe standard architecture for Retrieval-Augmented Generation (RAG) systems has ossified around a complex stack: an ingest pipeline, a heavy-duty vector database (Pinecone, Weaviate, Milvus), and an orchestration layer. This approach often introduces significant operational overhead. You must manage vector indexing, handle schema migrations, deal with embedding model drift, and provision additional infrastructure just to serve the data layer. For many use cases, particularly those involving unstructured enterprise documents (PDFs, legal contracts, technical manuals), this complexity is unnecessary. You don't always need to pre-compute embeddings and store them in a specialized index. You can use on-the-fly semantic search.\n\nEnter Gemini File Search. Google's Gemini API offers a \"File Search\" tool that allows the model to search across a directory of unstructured files (PDFs, DOCX, TXT, HTML) without requiring you to host them in a vector database. It handles the chunking, embedding, and retrieval internally. By combining this capability with the Go programming language, developers can build production-grade RAG pipelines that are incredibly lean. Go's concurrency model and type safety make it ideal for handling the HTTP layers and data transformations required to glue your application logic to the Gemini API. This article explores how to architect and build such a system, focusing on eliminating the \"vector DB tax\" while maintaining high retrieval accuracy and system reliability.\n\nTraditional RAG requires the client or a middle-tier service to know exactly what to look for. You must generate embeddings for your chunks and query a vector store using cosine similarity. This decouples the understanding of the query from the retrieval mechanism. If your user asks a nuanced question that implies a specific search strategy, a standard vector search might miss the context if the embeddings don't align perfectly.\n\nWith File Search, the LLM (Gemini) acts as the retrieval engine. It understands the intent of the question, reformulates search queries if necessary, and scans the file contents (using its internal vector indexing service) to find relevant context. This shifts the burden of \"semantic matching\" to the model.\n\nIt is crucial to understand that File Search is best suited for medium-to-large corpora of unstructured text. It is not a replacement for a structured relational database. If your RAG relies heavily on complex joins between tables or precise numeric filtering, a traditional vector DB or SQL engine is still superior. File Search is optimized for document retrieval, not data querying.\n\nWe will build a service that accepts user queries and returns grounded answers using Gemini File Search. The stack will consist of:\n\n`net/http` (or Chi/Echo for routing)`google.golang.org/genai`\n`viper` for environment management\nThe architecture follows a clean layered design:\n\nFirst, we set up the configuration. Production-grade Go apps should never hardcode secrets. We use environment variables for the Gemini API key and the project ID.\n\n```\npackage config\n\nimport (\n    \"fmt\"\n    \"os\"\n)\n\ntype Config struct {\n    GeminiAPIKey string\n    ProjectID    string\n    ModelName    string\n    MaxTokens    int\n    Temperature  float64\n}\n\nfunc LoadFromEnv() (*Config, error) {\n    apiKey := os.Getenv(\"GEMINI_API_KEY\")\n    if apiKey == \"\" {\n        return nil, fmt.Errorf(\"GEMINI_API_KEY must be set\")\n    }\n\n    projectID := os.Getenv(\"GCP_PROJECT_ID\")\n    if projectID == \"\" {\n        return nil, fmt.Errorf(\"GCP_PROJECT_ID must be set\")\n    }\n\n    return &Config{\n        GeminiAPIKey: apiKey,\n        ProjectID:    projectID,\n        ModelName:    \"gemini-1.5-pro\", // or flash for lower latency\n        MaxTokens:    2048,\n        Temperature:  0.2,\n    }, nil\n}\n```\n\nIn Gemini, tools are defined within the `GenerationConfig` or `ToolConfig`. The File Search tool requires specifying the directory in Google Cloud Storage where your files reside. The files must be in supported formats (`.pdf`, `.docx`, `.txt`, `.html`).\n\nWe need to create a helper to generate the tool configuration. Note that the directory must be accessible to the service account used by the Gemini API.\n\n```\npackage service\n\nimport (\n    \"context\"\n    \"google.golang.org/genai\"\n    \"google.golang.org/genai/google\"\n)\n\ntype FileSearchConfig struct {\n    GCSBucket string\n    Directory string\n}\n\nfunc NewFileSearchTool(ctx context.Context, cfg *FileSearchConfig) (*google.Tool, error) {\n    // Construct the file search tool specification\n    // Note: The exact struct definitions may vary slightly based on SDK version\n    // This is a conceptual representation of the required fields\n    tool := &google.Tool{\n        FunctionDeclarations: []*google.FunctionDeclaration{},\n        // In recent SDKs, file search is handled via specific tool types or parameters\n        // We will use the built-in FileSearch capability if available in the SDK version used\n    }\n    return tool, nil\n}\n```\n\n*Correction*: As of the latest stable SDKs, File Search is often enabled via specific model parameters or by using the `SearchRetrieval` or `FileSearch` tool types provided in the `google` package. Let's use the more standard approach of defining the `RagConfig` or passing the file directory explicitly if the SDK supports it. For this article, we will assume the use of the `genai` client where we can pass a `FileSearch` tool configuration.\n\nIn practice, you define the tool like this:\n\n```\nfunc CreateFileSearchTool(bucket, dir string) *google.Tool {\n    return &google.Tool{\n        FileSearch: &google.FileSearchToolConfig{\n            Directory: &google.FileSearchToolConfigDirectory{\n                Bucket: bucket,\n                Directory: dir,\n            },\n        },\n    }\n}\n```\n\nThe core of our system is the service that takes a prompt and a file search configuration. We must ensure that we are using a model that supports this tool. `gemini-1.5-pro` and `gemini-2.0-flash` are currently the primary candidates. `gemini-2.0-flash` is often preferred for its speed and cost efficiency in high-volume production environments.\n\n```\npackage service\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"time\"\n\n    \"google.golang.org/genai\"\n    \"google.golang.org/genai/google\"\n)\n\ntype GeminiService struct {\n    client   *genai.Client\n    model    string\n    fsConfig *FileSearchConfig\n}\n\nfunc NewGeminiService(client *genai.Client, model string, fsConfig *FileSearchConfig) *GeminiService {\n    return &GeminiService{\n        client:   client,\n        model:    model,\n        fsConfig: fsConfig,\n    }\n}\n\nfunc (s *GeminiService) Query(ctx context.Context, prompt string) (string, error) {\n    // Create the tool instance\n    tool := CreateFileSearchTool(s.fsConfig.GCSBucket, s.fsConfig.Directory)\n\n    // Construct the request\n    req := &google.GenerateContentRequest{\n        Model: s.model,\n        Contents: []*google.Content{\n            {\n                Role: \"user\",\n                Parts: []*google.Part{\n                    {\n                        Text: prompt,\n                    },\n                },\n            },\n        },\n        Tools: []*google.Tool{tool},\n        // System instruction to enforce grounding\n        SystemInstruction: &google.Content{\n            Parts: []*google.Part{\n                {\n                    Text: \"You are a helpful assistant. Answer questions based ONLY on the provided file context. If the context does not contain the answer, state that clearly. Do not use external knowledge.\",\n                },\n            },\n        },\n    }\n\n    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)\n    defer cancel()\n\n    // Generate the content\n    res, err := s.client.Models.GenerateContent(ctx, req)\n    if err != nil {\n        return \"\", fmt.Errorf(\"failed to generate content: %w\", err)\n    }\n\n    if len(res.Candidates) == 0 {\n        return \"\", fmt.Errorf(\"no candidates returned from model\")\n    }\n\n    if len(res.Candidates[0].Content.Parts) == 0 {\n        return \"\", fmt.Errorf(\"empty response from model\")\n    }\n\n    return res.Candidates[0].Content.Parts[0].Text, nil\n}\n```\n\nTo use File Search, your documents must be in GCS. A production pipeline needs a way to upload files. We can create a separate command or an HTTP endpoint that uses the `google.golang.org/api/storage` library to upload files to the designated directory.\n\n```\npackage ingest\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"io\"\n    \"net/http\"\n\n    \"google.golang.org/api/option\"\n    \"google.golang.org/api/storage/v1\"\n)\n\ntype Ingestor struct {\n    svc *storage.Service\n    bucket string\n    dir string\n}\n\nfunc NewIngestor(ctx context.Context, projectID string) (*Ingestor, error) {\n    client, err := storage.NewService(ctx, option.WithCredentialsFile(\"/path/to/service-account.json\"))\n    if err != nil {\n        return nil, fmt.Errorf(\"failed to create storage service: %w\", err)\n    }\n    return &Ingestor{\n        svc: client,\n    }, nil\n}\n\nfunc (i *Ingestor) UploadFile(file io.Reader, filename string) error {\n    // In a real app, you'd set specific content types based on extension\n    obj := &storage.Object{\n        Name: i.dir + \"/\" + filename,\n    }\n    err := i.svc.Objects.Insert(i.bucket, obj).Media(file).Run()\n    if err != nil {\n        return fmt.Errorf(\"failed to upload file: %w\", err)\n    }\n    return nil\n}\n```\n\nEven with zero extra infrastructure, network calls to the LLM are expensive and slow. You must implement caching. In Go, `go-cache` or a Redis-based cache is suitable. The cache key should be a hash of the prompt and the specific file directory version.\n\n```\nimport (\n    \"github.com/patrickmn/go-cache\"\n)\n\ntype Cache struct {\n    c *cache.Cache\n}\n\nfunc NewCache() *Cache {\n    return &Cache{\n        c: cache.New(10*time.Minute, 10*time.Minute),\n    }\n}\n\nfunc (c *Cache) Get(key string) (string, bool) {\n    if item, found := c.c.Get(key); found {\n        return item.(string), true\n    }\n    return \"\", false\n}\n\nfunc (c *Cache) Set(key, value string) {\n    c.c.Set(key, value, cache.DefaultExpiration)\n}\n```\n\nYou need to track:\n\nIntegrate with OpenTelemetry. The `genai` library supports OpenTelemetry middleware, which makes it easy to inject spans into your Go application.\n\n```\nuse google.golang.org/api/option\nset option.WithTelemetryEnabled() // or similar OTel compatible options\n```\n\nSince File Search runs on GCS, you must strictly control access to the GCS bucket. Use IAM policies to restrict who can read the documents. Never expose the GCS bucket publicly. The Gemini API key should be stored in a secrets manager (e.g., GCP Secret Manager) and injected at runtime. In a multi-tenant scenario, you might need separate GCS directories or buckets per tenant, which complicates the `FileSearch` configuration. In that case, you must dynamically construct the `FileSearch` tool configuration per request, which requires careful state management in your Go service.\n\nWhile File Search is powerful, it may miss specific metadata. You can combine it with a traditional vector search by implementing a custom tool that calls your vector DB and then passing the results to Gemini as context. However, this defeats the \"zero extra infrastructure\" goal. A better hybrid approach is to pre-process your files in GCS to include metadata in the file names or front-matter (if using TXT/HTML) that Gemini's indexer can pick up.\n\nFor better UX, implement streaming. The `genai` client supports `StreamGenerateContent`. This allows you to send the response to the user as it is generated.\n\n```\nfunc (s *GeminiService) StreamQuery(ctx context.Context, prompt string, w io.Writer) error {\n    req := /* ... same as Query ... */\n    stream, err := s.client.Models.StreamGenerateContent(ctx, req)\n    if err != nil {\n        return err\n    }\n    defer stream.Close()\n\n    for {\n        res, err := stream.Recv()\n        if err == io.EOF {\n            break\n        }\n        if err != nil {\n            return err\n        }\n        if len(res.Candidates) > 0 && len(res.Candidates[0].Content.Parts) > 0 {\n            if _, err := w.Write([]byte(res.Candidates[0].Content.Parts[0].Text)); err != nil {\n                return err\n            }\n            w.Flush() // if w is an http.ResponseWriter\n        }\n    }\n    return nil\n}\n```\n\nBuilding a RAG pipeline without a vector database is not just about simplifying infrastructure; it is about leveraging the semantic understanding of LLMs to handle the retrieval layer. By using Go for your application logic and Gemini's File Search for the data layer, you can create a robust, scalable, and maintainable system. The key to success lies in proper data hygiene (cleaning your documents before upload), rigorous caching, and monitoring the \"retrieval quality\" metrics. As models continue to improve, the boundary between \"retrieval\" and \"reasoning\" will blur further, making these server-side semantic tools increasingly viable for production workloads. For a deeper look at similar patterns, check out our previous analysis on [stateless RAG architectures](https://tamiz.pro/insights/stateless-rag) and [optimizing LLM inference costs](https://tamiz.pro/insights/llm-inference-optimization).\n\n**Q: Can I use File Search with structured data like CSVs?**\n\nA: Yes, CSVs are supported, but the semantic search performance depends on the column headers and content readability. For large structured datasets, a SQL engine or vector DB with metadata filtering is usually more accurate.\n\n**Q: What is the maximum file size for a single document?**\n\nA: Currently, the limit is 2GB per file. However, very large files may time out during indexing or search. It is best practice to chunk large PDFs into logical sections if possible.\n\n**Q: How does Gemini know which files to search?**\n\nA: It searches the entire directory specified in the `FileSearch` configuration. You cannot dynamically filter by metadata within the tool itself, so organize your GCS directories logically (e.g., `/docs/2023`, `/docs/2024`) if you need temporal separation.", "url": "https://wpnews.pro/news/building-production-grade-rag-with-go-and-gemini-file-search-a-zero-approach", "canonical_source": "https://dev.to/tamizuddin/building-production-grade-rag-with-go-and-gemini-file-search-a-zero-infrastructure-approach-il0", "published_at": "2026-09-22 18:01:33+00:00", "updated_at": "2026-09-22 18:23:05.899934+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "ai-infrastructure", "generative-ai"], "entities": ["Go", "Gemini File Search", "Google", "Gemini API", "Pinecone", "Weaviate", "Milvus"], "alternates": {"html": "https://wpnews.pro/news/building-production-grade-rag-with-go-and-gemini-file-search-a-zero-approach", "markdown": "https://wpnews.pro/news/building-production-grade-rag-with-go-and-gemini-file-search-a-zero-approach.md", "text": "https://wpnews.pro/news/building-production-grade-rag-with-go-and-gemini-file-search-a-zero-approach.txt", "jsonld": "https://wpnews.pro/news/building-production-grade-rag-with-go-and-gemini-file-search-a-zero-approach.jsonld"}}