◉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-search3embeddingllm3antigravity464ai-tools16chromadbadvanced20

✦ 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 $10 for lifetime access
View Membership →

Related Articles

⚙ 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-10-01
You Don't Need to Read Commands: Three Places to Check When an Agent Works
If strings of terminal text make the approve button feel risky, here is a way to check just three things: a way back before you start, the first word of a command, and the diff afterward.
⚙ AI Tools2026-09-14
The October 16 date I could not find — taking stock of Gemini model shutdown dates yourself
A widely repeated shutdown date for the Gemini 2.5 family is not on the official deprecations page. Here is how I pull every model ID out of a repository and reconcile it against a ledger I maintain myself, with working scripts.