About Portfolio Cases Services Blog Contact 🎙 Talk to AI
EN DE RU
🎙 Talk to AI
October 26, 2026 · 5 min read

Your production RAG is slow because of index sprawl: how to unify code, logs, PDFs and CSVs with one local MCP search layer

I’m Denis Shokhirev, an Agentic AI Systems Architect in Freiburg im Breisgau, Germany, and at DennisCraft AI Studio I ship production AI systems on Claude, Supabase, n8n, Doppler, and self-hosted Postgres. The most common reason I see production RAG slow down is not the model, and not even embeddings quality. It’s index sprawl: code in one retriever, logs in another, PDFs in a separate parser stack, and CSVs pushed through a side ETL that nobody fully trusts. That architecture looks reasonable

Denis Shokhirev
Denis Shokhirev
Agentic AI Systems Architect
Telegram LinkedIn

I’m Denis Shokhirev, an Agentic AI Systems Architect in Freiburg im Breisgau, Germany, and at DennisCraft AI Studio I ship production AI systems on Claude, Supabase, n8n, Doppler, and self-hosted Postgres. The most common reason I see production RAG slow down is not the model, and not even embeddings quality. It’s index sprawl: code in one retriever, logs in another, PDFs in a separate parser stack, and CSVs pushed through a side ETL that nobody fully trusts.

That architecture looks reasonable in a demo because each data type gets its own “best” pipeline. In production, it breaks at the exact point where users ask real questions. They do not ask for “semantic search over PDFs.” They ask why an invoice workflow failed, why an agent chose a supplier, or why a rule fired on one customer but not another. The answer usually spans code, runtime logs, documents, and tabular data at the same time.

Why index sprawl becomes a production problem

When retrieval is split by storage or file type, your agent has to decide where truth probably lives. That means more tool calls, more retries, more prompt tokens, and worse traceability. It also means inconsistent ranking. Your log search might prefer exact matches, your code retriever might prefer symbol locality, your PDF flow might rank by chunk similarity, and your CSV path may not rank at all.

Anthropic’s documentation on tool use recommends giving models clear tools with well-defined contracts rather than fuzzy overlapping capabilities, 2024, docs.anthropic.com. In practice, I’ve found the same thing: if the model sees one search tool with one interface, behavior is easier to test and easier to explain. If it sees four search tools that all look similar, it starts making routing decisions you now have to debug in production.

PatternLooks fine in a demoFails in production becauseBetter option
Separate retriever per sourceFast to prototypeRouting logic multipliesOne local search contract
SQL only for CSVsGood exact lookupsNo shared context with code or docsNormalize metadata across all sources
Standalone PDF serviceEasy extractionVersion drift and weak audit trailSingle tool layer with checksums
Log search outside agent stackOps already has itAgent cannot correlate runtime factsBring logs into the same retrieval contract

What I mean by one local MCP search layer

I’m not talking about another branded retrieval product. I mean a pattern: expose one local tool to the model, usually via MCP, and let that tool fan out internally to code, logs, PDFs, and CSVs. The model should call search(query, source_types, limit). It should not care which parser, table, or ranker sits behind that interface.

Local matters for two reasons. First, latency stays predictable when the tool runs close to Postgres, local file storage, and your logs. Second, for DACH B2B work in regulated settings, fewer external hops make audits and data handling simpler. If the answer path stays inside your controlled environment, your story to compliance, legal, and security gets shorter.

A minimal tool contract

from typing import List, Optional
from pydantic import BaseModel

class SearchRequest(BaseModel):
    query: str
    source_types: Optional[List[str]] = None
    limit: int = 8

class SearchHit(BaseModel):
    source_type: str
    uri: str
    title: str
    snippet: str
    score: float

def search_local(req: SearchRequest) -> List[SearchHit]:
    types = req.source_types or ["code", "logs", "pdf", "csv"]
    hits = []
    if "code" in types:
        hits += search_code(req.query, req.limit)
    if "logs" in types:
        hits += search_logs(req.query, req.limit)
    if "pdf" in types:
        hits += search_pdf(req.query, req.limit)
    if "csv" in types:
        hits += search_csv(req.query, req.limit)
    return sorted(hits, key=lambda h: h.score, reverse=True)[:req.limit]

The important part is not Python. The important part is that every hit comes back in the same shape. Once logs, code chunks, PDF paragraphs, and CSV rows all return a title, snippet, URI, score, and source type, the next reasoning step gets much simpler.

How I handle the four source types

Code

