ANTIGRAVITY LABJP
Articles/AI Tools
AI Tools/2026-03-26Advanced

Building a RAG Pipeline with Antigravity— Unlock Your Company's Knowledge with Vector Search and LLMs

Build a RAG pipeline with Antigravity — the three traps in migrating to gemini-embedding-001, incremental ChromaDB ingestion, and LLM-as-a-Judge evaluation.

rag8vector-search3embeddingllm3antigravity445ai-tools15chromadbadvanced20

Premium Article

Setup and context — What Is RAG and Why Does It Matter?

Large Language Models carry broad general knowledge, but they can't answer questions about your proprietary data — internal documentation, API specifications, knowledge bases, or customer interaction histories. Fine-tuning is expensive and requires retraining every time the data changes.

RAG (Retrieval-Augmented Generation) solves this problem elegantly. It retrieves documents relevant to a user's question via vector search, then feeds that information as context to the LLM, enabling accurate and up-to-date responses.

We'll build a complete RAG pipeline from scratch, leaning on Antigravity's AI assistance along the way. This guide is aimed at engineers with Python fundamentals who want to run an LLM over their own data. If you'd like a refresher on crafting effective prompts first, the Antigravity Prompt Engineering Advanced Guide makes the generation sections here much easier to follow.

One thing worth flagging before we start: the code below uses gemini-embedding-001 for embeddings. text-embedding-004 — the model you'll still find in most RAG tutorials — was shut down on January 14, 2026, and now returns a 404. Swapping it out looks like a one-line change, but it trips you up in three places: dimensionality, normalization, and task types. I've written up each one, in the order I hit them, in the second half of this article.

RAG Architecture Overview

A RAG system consists of three major phases.

Ingestion Phase

Documents are loaded, split into appropriately-sized chunks, vectorized using an embedding model, and stored in a vector database.

# RAG ingestion pipeline generated with Antigravity
# document_ingestor.py
 
import hashlib
import math
import os
from pathlib import Path
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import (
    DirectoryLoader,
    TextLoader,
    PyPDFLoader,
    UnstructuredMarkdownLoader,
)
from langchain_google_genai import GoogleGenerativeAIEmbeddings
import chromadb
from chromadb.config import Settings
 
# --- Configuration ---
CHROMA_PERSIST_DIR = "./chroma_db"
COLLECTION_NAME = "company_knowledge_v2"  # New dimensionality, new collection name
EMBED_MODEL = "gemini-embedding-001"
EMBED_DIM = 1536       # Recommended: 768 / 1536 / 3072. Anything but 3072 needs manual normalization
CHUNK_SIZE = 800       # Character count, not tokens
CHUNK_OVERLAP = 200    # Overlap between chunks
 
def load_documents(source_dir: str) -> list:
    """Load documents in multiple formats"""
    loaders = {
        "**/*.txt": TextLoader,
        "**/*.md": UnstructuredMarkdownLoader,
        "**/*.pdf": PyPDFLoader,
    }
    documents = []
    for glob_pattern, loader_cls in loaders.items():
        loader = DirectoryLoader(
            source_dir,
            glob=glob_pattern,
            loader_cls=loader_cls,
            show_progress=True,
        )
        documents.extend(loader.load())
    return documents
 
def chunk_documents(documents: list) -> list:
    """Split documents into semantically meaningful chunks"""
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=CHUNK_SIZE,
        chunk_overlap=CHUNK_OVERLAP,
        separators=["\n## ", "\n### ", "\n\n", "\n", ". ", " "],
        length_function=len,
    )
    return splitter.split_documents(documents)
 
def l2_normalize(vec: list[float]) -> list[float]:
    """Vectors below 3072 dimensions come back un-normalized"""
    norm = math.sqrt(sum(v * v for v in vec))
    return [v / norm for v in vec] if norm else vec
 
def chunk_id(chunk) -> str:
    """Stable ID derived from source + content, so re-runs don't duplicate"""
    source = chunk.metadata.get("source", "unknown")
    seed = f"{source}::{chunk.page_content}".encode("utf-8")
    return hashlib.sha1(seed).hexdigest()
 
