LlamaIndex node parser — SentenceSplitter vs TokenTextSplitter vs SemanticSplitter (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LlamaIndex Node parser choice
LlamaIndex Ingestion · Node Parser Severity: Medium HTTP n/a

LlamaIndex NodeParser — SentenceSplitter, TokenTextSplitter, SemanticSplitter selection

Node parser choice sits between raw documents and retrieval. Bad chunks silently degrade quality; good chunks let a mediocre retriever look great. This page maps the parsers to the situations where each shines.

By Sana K. · Cloud AI Reliability Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: Default SentenceSplitter works well for prose with a target chunk size. TokenTextSplitter is strict on token count but cuts mid-sentence. SemanticSplitterNodeParser finds topic boundaries via embedding similarity (higher cost, best for messy documents). Format-specific parsers (MarkdownNodeParser, CodeSplitter, HTMLNodeParser) preserve structure. Fix by picking the parser that matches content type, then tuning chunk_size against a labeled retrieval test set.

Real error messages you'll see

These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

Nodes cut mid-sentence
# node 42 ends: "The refund policy applies when the customer"
# node 43 starts: "requests a return within 30 days of purchase."
# TokenTextSplitter with no sentence awareness
Semantic splitter timeout
TimeoutError: SemanticSplitterNodeParser timed out on document — 45s per doc
# semantic splitter runs embeddings per sentence-pair; slow on long docs
Code chunks include function halves
# Node ends at `def helper():`
# Next node continues with function body
# Should have used CodeSplitter to respect function boundaries

Reference

Node parser comparison

ParserBest forSpeedChunk boundary quality
SentenceSplitter (default)General proseFastSentence-aware
TokenTextSplitterStrict token limitsFastPoor — cuts anywhere
SentenceWindowNodeParserPer-sentence retrieval + windowed contextFastSentence-based
SemanticSplitterNodeParserUnstructured docs, transcriptsSlow (embed calls)Topic-aware
MarkdownNodeParserMarkdown with headersFastHeader-based
HTMLNodeParserHTML with structureFastTag-based
JSONNodeParserStructured JSONFastPath-based
CodeSplitterSource codeFastAST-aware (per language)
HierarchicalNodeParserParent-child chunksFastMulti-scale

Root causes, ranked by frequency

Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.

  • 26%
    Default SentenceSplitter used on non-prose. Applied to code, JSON, or heavily-structured docs; loses structure.
  • 18%
    Chunk size wrong for content. 128-token chunks on technical reference; 2000-token chunks on FAQs.
  • 14%
    Overlap too small. 0-20 tokens overlap means concepts spanning boundaries never retrieve together.
  • 10%
    SemanticSplitter enabled by default on huge corpus. Ingestion cost explodes.
  • 8%
    Metadata lost during split. Custom node parser did not propagate document metadata.
  • 7%
    Tokenizer mismatch. Chunked with GPT tokenizer, embedded with a different model's tokenizer.
  • 7%
    Not using MarkdownNodeParser on Markdown. Header hierarchy lost; downstream cannot filter by section.
  • 10%
    Chunk sizes vary wildly with semantic splitter. Some chunks tiny, some over token limit.

Fixes — copy-paste solutions

Fix #1

Match the parser to the content type

Format-specific parsers preserve structure worth preserving.

For each file type in your corpus, choose the parser that respects that format's structure. Compose them via IngestionPipeline if you have mixed content.

format_specific_parsers.py
from llama_index.core.node_parser import (
    SentenceSplitter,
    TokenTextSplitter,
    SentenceWindowNodeParser,
    MarkdownNodeParser,
    HTMLNodeParser,
    CodeSplitter,
    HierarchicalNodeParser,
)

# --- Prose (default choice) ---
sentence_splitter = SentenceSplitter(
    chunk_size=512,           # target tokens per node
    chunk_overlap=64,         # overlap between adjacent nodes
    paragraph_separator="\n\n",
    secondary_chunking_regex="[^,.;。]+[,.;。]?",
)

# --- Markdown with header awareness ---
markdown_parser = MarkdownNodeParser(
    include_metadata=True,     # attach header context to each chunk
    include_prev_next_rel=True, # link chunks for context expansion
)

# --- Source code (per-language AST-aware) ---
python_splitter = CodeSplitter(
    language="python",         # respects function/class boundaries
    chunk_lines=40,
    chunk_lines_overlap=8,
    max_chars=1500,
)

# --- Hierarchical (parent-child for context expansion) ---
# Creates chunks at three levels: 2048, 512, 128 tokens
hierarchical = HierarchicalNodeParser.from_defaults(
    chunk_sizes=[2048, 512, 128]
)

# --- Sentence-window (retrieval on sentence, context is window) ---
sentence_window = SentenceWindowNodeParser.from_defaults(
    window_size=3,             # retrieve 1 sentence, expand to ±3 for context
    window_metadata_key="window",
    original_text_metadata_key="original_text",
)

# --- Route by file type ---
def choose_parser(doc):
    ext = doc.metadata.get("file_type", "")
    if ext == "md":
        return markdown_parser
    elif ext == "py":
        return python_splitter
    elif ext in ("html", "htm"):
        return HTMLNodeParser()
    else:
        return sentence_splitter

# Apply
all_nodes = []
for doc in documents:
    parser = choose_parser(doc)
    all_nodes.extend(parser.get_nodes_from_documents([doc]))
For codebases, CodeSplitter is essential — the default splitter cuts mid-function and destroys semantic units. Similarly, Markdown docs benefit enormously from MarkdownNodeParser's header awareness.
Fix #2

Use SentenceWindowNodeParser for high-precision retrieval + expanded context

Retrieve small, generate on large.

The sentence-window pattern embeds a single sentence per node but attaches the surrounding sentences as metadata. At query time, retrieve on the small embedding but hand the LLM the full window.

sentence_window_pattern.py
from llama_index.core import VectorStoreIndex
from llama_index.core.node_parser import SentenceWindowNodeParser
from llama_index.core.postprocessor import MetadataReplacementPostProcessor

# 1) Parse into sentence-window nodes
node_parser = SentenceWindowNodeParser.from_defaults(
    window_size=3,                     # ±3 sentences of context
    window_metadata_key="window",      # stored in metadata for later
    original_text_metadata_key="original_text",
)
nodes = node_parser.get_nodes_from_documents(documents)

# 2) Build the index — each node is embedded on its SINGLE sentence
#    but carries the surrounding window in metadata
index = VectorStoreIndex(nodes=nodes)

# 3) At query time, replace node text with the window before sending to LLM
query_engine = index.as_query_engine(
    similarity_top_k=6,
    node_postprocessors=[
        MetadataReplacementPostProcessor(target_metadata_key="window"),
    ],
)

response = query_engine.query("What is the refund policy?")
print(response)

# Retrieval precision: 1 sentence, so embedding matches focused topic
# Context to LLM: 7 sentences, so the model has full context to answer
# Common finding: ~10-20% hit-rate lift vs plain chunking with the same top_k
Sentence-window is one of the highest-leverage RAG techniques. Try it on any corpus where standard chunking is missing on the edges — where the retrieved chunk technically matches but lacks the surrounding context needed to answer.
Fix #3

Use SemanticSplitterNodeParser only for messy documents

Slow, expensive — but sometimes worth it.

SemanticSplitterNodeParser finds boundaries via embedding similarity. Best for transcripts, meeting notes, unstructured reports where paragraph breaks do not align with topic shifts.

semantic_splitter.py
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.openai import OpenAIEmbedding

# Semantic splitter needs an embedding model to compute sentence similarities
embed_model = OpenAIEmbedding(model="text-embedding-3-small")

semantic_splitter = SemanticSplitterNodeParser(
    buffer_size=1,                          # sentences to group before comparing
    breakpoint_percentile_threshold=95,     # split at top-5% similarity drops
    embed_model=embed_model,
)

# Ingest a document
nodes = semantic_splitter.get_nodes_from_documents(documents)
print(f"Semantic splitter produced {len(nodes)} nodes")

# --- Trade-offs ---
# + Chunks align with topic boundaries
# + Great for messy / unstructured text
# - Slow: embeds each sentence pair to measure boundary
# - Expensive: at scale, embedding cost during ingestion is 2-5x higher
# - Chunk sizes vary; may exceed limits without a cap

# Recommended pattern: semantic split THEN cap size
from llama_index.core.node_parser import SentenceSplitter

def safe_semantic_parse(documents, max_chunk_tokens=1500):
    nodes = semantic_splitter.get_nodes_from_documents(documents)

    # Cap over-sized chunks with a fallback splitter
    capper = SentenceSplitter(chunk_size=max_chunk_tokens, chunk_overlap=100)
    final_nodes = []
    for node in nodes:
        if len(node.text.split()) > max_chunk_tokens * 0.8:  # rough proxy
            # Re-split the oversized chunk
            sub_nodes = capper.get_nodes_from_documents(
                [Document(text=node.text, metadata=node.metadata)]
            )
            final_nodes.extend(sub_nodes)
        else:
            final_nodes.append(node)
    return final_nodes

nodes = safe_semantic_parse(documents, max_chunk_tokens=1500)
For most well-structured corpora (documentation, books, papers), SentenceSplitter is faster AND better. Save semantic splitting for the messy sources where it earns its cost.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Choose the parser based on content type — code deserves CodeSplitter, Markdown deserves MarkdownNodeParser.
  • Default chunk sizes: 512-1024 tokens with 64-128 overlap; adjust based on measurement.
  • For high-precision retrieval + generous LLM context, use SentenceWindowNodeParser.
  • For hierarchical retrieval (small → large), use HierarchicalNodeParser.
  • Use the same tokenizer for splitting as for embedding to avoid drift.
  • Cap semantic-splitter output with a sentence splitter to prevent runaway chunk sizes.
  • A/B test parser choices against a labeled retrieval test set; do not guess.

Frequently asked questions

Depends on corpus. For general prose, 512-1024 tokens is a good starting point. Very fine-grained retrieval (FAQ, snippets) prefers 200-400. Reference documentation with long procedural steps prefers 800-1500. Always A/B test.
Overlap catches concepts that span chunk boundaries. Too little (0-20 tokens): boundary concepts lost. Too much (>30% of chunk_size): storage cost balloons for marginal gain. 10-20% is the sweet spot.
For "small chunks retrieve, large chunks answer" workflows, yes. It creates parent-child chunks so you can retrieve at 128 tokens and hand the LLM the 512-token parent. Very effective for narrative documents.
Chunk_size must be less than the embedding model's context window. For text-embedding-3-small (8K), any reasonable chunk fits. For older models with 512-token limits, use smaller chunks.
Yes — subclass NodeParser and implement _parse_nodes. Useful for domain-specific formats (SQL schemas, IETF RFCs) where existing parsers do not preserve structure well.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.