Semantic Chunking

~15 min read

Group sentences into a chunk as long as consecutive segments stay semantically similar (via cosine similarity of their embeddings), starting a new chunk exactly where the similarity drops significantly.

Semantic chunking segments a document based on meaningful units — sentences, paragraphs, or thematic sections — rather than a fixed character count, and uses embedding similarity to decide where chunk boundaries actually belong.

The process: first, create an embedding for each segment (typically sentence-level). Start with the first segment and its embedding. If that segment's embedding has high cosine similarity with the next segment's embedding, the two segments belong in the same chunk — they're talking about the same thing. This continues, absorbing more segments into the current chunk, until the cosine similarity between consecutive segments drops significantly. The moment it does, that's the signal a topic shift has happened — a new chunk starts there, and the process repeats.

Unlike fixed-size chunks, this maintains the natural flow of language and preserves complete ideas — a chunk boundary lands where the topic actually changes, not wherever the character count happened to hit a round number. Because each resulting chunk is semantically richer and more coherent, this generally improves retrieval accuracy: a query's embedding is more likely to closely match a chunk that's entirely about one coherent topic than a chunk that's an arbitrary half-and-half mix of two different ideas — which in turn produces more coherent, relevant responses from the LLM.

The one real complication: the similarity-drop detection depends on a threshold that determines how much of a cosine-similarity drop counts as 'significant enough' to start a new chunk. That threshold isn't universal — it can vary meaningfully from document to document (a technical manual and a narrative essay have very different natural 'topic drift' patterns), so it typically needs to be tuned rather than hardcoded once and reused everywhere.

💻 Code example

import numpy as np

def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

def semantic_chunks(
    sentences: list[str], embeddings: list[np.ndarray], threshold: float = 0.75,
) -> list[str]:
    """Group consecutive sentences into a chunk while similarity stays high;
    start a new chunk the moment similarity drops below `threshold`."""
    chunks, current = [], [sentences[0]]
    for i in range(1, len(sentences)):
        sim = cosine_sim(embeddings[i - 1], embeddings[i])
        if sim >= threshold:
            current.append(sentences[i])       # same topic — extend the chunk
        else:
            chunks.append(" ".join(current))   # topic shift — close the chunk
            current = [sentences[i]]
    chunks.append(" ".join(current))
    return chunks

# sentences + embeddings computed upstream via your embedding model
# (e.g. OpenAI text-embedding-3-small), one embedding per sentence

💬 Deep Dive with AI

Key points

  • Chunks based on meaning: consecutive segments stay in one chunk while their embeddings remain highly similar
  • A significant drop in cosine similarity between consecutive segments signals a topic shift and starts a new chunk
  • Preserves the natural flow of language and complete ideas — unlike fixed-size chunking
  • Richer, more topically-coherent chunks generally improve retrieval accuracy and downstream response quality
  • The similarity-drop threshold isn't universal — it varies by document type and typically needs per-corpus tuning