For code, I start with repository-aware chunking: functions, classes, config files, and selected docs. Before anything becomes retrievable, I usually run semgrep, bandit, and gitleaks. That reduces the chance of pushing secrets or dangerous patterns into model context. OWASP lists prompt injection and sensitive information disclosure in the OWASP Top 10 for LLM Applications, 2025, genai.owasp.org. Retrieval is part of that attack surface, not a separate concern.

Logs

Logs are often the highest-value source in production incidents, but only if they are normalized. I treat timestamp, service, severity, correlation_id, and message as mandatory fields. That allows the agent to answer questions like “why did the approval chain skip step 3?” with runtime evidence instead of just code guesses.

PDFs

PDF ingestion should be boring. Extract text, store page number, heading if available, and a file checksum. The checksum matters more than teams expect. It prevents the system from quoting yesterday’s contract appendix or an outdated operating procedure after a file was replaced under the same name.

CSVs

CSV rows should not be treated as plain text blobs. I store the file name, row number, typed columns when possible, and a compact text rendering of the row. That lets one query return the row itself and the documentation explaining what the row means.

python ingest_code.py --repo /srv/app --out postgres
python ingest_logs.py --input /var/log/app.jsonl --out postgres
python ingest_pdf.py --input /srv/docs --out postgres
python ingest_csv.py --input /srv/data --out postgres

psql "$DATABASE_URL" -c "create index if not exists idx_docs_type on docs(source_type);"
psql "$DATABASE_URL" -c "create index if not exists idx_docs_uri on docs(uri);"
psql "$DATABASE_URL" -c "analyze docs;"

Where the gains actually show up

The first gain is latency. Instead of letting the model call multiple tools and then reconcile four ranking schemes, you merge results once inside the local search layer and return one top-k list. The second gain is observability. You can log query, filters, retrieved items, and final answer in one trace. The third gain is access control. Restricting one local search tool by path, table, or source type is easier than trying to keep four retrieval services aligned.

OpenAI’s Cookbook examples on retrieval and tool use consistently push toward simple, explicit interfaces between the model and external systems, 2024, cookbook.openai.com. That matters even more in production than in examples. Every extra tool decision creates more room for latency, inconsistency, and hard-to-explain failures.

tools:
  - name: local_search
    description: Search code, logs, pdf and csv from local sources
    input_schema:
      type: object
      properties:
        query: { type: string }
        source_types:
          type: array
          items: { type: string }
        limit: { type: integer, default: 8 }
      required: [query]

What not to do

Do not start by adding a vector database for every format before you have a stable search contract. Do not force the model to choose between code_search, pdf_search, csv_search, and logs_search unless there is a strong reason. Do not split metadata in one store and raw content in another without durable identifiers. And do not index repositories into RAG without secret scanning first. Gitleaks during ingestion is cheaper than incident response later.

FAQ

Do I need MCP for this pattern?

No. The core idea is a single local search contract. MCP is a good standard way to expose that tool to an agent, but the architectural benefit comes from unification, not from the protocol alone.

No. Logs and CSVs often perform better with metadata filters and exact or hybrid matching. I only add embeddings where recall improves enough to justify the complexity.

Can Postgres handle this by itself?

In many B2B systems, yes. Postgres is often enough for ingestion tracking, metadata, access control, auditing, and a meaningful portion of retrieval if the schema is disciplined.

How does this help with security?

A single search layer makes access control, secret filtering, and trace logging easier to enforce. It also makes it easier to explain exactly which sources influenced an answer.

When are separate indexes still justified?

When sources differ heavily in retention, query volume, or permissions. Even then, I still prefer one external search interface for the model and hide internal complexity behind it.

Which part of your production RAG is creating more pain right now: split retrieval across sources, metadata normalization, or explaining answer provenance to auditors and customers? I’d genuinely like to know.

I run a free 30-min stack audit for DACH founders building AI in regulated markets. DM me on LinkedIn or write to @ger_dennis_ai.

Continue reading
One agent hit its quota, the other lost context: how to run Claude Code + Codex as one team without manual handoffs
Always-on agents are here: how to stop Claude Duet from becoming a source of leaks, false actions, and access chaos
LiteLLM got hijacked again: how to avoid losing SSH keys and API secrets through your AI gateway in production
If your AI agent needs web access, MCP is becoming the standard: what Firecrawl MCP actually fixes in production
All articles →
Where this is applied
Services — what we build
Talk to the voice agent
Case studies
Ready to build?

Turn your process into an AI system

Production quality. DACH B2B focus.

Start a project → ← All articles