Source code for fastrag.ingestion.steps.chunking.recursive
"""``RecursiveCharacterTextSplitter``: split on a separator hierarchy, largest first."""
from __future__ import annotations
from collections.abc import Iterator, Mapping, Sequence
from typing import Any, Literal
from fastrag.exceptions import ConfigurationError
from fastrag.ingestion.cache import CacheStore
from fastrag.models import Document
from fastrag.ingestion.policy import Policy
from fastrag.ingestion.steps.chunking.base import Chunker
try:
from langchain_text_splitters import (
RecursiveCharacterTextSplitter as _RecursiveCharacterTextSplitter,
)
_LANGCHAIN_AVAILABLE = True
except ImportError: # pragma: no cover - exercised by installs without the extra
_LANGCHAIN_AVAILABLE = False
#: The splitter class, or ``None`` when the optional "chunkers" extra is
#: missing; :meth:`RecursiveCharacterTextSplitter.__init__` refuses to run
#: without it.
_LangchainSplitter: Any = (
_RecursiveCharacterTextSplitter if _LANGCHAIN_AVAILABLE else None
)
MISSING_EXTRA = (
'RecursiveCharacterTextSplitter needs the "chunkers" extra: '
'pip install "fastrag-ai[chunkers]"'
)
[docs]
class RecursiveCharacterTextSplitter(Chunker):
"""Split on paragraphs, then lines, then words, then characters.
The default chunker: it descends the separator hierarchy only when a
piece is still too large, so a chunk breaks at a blank line whenever it
can and mid-word only when it must. That makes it the right choice for
prose regardless of source format, including the Markdown
:class:`~fastrag.ingestion.steps.parsing.HtmlParser` produces.
.. code-block:: python
from fastrag.ingestion.steps.chunking import RecursiveCharacterTextSplitter
chunker = RecursiveCharacterTextSplitter(
chunk_size=500, chunk_overlap=50, cache=cache
)
"""
accepted_content_formats = frozenset(["text", "markdown", "html", "rst"])
def __init__(
self,
*,
chunk_size: int = 1000,
chunk_overlap: int = 200,
separators: Sequence[str] | None = None,
cache: CacheStore | Literal[False] | None = None,
policy: Policy | None = None,
) -> None:
"""Configure the splitter, validating its bounds immediately.
Args:
chunk_size: Target chunk length in characters. Must be at least 1.
chunk_overlap: Characters repeated between neighbours, so a
sentence split across a boundary is still whole in one of
them. Must be non-negative and smaller than ``chunk_size``.
separators: The hierarchy, largest unit first. ``None`` uses
paragraph, line, word, character — which suits both prose and
the Markdown :class:`~fastrag.ingestion.steps.parsing.HtmlParser`
produces.
cache: Cache backend, or ``False``/``None`` — see
:class:`~fastrag.ingestion.steps.base.PipelineStep`.
policy: Execution policy overriding the pipeline default.
Raises:
fastrag.exceptions.ConfigurationError: The "chunkers" extra is not
installed, ``chunk_size < 1``, or ``chunk_overlap`` is not in
``[0, chunk_size)``.
"""
if _LangchainSplitter is None:
raise ConfigurationError(MISSING_EXTRA)
if chunk_size < 1:
raise ConfigurationError(
"RecursiveCharacterTextSplitter.chunk_size must be at least 1"
)
if not 0 <= chunk_overlap < chunk_size:
raise ConfigurationError(
"RecursiveCharacterTextSplitter.chunk_overlap must be "
f"non-negative and smaller than chunk_size ({chunk_size})"
)
super().__init__(cache=cache, policy=policy)
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
self.separators = separators
self._splitter = _LangchainSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=list(separators) if separators is not None else None,
keep_separator=True,
)
[docs]
def config(self) -> Mapping[str, Any]:
"""This splitter's configuration, for cache-identity hashing."""
return {
"chunk_size": self.chunk_size,
"chunk_overlap": self.chunk_overlap,
"separators": (
list(self.separators) if self.separators is not None else None
),
}
[docs]
def split(self, document: Document) -> Iterator[str]:
"""Split ``document.content`` on the paragraph/line/word/character hierarchy."""
self.validate_input(document)
yield from self._splitter.split_text(document.content)