GraphLoader, GraphLoaderConfig, InMemoryGraphLoader

On this page

Module: graph_loader — the Graph Reader

[!NOTE] Bilingual source This module's comments and docstrings are written primarily in Arabic in the source code. This page translates and documents the described behavior in English; code identifiers (class/method/field names) are unchanged from the source.

Overview

Loads knowledge graphs from JSON (node-link format), GraphML, GEXF, or a simple edge-list CSV, producing one LoadedDocument per node and/or edge. This is purpose-built for GraphRAG-style pipelines, where graph structure (entities and relationships) needs to become independently retrievable, embeddable text units.

GraphLoader and TableLoader are not wired into EXTENSION_MAP or AutoLoader/DirectoryLoader's automatic dispatch — both must be imported and used explicitly.

GraphLoaderConfig

Python · 13 lines
@dataclass
class GraphLoaderConfig:
    include_nodes: bool = True
    include_edges: bool = True
    node_text_keys: List[str] = field(
        default_factory=lambda: ["label", "name", "title", "content", "description", "text"]
    )
    edge_text_template: str = "{source} --[{relation}]--> {target}"
    source_col: str = "source"
    target_col: str = "target"
    relation_col: Optional[str] = "relation"
    weight_col: Optional[str] = "weight"
    encoding: str = "utf-8"
FieldTypeDefaultPurpose
include_nodesboolTrueEmit a LoadedDocument per node
include_edgesboolTrueEmit a LoadedDocument per edge
node_text_keysList[str]["label", "name", "title", "content", "description", "text"]Tried in order; the first key present with a truthy value on a node becomes its page_content. Falls back to f"node:{node_id}" if none match.
edge_text_templatestr"{source} --[{relation}]--> {target}"str.format() template; must use exactly the placeholders {source}, {target}, {relation}
source_colstr"source"CSV column name for edge source (edge-CSV format only)
target_colstr"target"CSV column name for edge target
relation_colOptional[str]"relation"CSV column for relation type; None disables relation extraction (defaults to "related")
weight_colOptional[str]"weight"CSV column for edge weight; None disables (defaults to "1.0")
encodingstr"utf-8"Text encoding for JSON/CSV parsing

GraphLoader

Extends BaseFileLoader

Overview

Auto-detects graph file format from the extension and parses accordingly.

Signature

Python · 15 lines
class GraphLoader(BaseFileLoader):
    SUPPORTED_EXTENSIONS = [".json", ".graphjson", ".graphml", ".gexf", ".csv", ".tsv"]

    def __init__(
        self,
        file_path: str,
        config: Optional[GraphLoaderConfig] = None,
        *,
        include_nodes: bool = True,
        include_edges: bool = True,
        encoding: str = "utf-8",
    ): ...

    def load(self) -> List[LoadedDocument]: ...
    def lazy_load(self) -> Iterator[LoadedDocument]: ...

Parameters

NameTypeDefaultDescription
file_pathstrrequiredPath to the graph file
configOptional[GraphLoaderConfig]NoneFull config; defaults to GraphLoaderConfig(encoding=encoding)
include_nodesbool (keyword-only)TrueAlways applied, overwriting whatever config.include_nodes was set to — see caveat below
include_edgesbool (keyword-only)TrueSame caveat as above
encodingstr"utf-8"File encoding

[!WARNING] include_nodes/include_edges always override config Unlike most other loaders in this package, GraphLoader.__init__ unconditionally does:

Python · 2 lines
self.config.include_nodes = include_nodes
self.config.include_edges = include_edges

after setting self.config. This means even if you pass a fully customized GraphLoaderConfig(include_nodes=False, ...), it gets silently overwritten back to the constructor's include_nodes default of True unless you also explicitly pass include_nodes=False as a direct keyword argument. This is different from the pattern used by PDFLoader/CSVLoader/etc., where config= takes full precedence when provided.

Returns

List[LoadedDocument] — one per included node/edge, in source-file order.

Raises

  • FileNotFoundError / ValueError — at construction
  • ValueError — for an unrecognized extension reaching lazy_load() (defensive; SUPPORTED_EXTENSIONS validation at construction should normally prevent this)
  • xml.etree.ElementTree.ParseError — for malformed GraphML/GEXF XML
  • json.JSONDecodeError — for malformed JSON

Internal workflow — format dispatch

Python · 5 lines
ext = self.file_path.suffix.lower()
if ext in (".json", ".graphjson"):      yield from self._parse_json()
elif ext == ".graphml":                  yield from self._parse_graphml()
elif ext == ".gexf":                     yield from self._parse_gexf()
elif ext in (".csv", ".tsv"):            yield from self._parse_edge_csv(delimiter=("\t" if tsv else ","))

Supports three input shapes:

  1. {"nodes": [...], "links": [...]} — NetworkX's standard node-link export format
  2. {"nodes": [...], "edges": [...]} — a custom alias (edges is checked if links is absent)
  3. A bare list [...] — treated as nodes-only ({"nodes": data, "edges": []})

Each node dict and edge dict is converted via _node_to_doc/_edge_to_doc (see below).

Format: GraphML — _parse_graphml()

Parses standard GraphML XML. Builds a key_id → attr_name map from <key> elements (handling attr.name and <default> values), then for each <node>/<edge> collects its <data key="..."> values, resolved through the key map, applying any unset defaults. Iterates over every <graph> element found (a GraphML file can contain multiple graphs).

Format: GEXF — _parse_gexf()

Parses GEXF (Gephi Exchange Format), trying several namespace URI candidates in order (1.3, 1.2, the legacy 1.2draft, 1.1, and no namespace) since GEXF's namespace varies by exporting tool/version. Builds an attribute id → title map from <attributes><attribute> elements, then extracts <nodes><node> and <edges><edge> elements with their <attvalues>.

Format: edge CSV — _parse_edge_csv(delimiter)

