Semantic chunking is a text splitting strategy that divides documents based on meaning rather than arbitrary size limits. Instead of cutting text every N characters or tokens, semantic chunking analyzes the relationships between sentences and creates boundaries where the topic or meaning shifts. The result is chunks that contain complete, coherent thoughts — each one a self-contained unit of information.
This approach directly addresses the primary weakness of fixed-size chunking: severed context. When a fixed-size chunk cuts a paragraph in half, the resulting fragments embed poorly because they represent incomplete ideas. Semantic chunking produces variable-sized chunks that respect natural language boundaries, leading to higher-quality embeddings and more accurate retrieval in RAG systems.
The tradeoff is computational cost. Semantic chunking requires an embedding model to generate sentence-level vectors and then performs similarity calculations to detect topic shifts. For a 10,000-document corpus, this can mean millions of additional embedding calls during preprocessing. Understanding when this cost is justified — and when simpler methods suffice — is a critical decision in pipeline design.
Semantic chunking follows a three-step process:
text-embedding-3-small, all-MiniLM-L6-v2).The algorithm uses a sliding window to compute cosine similarity between consecutive sentence embeddings. A significant drop in similarity signals a semantic shift — the text is moving from one topic to another. This drop point becomes the chunk boundary. The threshold can be set as an absolute value (e.g., cosine similarity < 0.75) or as a percentile of all observed similarity scores (e.g., break at the bottom 10% of similarities).
A more sophisticated variant uses a sliding window of sentences rather than comparing only adjacent pairs. For example, with a window size of 3, the algorithm computes the average embedding of sentences [i-1, i, i+1] and compares it to [i, i+1, i+2]. This smooths out noise from individual sentence variations and produces more stable breakpoints:
import numpy as np
from sentence_transformers import SentenceTransformer
def semantic_chunk(text, threshold=0.75, window_size=1):
# Step 1: Split into sentences
sentences = split_into_sentences(text)
# Step 2: Embed each sentence
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(sentences)
# Step 3: Find breakpoints via cosine similarity
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
# Compare embedding of current sentence to previous
sim = cosine_similarity(
embeddings[i].reshape(1, -1),
embeddings[i - 1].reshape(1, -1)
)[0][0]
if sim < threshold:
# Semantic shift detected — start new chunk
chunks.append(" ".join(current_chunk))
current_chunk = [sentences[i]]
else:
current_chunk.append(sentences[i])
# Don't forget the last chunk
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks
Setting an absolute similarity threshold is fragile — what works for one document may fail for another. A more robust approach computes all pairwise adjacent similarities first, then sets the threshold as a percentile. For example, breaking at the bottom 10th percentile of similarity scores means you create boundaries at the 10% most dramatic topic shifts in each specific document:
# Compute all adjacent similarities
similarities = []
for i in range(1, len(embeddings)):
sim = cosine_similarity(embeddings[i], embeddings[i-1])
similarities.append(sim)
# Set threshold at 10th percentile (break at biggest shifts)
threshold = np.percentile(similarities, 10)
# Use this document-specific threshold for breakpoint detection
LangChain provides a built-in SemanticChunker that implements this pattern with configurable breakpoint types:
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
chunker = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile", # or "standard_deviation", "interquartile"
breakpoint_threshold_amount=10 # bottom 10% similarity = breakpoints
)
chunks = chunker.split_text(document)
A defining characteristic of semantic chunking is that chunk sizes vary significantly. A dense technical paragraph might produce a 50-token chunk, while a long narrative section on a single topic might yield a 600-token chunk. This variability is both a strength (chunks are semantically coherent) and a challenge (you may need to enforce minimum and maximum size constraints to avoid very small or very large chunks that cause problems downstream).
Semantic chunking is significantly more expensive than fixed-size chunking at preprocessing time:
text-embedding-3-small at ~$0.02/1M tokens, a 10,000-document corpus might cost $5-15 extra for the chunking step alone.However, these are one-time preprocessing costs. If improved retrieval quality leads to better user-facing answers, the investment pays for itself many times over.
In production systems, pure semantic chunking is often combined with size guards. After semantic breakpoints are identified, any chunk exceeding a maximum size (e.g., 512 tokens) is further split using fixed-size or recursive chunking, while chunks below a minimum size (e.g., 50 tokens) are merged with their neighbors. This hybrid approach preserves semantic coherence while respecting practical embedding model limits.