Chunking Is the Most Underrated Part of Your Search Stack
Every vector search tutorial spends ninety percent of its time on embedding models and vector databases, and about two sentences on how to chunk your documents. This is backwards. In my experience building search systems over enterprise document collections, chunking strategy accounts for more retrieval quality variance than the choice of embedding model. A mediocre model with smart chunking beats a modern model with naive fixed-size splits, and I've measured this across three different production systems.
Why Fixed-Size Chunking Fails
The standard approach — split every document into 512-token chunks with 50-token overlap — creates three problems that compound as your corpus grows.
First, it cuts through semantic boundaries. A chunk that starts mid-paragraph and ends mid-sentence embeds poorly because the text fragment doesn't express a complete thought. The embedding model tries to represent something that isn't meaningful on its own, and the resulting vector ends up in a weird part of the embedding space.
Second, it loses structural context. A chunk from the "Limitations" section of a research paper and a chunk from the "Results" section look very different in meaning, but fixed chunking strips the section heading that tells you which is which. Without that signal, the retriever can't distinguish between a finding and a caveat.
This connects to the ideas in Image Quality Assessment Metrics for Automated Visual Inspec.
Third, overlap wastes storage. With 50-token overlap on 512-token chunks, you're storing roughly 10% redundant content. At millions of documents, that's real money in vector database costs — and it doesn't even help retrieval quality because the overlapping text gets embedded with different surrounding context each time.
Semantic Chunking: Split Where Meaning Shifts
Semantic chunking detects topic boundaries within a document and splits there. The implementation uses embedding similarity between consecutive text segments — when similarity drops sharply, you've found a topic shift.
import numpy as np
from sentence_transformers import SentenceTransformer
class SemanticChunker:
def __init__(self, model_name='all-MiniLM-L6-v2', threshold=0.3):
self.model = SentenceTransformer(model_name)
self.threshold = threshold
def chunk(self, text, min_size=100, max_size=1000):
sentences = self._split_sentences(text)
if len(sentences) <= 1:
return [text]
embeddings = self.model.encode(sentences)
similarities = []
for i in range(len(embeddings) - 1):
sim = np.dot(embeddings[i], embeddings[i+1]) / (
np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i+1])
)
similarities.append(sim)
chunks = []
current_chunk = [sentences[0]]
for i, sim in enumerate(similarities):
current_chunk.append(sentences[i + 1])
current_text = ' '.join(current_chunk)
if (sim < self.threshold and len(current_text) >= min_size) or \
len(current_text) >= max_size:
chunks.append(current_text)
current_chunk = []
if current_chunk:
last = ' '.join(current_chunk)
if chunks and len(last) < min_size:
chunks[-1] += ' ' + last
else:
chunks.append(last)
return chunks
Tuning the Threshold
The similarity threshold varies by document type. Technical documentation with clear section structure works well with a threshold of 0.2-0.3. Conversational text like support transcripts needs a lower threshold (0.15-0.2) because topic shifts are gradual. Legal documents need a higher threshold (0.35-0.45) because the formal language makes even unrelated clauses sound similar at the embedding level.
We covered a related topic in Quantization-Aware Training vs Post-Training Quantization: P.
Structure-Aware Chunking for Formatted Documents
When your documents have structure — HTML, Markdown, PDF with headings — use that structure instead of computing semantic boundaries. The author already told you where the topic shifts are with their headings, and that signal is free.
import re
def structure_chunk(html, max_size=1500):
heading_pat = r'(<h[1-6][^>]*>.*?</h[1-6]>)'
parts = re.split(heading_pat, html, flags=re.DOTALL)
chunks = []
current_heading = ""
current_content = []
current_size = 0
for part in parts:
if re.match(r'<h[1-6]', part):
if current_content:
text = strip_html(current_heading + ' '.join(current_content))
if len(text.split()) >= 30:
chunks.append({'content': text, 'heading': strip_html(current_heading)})
current_heading = part
current_content = []
current_size = 0
else:
size = len(strip_html(part).split())
if current_size + size > max_size and current_content:
text = strip_html(current_heading + ' '.join(current_content))
chunks.append({'content': text, 'heading': strip_html(current_heading)})
current_content = [part]
current_size = size
else:
current_content.append(part)
current_size += size
if current_content:
text = strip_html(current_heading + ' '.join(current_content))
if len(text.split()) >= 30:
chunks.append({'content': text, 'heading': strip_html(current_heading)})
return chunks
Enriching Chunks with Context
A chunk in isolation often lacks the context needed for accurate retrieval. The sentence "This approach reduces latency by 40%" is useless without knowing which approach. We prepend contextual metadata to each chunk before embedding: document title, section heading, and a one-sentence document summary. This costs extra tokens during embedding but dramatically improves retrieval quality.
def enrich_chunk(chunk, doc_meta):
prefix = f"Document: {doc_meta['title']}"
if chunk.get('heading'):
prefix += f" | Section: {chunk['heading']}"
return f"{prefix}\n\n{chunk['content']}"
The prefix approach is surprisingly effective. In our A/B test, enriched chunks improved Recall@5 by 12 percentage points compared to raw chunks with the same embedding model. The embedding model has more context to work with, so the resulting vectors better represent what the chunk is actually about rather than just what words it contains.
See also: Building Production Tokenizers: BPE, WordPiece, and Sentence.
Measuring Chunking Quality
You can't optimize chunking without measuring it. We evaluate chunking strategies with two metrics: self-containedness and retrieval contribution.
For self-containedness, we run each chunk through a classifier trained to distinguish coherent text segments from fragments. Chunks that score below 0.6 are likely split at bad boundaries. For retrieval contribution, we compare end-to-end retrieval quality (Recall@5) across different chunking strategies on a held-out query set. This is the metric that actually matters for production.
Chunking deserves the same iterative attention you give to model selection and prompt engineering. Measure it, tune it, and don't settle for the first approach that seems to work. The quality of your search system has a ceiling set by the quality of your chunks, and no embedding model can raise that ceiling for you.