Most RAG prototypes look the same under the hood. Someone feeds a PDF into a pipeline, slices the text into neat 512-token chunks, dumps them into a vector database, and calls the job done. For a hallway demo, this can look impressive. In production, it collapses.
A fixed chunk does not care what it cuts. It will split a legal contract in the middle of an indemnity clause. It will stuff five unrelated API endpoints into the same context window and drown the model in noise. It will force you to retrieve more fragments than necessary, bloating latency and burning tokens. The result is half-answers, hallucinations, and frustrated users.
We tore our retrieval layer down to the studs and rebuilt it. The outcome was a system that hit 95 percent recall while cutting latency by 40 percent. Here is exactly how we did it.
Why Fixed Chunks Die in Production
The 512-token default is not a design choice. It is a by-product of early embedding model context windows and tidy library defaults. It is easy to implement and catastrophic to rely on.
Documents are not uniform. A legal clause can run seven hundred tokens without a clean break. Slice it at five hundred and twelve, and you create two orphaned fragments. When a lawyer or compliance officer asks about liability caps, the system returns half the obligation. The language model hallucinates the missing half, or worse, denies the cap exists.
API documentation suffers from the opposite ailment. A five-hundred-token chunk might swallow an entire module: authentication headers, error codes, rate limits, and webhook schemas. When a developer asks how to handle AUTH_4027, the retriever surfaces a stew of unrelated functions. The model has no choice but to average them into generic mush.
Bad chunking also inflates latency. Weak fragments mean you need a larger top-k to cover a topic. More chunks mean longer prompts. Longer prompts mean slower generation and higher bills. The user experience dies by a thousand cuts.
Match the Chunk to the Document
We stopped counting tokens and started reading the material. The right chunking strategy depends on the structure of the source.
Legal documents need recursive character chunking with clause-aware boundaries. The splitter respects hierarchy: it looks for section headers first, then numbered paragraphs, then natural sentence breaks. It never severs a sub-clause or splits an obligational phrase across chunks. When you retrieve a passage about indemnification, you get the full clause, the cap, and the exceptions.
API documentation demands structure-aware chunking. We parse by function definition, not token budget. Each chunk contains a complete function signature, its parameter descriptions, and the immediately adjacent error handling notes. If a developer searches for a specific method, they receive the entire contract, not a fragment caught inside an arbitrary split.
Support tickets are noisy and nonlinear. A thread might start with a bug report, introduce a workaround, and end with an internal escalation note. Semantic chunking detects topic shifts by measuring embedding similarity between sentences. We allow breaks only at natural thematic boundaries, so a conversation about login failures stays separate from a follow-up about billing cycles.
Wikis were the hardest. They are sprawling, cross-linked, and loosely organized. We used agentic chunking, where a lightweight LLM reads a page and decides the breaks based on thematic coherence. It costs slightly more at ingestion time, but the resulting chunks are self-contained and retrieval-ready. A page about deployment best practices splits into logical units: pre-flight checks, rollback procedures, and monitoring setup, rather than arbitrary text blocks.
Hybrid Retrieval: Keywords and Vectors Together
Dense vector search understands meaning. It is terrible at exact strings. If a user searches for a precise error code like AUTH_4027 or a customer name like "Stark Industries," vector embeddings can miss the target because they optimize for conceptual proximity, not character-level accuracy.
Pure keyword search through BM25 has the inverse flaw. It will find AUTH_4027 perfectly, but it will miss the conceptual bridge between "authorization failure" and "login denied."
ഞങ്ങൾ ഇവ രണ്ടും സമാന്തരമായി പ്രവർത്തിപ്പിക്കുന്നു. BM25-ഉം വെക്റ്റർ സെർച്ചും (vector search) ഒരേ കോർപ്പസിലൂടെയും (corpus) സ്വതന്ത്രമായാണ് പ്രവർത്തിക്കുന്നത്. അവയുടെ റിസൾട്ട് ലിസ്റ്റുകൾ Reciprocal Rank Fusion ഉപയോഗിച്ച് സംയോജിപ്പിക്കുന്നു, ഇത് സ്ഥാനക്രമത്തിലുള്ള റാങ്കുകൾ (positional ranks) സന്തുലിതമാക്കിക്കൊണ്ട് ഉദ്യോഗാർത്ഥികളെ പുനർക്രമീകരിക്കുന്നു. നിങ്ങൾക്ക് കാലിബ്രേറ്റ് ചെയ്ത വെയ്റ്റുകൾ (calibrated weights) ആവശ്യമില്ല. കൃത്യമായ പൊരുത്തത്തിന്റെ (exact match) കൃത്യതയും സെമാന്റിക് സെർച്ചിലൂടെയുള്ള (semantic search) ഉൾക്കാഴ്ചയും ഒരൊറ്റ റാങ്ക് ചെയ്ത ലിസ്റ്റിൽ നിങ്ങൾക്ക് ലഭിക്കുന്നു.
തുടർന്ന് ഞങ്ങൾ ഒരു cross-encoder reranker ചേർക്കുന്നു. ഇത് ഓരോ പാസേജും (passage) യഥാർത്ഥ ക്വറിയുമായി (query) താരതമ്യം ചെയ്ത് സ്കോർ നൽകുന്ന ഒരു പ്രത്യേക മോഡലാണ്, ഇത് ഏതെങ്കിലും ഒരു റിട്രീവറിനേക്കാളും (retriever) വളരെ സൂക്ഷ്മമായ ഒരു റിലവൻസ് സിഗ്നൽ (relevance signal) നൽകുന്നു. ഇത് ഏകദേശം 50 മില്ലിസെക്കൻഡ് ലേറ്റൻസി (latency) കൂട്ടുന്നു. ഇത് റീകോൾ (recall) 15 ശതമാനം വർദ്ധിപ്പിക്കുന്നു. ഉത്തരത്തിന്റെ ഗുണനിലവാരത്തിന് നിങ്ങൾ പ്രാധാന്യം നൽകുന്നുണ്ടെങ്കിൽ, ആ മാറ്റം ഒഴിവാക്കാനാവാത്തതാണ്.
ക്വറി എക്സ്പാൻഷൻ (Query Expansion): സെർച്ച് തുടങ്ങുന്നതിന് മുമ്പ് തന്നെ അത് പരിഹരിക്കുക
മോശം ക്വറികൾ (queries) എല്ലാ റിട്രീവൽ സിസ്റ്റങ്ങളുടെയും ഒരു രഹസ്യ പ്രശ്നമാണ്. ഉപയോക്താക്കൾ നിങ്ങളുടെ എംബെഡിംഗ് സ്പേസ് (embedding space) പോലെ എഴുതുന്നില്ല. അവർ "it broke" എന്ന് ടൈപ്പ് ചെയ്യുന്നു. അവർ അപൂർണ്ണമായ സ്റ്റാക്ക് ട്രേസുകൾ (stack traces) പേസ്റ്റ് ചെയ്യുന്നു. നിങ്ങളുടെ ഇൻഡക്സ് ഇതുവരെ കണ്ടിട്ടില്ലാത്ത ആഭ്യന്തര സാങ്കേതിക പദങ്ങൾ (internal jargon) അവർ ഉപയോഗിക്കുന്നു.
ഇൻഡക്സിൽ എത്തുന്നതിന് മുമ്പ് തന്നെ ഞങ്ങൾ ക്വറിയെ മാറ്റം വരുത്തുന്നു. ആദ്യം, ഞങ്ങൾ ഒരു ക്വറിയെ മൂന്ന് മുതൽ അഞ്ച് വരെ വൈവിധ്യമാർന്ന സെർച്ച് പദങ്ങളായി വികസിപ്പിക്കുന്നു. യഥാർത്ഥ ക്വറി "payment failed" എന്നാണെങ്കിൽ, ഞങ്ങൾ "transaction error," "billing declined," "charge unsuccessful" എന്നിവയ്ക്കും സെർച്ച് ചെയ്യുന്നു.
