LoadedDocument, BaseDocumentLoader, BaseFileLoader
On this page
Module: base_loader
These three types form the foundation every other loader in the package builds on.
LoadedDocument
@dataclass
class LoadedDocument:
page_content: str
metadata: Dict[str, Any] = field(default_factory=dict)
doc_id: Optional[str] = None
Overview
The universal unit of output for every loader in this package. Conceptually equivalent to a "Document" type in other RAG/LLM frameworks (e.g. LangChain's Document).
Fields
__post_init__ — deterministic ID generation
def __post_init__(self):
if not self.doc_id:
source = self.metadata.get("source", "")
page = str(self.metadata.get("page_number", ""))
seed = f"{source}:{page}:{self.page_content}"
self.doc_id = "doc_" + hashlib.sha256(seed.encode()).hexdigest()[:16]
Algorithm: if no doc_id is supplied at construction, one is derived by SHA-256 hashing "{source}:{page}:{content}" and truncating to the first 16 hex characters, prefixed with doc_.
[!TIP] Implementation note A docstring comment in the source explicitly documents that this replaced an earlier version which used
time.time()to generate IDs — meaning identical documents got a different ID on every run, breaking deduplication and caching. The current scheme is purely content-based and therefore stable across runs: loading the same file twice produces identicaldoc_idvalues for identical pages, which is useful for incremental indexing (skip-if-exists) and deduplication.
Caveat: the ID only incorporates source and page_number from metadata, not the full metadata dict. Two documents with identical content and source but different metadata (e.g. different row_number) will collide if page_number also matches or is absent in both — doc_id uniqueness is not guaranteed across all metadata variations, only across (source, page_number, page_content) triples.
Methods
to_dict() -> Dict[str, Any]
Returns {"doc_id": ..., "page_content": ..., "metadata": ...}. Useful for JSON serialization or passing to systems expecting plain dicts.
__len__() -> int
Returns len(self.page_content) — character count, not token count.
__eq__(other) -> bool
Two LoadedDocument instances are equal iff their doc_id matches — content and metadata are not compared directly (though they factor into doc_id indirectly).
__hash__() -> int
hash(self.doc_id) — makes LoadedDocument usable in sets and as dict keys, consistent with __eq__.
__repr__() -> str
Returns a readable preview: LoadedDocument(doc_id='doc_...', chars=N, preview='first 80 chars...').
Example
from zerda.document_loaders.base_loader import LoadedDocument
doc = LoadedDocument(
page_content="Cairo is the capital of Egypt.",
metadata={"source": "facts.txt", "page_number": 1},
)
print(doc.doc_id) # doc_<16 hex chars>, stable across runs
print(len(doc)) # 31
print(doc.to_dict())
BaseDocumentLoader (abstract)
class BaseDocumentLoader(ABC):
def __init__(self, encoding: str = "utf-8", **kwargs): ...
@abstractmethod
def load(self) -> List[LoadedDocument]: ...
Overview
The root abstract base for every loader in the package, including non-file-based ones (WebLoader, DirectoryLoader, HTMLStringLoader, InMemoryGraphLoader, InMemoryTableLoader). Defines the contract: a loader is anything with a load() method returning List[LoadedDocument].
Constructor
Abstract method
load() -> List[LoadedDocument]
Must be implemented by every subclass. This is where format-specific extraction logic lives.
Concrete methods
lazy_load() -> Iterator[LoadedDocument]
Default implementation: yield from self.load(). Subclasses that benefit from true incremental processing (CSVLoader, JSONLinesLoader, DirectoryLoader, GraphLoader, TableLoader) override this for real streaming; everything else gets eager-load-then-iterate behavior with no memory savings.
load_and_split(text_splitter=None) -> List[LoadedDocument]
def load_and_split(self, text_splitter=None) -> List[LoadedDocument]:
documents = self.load()
if text_splitter is None:
return documents
chunked = []
for doc in documents:
chunks = text_splitter.split_text(doc.page_content)
for i, chunk in enumerate(chunks):
meta = doc.metadata.copy()
meta.update({"chunk_index": i, "total_chunks": len(chunks), "original_doc_id": doc.doc_id})
chunked.append(LoadedDocument(page_content=chunk, metadata=meta))
return chunked
Algorithm: loads all documents, then for each one calls text_splitter.split_text(doc.page_content) — text_splitter is a duck-typed object, not a specific class from this package; any object with a split_text(str) -> List[str] method works. Each resulting chunk becomes a new LoadedDocument with metadata copied from the parent plus chunk_index, total_chunks, and original_doc_id.
Raises: AttributeError if text_splitter does not implement split_text. Whatever self.load() can raise also propagates.
_build_metadata(source: str, **extra) -> Dict[str, Any] (protected)
Builds the common metadata envelope: {"source": ..., "loader_type": self.__class__.__name__, "loaded_at": "YYYY-MM-DD HH:MM:SS"}, merged with extra. Used by non-file loaders (web, directory, graph/table in-memory variants).
Example: implementing a custom loader
from zerda.document_loaders.base_loader import BaseDocumentLoader, LoadedDocument
class StaticStringLoader(BaseDocumentLoader):
def __init__(self, text: str, source: str = "manual"):
super().__init__()
self.text = text
self.source = source
def load(self):
meta = self._build_metadata(source=self.source)
return [LoadedDocument(page_content=self.text, metadata=meta)]
docs = StaticStringLoader("Hello world").load()
BaseFileLoader(BaseDocumentLoader) (abstract)
class BaseFileLoader(BaseDocumentLoader):
SUPPORTED_EXTENSIONS: List[str] = []
def __init__(self, file_path: str, encoding: str = "utf-8", **kwargs): ...
Overview
Specializes BaseDocumentLoader for loaders backed by a single file path. Adds path validation and file-specific metadata building. Every concrete file-based loader in the package (TextLoader, PDFLoader, DocxLoader, CSVLoader, ExcelLoader, JSONLoader, JSONLinesLoader, HTMLLoader, GraphLoader, TableLoader, and the multimodal loaders) subclasses this.
Class attribute
SUPPORTED_EXTENSIONS: List[str] — overridden by each subclass; an empty list (the base default) disables extension checking entirely.
Constructor
Calls self._validate_file() automatically during __init__.
_validate_file() (protected)
Raises:
FileNotFoundError— iffile_pathdoes not existValueError— if the path exists but is not a regular file (e.g. it's a directory)ValueError— ifSUPPORTED_EXTENSIONSis non-empty and the file's suffix (lowercased) is not in that list
This validation runs eagerly at construction time, not at load() time — instantiating a loader for a missing file fails immediately.
_build_file_metadata(**extra) -> Dict[str, Any] (protected)
def _build_file_metadata(self, **extra) -> Dict[str, Any]:
stat = self.file_path.stat()
meta = self._build_metadata(
source=str(self.file_path.resolve()),
file_name=self.file_path.name,
file_type=self.file_path.suffix.lower(),
file_size_bytes=stat.st_size,
)
meta.update(extra)
return meta
Every file-based loader's load() method calls this to populate the base metadata fields (source, loader_type, loaded_at, file_name, file_type, file_size_bytes) before adding its own format-specific keys.
Example
from zerda.document_loaders.base_loader import BaseFileLoader, LoadedDocument
class FirstLineLoader(BaseFileLoader):
SUPPORTED_EXTENSIONS = [".txt"]
def load(self):
first_line = self.file_path.read_text(encoding=self.encoding).splitlines()[0]
meta = self._build_file_metadata(note="first line only")
return [LoadedDocument(page_content=first_line, metadata=meta)]
Thread safety & async support
Neither LoadedDocument, BaseDocumentLoader, nor BaseFileLoader perform any locking or async I/O. LoadedDocument is an effectively-immutable-by-convention dataclass (nothing prevents mutation, but nothing in the package mutates an instance after construction except load_and_split's copy-then-build pattern, which never mutates an existing instance). Concrete loaders that need concurrency (DirectoryLoader, MultiURLLoader) implement their own locking — see Architecture → Concurrency model.
Version History
Cannot be determined from the repository — no changelog or git history was available for analysis. The LoadedDocument.__post_init__ docstring documents one specific historical fix (switching from time-based to content-based ID generation) but gives no version or date.
Related
- Configuration Reference
- Architecture
- Every other loader page subclasses
BaseFileLoaderorBaseDocumentLoaderdirectly.