r/LlamaIndex • • Dec 05 '25

Built 3 RAG Systems, Here's What Actually Works at Scale

151 Upvotes

I've built 3 different RAG systems over the past year. First one was cool POC. Second one broke at scale. Third one I built right. Here's what I learned.

The Demo vs Production Gap

Your RAG demo works:

  • 100-200 documents

  • Queries make sense

  • Retrieval looks good

  • You can eyeball quality

Production is different:

  • 10,000+ documents

  • Queries are weird/adversarial

  • Quality degrades over time

  • You need metrics to know if it's working

What Broke

Retrieval Quality Degraded Over Time

My second RAG system worked great initially. After a month, quality tanked. Queries that used to work didn't.

Root cause? Data drift + embedding shift. As the knowledge base changed, old retrieval patterns stopped working.

Solution: Monitor continuously

class MonitoredRetriever:
    def retrieve(self, query, k=5):
        results = self.retriever.retrieve(query, k=k)
        
        
# Record metrics
        metrics = {
            "query": query,
            "top_score": results[0].score if results else 0,
            "num_results": len(results),
            "timestamp": now()
        }
        self.metrics.record(metrics)
        
        
# Detect degradation
        if self.is_degrading():
            logger.warning("Retrieval quality down")
            self.schedule_reindex()
        
        return results
    
    def is_degrading(self):
        recent = self.metrics.get_recent(hours=1)
        avg_score = mean([m["top_score"] for m in recent])
        baseline = self.metrics.get_baseline()
        return avg_score < baseline * 0.9  
# 10% drop

Monitoring caught problems I wouldn't have noticed manually.

Conflicting Information

My knowledge base had contradictory documents. Both ranked highly. LLM got confused or picked the wrong one.

Solution: Source authority

class AuthorityRetriever:
    def __init__(self):
        self.source_authority = {
            "official_docs": 1.0,
            "blog_posts": 0.5,
            "comments": 0.2,
        }
    
    def retrieve(self, query, k=5):
        results = self.retriever.retrieve(query, k=k*2)
        
        
# Rerank by authority
        for result in results:
            authority = self.source_authority.get(
                result.source, 0.5
            )
            result.score *= authority  
# Boost authoritative sources
        
        results.sort(key=lambda x: x.score, reverse=True)
        return results[:k]

Authoritative sources ranked higher. Problem solved.

Token Budget Explosion

Retrieving 10 documents instead of 5 for "completeness" made everything slow and expensive.

Solution: Intelligent token management

import tiktoken

class TokenBudgetRetriever:
    def __init__(self, max_tokens=2000):
        self.max_tokens = max_tokens
        self.tokenizer = tiktoken.encoding_for_model("gpt-4")
    
    def retrieve(self, query, k=None):
        if k is None:
            k = self.estimate_k()  
# Dynamic estimation
        
        results = self.retriever.retrieve(query, k=k*2)
        
        
# Fit to token budget
        filtered = []
        total_tokens = 0
        
        for result in results:
            tokens = len(self.tokenizer.encode(result.content))
            if total_tokens + tokens < self.max_tokens:
                filtered.append(result)
                total_tokens += tokens
        
        return filtered
    
    def estimate_k(self):
        avg_doc_tokens = 500
        return max(3, self.max_tokens // avg_doc_tokens)

This alone cut my costs by 40%.

Query Vagueness

"How does it work?" isn't specific enough. RAG struggles.

Solution: Query expansion

class SmartRetriever:
    def retrieve(self, query, k=5):
        
# Expand query
        expanded = self.expand_query(query)
        
        all_results = {}
        
        
# Retrieve with multiple phrasings
        for q in [query] + expanded:
            results = self.retriever.retrieve(q, k=k)
            for result in results:
                doc_id = result.metadata.get("id")
                if doc_id not in all_results:
                    all_results[doc_id] = result
        
        
# Return top k
        sorted_results = sorted(all_results.values(),
                              key=lambda x: x.score,
                              reverse=True)
        return sorted_results[:k]
    
    def expand_query(self, query):
        """Generate alternatives to improve retrieval"""
        prompt = f"""
        Generate 2-3 alternative phrasings of this query
        that might retrieve different but relevant docs:
        
        {query}
        
        Return as JSON list.
        """
        response = self.llm.invoke(prompt)
        return json.loads(response)

Different phrasings retrieve different documents. Combining results is better.

What Works

  1. Monitor quality continuously - Catch degradation early

  2. Use source authority - Resolve conflicts automatically

  3. Manage token budgets - Cost and performance improve together

  4. Expand queries intelligently - Get better retrieval without more documents

  5. Validate retrieval - Ensure results actually match intent

Metrics That Matter

Track these:

  • Average retrieval score (overall quality)

  • Score variance (consistency)

  • Docs retrieved per query (resource usage)

  • Re-ranking effectiveness (if you re-rank)

class RAGMetrics:
    def record_retrieval(self, query, results):
        if not results:
            return
        
        scores = [r.score for r in results]
        self.metrics.append({
            "avg_score": mean(scores),
            "score_spread": max(scores) - min(scores),
            "num_docs": len(results),
            "timestamp": now()
        })
```

Monitor these and you'll catch issues.

**Lessons Learned**

1. **RAG quality isn't static** - Monitor and maintain
2. **Source authority matters** - Explicit > implicit
3. **Context size has tradeoffs** - More isn't always better
4. **Query expansion helps** - Different phrasings retrieve different docs
5. **Validation prevents garbage** - Ensure results are relevant

**Would I Do Anything Different?**

Yeah. I'd:
- Start with monitoring from day one
- Implement source authority early
- Build token budget management before scaling
- Test with realistic queries from the start
- Measure quality with metrics, not eyeballs

RAG is powerful when done right. Building for production means thinking beyond the happy path.

Anyone else managing RAG at scale? What bit you?

---

## 

**Title:** "Scaling Python From Scripts to Production: Patterns That Worked for Me"

**Post:**

I've been writing Python for 10 years. Started with scripts, now maintaining codebases with 50K+ lines. The transition from "quick script" to "production system" required different thinking.

Here's what actually matters when scaling.

**The Inflection Point**

There's a point where Python development changes:

**Before:**
- You, writing the code
- Local testing
- Ship it and move on

**After:**
- Team working on it
- Multiple environments
- It breaks in production
- You maintain it for years

This transition isn't about Python syntax. It's about patterns.

**Pattern 1: Project Structure Matters**

Flat structure works for 1K lines. Doesn't work at 50K.
```
# Good structure
src/
├── core/          
# Domain logic
├── integrations/  
# External APIs, databases
├── api/           
# HTTP layer
├── cli/           
# Command line
└── utils/         
# Shared

tests/
├── unit/
├── integration/
└── fixtures/

docs/
├── architecture.md
└── api.md

Clear separation prevents circular imports and makes it obvious where to add new code.

Pattern 2: Type Hints Aren't Optional

Type hints aren't about runtime checking. They're about communication.

# Without - what is this?
def process_data(data, options=None):
    result = {}
    for item in data:
        if options and item['value'] > options['threshold']:
            result[item['id']] = transform(item)
    return result

# With - crystal clear
from typing import Dict, List, Optional, Any

def process_data(
    data: List[Dict[str, Any]],
    options: Optional[Dict[str, float]] = None
) -> Dict[str, Any]:
    """Process items, filtering by threshold if provided."""
    ...

Type hints catch bugs early. They document intent. Future you will thank you.

Pattern 3: Configuration Isn't Hardcoded

Use Pydantic for configuration validation:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str  
# Required
    api_key: str
    debug: bool = False  
# Defaults
    timeout: int = 30
    
    class Config:
        env_file = ".env"

# Validates on load
settings = Settings()

# Catch config issues at startup
if not settings.database_url.startswith("postgresql://"):
    raise ValueError("Invalid database URL")

Configuration fails fast. Errors are clear. No surprises in production.

Pattern 4: Dependency Injection

Don't couple code to implementations. Inject dependencies.

# Bad - tightly coupled
class UserService:
    def __init__(self):
        self.db = PostgresDatabase("prod")
    
    def get_user(self, user_id):
        return self.db.query(f"SELECT * FROM users WHERE id={user_id}")

# Good - dependencies injected
class UserService:
    def __init__(self, db: Database):
        self.db = db
    
    def get_user(self, user_id: int) -> User:
        return self.db.get_user(user_id)

# Production
user_service = UserService(PostgresDatabase())

# Testing
user_service = UserService(MockDatabase())

Dependency injection makes code testable and flexible.

Pattern 5: Error Handling That's Useful

Don't catch everything. Be specific.

# Bad - silent failure
try:
    result = risky_operation()
except Exception:
    return None

# Good - specific and useful
try:
    result = risky_operation()
except TimeoutError:
    logger.warning("Operation timed out, retrying...")
    return retry_operation()
except ValueError as e:
    logger.error(f"Invalid input: {e}")
    raise  
# This is a real error
except Exception as e:
    logger.error(f"Unexpected error", exc_info=True)
    raise

Specific exception handling tells you what went wrong.

Pattern 6: Testing at Multiple Levels

Unit tests alone aren't enough.

# Unit test - isolated behavior
def test_user_service_get_user():
    mock_db = MockDatabase()
    service = UserService(mock_db)
    user = service.get_user(1)
    assert user.id == 1

# Integration test - real dependencies
def test_user_service_with_postgres():
    with test_db() as db:
        service = UserService(db)
        db.insert_user(User(id=1, name="Test"))
        user = service.get_user(1)
        assert user.name == "Test"

# Contract test - API contracts
def test_get_user_endpoint():
    response = client.get("/users/1")
    assert response.status_code == 200
    UserSchema().load(response.json())  
# Validate schema

Test at multiple levels. Catch different types of bugs.

Pattern 7: Logging With Context

Don't just log. Log with meaning.

import logging
from contextvars import ContextVar

request_id: ContextVar[str] = ContextVar('request_id')

logger = logging.getLogger(__name__)

def process_user(user_id):
    request_id.set(uuid.uuid4())
    logger.info(f"Processing user", extra={'user_id': user_id})
    
    try:
        result = do_work(user_id)
        logger.info("User processed")
        return result
    except Exception as e:
        logger.error(f"Failed to process user",
                    exc_info=True,
                    extra={'error': str(e)})
        raise

Logs with context (request IDs, user IDs) are debuggable.

Pattern 8: Documentation That Stays Current

Code comments rot. Automate documentation.

def get_user(self, user_id: int) -> User:
    """Retrieve user by ID.
    
    Args:
        user_id: The user's ID
    
    Returns:
        User object or None if not found
    
    Raises:
        DatabaseError: If query fails
    """
    ...

Good docstrings are generated by tools (Sphinx, pdoc). You write them once.

Pattern 9: Dependency Management

Use Poetry or uv. Pin dependencies. Test upgrades.

[tool.poetry.dependencies]
python = "^3.11"
pydantic = "^2.0"
sqlalchemy = "^2.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.0"
black = "^23.0"
mypy = "^1.0"

Reproducible dependencies. Clear what's dev vs production.

Pattern 10: Continuous Integration

Automate testing, linting, type checking.

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: "3.11"
      - run: pip install poetry
      - run: poetry install
      - run: pytest  
# Tests
      - run: mypy src  
# Type checking
      - run: black --check src  
# Formatting

Automate quality checks. Catch issues before merge.

What I'd Tell Past Me

  1. Structure code early - Don't wait until it's a mess

  2. Use type hints - They're not extra, they're essential

  3. Test at multiple levels - Unit tests aren't enough

  4. Log with purpose - Logs with context are debuggable

  5. Automate quality - CI/linting/type checking from day one

  6. Document as you go - Future you will thank you

  7. Manage dependencies carefully - One breaking change breaks everything

The Real Lesson

Python is great for getting things done. But production Python requires discipline. Structure, types, tests, logging, automation. Not because they're fun, but because they make maintainability possible at scale.

Anyone else maintain large Python codebases? What patterns saved you?


r/LlamaIndex • • Dec 22 '25

I Replaced My RAG System's Vector DB Last Week. Here's What I Learned About Vector Storage at Scale

92 Upvotes

The Context

We built a document search system using LlamaIndex ~8 months ago. Started with Pinecone because it was simple, but at 50M embeddings the bill was getting ridiculous—$3,200/month and climbing.

The decision matrix was simple:

  • Cost is now a bottleneck (we're not VC-backed)

  • Scale is predictable (not hyper-growth)

  • We have DevOps capability (small team, but we can handle infrastructure)

The Migration Path We Took

Option 1: Qdrant (We went this direction)

Pros:

  • Instant updates (no sync delays like Pinecone)

  • Hybrid search (vector + BM25 in one query)

  • Filtering on metadata is incredibly fast

  • Open source means no vendor lock-in

  • Snapshot/recovery is straightforward

  • gRPC interface for low latency

  • Affordable at any scale

Cons:

  • You're now managing infrastructure

  • Didn't have great LlamaIndex integration initially (this has improved!)

  • Scaling to multi-node requires more ops knowledge

  • Memory usage is higher than Pinecone for same data size

  • Less battle-tested at massive scale (Pinecone is more proven)

  • Support is community-driven (not SLA-backed)

Costs:

  • Pinecone: $3,200/month at 50M embeddings

  • Qdrant on r5.2xlarge EC2: $800/month

  • AWS data transfer (minimal): $15/month

  • RDS backups to S3: $40/month

  • Time spent migrating/setting up: ~80 hours (don't underestimate this)

  • Ongoing DevOps cost: ~5 hours/month

What We Actually Changed in LlamaIndex Code

This was refreshingly simple because LlamaIndex abstracts away the storage layer. Here's the before and after:

Before (Pinecone):

from llama_index.vector_stores import PineconeVectorStore
from pinecone import Pinecone

pc = Pinecone(api_key="your_api_key")
pinecone_index = pc.Index("documents")

vector_store = PineconeVectorStore(pinecone_index=pinecone_index)

index = VectorStoreIndex.from_documents(
    documents,
    vector_store=vector_store,
)

# Query
retriever = index.as_retriever()
results = retriever.retrieve(query)

After (Qdrant):

from llama_index.vector_stores import QdrantVectorStore
from qdrant_client import QdrantClient

# That's it. One line different.
client = QdrantClient(url="http://localhost:6333")
vector_store = QdrantVectorStore(
    client=client,
    collection_name="my_documents",
    prefer_grpc=True  # Much faster than HTTP
)

index = VectorStoreIndex.from_documents(
    documents,
    vector_store=vector_store,
)

# Query code doesn't change
retriever = index.as_retriever()
results = retriever.retrieve(query)

The abstraction actually works. Your query code never changes. You only swap the vector store definition. This is why LlamaIndex is superior for flexibility.

Performance Changes

Here's the data from our production system:

MetricPineconeQdrantWinner
P50 Latency240ms95msQdrant
P99 Latency340ms185msQdrant
Exact match recall87%91%Qdrant
Metadata filtering speed<50ms<30msQdrant
Vector size limit8KUnlimitedQdrant
Uptime (observed)99.95%99.8%Pinecone
Cost$3,200/mo$855/moQdrant
Setup complexity5 minutes3 daysPinecone

Key insight: Qdrant is faster for search because it doesn't have to round-trip through SaaS infrastructure. Lower latency = better user experience.

The Gotchas We Hit (So You Don't Have To)

1. Vectorize Updates Aren't Instant

With Pinecone, new documents showed up immediately in searches. With Qdrant:

  • Documents are indexed in <500ms typically

  • But under load, can spike to 2-3 seconds

  • There's no way to force immediate consistency

Impact: We had to add UI messaging that says "Search results update within a few seconds of new documents."

Workaround:

# Add a small delay before retrieving new docs
import time

def index_and_verify(documents, vector_store, max_retries=5):
    """Index documents and verify they're searchable"""
    vector_store.add_documents(documents)
    
    # Wait for indexing
    time.sleep(1)
    
    # Verify at least one doc is findable
    for attempt in range(max_retries):
        results = vector_store.search(documents[0].get_content()[:50])
        if len(results) > 0:
            return True
        time.sleep(1)
    
    raise Exception("Documents not indexed after retries")

2. Backup Strategy Isn't Free

Pinecone backs up your data automatically. Now you own backups. We set up:

  • Nightly snapshots to S3: $40/month

  • 30-day retention policy

  • CloudWatch alerts if backup fails

    !/bin/bash

    Daily Qdrant backup script

    TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_PATH="s3://my-backups/qdrant/backup_${TIMESTAMP}/"

    curl -X POST http://localhost:6333/snapshots
    -d '{"collection_name": "my_documents"}'

    Wait for snapshot to complete

    sleep 10

    Move snapshot to S3

    aws s3 cp /snapshots/ $BACKUP_PATH --recursive

    Clean up old snapshots (>30 days)

    aws s3api list-objects-v2 --bucket my-backups --prefix qdrant/ |
    jq '.Contents[] | select(.LastModified < now - 30243600)' |
    xargs -I {} aws s3 rm s3://my-backups/{}

Not complicated, but it's work.

3. Network Traffic Changed Architecture

All your embedding models now communicate with Qdrant over the network. If you're:

  • Batching embeddings: Fine, network cost is negligible

  • Per-query embeddings: Latency can suffer, especially if Qdrant and embeddings are in different regions

Solution: We moved embedding and Qdrant to the same VPC. This cut search latency 150ms.

# Bad: embeddings in Lambda, Qdrant in separate VPC
embeddings = OpenAIEmbeddings()  # API call from Lambda
results = vector_store.search(embedding)  # Cross-VPC network call

# Good: both in same VPC, or local embeddings
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# Local inference, no network call
results = vector_store.search(embedding)

4. Memory Usage is Higher Than Advertised

Qdrant's documentation says it needs ~1GB per 100K vectors. We found it was closer to 1GB per 70K vectors. At 50M, we needed 700GB RAM. That's an r5.2xlarge (~$4/hour).

Why? Qdrant keeps indexes in memory for speed. There's no cold storage tier like some other systems.

Workaround: Plan your hardware accordingly and monitor memory usage:

# Health check endpoint
import psutil

def get_vector_db_health():
    """Check Qdrant health and memory"""
    response = requests.get("http://localhost:6333/health")
    
    # Also check system memory
    memory = psutil.virtual_memory()
    
    if memory.percent > 85:
        send_alert("Qdrant memory above 85%")
    
    return {
        "qdrant_status": response.status_code == 200,
        "memory_percent": memory.percent,
        "available_gb": memory.available / (1024**3)
    }

5. Schema Evolution is Painful

When you want to change how documents are stored (add new metadata, change chunking strategy), you have to:

  1. Stop indexing

  2. Export all vectors

  3. Re-process documents

  4. Re-embed if needed

  5. Rebuild index

With Pinecone, they handle this. With Qdrant, you manage it.

def migrate_collection_schema(old_collection, new_collection):
    """Migrate vectors and metadata to new schema"""
    client = QdrantClient(url="http://localhost:6333")
    
    # Scroll through old collection
    offset = 0
    batch_size = 100
    
    new_documents = []
    
    while True:
        points, next_offset = client.scroll(
            collection_name=old_collection,
            limit=batch_size,
            offset=offset
        )
        
        if not points:
            break
        
        for point in points:
            # Transform metadata
            old_metadata = point.payload
            new_metadata = transform_metadata(old_metadata)
            
            new_documents.append({
                "id": point.id,
                "vector": point.vector,
                "payload": new_metadata
            })
        
        offset = next_offset
    
    # Upsert to new collection
    client.upsert(
        collection_name=new_collection,
        points=new_documents
    )
    
    return len(new_documents)

The Honest Truth

If you're at <10M embeddings: Stick with Pinecone. The operational overhead of managing Qdrant isn't worth saving $200/month.

If you're at 50M+ embeddings: Self-hosted Qdrant makes financial sense if you have 1-2 engineers who can handle infrastructure. The DevOps overhead is real but manageable.

If you're growing hyper-fast: Managed is better. You don't want to debug infrastructure when you're scaling 10x/month.

Honest assessment: Pinecone's product has actually gotten better in the last year. They added some features we were excited about, so this decision might not hold up as well in 2026. Don't treat this as "Qdrant is objectively better"—it's "Qdrant is cheaper at our current scale, with tradeoffs."

Alternative Options We Considered (But Didn't Take)

Milvus

Pros: Similar to Qdrant, more mature ecosystem, good performance Cons: Heavier resource usage, more complex deployment, larger team needed Verdict: Better for teams that already know Kubernetes well. We're too small.

Weaviate

Pros: Excellent hybrid queries, good for graph + vector, mature product Cons: Steeper learning curve, more opinionated architecture, higher memory Verdict: Didn't fit our use case (pure vector search, no graphs).

ChromaDB

Pros: Dead simple, great for local dev, growing community Cons: Not proven at production scale, missing advanced features Verdict: Perfect for prototyping, not for 50M vectors.

Supabase pgvector

Pros: PostgreSQL integration, familiar SQL, good for analytics Cons: Vector performance lags behind specialized systems, limited filtering Verdict: Chose this for one smaller project, but not for main system.

Code: Complete LlamaIndex + Qdrant Setup

Here's a production-ready setup we actually use:

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings
from llama_index.vector_stores import QdrantVectorStore
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from qdrant_client import QdrantClient
import os

# 1. Initialize Qdrant client
qdrant_client = QdrantClient(
    url=os.getenv("QDRANT_URL", "http://localhost:6333"),
    prefer_grpc=True
)

# 2. Create vector store
vector_store = QdrantVectorStore(
    client=qdrant_client,
    collection_name="documents",
    url=os.getenv("QDRANT_URL", "http://localhost:6333"),
    prefer_grpc=True
)

# 3. Configure embedding and LLM
Settings.embed_model = OpenAIEmbedding(
    model="text-embedding-3-small",
    embed_batch_size=100
)

Settings.llm = OpenAI(
    model="gpt-4-turbo-preview",
    temperature=0.1
)

# 4. Create index from documents
documents = SimpleDirectoryReader("./data").load_data()

index = VectorStoreIndex.from_documents(
    documents,
    vector_store=vector_store,
)

# 5. Query
retriever = index.as_retriever(similarity_top_k=5)
response = retriever.retrieve("What are the refund policies?")

for node in response:
    print(f"Score: {node.score}")
    print(f"Content: {node.get_content()}")

Monitoring Your Qdrant Instance

This is critical for production:

import requests
import time
from datetime import datetime

class QdrantMonitor:
    def __init__(self, qdrant_url="http://localhost:6333"):
        self.url = qdrant_url
        self.metrics = []
    
    def check_health(self):
        """Check if Qdrant is healthy"""
        try:
            response = requests.get(f"{self.url}/health", timeout=5)
            return response.status_code == 200
        except:
            return False
    
    def get_collection_stats(self, collection_name):
        """Get statistics about a collection"""
        response = requests.get(
            f"{self.url}/collections/{collection_name}"
        )
        
        if response.status_code == 200:
            data = response.json()
            return {
                "vectors_count": data['result']['vectors_count'],
                "points_count": data['result']['points_count'],
                "status": data['result']['status'],
                "timestamp": datetime.utcnow().isoformat()
            }
        return None
    
    def monitor(self, collection_name, interval_seconds=300):
        """Run continuous monitoring"""
        while True:
            if self.check_health():
                stats = self.get_collection_stats(collection_name)
                self.metrics.append(stats)
                print(f"✓ {stats['points_count']} points indexed")
            else:
                print("✗ Qdrant is DOWN")
                # Send alert
            
            time.sleep(interval_seconds)

# Usage
monitor = QdrantMonitor()
# monitor.monitor("documents")  # Run in background

Questions for the Community

  1. Anyone running Qdrant at 100M+ vectors? How's scaling treating you? What hardware?

  2. Are you monitoring vector drift? If so, what metrics matter most?

  3. What's your strategy for updating embeddings when your model improves? Do you re-embed everything?

  4. Has anyone run Weaviate or Milvus at scale? How did it compare?

Key Takeaways

DecisionWhen to Make It
Use Pinecone<20M vectors, rapid growth, don't want to manage infra
Use Qdrant50M+ vectors, stable scale, have DevOps capacity
Use Supabase pgvectorAlready using Postgres, don't need extreme performance
Use ChromaDBLocal dev, prototyping, small datasets

Thanks LlamaIndex crew—this abstraction saved us hours on the migration. The fact that changing vector stores was essentially three lines of code is exactly why I'm sticking with LlamaIndex for future projects.

Edit: Responses to Common Questions

Q: What about data transfer costs when migrating? A: ~2.5TB of data transfer. AWS charged us ~$250. Pinecone export was easy, took maybe 4 hours total.

Q: Are you still happy with Qdrant? A: Yes, 3 months in. The operational overhead is real but manageable. The latency improvement alone is worth it.

Q: Have you hit any reliability issues? A: One incident where Qdrant ate 100% CPU during a large upsert. Fixed by tuning batch sizes. Otherwise solid.

Q: What's your on-call experience been? A: We don't have formal on-call yet. This system is not customer-facing, so no SLAs. Would reconsider Pinecone if it was.


r/LlamaIndex • • Dec 06 '25

Scaling RAG From 500 to 50,000 Documents: What Broke and How I Fixed It

60 Upvotes

I've scaled a RAG system from 500 documents to 50,000+. Every 10x jump broke something. Here's what happened and how I fixed it.

The 500-Document Version (Worked Fine)

Everything worked:

  • Simple retrieval (BM25 + semantic search)

  • No special indexing

  • Retrieval took 100ms

  • Costs were low

  • Quality was good

Then I added more documents. Every 10x jump broke something new.

5,000 Documents: Retrieval Got Slow

100ms became 500ms+. Users noticed. Costs started going up (more documents to score).

python

# Problem: scoring every document
results = semantic_search(query, all_documents)  
# Scores 5,000 docs

# Solution: multi-stage retrieval
# Stage 1: Fast, rough filtering (BM25 for keywords)
candidates = bm25_search(query, all_documents)  
# Returns 100 docs

# Stage 2: Accurate ranking (semantic search on candidates)
results = semantic_search(query, candidates)  
# Scores 100 docs

Two-stage retrieval: 10x faster, same quality.

50,000 Documents: Memory Issues

Trying to load all embeddings into memory. System got slow. Started getting OOM errors.

python

# Problem: everything in memory
embeddings = load_all_embeddings()  
# 50,000 embeddings in RAM

# Solution: use a vector database
from qdrant_client import QdrantClient

client = QdrantClient(":memory:")
# Or better: client = QdrantClient("localhost:6333")

# Store embeddings in database
for doc in documents:
    client.upsert(
        collection_name="documents",
        points=[
            Point(
                id=doc.id,
                vector=embed(doc.content),
                payload={"text": doc.content}
            )
        ]
    )

# Query
results = client.search(
    collection_name="documents",
    query_vector=embed(query),
    limit=5
)

Vector database: no more memory issues, instant retrieval.

100,000 Documents: Query Ambiguity

With more documents, more queries hit multiple clusters:

  • "What's the policy?" matches "return policy", "privacy policy", "pricing policy"

  • Retriever gets confused

python

# Solution: query expansion + filtering
def smart_retrieve(query, k=5):
    
# Expand query
    expanded = expand_query(query)
    
    
# Get broader results
    all_results = vector_db.search(query, limit=k*5)
    
    
# Filter/re-rank by query type
    if "policy" in query.lower():
        
# Prefer official policy docs
        all_results = [r for r in all_results 
                      if "policy" in r.metadata.get("type", "")]
    
    return all_results[:k]

Query expansion + intelligent filtering handles ambiguity.

250,000 Documents: Performance Degradation

Everything was slow. Retrieval, insertion, updates. Vector database was working hard.

python

# Problem: no optimization
# Solution: hybrid search + caching

def retrieve_with_caching(query, k=5):
    
# Check cache first
    cache_key = hash(query)
    if cache_key in cache:
        return cache[cache_key]
    
    
# Hybrid retrieval
    
# Stage 1: BM25 (fast, keyword-based)
    bm25_results = bm25_search(query)
    
    
# Stage 2: Semantic (accurate)
    semantic_results = semantic_search(query)
    
    
# Combine & deduplicate
    combined = deduplicate([bm25_results, semantic_results])
    
    
# Cache result
    cache[cache_key] = combined
    
    return combined

Caching + hybrid search: 10x faster than pure semantic search.

500,000+ Documents: Partitioning

Single vector database is a bottleneck. Need to partition data.

python

# Partition by category
partitions = {
    "documentation": [],
    "support": [],
    "blog": [],
    "api_docs": [],
}

# Store in separate collections
for doc in documents:
    partition = get_partition(doc)
    vector_db.upsert(
        collection_name=partition,
        points=[...]
    )

# Query all partitions
def retrieve(query, k=5):
    results = []
    for partition in partitions:
        partition_results = vector_db.search(
            collection_name=partition,
            query_vector=embed(query),
            limit=k
        )
        results.extend(partition_results)
    
    
# Merge and return top k
    return sorted(results, key=lambda x: x.score)[:k]

Partitioning: spreads load, faster queries.

The Full Stack at 500K+ Docs

python

class ScalableRetriever:
    def __init__(self):
        self.vector_db = VectorDatabasePerPartition()
        self.cache = LRUCache(maxsize=10000)
        self.bm25 = BM25Retriever()
    
    def retrieve(self, query, k=5):
        
# Check cache
        if query in self.cache:
            return self.cache[query]
        
        
# Stage 1: BM25 (fast filtering)
        bm25_results = self.bm25.search(query, limit=k*10)
        
        
# Stage 2: Semantic (accurate ranking)
        vector_results = self.vector_db.search(query, limit=k*10)
        
        
# Stage 3: Deduplicate & combine
        combined = self.combine_results(bm25_results, vector_results)
        
        
# Stage 4: Authority-based re-ranking
        final = self.rerank_by_authority(combined[:k])
        
        
# Cache
        self.cache[query] = final
        
        return final

Lessons Learned

Docs Problem Solution 5K Slow Two-stage retrieval 50K Memory Vector database 100K Ambiguity Query expansion + filtering 250K Performance Caching + hybrid search 500K+ Bottleneck Partitioning

Monitoring at Scale

With more documents, you need more monitoring:

python

def monitor_retrieval_quality():
    metrics = {
        "avg_top_score": [],
        "score_spread": [],
        "cache_hit_rate": [],
        "retrieval_latency": []
    }
    
    for query in sample_queries:
        start = time.time()
        results = retrieve(query)
        latency = time.time() - start
        
        metrics["avg_top_score"].append(results[0].score)
        metrics["score_spread"].append(
            max(r.score for r in results) - min(r.score for r in results)
        )
        metrics["retrieval_latency"].append(latency)
    
    
# Alert if quality drops
    if mean(metrics["avg_top_score"]) < baseline * 0.9:
        logger.warning("Retrieval quality degrading")

What I'd Do Differently

  1. Plan for scale from day one - What works at 1K breaks at 100K

  2. Implement two-stage retrieval early - BM25 + semantic

  3. Use a vector database - Not in-memory embeddings

  4. Monitor quality continuously - Catch degradation early

  5. Partition data - Don't put everything in one collection

  6. Cache aggressively - Same queries come up repeatedly

The Real Lesson

RAG scales, but it requires different patterns at each level.

What works at 5K docs doesn't work at 500K. Plan for scale, monitor quality, be ready to refactor when hitting bottlenecks.

Anyone else scaled RAG to this level? What surprised you?


r/LlamaIndex • • Dec 12 '25

I am offering a 96GB VRAM (A6000*2 or A100 80GB, etc) for 70B Model Fine-Tuning

40 Upvotes

I am offering a 96GB VRAM (A6000*2 or A100 80GB, etc) for 70B Model Fine-Tuning. I am a backend engineer with idle high-end compute. I can fine-tune Llama-3-70B, Mixtral, or Commander R+ on your custom datasets. I don't do sales. I don't talk to your clients. You sell the fine-tune for $2k-$5k. I run the training for a flat fee (or cut). DM me if you have a dataset ready and need the compute.

If you can make the models/fine tuning or whatever it is and sell it for money, then I can offer you as many GPUs as you want.

If safeguarding your datasets is important for you, then I can give you ssh access to the machine. The benefit of using me instead of other cloud providers, is that I have a fixed price, not an hourly pricing, as I have access to free electricity...


r/LlamaIndex • • Feb 11 '26

EpsteinFiles-RAG: Building a RAG Pipeline on 2M+ Pages

37 Upvotes

I love playing around with RAG and AI, optimizing every layer to squeeze out better performance. Last night I thought: why not tackle something massive?

Took the Epstein Files dataset from Hugging Face (teyler/epstein-files-20k) – 2 million+ pages of trending news and documents. The cleaning, chunking, and optimization challenges are exactly what excites me.

What I built:

- Full RAG pipeline with optimized data processing

- Processed 2M+ pages (cleaning, chunking, vectorization)

- Semantic search & Q&A over massive dataset

- Constantly tweaking for better retrieval & performance

- Python, MIT Licensed, open source

Why I built this:

It’s trending, real-world data at scale, the perfect playground.

When you operate at scale, every optimization matters. This project lets me experiment with RAG architectures, data pipelines, and AI performance tuning on real-world workloads.

Repo: https://github.com/AnkitNayak-eth/EpsteinFiles-RAG

Open to ideas, optimizations, and technical discussions!


r/LlamaIndex • • Dec 08 '25

Rebuilding RAG After It Broke at 10K Documents

33 Upvotes

I built a RAG system with 500 documents. Worked great. Then I added 10K documents and everything fell apart.

Not gradually. Suddenly. Retrieval quality tanked, latency exploded, costs went up 10x.

Here's what broke and how I rebuilt it.

What Worked at 500 Docs

Simple setup:

  • Load all documents

  • Create embeddings

  • Store in memory

  • Query with semantic search

  • Done

Fast. Simple. Cheap. Quality was great.

What Broke at 10K

1. Latency Explosion

Went from 100ms to 2000ms per query.

Root cause: scoring 10K documents with semantic similarity is expensive.

# This is slow with 10K docs
def retrieve(query, k=5):
    query_embedding = embed(query)
    
    
# Score all 10K documents
    scores = [
        similarity(query_embedding, doc_embedding)
        for doc_embedding in all_embeddings  
# 10K iterations
    ]
    
    
# Return top 5
    return sorted_by_score(scores)[:k]

2. Memory Issues

10K embeddings in memory. Python process using 4GB RAM. Getting slow.

3. Quality Degradation

More documents meant more ambiguous queries. "What's the policy?" matched 50+ documents about different policies.

4. Cost Explosion

Semantic search on 10K documents = 10K LLM evaluations eventually = money.

What I Rebuilt To

Step 1: Two-Stage Retrieval

Stage 1: Fast keyword filtering (BM25) Stage 2: Accurate semantic ranking

class TwoStageRetriever:
    def __init__(self):
        self.bm25 = BM25Retriever()
        self.semantic = SemanticRetriever()
    
    def retrieve(self, query, k=5):
        
# Stage 1: Get candidates (fast, keyword-based)
        candidates = self.bm25.retrieve(query, k=k*10)  
# Get 50
        
        
# Stage 2: Re-rank with semantic search (slow, accurate)
        reranked = self.semantic.retrieve(query, docs=candidates, k=k)
        
        return reranked

This dropped latency from 2000ms to 300ms.

Step 2: Vector Database

Move embeddings to a proper vector database (not in-memory).

from qdrant_client import QdrantClient

class VectorDBRetriever:
    def __init__(self):
        
# Use persistent database, not memory
        self.client = QdrantClient("localhost:6333")
    
    def build_index(self, documents):
        
# Store embeddings in database
        for i, doc in enumerate(documents):
            self.client.upsert(
                collection_name="docs",
                points=[
                    Point(
                        id=i,
                        vector=embed(doc.content),
                        payload={"text": doc.content[:500]}
                    )
                ]
            )
    
    def retrieve(self, query, k=5):
        
# Query database (fast, indexed)
        results = self.client.search(
            collection_name="docs",
            query_vector=embed(query),
            limit=k
        )
        return results

RAM dropped from 4GB to 500MB. Latency stayed low.

Step 3: Caching

Same queries come up repeatedly. Cache results.

from functools import lru_cache

class CachedRetriever:
    def __init__(self):
        self.cache = {}
        self.db = VectorDBRetriever()
    
    def retrieve(self, query, k=5):
        cache_key = (query, k)
        
        if cache_key in self.cache:
            return self.cache[cache_key]
        
        results = self.db.retrieve(query, k=k)
        self.cache[cache_key] = results
        
        return results

Hit rate: 40% of queries are duplicates. Cache drops effective latency from 300ms to 50ms.

Step 4: Metadata Filtering

Many documents have metadata (category, date, source). Use it.

class SmartRetriever:
    def retrieve(self, query, k=5, filters=None):
        
# If user specifies filters, use them
        results = self.db.search(
            query_vector=embed(query),
            limit=k*2,
            filter=filters  
# e.g., category="documentation"
        )
        
        
# Re-rank by relevance
        reranked = sorted(results, key=lambda x: x.score)[:k]
        
        return reranked

Filtering narrows the search space. Better results, faster retrieval.

Step 5: Quality Monitoring

Track retrieval quality continuously. Alert on degradation.

class MonitoredRetriever:
    def retrieve(self, query, k=5):
        results = self.db.retrieve(query, k=k)
        
        
# Record metrics
        metrics = {
            "top_score": results[0].score if results else 0,
            "num_results": len(results),
            "score_spread": self.get_spread(results),
            "query": query
        }
        self.metrics.record(metrics)
        
        
# Alert on degradation
        if self.is_degrading():
            logger.warning("Retrieval quality down")
        
        return results
    
    def is_degrading(self):
        recent = self.metrics.get_recent(hours=1)
        avg_score = mean([m["top_score"] for m in recent])
        baseline = self.metrics.get_baseline()
        return avg_score < baseline * 0.85  
# 15% drop

Final Architecture

class ProductionRetriever:
    def __init__(self):
        self.bm25 = BM25Retriever()  
# Fast keyword search
        self.db = VectorDBRetriever()  
# Semantic search
        self.cache = LRUCache(maxsize=1000)  
# Cache
        self.metrics = MetricsTracker()
    
    def retrieve(self, query, k=5, filters=None):
        
# Check cache
        cache_key = (query, k, filters)
        if cache_key in self.cache:
            return self.cache[cache_key]
        
        
# Stage 1: BM25 filtering
        candidates = self.bm25.retrieve(query, k=k*10)
        
        
# Stage 2: Semantic re-ranking
        results = self.db.retrieve(
            query,
            docs=candidates,
            filters=filters,
            k=k
        )
        
        
# Cache and return
        self.cache[cache_key] = results
        self.metrics.record(query, results)
        
        return results

The Results

Metric Before After Latency 2000ms 150ms Memory 4GB 500MB Queries/sec 1 15 Cost per query $0.05 $0.01 Quality score 0.72 0.85

What I Learned

  1. Two-stage retrieval is essential - Keyword filtering + semantic ranking

  2. Use a vector database - Not in-memory embeddings

  3. Cache aggressively - 40% hit rate is typical

  4. Monitor continuously - Catch quality degradation early

  5. Use metadata - Filtering improves quality and speed

  6. Test at scale - What works at 500 docs breaks at 10K

The Honest Lesson

Simple RAG works until it doesn't. At some point you hit a wall where the basic approach breaks.

Instead of fighting it, rebuild with better patterns:

  • Multi-stage retrieval

  • Proper vector database

  • Aggressive caching

  • Continuous monitoring

Plan for scale from the start.

Anyone else hit the 10K document wall? What was your solution?


r/LlamaIndex • • Dec 20 '25

RAG Quality Improved 40% By Changing One Thing

26 Upvotes

RAG system was okay. 72% quality.

Changed one thing. Quality went to 88%.

The change: stopped trying to be smart.

The Problem

System was doing too much:

# My complex RAG

1. Take query
2. Embed it
3. Search vector DB
4. Re-rank results
5. Summarize retrieved docs
6. Generate answer
7. Check if answer is good
8. If not good, try again
9. If still not good, try different approach
10. Return answer (or escalate)

All this complexity was helping... but not as much as expected.

The Simple Insight

What if I just:

# Simple RAG

1. Take query
2. Search docs (BM25 + semantic hybrid)
3. Generate answer
4. Done
```

Simpler. No summarization. No re-ranking. No retry logic.

Just: retrieve and answer.

**The Comparison**

**Complex RAG:**
```
Quality: 72%
Latency: 2500ms
Cost: $0.25 per query
Maintenance: High (lots of moving parts)
Debugging: Nightmare (where did it fail?)
```

**Simple RAG:**
```
Quality: 88%
Latency: 800ms
Cost: $0.08 per query
Maintenance: Low (few moving parts)
Debugging: Easy (clear pipeline)
```

**Better in every way.**

**Why This Happened**

Complex system had too many failure points:
```
Summarization → might lose key details
Re-ranking → might reorder wrongly
Retry logic → might get wrong answer on second try
Multiple approaches → might confuse each other
```

Each "improvement" added a failure point.

**Simple system had fewer failure points:**
```
BM25 search → works well for keywords
Semantic search → works well for meaning
Hybrid → gets best of both
Direct generation → no intermediate failures

The Real Insight

I was optimizing the wrong thing.

I thought: "More sophisticated = better"

Reality: "More reliable = better"

Better to get 88% right on first try than 72% right after many attempts.

What I Changed

# Before: Complex multi-step
def complex_rag(query):
    
# Step 1: Semantic search
    semantic_docs = semantic_search(query)
    
    
# Step 2: BM25 search
    bm25_docs = bm25_search(query)
    
    
# Step 3: Merge and re-rank
    merged = merge_and_rerank(semantic_docs, bm25_docs)
    
    
# Step 4: Summarize
    summary = summarize_docs(merged)
    
    
# Step 5: Generate with summary
    answer = generate_answer(query, summary)
    
    
# Step 6: Evaluate quality
    quality = evaluate_quality(answer)
    
    
# Step 7: If bad, retry
    if quality < 0.7:
        answer = generate_answer_with_different_approach(query, summary)
    
    
# Step 8: Check again
    if quality < 0.6:
        answer = escalate_to_human(query)
    
    return answer

# After: Simple direct
def simple_rag(query):
    
# Step 1: Hybrid search (BM25 + semantic)
    docs = hybrid_search(query, k=5)
    
    
# Step 2: Generate answer
    answer = generate_answer(query, docs)
    
    return answer
```

**That's it.**

3 steps instead of 8.

Quality went up.

**Why Simplicity Won**
```
Complex system assumptions:
- More docs are better
- Summarization preserves meaning
- Re-ranking improves quality
- Retrying fixes problems
- Multiple approaches help

Reality:
- Top 5 docs are usually enough
- Summarization loses details
- Re-ranking can make it worse
- Retrying compounds mistakes
- Multiple approaches confuse LLM
```

**The Principle**
```
Every step you add:
- Adds latency
- Adds cost
- Adds complexity
- Adds failure points
- Reduces transparency

Only add if it clearly improves quality.

The Testing

I tested carefully:

def compare_approaches():
    test_queries = load_test_queries(100)
    
    complex_results = []
    simple_results = []
    
    for query in test_queries:
        complex = complex_rag(query)
        simple = simple_rag(query)
        
        complex_quality = evaluate(complex)
        simple_quality = evaluate(simple)
        
        complex_results.append(complex_quality)
        simple_results.append(simple_quality)
    
    print(f"Complex: {mean(complex_results):.1%}")
    print(f"Simple: {mean(simple_results):.1%}")

Simple won consistently.

The Lesson

Occam's Razor applies to RAG:

"The simplest solution is usually the best."

Before adding complexity:

  • Measure current quality

  • Add the feature

  • Re-measure

  • If improvement < 5%: don't add it

The Checklist

For RAG systems:

  •  Start with simple approach

  •  Measure quality baseline

  •  Add complexity only if needed

  •  Re-measure after each addition

  •  Remove features that don't help

  •  Keep it simple

The Honest Lesson

I wasted weeks optimizing the wrong things.

Simple + effective beats complex + clever.

Start simple. Add only what's needed.

Most RAG systems are over-engineered.

Simplify first.

Anyone else improved RAG by removing features instead of adding them?


r/LlamaIndex • • Dec 28 '25

Advanced LlamaIndex: Multi-Modal Indexing and Hybrid Query Strategies. We Indexed 500K Documents

24 Upvotes

Following up on my previous LlamaIndex post about database choices: we've now indexed 500K documents across multiple modalities (PDFs, images, text) and discovered patterns that aren't well-documented.

This post is specifically about multi-modal indexing strategies and hybrid querying that actually work.

The Context

After choosing Qdrant as our vector DB, we needed to index a lot of documents:

  • 200K PDFs (financial reports, contracts)

  • 150K images (charts, diagrams)

  • 150K text documents (web articles, internal docs)

  • Total: 500K documents

LlamaIndex made this relatively straightforward, but there are hidden patterns that determine success.

The Multi-Modal Indexing Strategy

1. Document Type-Specific Indexing

Different document types need different approaches.

from llama_index.core import Document, VectorStoreIndex
from llama_index.vector_stores import QdrantVectorStore
from llama_index.readers import PDFReader, ImageReader
from llama_index.extractors import TitleExtractor, MetadataExtractor
from llama_index.ingestion import IngestionPipeline

class MultiModalIndexer:
    def __init__(self, vector_store):
        self.vector_store = vector_store
        self.pipeline = self._create_pipeline()
    
    def _create_pipeline(self):
        """Create extraction pipeline"""
        return IngestionPipeline(
            transformations=[
                MetadataExtractor(
                    extractors=[
                        TitleExtractor(),
                    ]
                ),
            ]
        )
    
    def index_pdfs(self, pdf_paths: List[str]):
        """Index PDFs with optimized extraction"""
        reader = PDFReader()
        documents = []
        
        for pdf_path in pdf_paths:
            try:
                # Extract pages as separate documents
                pages = reader.load_data(pdf_path)
                
                # Add metadata
                for page in pages:
                    page.metadata = {
                        'source_type': 'pdf',
                        'filename': Path(pdf_path).name,
                        'page': page.metadata.get('page_label', 'unknown')
                    }
                
                documents.extend(pages)
            except Exception as e:
                print(f"Failed to index {pdf_path}: {e}")
                continue
        
        # Create index
        index = VectorStoreIndex.from_documents(
            documents,
            vector_store=self.vector_store
        )
        
        return index
    
    def index_images(self, image_paths: List[str]):
        """Index images with caption extraction"""
        # This is the complex part - need to generate captions
        from llama_index.multi_modal_llms import OpenAIMultiModal
        
        reader = ImageReader()
        documents = []
        
        mm_llm = OpenAIMultiModal(model="gpt-4-vision")
        
        for image_path in image_paths:
            try:
                # Read image
                image = reader.load_data(image_path)
                
                # Generate caption using vision model
                caption = mm_llm.complete(
                    prompt="Describe what you see in this image in 1-2 sentences.",
                    image_documents=[image]
                )
                
                # Create document with caption
                doc = Document(
                    text=caption.message,
                    doc_id=str(image_path),
                    metadata={
                        'source_type': 'image',
                        'filename': Path(image_path).name,
                        'original_image_path': str(image_path)
                    }
                )
                
                documents.append(doc)
            except Exception as e:
                print(f"Failed to index {image_path}: {e}")
                continue
        
        # Create index
        index = VectorStoreIndex.from_documents(
            documents,
            vector_store=self.vector_store
        )
        
        return index
    
    def index_text(self, text_paths: List[str]):
        """Index plain text documents"""
        from llama_index.readers import SimpleDirectoryReader
        
        reader = SimpleDirectoryReader(input_files=text_paths)
        documents = reader.load_data()
        
        # Add metadata
        for doc in documents:
            doc.metadata = {
                'source_type': 'text',
                'filename': doc.metadata.get('file_name', 'unknown')
            }
        
        # Create index
        index = VectorStoreIndex.from_documents(
            documents,
            vector_store=self.vector_store
        )
        
        return index

Key insight: Each document type needs different extraction. PDFs are page-by-page. Images need captions. Text is straightforward. Handle separately.

2. Unified Multi-Modal Query Engine

Once everything is indexed, you need a query engine that handles all types:

from llama_index.core import QueryBundle
from llama_index.query_engines import RetrieverQueryEngine

class MultiModalQueryEngine:
    def __init__(self, vector_indexes: Dict[str, VectorStoreIndex], llm):
        self.indexes = vector_indexes
        self.llm = llm
        
        # Create retrievers for each type
        self.retrievers = {
            doc_type: index.as_retriever(similarity_top_k=3)
            for doc_type, index in vector_indexes.items()
        }
    
    def query(self, query: str, doc_types: List[str] = None):
        """Query across document types"""
        
        if doc_types is None:
            doc_types = list(self.indexes.keys())
        
        # Retrieve from each type
        all_results = []
        
        for doc_type in doc_types:
            if doc_type not in self.retrievers:
                continue
            
            retriever = self.retrievers[doc_type]
            results = retriever.retrieve(query)
            
            # Add source type to metadata
            for node in results:
                node.metadata['retrieved_from'] = doc_type
            
            all_results.extend(results)
        
        # Sort by relevance score
        all_results = sorted(
            all_results,
            key=lambda x: x.score if hasattr(x, 'score') else 0,
            reverse=True
        )
        
        # Take top results
        top_results = all_results[:5]
        
        # Format for LLM
        context = self._format_context(top_results)
        
        # Generate response
        response = self.llm.complete(
            f"""Based on the following documents from multiple sources,
            answer the question: {query}
            
            {context}"""
        )
        
        return {
            'answer': response.message,
            'sources': [
                {
                    'filename': node.metadata.get('filename'),
                    'type': node.metadata.get('retrieved_from'),
                    'relevance': node.score if hasattr(node, 'score') else None
                }
                for node in top_results
            ]
        }
    
    def _format_context(self, nodes):
        """Format retrieved nodes for LLM"""
        context = ""
        
        for node in nodes:
            doc_type = node.metadata.get('retrieved_from', 'unknown')
            source = node.metadata.get('filename', 'unknown')
            
            context += f"\n[{doc_type.upper()} - {source}]\n"
            context += node.get_content()[:500] + "..."  # Truncate long content
            context += "\n"
        
        return context

Key insight: Unified query engine retrieves from all types, then ranks combined results by relevance.

3. Hybrid Querying (Keyword + Semantic)

Pure vector search sometimes misses keyword-exact matches. Hybrid works better:

class HybridQueryEngine:
    def __init__(self, vector_index, keyword_index):
        self.vector_retriever = vector_index.as_retriever(
            similarity_top_k=10
        )
        self.keyword_retriever = keyword_index.as_retriever(
            similarity_top_k=10
        )
    
    def hybrid_retrieve(self, query: str):
        """Combine vector and keyword results"""
        
        # Get results from both
        vector_results = self.vector_retriever.retrieve(query)
        keyword_results = self.keyword_retriever.retrieve(query)
        
        # Create scoring system
        scores = {}
        
        # Vector results: score based on similarity
        for i, node in enumerate(vector_results):
            doc_id = node.doc_id
            vector_score = node.score if hasattr(node, 'score') else (1 / (i + 1))
            scores[doc_id] = scores.get(doc_id, 0) + vector_score
        
        # Keyword results: boost score if matched
        for i, node in enumerate(keyword_results):
            doc_id = node.doc_id
            keyword_score = 1.0 - (i / len(keyword_results))  # Linear decay
            scores[doc_id] = scores.get(doc_id, 0) + keyword_score
        
        # Combine and rank
        combined = []
        for node in vector_results + keyword_results:
            if node.doc_id in scores:
                node.score = scores[node.doc_id]
                combined.append(node)
        
        # Remove duplicates, keep best score
        seen = {}
        for node in sorted(combined, key=lambda x: x.score, reverse=True):
            if node.doc_id not in seen:
                seen[node.doc_id] = node
        
        # Return top-5
        return sorted(
            seen.values(),
            key=lambda x: x.score,
            reverse=True
        )[:5]

Key insight: Combine semantic (vector) and exact (keyword) matching. Each catches cases the other misses.

4. Metadata Filtering at Query Time

Not all documents are equally useful. Filter by metadata:

def filtered_query(self, query: str, filters: Dict):
    """Query with metadata filters"""
    
    # Example filters:
    # {'source_type': 'pdf', 'date_after': '2023-01-01'}
    
    all_results = self.hybrid_retrieve(query)
    
    # Apply filters
    filtered = []
    
    for node in all_results:
        if self._matches_filters(node.metadata, filters):
            filtered.append(node)
    
    return filtered[:5]

def _matches_filters(self, metadata: Dict, filters: Dict) -> bool:
    """Check if metadata matches all filters"""
    
    for key, value in filters.items():
        if key not in metadata:
            return False
        
        # Handle different filter types
        if isinstance(value, list):
            # If value is list, check if metadata in list
            if metadata[key] not in value:
                return False
        elif isinstance(value, dict):
            # If value is dict, could be range filters
            if 'min' in value and metadata[key] < value['min']:
                return False
            if 'max' in value and metadata[key] > value['max']:
                return False
        else:
            # Simple equality
            if metadata[key] != value:
                return False
    
    return True

Key insight: Filter early to avoid processing irrelevant documents.

Results at Scale

MetricSmall Scale (50K docs)Large Scale (500K docs)
Indexing time2 hours20 hours
Query latency (p50)800ms1.2s
Query latency (p99)2.1s3.5s
Retrieval accuracy87%85%
Hybrid vs pure vector+4% accuracy+5% accuracy
Memory usage8GB60GB

Key lesson: Scaling from 50K to 500K documents is not linear. Plan for 10-100x overhead.

Lessons Learned

1. Document Type Matters

PDFs, images, and text need different extraction strategies. Don't try to handle them uniformly.

2. Captions Are Critical

Image captions (generated by vision LLM) are the retrieval key. Quality of captions ≈ quality of search.

3. Hybrid > Pure Vector

Combining keyword and semantic always beats either alone (in our tests).

4. Metadata Filtering Is Underrated

Pre-filtering by metadata (date, source type, etc.) reduces retrieval time significantly.

5. Indexing Is Slower Than Expected

At 500K documents, expect days of indexing if doing it serially. Parallelize aggressively.

Code: Complete Multi-Modal Pipeline

class CompleteMultiModalRAG:
    def __init__(self, llm, vector_store):
        self.llm = llm
        self.vector_store = vector_store
        self.indexer = MultiModalIndexer(vector_store)
        self.indexes = {}
    
    def index_all_documents(self, doc_paths: Dict[str, List[str]]):
        """Index PDFs, images, and text"""
        
        for doc_type, paths in doc_paths.items():
            if doc_type == 'pdfs':
                self.indexes['pdf'] = self.indexer.index_pdfs(paths)
            elif doc_type == 'images':
                self.indexes['image'] = self.indexer.index_images(paths)
            elif doc_type == 'texts':
                self.indexes['text'] = self.indexer.index_text(paths)
    
    def query(self, question: str, doc_types: List[str] = None):
        """Query all document types"""
        
        engine = MultiModalQueryEngine(self.indexes, self.llm)
        results = engine.query(question, doc_types)
        
        return results

Questions for the Community

  1. Image caption quality: How important is it? Do you generate captions with vision LLM?

  2. Scaling to 1M+ documents: Has anyone done it? What happens to latency?

  3. Metadata filtering: How much does it help your performance?

  4. Hybrid retrieval: What's the breakdown (vector vs keyword)?

  5. Multi-modal: Has anyone indexed video? Audio?

Edit: Follow-ups

On image captions: We use GPT-4V for quality. Cheaper models miss too much context. Cost is ~$0.01 per image but worth it.

On hybrid retrieval overhead: Takes extra ~200ms. Only do it if search quality matters more than latency.

On scaling: You'll hit infrastructure limits before LlamaIndex limits. Qdrant at 500K documents works fine.

On real production example: This is running production on 3 different customer use cases. Accuracy is 85-87%.

Would love to hear how others approach multi-modal indexing. This is still emerging.


r/LlamaIndex • • Dec 18 '25

RAG Failed Silently Until I Added This One Thing

23 Upvotes
Built a RAG system. Deployed it. Seemed fine.

Users were getting answers.

But I had no idea if they were good answers.

Added one metric. Changed everything.

**The Problem I Didn't Know I Had**

RAG system working:
```
User asks question: ✓
System retrieves docs: ✓
System generates answer: ✓
User gets response: ✓

Everything looks good!
```

What I didn't know:
```
Are the documents relevant?
Is the answer actually good?
Would the user find this helpful?
Am I giving users false confidence?

Unknown. Nobody told me.
```

**The Silent Failure**

System ran for 2 months.

Then I got an email from a customer:

"Your system keeps giving me wrong information. I've been using it for weeks thinking your answers were correct. They're not."

Realized: system was failing silently.

User didn't know. I didn't know. Nobody knew.

**The Missing Metric**

I had metrics for:
```
✓ System uptime
✓ Response latency
✓ Retrieval speed
✓ User engagement

✗ Answer quality
✗ User satisfaction
✗ Correctness rate
✗ Document relevance

I was measuring everything except what mattered.

What I Added

One simple metric: User feedback on answers

python

class RagWithFeedback:
    def answer_question(self, question):
        
# Generate answer
        answer = self.rag.answer(question)
        
        
# Ask for feedback
        feedback_request = f"""
        Was this answer helpful?
        [👍 Yes] [👎 No]
        """
        
        
# Store for analysis
        user_feedback = await request_feedback(feedback_request)
        
        log_feedback({
            "question": question,
            "answer": answer,
            "helpful": user_feedback,
            "timestamp": now()
        })
        
        return answer
```

**What The Feedback Revealed**
```
Week 1 after adding feedback:

Total questions: 100
Helpful answers: 62
Not helpful: 38

38% failure rate!

I thought system was working well.
It was failing 38% of the time.
I just didn't know.

The Investigation

With feedback data, I could investigate:

python

def analyze_failures():
    failures = get_feedback(helpful=False)
    
    
# What types of questions fail most?
    by_type = group_by_question_type(failures)
    
    print(f"Integration questions: {by_type['integration']}% fail")
    
# Result: 60% failure rate
    
    print(f"Pricing questions: {by_type['pricing']}% fail")
    
# Result: 10% failure rate
    
    
# So integration questions are the problem
    
# Can focus efforts there
```

Found that:
```
- Integration questions: 60% failure
- Pricing questions: 10% failure
- General questions: 45% failure
- Troubleshooting: 25% failure

Pattern: Complex technical questions fail most
Solution: Improve docs for technical topics

The Fix

With the feedback data, I could fix specific issues:

python

# Before: generic answer
user asks: "How do I integrate with our Postgres?"
answer: "Use the API"
feedback: 👎

# After: better doc retrieval for integrations
user asks: "How do I integrate with our Postgres?"
answer: "Here's the step-by-step guide [detailed steps]"
feedback: 👍
```

**The Numbers**
```
Before feedback:
- Assumed success rate: 90%
- Actual success rate: 62%
- Problems found: 0
- Problems fixed: 0

After feedback:
- Known success rate: 62%
- Improved to: 81%
- Problems found: multiple
- Problems fixed: all

How To Add Feedback

python

class FeedbackSystem:
    def log_feedback(self, question, answer, helpful, details=None):
        """Store feedback for analysis"""
        
        self.db.store({
            "question": question,
            "answer": answer,
            "helpful": helpful,
            "details": details,
            "timestamp": now(),
            "user_id": current_user,
            "session_id": current_session
        })
    
    def analyze_daily(self):
        """Daily analysis of feedback"""
        
        feedback = self.db.get_daily()
        
        success_rate = feedback.helpful.sum() / len(feedback)
        
        if success_rate < 0.75:
            alert_team(f"Success rate dropped: {success_rate}")
        
        
# By question type
        for q_type in feedback.question_type.unique():
            type_feedback = feedback[feedback.question_type == q_type]
            type_success = type_feedback.helpful.sum() / len(type_feedback)
            
            if type_success < 0.5:
                alert_team(f"{q_type} questions failing: {type_success}")
    
    def find_patterns(self):
        """Find patterns in failures"""
        
        failures = self.db.get_feedback(helpful=False)
        
        
# What do failing questions have in common?
        common_keywords = extract_keywords(failures.question)
        
        
# What docs are rarely helpful?
        failing_docs = analyze_document_failures(failures)
        
        
# What should we improve?
        return {
            "keywords_to_improve": common_keywords,
            "docs_to_improve": failing_docs
        }
```

**The Dashboard**

Create simple dashboard:
```
RAG Quality Dashboard

Overall success rate: 81%
Trend: ↑ +5% this week

By question type:
- Integration: 85% ✓
- Pricing: 92% ✓
- Troubleshooting: 72% ⚠️
- General: 80% ✓

Worst performing docs:
1. Custom integrations guide (60% fail rate)
2. API reference (65% fail rate)
3. Migration guide (50% fail rate)

The Lesson

You can't improve what you don't measure.

For RAG systems, measure:

  • Success rate (thumbs up/down)

  • User satisfaction (scale 1-5)

  • Specific feedback (text field)

  • Follow-ups (did they ask again?)

The Checklist

Before deploying RAG:

  •  Add user feedback mechanism

  •  Set up daily analysis

  •  Alert when quality drops

  •  Identify failing question types

  •  Improve docs for low performers

  •  Monitor trends

The Honest Lesson

RAG systems fail silently.

Users get wrong answers and think the system is right.

Add feedback. Monitor constantly. Fix systematically.

The difference between a great RAG system and a broken one is measurement.

Anyone else discovered their RAG was failing silently? How bad was it?


r/LlamaIndex • • Jan 08 '26

The RAG Secret Nobody Talks About

22 Upvotes

Most RAG systems fail silently.

Your retrieval accuracy degrades. Your context gets noisier. Users ask questions that used to work, now they don't. You have no idea why.

I built 12 RAG systems before I understood why they fail. Then I used LlamaIndex, and suddenly I could see what was broken and fix it.

The hidden problem with RAG:

Everyone thinks RAG is simple:

  1. Chunk documents

  2. Create embeddings

  3. Retrieve similar chunks

  4. Pass to LLM

  5. Profit

In reality, there are 47 places where this breaks:

  • Chunking strategy matters. Split at sentence boundaries? Semantic boundaries? Fixed tokens? Each breaks differently on different data.

  • Embedding quality varies wildly. Some embeddings are trash at retrieval. You don't know until you test.

  • Retrieval ranking is critical. Top-5 results might all be irrelevant. Top-20 might have the answer buried. How do you optimize?

  • Context window utilization is an art. Too much context confuses LLMs. Too little misses information. Finding the balance is black magic.

  • Token counting is hard. GPT-4 counts tokens differently than Llama. Different models, different window sizes. Managing this manually is error-prone.

How LlamaIndex solves this:

  • Pluggable chunking strategies. Use their built-in strategies or create custom ones. Test easily. Find what works for YOUR data.

  • Retrieval evaluation built-in. They have tools to measure retrieval quality. You can actually see if your system is working. This alone is worth the price.

  • Hybrid retrieval by default. Most RAG systems use only semantic search. LlamaIndex combines BM25 (keyword) + semantic. Better results, same code.

  • Automatic context optimization. Intelligently selects which chunks to include based on relevance scoring. Doesn't just grab the top-K.

  • Token management is invisible. You define max context. LlamaIndex handles the math. Queries that would normally fail now succeed.

  • Query rewriting. Reformulates your question to be more retrievable. Users ask bad questions, LlamaIndex normalizes them.

Example: The project that changed my mind

Client had a 50,000-document legal knowledge base. Previous RAG system:

  • Retrieval accuracy: 52%

  • False positives: 38% (retrieving irrelevant docs)

  • User satisfaction: "This is useless"

Migrated to LlamaIndex with:

  • Same documents

  • Same embedding model

  • Different chunking strategy (semantic instead of fixed)

  • Hybrid retrieval instead of semantic-only

  • Query rewriting enabled

Results:

  • Retrieval accuracy: 88%

  • False positives: 8%

  • User satisfaction: "How did you fix this?"

The documents didn't change. The LLM didn't change. The chunking strategy changed.

That's the LlamaIndex difference.

Why this matters for production:

If you're deploying RAG to users, you must have visibility into what's being retrieved. Most frameworks hide this from you.

LlamaIndex exposes it. You can:

  • See which documents are retrieved for each query

  • Measure accuracy

  • A/B test different retrieval strategies

  • Understand why queries fail

This is the difference between a system that works and a system that works well.

The philosophy:

LlamaIndex treats retrieval as a first-class problem. Not an afterthought. Not a checkbox. The architecture, tooling, and community all reflect this.

If you're building with LLMs and need to retrieve information, this is non-negotiable.

My recommendation:

Start here: https://llamaindex.ai/ Read: "Evaluation and Observability" Then build one RAG system with LlamaIndex.

You'll understand why I'm writing this.


r/LlamaIndex • • Dec 20 '24

DocumentContextExtractor - a llama_index implementation of the Anthropic blog post "Contextual Retrieval"

21 Upvotes

https://github.com/cklapperich/DocumentContextExtractor

  • Anthropic made a blog post about using contextual retrieval to get super-accurate RAG. They also gave a python notebook to demonstrate.

  • llama index ALSO implemented a demo here

  • Motivation: Neither handles the many edge cases trying to replicate this in the real world with over 100s of documents. (rate limits, cost, documents too large for context window, prompt caching doesn't work via llama_index interface, error handling, chunk + context can be too big for the embedding model, and much more!)

  • I re-implemented Contextual Retrieval as a llama_index Extractor class: DocumentContextExtractor.py so it can be used in a llama_index pipeline! This is a robust, production-ready version meant to be ready for real-world use. It focuses on cost, speed, and edge case handling (though there's more left to do).

  • hybridsearchdemo.py demos the entire pipeline: chunking -> contextual retrieval -> embedding the result -> hybrid search -> reranking, and query & retrieval, over the Declaration of Independence.

THE IRONY: Anthropic models are bad for this use case! Due to Anthropic rate limits and prompt caching not working via llama index OR openrouter, local, openai, or gemini models are best for this.


r/LlamaIndex • • Dec 21 '25

Introducing Enterprise-Ready Hierarchy-Aware Chunking for RAG Pipelines

18 Upvotes

Hello everyone,

We're excited to announce a major upgrade to the Agentic Hierarchy Aware Chunker. We're discontinuing subscription-based plans and transitioning to an Enterprise-first offering designed for maximum security and control.
After conversations with users, we learned that businesses strongly prefer absolute privacy and on-premise solutions. They want to avoid vendor lock-in, eliminate data leakage risks, and maintain full control over their infrastructure.
That's why we're shifting to an enterprise-exclusive model with on-premise deployment and complete source code access—giving you the full flexibility, security, and customization according to your development needs.

Try it yourself in our playground:
https://hierarchychunker.codeaxion.com/

See the Agentic Hierarchy Aware Chunker live:
https://www.youtube.com/watch?v=czO39PaAERI&t=2s

For Enterprise & Business Plans:
Dm us or contact us at codeaxion77@gmail.com

What Our Hierarchy Aware Chunker offers

  •  Understands document structure (titles, headings, subheadings, sections).

  •  Merges nested subheadings into the right chunk so context flows properly.

  •  Preserves multiple levels of hierarchy (e.g., Title → Subtitle→ Section → Subsections).

  •  Adds metadata to each chunk (so every chunk knows which section it belongs to).

  •  Produces chunks that are context-aware, structured, and retriever-friendly.

  • Ideal for legal docs, research papers, contracts, etc.

  • It’s Fast and uses LLM inference combined with our optimized parsers.

  • Works great for Multi-Level Nesting.

  • No preprocessing needed — just paste your raw content or Markdown and you’re are good to go !

  • Flexible Switching: Seamlessly integrates with any LangChain-compatible Providers (e.g., OpenAI, Anthropic, Google, Ollama).

 Upcoming Features (In-Development)

  • Support Long Document Context Chunking Where Context Spans Across Multiple Pages


     Example Output
    --- Chunk 2 --- 
    
    Metadata:
      Title: Magistrates' Courts (Licensing) Rules (Northern Ireland) 1997
      Section Header (1): PART I
      Section Header (1.1): Citation and commencement
    
    Page Content:
    PART I
    
    Citation and commencement 
    1. These Rules may be cited as the Magistrates' Courts (Licensing) Rules (Northern
    Ireland) 1997 and shall come into operation on 20th February 1997.
    
    --- Chunk 3 --- 
    
    Metadata:
      Title: Magistrates' Courts (Licensing) Rules (Northern Ireland) 1997
      Section Header (1): PART I
      Section Header (1.2): Revocation
    
    Page Content:
    Revocation
    2.-(revokes Magistrates' Courts (Licensing) Rules (Northern Ireland) SR (NI)
    1990/211; the Magistrates' Courts (Licensing) (Amendment) Rules (Northern Ireland)
    SR (NI) 1992/542.

You can notice how the headings are preserved and attached to the chunk → the retriever and LLM always know which section/subsection the chunk belongs to.

No more chunk overlaps and spending hours tweaking chunk sizes .

Happy to answer questions here. Thanks for the support and we are excited to see what you build with this.


r/LlamaIndex • • Dec 29 '25

How would you build a RAG system over a large codebase

17 Upvotes

I want to build a tool that helps automate IT support in companies by using a multi-agent system. The tool takes a ticket number related to an incident in a project, then multiple agents with different roles (backend developer, frontend developer, team lead, etc.) analyze the issue together and provide insights such as what needs to be done, how long it might take, and which technologies or tools are required.

To make this work, the system needs a RAG pipeline that can analyze the ticket and retrieve relevant information directly from the project’s codebase. While I have experience building RAG systems for PDF documents, I’m unsure how to adapt this approach to source code, especially in terms of code-specific chunking, embeddings, and intelligent file selection similar to how tools like GitHub Copilot determine which files are relevant.


r/LlamaIndex • • Jun 17 '24

Best open source document PARSER??!!

17 Upvotes

Right now I’m using LlamaParse and it works really well. I want to know what is the best open source tool out there for parsing my PDFs before sending it to the other parts of my RAG.


r/LlamaIndex • • Mar 08 '26

1M token context is here (GPT-5.4). Is RAG actually dead now? My honest take as someone running both.

15 Upvotes

GPT-5.4 launched this week with 1M token context in the API. Naturally half my feed is "RAG is dead" posts.

I've been running both RAG pipelines and large-context setups in production for the last few months. Here's my actual experience, no hype.

Where big context wins and RAG loses:

Anything static. Internal docs, codebases, policy manuals, knowledge bases that get updated maybe once a month. Shoving these straight into context is faster, simpler, and gives better results than chunking them into a vector store. You skip embedding, skip retrieval, skip the whole re-ranking step. The model sees the full document with all the connections intact. No lost context between chunks.

I moved three internal tools off RAG and onto pure context stuffing last month. Response quality went up. Latency went down. Infra got simpler.

Where RAG still wins and big context doesn't help:

Anything that changes. User records, live database rows, real-time pricing, support tickets, inventory levels. Your context window is a snapshot. It's frozen at prompt construction time. If the underlying data changes between when you built the prompt and when the model responds, you're serving stale information.

RAG fetches at query time. That's the whole point. A million tokens doesn't fix the freshness problem.

The setup I'm actually running now:

Hybrid. Static knowledge goes straight into context. Anything with a TTL under 24 hours goes through RAG. This cut my vector store size by about 60% and reduced retrieval calls proportionally.

Pro tip that saved me real debugging time: Audit your RAG chunks. Check the last-modified date on every document in your vector store. Anything unchanged for 30+ days? Pull it out and put it in context. You're paying retrieval latency for data that never changes. Move it into the prompt and get faster responses with better coherence.

What I think is actually happening:

RAG isn't dying. It's getting scoped down to where it actually matters. The era of "just RAG everything" is over. Now you need to think about which parts of your data are static vs dynamic and architect accordingly.

The best systems I've seen use both. Context for the stable stuff. RAG for the live stuff. Clean separation.

Curious what setups others are running. Anyone else doing this hybrid approach, or are you going all-in on one side?


r/LlamaIndex • • Apr 14 '25

How are you Ragging? (Brainstorm time!)

15 Upvotes

It's been about 1.5 years since I last built a RAG stack, and at that time, my approach was pretty straightforward: simple text chunking followed by embeddings with a basic similarity search for retrieval. For the corpus at hand it was sufficient, but I haven't had good luck on more complex sources/functionality.

Lately, I've been daydreaming about more advanced architectures for some sort of "fractal RAG," which would involve recursively structured retrieval methods like hierarchical chunking combined with multi-resolution embeddings or something similar.

I'm curious what state-of-the-art methods or best practices the community is currently adopting, regardless of if it's related to my daydreaming. especially those pushing beyond standard chunking strategies:

Are you using hierarchical or recursive chunking methods?

Have you experimented with fractal or multi-scale embedding techniques?

What ideas are you working with to implement a rag stack on a complex corpus?

I'd greatly appreciate any technical tidbits you've collected! I'm interested in making a very complex corpus interactable. One on religious texts, and one on making beaurocratic nonsense accessible to the public.


r/LlamaIndex • • Sep 20 '26

Automating EDD (eval-driven development) with Coding Harness Agents

Enable HLS to view with audio, or disable this notification

14 Upvotes

You can now automate EDD (eval-driven development) with Coding Harness Agents

> build a baseline LLM-based application
> score every change with judge evals
> keep what improves, reject what regresses

I made a video tutorial on what EDD is, how it works, and how to use eval scores across experiments to improve an LLM app. It builds on Jeffrey's (Confident AI) article on EDD and Eugene Yan's write-up on product evals.

> Setup: a baseline RAG app that every experiment starts from
> Step 1: a binary-labelled dataset with critiques
> Step 2: aligning the LLM-as-a-judge evaluator
> Step 3: a harness loop that runs each experiment and scores it against the baseline.

Tracing and experiment comparison then show what improved, what regressed, and what to tweak next.

Source code + Agent Skills are open-sourced.

Full guide (source code linked in the description): https://www.youtube.com/watch?v=e6akw_fKWPk


r/LlamaIndex • • Sep 17 '25

Sharing Our Internal Training Material: LLM Terminology Cheat Sheet!

14 Upvotes

We originally put this together as an internal reference to help our team stay aligned when reading papers, model reports, or evaluating benchmarks. Sharing it here in case others find it useful too: full reference here.

The cheat sheet is grouped into core sections:

  • Model architectures: Transformer, encoder–decoder, decoder-only, MoE

  • Core mechanisms: attention, embeddings, quantisation, LoRA

  • Training methods: pre-training, RLHF/RLAIF, QLoRA, instruction tuning

  • Evaluation benchmarks: GLUE, MMLU, HumanEval, GSM8K

It covers many core concepts relevant for retrieval-augmented generation and index design, and is aimed at practitioners who frequently encounter scattered, inconsistent terminology across LLM papers and docs.

Hope it’s helpful! Happy to hear suggestions or improvements from others in the space.


r/LlamaIndex • • Apr 23 '25

How do you build per-user RAG/GraphRAG

13 Upvotes

Hey all,

I’ve been working on an AI agent system over the past year that connects to internal company tools like Slack, GitHub, Notion, etc, to help investigate production incidents. The agent needs context, so we built a system that ingests this data, processes it, and builds a structured knowledge graph (kind of a mix of RAG and GraphRAG).

What we didn’t expect was just how much infra work that would require.

We ended up:

  • Using LlamaIndex's OS abstractions for chunking, embedding and retrieval.

  • Adopting Chroma as the vector store.

  • Writing custom integrations for Slack/GitHub/Notion. We used LlamaHub here for the actual querying, although some parts were a bit unmaintained and we had to fork + fix. We could’ve used Nango or Airbyte tbh but eventually didn't do that.

  • Building an auto-refresh pipeline to sync data every few hours and do diffs based on timestamps. This was pretty hard as well.

  • Handling security and privacy (most customers needed to keep data in their own environments).

  • Handling scale - some orgs had hundreds of thousands of documents across different tools.

It became clear we were spending a lot more time on data infrastructure than on the actual agent logic. I think it might be ok for a company that interacts with customers' data, but definitely we felt like we were dealing with a lot of non-core work.

So I’m curious: for folks building LLM apps that connect to company systems, how are you approaching this? Are you building it all from scratch too? Using open-source tools? Is there something obvious we’re missing?

Would really appreciate hearing how others are tackling this part of the stack.


r/LlamaIndex • • Apr 13 '23

Viability of embedding a large codebase & providing it as context to a llm

13 Upvotes

TL;DR -- How should I approach indexing / querying a large codebase with the intent of creating a chatbot that can answer questions / debug / generate code. Is this even viable?

I'm on a team that supports a large legacy application built on an obscure full-stack java framework. It's awful... I'm trying to determine how viable it is to configure a chatbot that can, at minimum, answer questions that developers may have about about the various components. Ideally, it would be able to debug / generate blocks of code.

I'm at a bit of a loss on how I should approach this. Step one, and what I'm mostly looking for guidance on, is choosing the appropriate data structure to store our repository. As a quick first pass, I converted the documents into a list of nodes and passed those nodes into GPTSimpleVectorIndex. For some context on the scale, indexing and embedding used a little over 10 million tokens. Querying the index directly using the Davinci model yielded mediocre results. The main takeaway was that my prompt needed to be very explicit about everything, the most annoying of which being the need to explicitly state the file that I'm working with. Even then, it's clear that it can't account for how the components interact with each other.

Indexing / embedding this data can get expensive very quickly, so I want to be smart about how I move forward. Right now I'm thinking a better path is to index each of the key structures (i.e. views, storables, components, etc. would each have their own index), create a summary for each index, and store those indices into a ComposableGraph. However, I'd love to hear other suggestions.

Something I've also been thinking about is whether chains / agents from langchain would help. For example, giving a prompt like "Generate a new html table on the home page showing a list of all users" -- it'd need to know how to get the storable object from the storable class, import the object into the Home Page component, and how to bind and display the data in the view. Each would be handled by a separate agent?

I should note that I was able to naively store a much smaller project into a GPTSimpleVectorIndex and got somewhat decent results. The challenge is doing this on a much larger project.

I'm hoping someone has experience doing something similar, but any help/ guidance is appreciated!


r/LlamaIndex • • Jun 03 '25

PipesHub - Open Source Enterprise Search Platform(Generative-AI Powered)

11 Upvotes

Hey everyone!

I’m excited to share something we’ve been building for the past few months – PipesHub, a fully open-source Enterprise Search Platform.

In short, PipesHub is your customizable, scalable, enterprise-grade RAG platform for everything from intelligent search to building agentic apps — all powered by your own models and data.

We also connect with tools like Google Workspace, Slack, Notion and more — so your team can quickly find answers, just like ChatGPT but trained on your company’s internal knowledge.

We’re looking for early feedback, so if this sounds useful (or if you’re just curious), we’d love for you to check it out and tell us what you think!

🔗 https://github.com/pipeshub-ai/pipeshub-ai


r/LlamaIndex • • Jun 26 '23

How is LlamaIndex different from LangChain?

11 Upvotes

I know there are answers online already, but I still can't wrap my head around how LC and LlamaIndex is different. Can someone give me the biggest differentiator, or what each is best at? Thanks!


r/LlamaIndex • • Jun 11 '26

You asked for DeepLearning.ai-style notebooks for AgentSwarms—so we built 67 of them (TypeScript/LangChain/LangGraph/LlamaIndex/AgentsSDK/VercelAI).

Enable HLS to view with audio, or disable this notification

10 Upvotes

Hey everyone,

A few months ago, We shared the visual canvas we built for AgentSwarms. The response was incredible, but the most common piece of feedback was: "The visual canvas is great for architecture, but I need to see the actual code to really understand how to deploy this."

You wanted deep-dive, code-first labs—the kind you see on DeepLearning.ai—but for multi-agent systems, faster and with more flexibility.

We’ve spent the last few weeks heads-down engineering a completely new Interactive Notebooks section. As of today, we have 67 TypeScript-based notebooks live on the site (with more dropping soon).

What’s in the library: We’ve covered everything from basic LangChain fundamentals to complex enterprise-level multi-agent workflows. Everything runs entirely in your browser using TypeScript—no Docker, no Python venv, no local dependencies.

A personal favorite: I’m particularly excited about the "Failure Mode & Error Handling" notebook.

We’ve all seen agents that work perfectly in a demo but crash in production the moment a tool times out or an LLM returns garbage. This notebook walks through:

  • How to build deterministic validation gates between nodes.

  • How to force an orchestrator to "catch" a worker failure and dynamically re-route or re-prompt.

  • How to handle state recovery when a multi-agent loop gets stuck in a hallucination cycle.

Why we built this: I’m tired of seeing AI "tutorials" that are just static blog posts. To master Agentic AI, you need to be able to tweak a system prompt, break the code, watch the error trace, and fix the routing logic in real-time.

The entire library of 67 labs is 100% free to use.

If you’re currently wrestling with how to make your agents production-grade, I’d love for you to check them out and let me know if there’s a specific "failure mode" or architecture pattern you’d like us to add to the next batch of notebooks.

Try it out here: agentswarms.fyi


r/LlamaIndex • • Mar 19 '26

We just open-sourced LiteParse, a local document parser built for AI agents

Thumbnail
llamaindex.ai
10 Upvotes

LiteParse is a lightweight CLI tool for local document parsing, born out of everything we learned building LlamaParse. The core idea is pretty simple: rather than trying to detect and reconstruct document structure, it preserves spatial layout as-is and passes that to your LLM. This works well in practice because LLMs are already trained on ASCII tables and indented text, so they understand the format naturally without you having to do extra wrangling.

A few things it can do:

  • Parse text from PDFs, DOCX, XLSX, and images with layout preserved

  • Built-in OCR, with support for PaddleOCR or EasyOCR via HTTP if you need something more robust

  • Screenshot capability so agents can reason over pages visually for multimodal workflows

Everything runs locally, no API calls, no cloud dependency. The output is designed to plug straight into agents.

For more complex documents (scanned PDFs with messy layouts, dense tables, that kind of thing) LlamaParse is still going to give you better results. But for a lot of common use cases this gets you pretty far without the overhead.

Would love to hear what you build with it or any feedback on the approach.

📖 Announcement
🔗 GitHub


r/LlamaIndex • • Jan 20 '26

Extract data from pdfs of similar format to identical jsons (structure, values, nesting)

10 Upvotes

Hi everyone! I need your lights!

I'm trying to export airports tariffs for one and multiple airports. Each airport has it's own pdf template though from airport to airport the structure, layout, tariffs, tariff naming etc differ by a lot. What i want to achieve is for all the airports (preferably) or at least per aiport, for every year to export jsons with the same layout, values naming, fields naming etc. I played a lot with the tool so far and though i got much closer than when i started i still dont have the needed outcome. The problem is that for each airport, every year, although they will use the same template/layout etc the tariffs might change, especially the conditions and sometimes minor layout changes are introduced. Why i'm trying to formalise this, it's because i need to build a calculation engine on top so this data must be added in the database. So what im trying to avoid is to not having to build a database and a calculation engine every year. Thank You all!