def create_embeddings_and_store(chunks: list):
    """Generate embeddings and store in ChromaDB"""
    embeddings = GoogleGenerativeAIEmbeddings(
        model=EMBED_MODEL,
        google_api_key=os.getenv("GOOGLE_API_KEY"),
        output_dimensionality=EMBED_DIM,
    )
 
    client = chromadb.PersistentClient(
        path=CHROMA_PERSIST_DIR,
        settings=Settings(anonymized_telemetry=False),
    )
 
    collection = client.get_or_create_collection(
        name=COLLECTION_NAME,
        metadata={"hnsw:space": "cosine"},  # Cosine similarity
    )
 
    batch_size = 100  # Gemini API's per-request ceiling
    for i in range(0, len(chunks), batch_size):
        batch = chunks[i:i + batch_size]
        texts = [chunk.page_content for chunk in batch]
        metadatas = [chunk.metadata for chunk in batch]
        ids = [chunk_id(chunk) for chunk in batch]
 
        # Be explicit about the document-side task type
        vectors = embeddings.embed_documents(
            texts,
            task_type="RETRIEVAL_DOCUMENT",
        )
        if EMBED_DIM != 3072:
            vectors = [l2_normalize(v) for v in vectors]
 
        # upsert, not add — running this twice won't duplicate anything
        collection.upsert(
            ids=ids,
            documents=texts,
            embeddings=vectors,
            metadatas=metadatas,
        )
        print(f"  Stored: {i + len(batch)}/{len(chunks)} chunks")
 
    print(f"✅ All {len(chunks)} chunks synced to ChromaDB")
 
# --- Execution ---
if __name__ == "__main__":
    docs = load_documents("./knowledge_base")
    print(f"📄 Loaded {len(docs)} documents")
 
    chunks = chunk_documents(docs)
    print(f"✂️  Split into {len(chunks)} chunks")
 
    create_embeddings_and_store(chunks)

Expected output:

📄 Loaded 47 documents
✂️  Split into 312 chunks
  Stored: 100/312 chunks
  Stored: 200/312 chunks
  Stored: 300/312 chunks
  Stored: 312/312 chunks
✅ All 312 chunks synced to ChromaDB

A quick word on why the IDs are content hashes rather than sequential numbers. With doc_0, doc_1, and so on, adding a single document upstream shifts the ID of every chunk after it. The same text gets stored twice under different IDs, and your top search results fill up with duplicates of one another. Hashing means unchanged chunks land on the same ID no matter how many times you run the ingestion. That's also why this uses upsert instead of add.

The same ID scheme becomes the foundation for incremental updates later: store the previous hash list, upsert only what's new or changed, and delete the IDs that disappeared.

Retrieval Phase

The user's question is vectorized, and the most similar chunks are retrieved. Retrieval accuracy is the single biggest factor in overall RAG quality.

Generation Phase

Retrieved results are injected into a prompt, and the LLM generates a grounded answer.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Understand the design principles and key components of RAG architecture systematically
Build a vector search pipeline in Antigravity that instantly incorporates your own data into LLM responses
Master chunking strategies, reranking techniques, and evaluation metrics essential for production RAG systems
Avoid the dimensionality, normalization, and task-type traps that surface when migrating to gemini-embedding-001
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

AI Tools2026-03-30
Antigravity × Custom AI Chatbot Pipeline — Building Production-Grade Assistants with RAG, Function Calling, and Streaming UI
Learn how to build a production-grade AI chatbot by integrating RAG, Function Calling, and Streaming UI with Antigravity — from architecture design to Cloudflare Workers deployment.
AI Tools2026-06-12
Cutting Down 'Plausible but Wrong' RAG Answers — A Retrieval Evaluation Harness for Gemma 4 and Antigravity
Replace gut feeling with recall@5, MRR and faithfulness scores — a 30-question golden dataset and a small Python harness for evaluating a local Gemma 4 RAG stack.
AI Tools2026-05-11
Three Months Using Antigravity as a Creative Assistant: An Artist's Honest Review
An indie creator who develops apps while maintaining an international art practice shares an honest, three-month account of using Antigravity for the work that surrounds creation. What can you delegate? What must stay in your own hands? Here's the dividing line I found.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →