Entity Cardinality and Ranking#
Overview#
Cardinality bounding and instance ranking are two cooperating mechanisms in docling-graph's GraphConverter that prevent discovery-spam entities from flooding the knowledge graph and ensure only the highest-quality instances survive when a class is over-populated.
They are activated by a single model_config key — graph_max_instances — and enforced after alias reconciliation, so every ranking signal already reflects the fully-merged state of the graph.
graph_max_instances: Declaring a Bound#
Set graph_max_instances in a model's ConfigDict to cap how many instances of that class are kept in the final graph :
class BusinessSegment(BaseModel):
"""ONE of the 3-6 REPORTABLE segments named in the segment note..."""
model_config = ConfigDict(graph_id_fields=["name"], graph_max_instances=10)
Practical sizing rule: target ~2× the documented maximum so genuine instances have a safety margin. A bound of 10 on a class expected to have 4 instances lifted class F1 from 0.05 to 0.57 in the report benchmark with zero true instances lost .
Validity constraints: the value must be an integer ≥ 1 (not a bool). 0 and negative values are logged as warnings and silently ignored, leaving the class unbounded .
How Bounds Are Collected#
_collect_cardinality_bounds() walks the full model instance tree recursively, reading graph_max_instances from every BaseModel it encounters. Classes without the key are left unbounded and are unaffected by enforcement. The collected {class_name: bound} dict is passed directly to _enforce_cardinality_bounds().
Enforcement: Ranking and Demotion#
GraphConverter._enforce_cardinality_bounds() runs after alias reconciliation and cleanup. For each bounded class with more nodes than its limit, it:
-
Scores every node of that class with a 4-key tuple :
_attr_richness— count of meaningfully filled content attributes (non-metadata, non-empty)_provenance_weight— distinct document chunks supporting the nodeext_in— in-degree from non-root nodes onlyidentity— canonical identity string, used as a stable tiebreak
-
Sorts nodes descending by
(-filled, -chunks, -ext_in, identity, node_id) -
Demotes every node ranked past the bound — removes it with all incident edges, and appends a record to
graph.graph["demoted_nodes"]:{"id": "...", "class": "Segment", "identity": {"name": "Total"}, "filled": 1, "chunks": 300, "ext_in": 0, "reason": "cardinality_bound"}
Why Filled-First, Not Chunk-First#
Provenance-chunk count is a useful signal but is deliberately ranked second. Alias-merged junk rows ("Total", "Other", "Net income") accumulate hundreds of source chunks while carrying almost no attribute data, whereas a true segment instance may be grounded to only one or two chunks. Ranking by filled-attribute count first prevents junk from outranking signal-carrying instances — this is the explicit design rationale in both code and tests .
Execution Order in the Pipeline#
Cardinality enforcement runs at a specific point in pydantic_list_to_graph():
node creation → edge creation → provenance binding
→ auto-cleanup → alias reconciliation → closed-catalog enforcement
→ cardinality enforcement ← HERE
→ empty-identity check → validation → stats
Running after alias reconciliation is critical: merged nodes carry the union of provenance from all absorbed duplicates, so the ranking signals are final and accurate by the time bounds are applied.
Kill Switch#
Pass enforce_cardinality_bounds=False to GraphConverter to disable all enforcement globally. Templates without graph_max_instances are unaffected either way .
Audit Trail#
Demoted nodes are always recorded under graph.graph["demoted_nodes"] — a list of dicts with id, class, identity, filled, chunks, ext_in, and reason: "cardinality_bound". Incident edges are removed alongside the node .
Key Source Files#
| File | Purpose |
|---|---|
graph_converter.py | _collect_cardinality_bounds(), _enforce_cardinality_bounds(), _provenance_weight() |
alias_reconciler.py | _attr_richness() (ranking signal shared with alias reconciliation) |
test_cardinality_bound.py | Unit tests covering demotion, ranking, kill switch, and invalid-bound handling |
best-practices.md | Schema design guidance: when and how to set graph_max_instances |