Treats every row as one edge, reading source_col/target_col/relation_col/weight_col per GraphLoaderConfig, with any columns not matching those four added directly into the edge dict (and subsequently into the resulting document's metadata).

_node_to_doc(node, source_fmt) -> LoadedDocument

  • text: first non-empty value found by trying config.node_text_keys in order; falls back to f"node:{node_id}".
  • metadata: source (file path), element_type: "node", node_id, graph_format (which parser produced it), plus every other key from the node dict (excluding id) spread directly into metadata.

_edge_to_doc(edge, source_fmt) -> LoadedDocument

  • text: config.edge_text_template.format(source=..., target=..., relation=...).
  • metadata: source, element_type: "edge", source_node, target_node, relation, weight (stringified), graph_format, plus any other edge fields (excluding the template-consumed ones) spread in.

Examples

Basic — JSON node-link graph:

Python · 6 lines
from zerda.document_loaders import GraphLoader

loader = GraphLoader("knowledge_graph.json")
docs = loader.load()
for doc in docs:
    print(doc.metadata["element_type"], doc.page_content[:80])

GraphML knowledge graph:

Python
docs = GraphLoader("ontology.graphml").load()

Edges-only (skip nodes):

Python · 2 lines
loader = GraphLoader("graph.json", include_nodes=False, include_edges=True)
docs = loader.load()  # only edge documents

Custom edge CSV column names:

Python · 4 lines
from zerda.document_loaders import GraphLoader, GraphLoaderConfig

config = GraphLoaderConfig(source_col="from_entity", target_col="to_entity", relation_col="rel_type")
docs = GraphLoader("relations.csv", config=config, include_nodes=False).load()

Custom node-text priority (prefer a specific field):

Python · 2 lines
config = GraphLoaderConfig(node_text_keys=["full_description", "label"])
docs = GraphLoader("graph.json", config=config).load()

Streaming a large graph:

Python · 3 lines
loader = GraphLoader("large_graph.graphml")
for doc in loader.lazy_load():
    index_in_vector_store(doc)

Metadata produced

See _node_to_doc/_edge_to_doc above. Common keys across all formats: source, element_type ("node" or "edge"), graph_format ("json", "graphml", "gexf", or "csv").

Complexity

  • Time: O(nodes + edges), single pass per format.
  • Space: GraphML/GEXF parsing uses ElementTree.parse(), which loads the entire XML document tree into memory; JSON parsing similarly loads the whole file via json.load(). There is no true streaming XML/JSON parser in use, even though lazy_load() yields documents incrementally after the full underlying file has been parsed into memory.

Thread safety

No shared mutable state between instances; safe for concurrent use across different GraphLoader instances on different files.

Best practices

  • Always pass include_nodes/include_edges as direct keyword arguments to GraphLoader(...) rather than only setting them on a GraphLoaderConfig you pass via config= — per the caveat above, the constructor keyword arguments always win.
  • Customize node_text_keys to match your graph's actual schema — the defaults (label, name, title, content, description, text) cover common conventions but won't match arbitrary custom field names.

Common mistakes

  • Passing GraphLoaderConfig(include_nodes=False) via config= and expecting nodes to be excluded — without also passing include_nodes=False directly to GraphLoader(...), the config value is overwritten back to the default True.
  • Assuming GEXF/GraphML parsing is memory-efficient for very large graphs — the whole file is parsed into an in-memory XML tree first.

InMemoryGraphLoader

Extends BaseDocumentLoader directly

Overview

Loads graph data passed directly as Python dict/list structures — no file required. Useful when building a graph programmatically (e.g. from an NLP entity-extraction pipeline) without persisting it to disk first.

Signature

Python · 11 lines
class InMemoryGraphLoader(BaseDocumentLoader):
    def __init__(
        self,
        nodes: Optional[List[Dict[str, Any]]] = None,
        edges: Optional[List[Dict[str, Any]]] = None,
        config: Optional[GraphLoaderConfig] = None,
        source_name: str = "in_memory_graph",
    ): ...

    def load(self) -> List[LoadedDocument]: ...
    def lazy_load(self) -> Iterator[LoadedDocument]: ...

Parameters

NameTypeDefaultDescription
nodesOptional[List[Dict]]None → []Node dicts (same shape as JSON node-link nodes)
edgesOptional[List[Dict]]None → []Edge dicts (same shape as JSON node-link edges)
configOptional[GraphLoaderConfig]NoneHonored as given here — unlike GraphLoader, there is no constructor-kwarg override for include_nodes/include_edges
source_namestr"in_memory_graph"Logical source identifier stored in metadata

Internal workflow

Delegates to a private _GraphHelper class (a lightweight, file-independent reimplementation of GraphLoader._node_to_doc/_edge_to_doc) rather than reusing GraphLoader directly, since GraphLoader requires a real file path for construction.

Examples

Basic:

Python · 7 lines
from zerda.document_loaders import InMemoryGraphLoader

loader = InMemoryGraphLoader(
    nodes=[{"id": "A", "label": "مصر"}, {"id": "B", "label": "القاهرة"}],
    edges=[{"source": "A", "target": "B", "relation": "عاصمة"}],
)
docs = loader.load()

Building a graph programmatically from extracted entities:

Python · 9 lines
entities = extract_entities(text)  # your own NLP pipeline
relations = extract_relations(text)

loader = InMemoryGraphLoader(
    nodes=[{"id": e.id, "label": e.text, "type": e.entity_type} for e in entities],
    edges=[{"source": r.head, "target": r.tail, "relation": r.label} for r in relations],
    source_name="extracted_from_doc_42",
)
docs = loader.load()

Nodes only, no edges:

Python · 5 lines
from zerda.document_loaders import GraphLoaderConfig

config = GraphLoaderConfig(include_edges=False)
loader = InMemoryGraphLoader(nodes=my_nodes, config=config)
docs = loader.load()

Best practices

  • Use InMemoryGraphLoader instead of writing a temporary JSON file and using GraphLoader when your graph data already exists as Python objects — it avoids unnecessary serialization round-trips.

Common mistakes

  • Expecting InMemoryGraphLoader's config handling to match GraphLoader's override behavior — it doesn't; here, config.include_nodes/include_edges are respected as-is with no constructor-kwarg override mechanism.

Related pages