The Big Picture
Step 1 — Entity NER
The first step identifies what things are mentioned in the text. It’s pluggable — you choose which NER backend to use.EntityExtractor ABC
All extractors implement the same interface:- name — the entity’s name as it appears in the text (e.g., “Professor Harmon”)
- type — one of the allowed entity types (e.g., “Person”)
- description — a brief description
- confidence — how confident the model is (0.0 to 1.0)
- spans — character offsets where the entity appears:
{chunk_id: [{start, end}]} - source_chunk_ids — which chunks mention this entity
GLiNERExtractor (Default)
A local transformer model that runs on your machine — no API calls needed.- The model is loaded lazily on first use (thread-safe via
threading.Lock) - Runs inference via
asyncio.to_thread()to avoid blocking the event loop - Returns predictions with character-level spans — more precise than LLM spans
- Entities below the confidence threshold are labeled
"Unknown"(not discarded)
LLMExtractor
Uses your LLM for entity extraction via a structured prompt.NER_PROMPT to the LLM asking it to extract entities with names, types, descriptions, confidence scores, and character offsets. The response is parsed as JSON.
Best for: When you need richer entity descriptions or when GLiNER doesn’t perform well on your domain.
Custom Extractors
SubclassEntityExtractor to plug in any NER backend:
Step 2 — LLM Verify + Relationship Extraction
The second step uses the LLM to do two things at once:- Verify entities — remove false positives from step 1, fix naming errors, and add any entities the NER model missed
- Extract relationships — identify all factual connections between the verified entities
What the LLM Receives
A structured prompt (VERIFY_EXTRACT_RELS_PROMPT) containing:
- The list of entity types
- The pre-extracted entities from step 1 (as JSON)
- The original chunk text
What the LLM Returns
A JSON object with two arrays:Metadata Merging
After step 2, GLiNER spans from step 1 are carried forward into the verified entities. GLiNER character offsets are more precise than LLM-generated offsets, so step 1 spans take priority. If the LLM found a new entity that GLiNER missed, the LLM’s own spans are kept.Fallback Behavior
If step 2 fails for a chunk (bad JSON, API error), the pipeline falls back to using step 1 entities without relationships — you still get entities, just no relationships for that chunk.The Ontology — Entity Types
Every extracted entity is mapped to one of the allowed entity types. The SDK ships with 11 default types:"Unknown".
Customizing the Ontology
There are three ways to define entity types, listed by priority: 1. GraphSchema entities (highest priority):entity_types parameter on GraphExtraction:
Entity Name Validation
Not every string the NER model produces is a valid entity. The SDK filters names through quality gates:
The full stoplist includes ~50 pronouns and generic references. See
_ENTITY_STOPLIST in entity_extractors.py.
Entity Aggregation
After extraction runs on all chunks, entities are deduplicated across chunks by(normalized_name.lower(), type.lower()):
- If the same entity appears in multiple chunks, the one with the longer description wins
source_chunk_idsare merged (the entity knows every chunk it appeared in)spansare merged (character offsets from every chunk)- Capitalized names are preferred over lowercase
source_chunk_ids = ["chunk_3", "chunk_7", "chunk_12"] and spans from all three chunks.
Relationship Aggregation
Relationships are similarly deduplicated by(source.lower(), type.lower(), target.lower()):
- Longer descriptions win
source_chunk_idsare merged- Spans are merged across chunks
How Entities Become Graph Nodes
After aggregation, each entity becomes aGraphNode:
- ID:
compute_entity_id(name, type)— deterministic:"alice__person"(lowercase, spaces replaced with underscores, type-qualified to prevent collisions) - Label: The entity type (e.g.,
Person,Organization) - Properties:
name,description,source_chunk_ids, and optionallyspans
How Relationships Become Graph Edges
All relationships becomeGraphRelationship objects with type "RELATES":
- Type: Always
"RELATES"— a single unified edge type - Properties:
rel_type— the original relationship type (e.g.,"WORKS_AT")fact— a human-readable fact string:"(Alice, WORKS_AT, Acme Corp): Alice is a senior engineer at Acme Corp"description— the relationship descriptionkeywords— comma-separated terms for fulltext searchweight— confidence (1.0 = explicitly stated, 0.5 = implied)src_name,tgt_name— endpoint entity namessource_chunk_ids— provenancespans— character offsets of the evidence
RELATES type with a rel_type property avoids creating dozens of relationship types in the graph (each needing its own index). The original type is preserved in the rel_type property and is used for display and filtering.
Optional: Coreference Resolution
Coreference resolution replaces pronouns with the entities they refer to, before extraction runs:FastCorefResolver
"biu-nlp/lingmess-coref" (LingMessCoref)
How it works:
- Predict coreference clusters (groups of spans referring to the same entity)
- Find the canonical mention for each cluster (the longest non-pronoun span)
- Replace pronouns with canonical mentions, right-to-left to preserve offsets
- Handles possessives: “her” becomes “Voss’s”
Concurrency
- GLiNER/custom extractors: Use
asyncio.Semaphore(max_concurrency or 12)for parallel chunk processing - LLM extractors: Use
llm.abatch_invoke(max_concurrency=...)for batched LLM calls - Step 2 (verify + rels): Always uses
llm.abatch_invoke()regardless of step 1 backend