text-splitter

repository·main·Indexed 20 days ago

https://github.com/benbrandt/text-splitter

A semantic text splitter for breaking large documents into smaller chunks for LLMs, available for Rust and Python. It maximizes chunk size while respecting semantic boundaries such as sentences, words, and Markdown structure. It supports character-based splitting, token-based splitting via Hugging Face and Tiktoken tokenizers, and specialized splitters for Markdown and code using tree-sitter parsers.

Tokens
13.4K
Snippets
43
Records
51
Agent score
70%

What's inside text-splitter

  1. How semantic splitting works

    main

    The splitter aims to maximize chunk size while respecting semantic boundaries. It follows these steps:

    1. Identify Levels: It splits text by increasing semantic levels (e.g., from characters up to headings or syntax tree depth).
    2. Select Highest Level: It checks the first item of each level and selects the highest level whose first item still fits within the chunk size.
    3. Merge: It merges neighboring sections of that level (or higher) into a chunk to maximize length without crossing semantic boundaries.

    Semantic Levels by Splitter Type

    TextSplitter (Ascending order)

    1. Characters
    2. Unicode Grapheme Cluster Boundaries
    3. Unicode Word Boundaries
    4. Unicode Sentence Boundaries
    5. Newline sequences (e.g., \n\n is a higher level than \n)

    MarkdownSplitter (Ascending order)

    1. Characters
    2. Unicode Grapheme Cluster Boundaries
    3. Unicode Word Boundaries
    4. Unicode Sentence Boundaries
    5. Soft line breaks (single newline)
    6. Inline elements (text, emphasis, links, etc.)
    7. Block elements (paragraphs, code blocks, metadata, etc.)
    8. Thematic breaks (horizontal rules)
    9. Headings by level

    CodeSplitter (Ascending order)

    1. Characters
    2. Unicode Grapheme Cluster Boundaries
    3. Unicode Word Boundaries
    4. Unicode Sentence Boundaries
    5. Syntax tree depth (e.g., a function is a higher level than a statement inside it)

    Note: Splitting never occurs below the character level to ensure valid Unicode strings.

  2. Split text using Hugging Face Tokenizer

    main

    To use a Hugging Face tokenizer for token-based splitting, you must enable the tokenizers feature in text-splitter and the http feature in tokenizers.

    Setup:

    cargo add text-splitter --features tokenizers
    cargo add tokenizers --features http

    Usage: Note: If your tokenizer has truncation enabled, disable it before passing it to the splitter to prevent chunk sizes from being capped by the tokenizer's limit.

    use text_splitter::{ChunkConfig, TextSplitter};
    use tokenizers::Tokenizer;
    
    let mut tokenizer = Tokenizer::from_pretrained("bert-base-cased", None).unwrap();
    // Disable truncation to avoid capping chunk sizes
    tokenizer.with_truncation(None).unwrap();
    
    let max_tokens = 1000;
    let splitter = TextSplitter::new(ChunkConfig::new(max_tokens).with_sizer(tokenizer));
    
    let chunks = splitter.chunks("your document text");
    use text_splitter::{ChunkConfig, TextSplitter};
    // Can also use anything else that implements the ChunkSizer
    // trait from the text_splitter crate.
    use tokenizers::Tokenizer;
    
    let mut tokenizer = Tokenizer::from_pretrained("bert-base-cased", None).unwrap();
    // If your tokenizer has truncation enabled, disable it before passing it to
    // the splitter. Otherwise chunk sizes can be capped by the tokenizer's
    // truncation limit.
    tokenizer.with_truncation(None).unwrap();
    
    let max_tokens = 1000;
    let splitter = TextSplitter::new(ChunkConfig::new(max_tokens).with_sizer(tokenizer));
    
    let chunks = splitter.chunks("your document text");
  3. Split text by number of characters with TextSplitter

    main

    Use TextSplitter to split text into chunks based on a maximum character count. The splitter attempts to find semantically sensible boundaries (like words or sentences) to avoid breaking in the middle of a unit. By default, the splitter trims whitespace from chunks, but you can disable this using the trim parameter.

    from semantic_text_splitter import TextSplitter
    
    # Maximum number of characters in a chunk
    max_characters = 1000
    # Optionally can also have the splitter not trim whitespace for you
    splitter = TextSplitter(max_characters)
    # splitter = TextSplitter(max_characters, trim=False)
    
    chunks = splitter.chunks("your document text")
  4. Split code with CodeSplitter

    main

    To split code using tree-sitter parsers, enable the code feature and add the appropriate tree-sitter-<language> crate.

    Setup:

    cargo add text-splitter --features code
    cargo add tree-sitter-<language>

    Usage:

    use text_splitter::CodeSplitter;
    
    let max_characters = 1000;
    // Requires a tree-sitter language parser
    let splitter = CodeSplitter::new(tree_sitter_rust::LANGUAGE, max_characters).expect("Invalid tree-sitter language");
    
    let chunks = splitter.chunks("your code file");
    use text_splitter::CodeSplitter;
    // Maximum number of characters in a chunk. Can also use a range.
    let max_characters = 1000;
    // Default implementation uses character count for chunk size.
    // Can also use all of the same tokenizer implementations as `TextSplitter`.
    let splitter = CodeSplitter::new(tree_sitter_rust::LANGUAGE, max_characters).expect("Invalid tree-sitter language");
    
    let chunks = splitter.chunks("your code file");
  5. Split text using Tiktoken Tokenizer

    main

    To use tiktoken (useful for OpenAI models), enable the tiktoken-rs feature in text-splitter.

    Setup:

    cargo add text-splitter --features tiktoken-rs
    cargo add tiktoken-rs

    Usage:

    use text_splitter::{ChunkConfig, TextSplitter};
    use tiktoken_rs::cl100k_base;
    
    let tokenizer = cl100k_base().unwrap();
    let max_tokens = 1000;
    let splitter = TextSplitter::new(ChunkConfig::new(max_tokens).with_sizer(tokenizer));
    
    let chunks = splitter.chunks("your document text");
    use text_splitter::{ChunkConfig, TextSplitter};
    // Can also use anything else that implements the ChunkSizer
    // trait from the text_splitter crate.
    use tiktoken_rs::cl100k_base;
    
    let tokenizer = cl100k_base().unwrap();
    let max_tokens = 1000;
    let splitter = TextSplitter::new(ChunkConfig::new(max_tokens).with_sizer(tokenizer));
    
    let chunks = splitter.chunks("your document text");
  6. Split Markdown documents with MarkdownSplitter

    main

    To split Markdown files while respecting semantic boundaries like headings and block elements, enable the markdown feature.

    Setup:

    cargo add text-splitter --features markdown

    Usage:

    use text_splitter::MarkdownSplitter;
    
    let max_characters = 1000;
    let splitter = MarkdownSplitter::new(max_characters);
    
    let chunks = splitter.chunks("# Header\n\nyour document text");
    use text_splitter::MarkdownSplitter;
    // Maximum number of characters in a chunk. Can also use a range.
    let max_characters = 1000;
    // Default implementation uses character count for chunk size.
    // Can also use all of the same tokenizer implementations as `TextSplitter`.
    let splitter = MarkdownSplitter::new(max_characters);
    
    let chunks = splitter.chunks("# Header\n\nyour document text");
  7. Configure chunk capacity with ChunkCapacity

    main

    The ChunkCapacity struct defines the target and maximum size for text chunks. It allows for two distinct modes of operation:

    1. Fixed Size: Set desired and max to the same value. This is useful when you need to strictly adhere to a limit, such as a model's context window.
    2. Loose Targeting: Set max to a value larger than desired. The splitter will attempt to stay close to desired but is permitted to grow up to max if it allows the chunk to stay at a higher semantic level (e.g., not breaking a paragraph mid-sentence).

    You can create a ChunkCapacity from a single usize (where desired == max) or from various Rust range types.

    // Fixed size (desired = 512, max = 512)
    let capacity = ChunkCapacity::new(512);
    
    // Loose targeting (desired = 512, max = 1024)
    let capacity = ChunkCapacity::new(512).with_max(1024).unwrap();
    
    // From a range (desired = 5, max = 10)
    let capacity = ChunkCapacity::from(5..11);
  8. Understand TextSplitter semantic levels

    main

    The TextSplitter uses semantic levels to decide where to break text. For the default TextSplitter, the semantic levels are defined by LineBreaks(usize), where the usize represents the number of consecutive newlines (e.g., \n\n is a higher level than \n).

    When splitting, the algorithm:

    1. Identifies all potential semantic boundaries.
    2. Selects the highest level boundary that allows the resulting chunk to stay within the chunk_capacity.
    3. Merges adjacent sections of that level to maximize the chunk size without crossing into a higher semantic boundary.
  9. Understand Markdown semantic splitting levels

    main

    The MarkdownSplitter identifies several semantic levels to ensure chunks respect the structure of the document. The hierarchy of splitting (from finest to coarsest) follows this order:

    1. Characters
    2. Unicode Grapheme Cluster Boundaries
    3. Unicode Word Boundaries
    4. Unicode Sentence Boundaries
    5. Soft line breaks (single newline)
    6. Inline elements: text nodes, emphasis, strong, strikethrough, link, image, table cells, inline code, footnote references, task list markers, and inline HTML.
    7. Block elements: paragraphs, code blocks, footnote definitions, metadata, block quotes, table rows/items, lists, and tables.
    8. Thematic breaks (horizontal rules).
    9. Headings (by level).

    Splitting never occurs below the character level to avoid invalid Unicode strings.

  10. Configure fallback behavior with FallbackLevel

    main

    When using custom semantic levels for text splitting, it is possible that a semantic unit (like a paragraph or a sentence) is larger than the target chunk size. To prevent the splitter from getting stuck, it uses a FallbackLevel to break down text into smaller, standard Unicode segments.

    Available fallback levels:

    • Char: Splits by individual characters. This is the smallest possible unit that ensures valid UTF-8 strings.
    • GraphemeCluster: Splits by Unicode grapheme clusters.
    • Word: Splits by Unicode words.
    • Sentence: Splits by Unicode sentences.

    You can use the sections method on a FallbackLevel to iterate over the text segments, returning an iterator of (index, &str) tuples representing the start position and the content of each segment.

    // Example of how FallbackLevel segments text
    use text_splitter::FallbackLevel;
    
    let text = "Hello, world!";
    let segments: Vec<(usize, &str)> = FallbackLevel::Word.sections(text).collect();
    // segments will contain the indices and substrings for each word
  11. Initialize CodeSplitter with a Custom Callback

    main

    If you need a custom way to measure size, use from_callback. The callback must be a callable that accepts a str and returns an int representing the size.

    from semantic_text_splitter import CodeSplitter
    import tree_sitter_python
    
    # Example: using a lambda to define size
    splitter = CodeSplitter.from_callback(tree_sitter_python.language(), lambda text: len(text), (200, 1000))
    from semantic_text_splitter import CodeSplitter
    # Import the tree-sitter grammar you want to use
    import tree_sitter_python
    
    # Optionally can also have the splitter trim whitespace for you
    splitter = CodeSplitter.from_callback(tree_sitter_python.language(), lambda text: len(text), (200,1000))
    
    # Maximum number of tokens in a chunk. Will fill up the
    # chunk until it is somewhere in this range.
    chunks = splitter.chunks("# Header\n\nyour document text")