Knowledge Graph Construction from Unstructured Data: Building the Pipeline
Why Construction Is the Hard Part
Knowledge graphs are only as valuable as the data they contain. Querying, visualising, and reasoning over a graph are well-understood problems — but the hardest engineering challenge remains the first mile: how do you populate a knowledge graph from raw, unstructured text?
Most enterprises sit on terabytes of documents — PDFs, emails, internal wikis, support tickets, news articles — all containing entities and relationships that never make it into a structured database. Extracting that structure at scale is the critical bottleneck.
This article walks through the end-to-end pipeline for constructing a knowledge graph from unstructured text, from document ingestion through entity extraction, relationship inference, entity resolution, and graph population. We compare traditional NLP approaches with modern LLM-based pipelines and give you a decision framework for when each makes sense.
The Pipeline at a Glance
A production knowledge graph construction pipeline has six stages:
- Document Ingestion — Parse PDFs, HTML, Markdown, and plain text into standardised chunks
- Entity Extraction — Identify entities (people, organisations, concepts, products) in each chunk
- Relationship Inference — Determine how entities relate to one another
- Entity Resolution — Deduplicate: merge "Apple Inc." and "Apple Computer Company" into a single node
- Graph Population — Write nodes, relationships, and properties to Neo4j (or your graph database of choice)
- Quality Assurance — Validate against schema constraints, measure precision and recall
Each stage introduces design decisions that ripple through the entire pipeline. Let us examine each in turn.
1. Document Ingestion
The ingestion layer normalises diverse input formats into a uniform representation. A common pattern stores each document as a :Document node with its text content, metadata, and chunked passages as :ContentChunk children:
(:Document {title, source, date})-[:CONTAINS]->(:ContentChunk {text, index})
Chunking strategy matters. Overlapping chunks with sentence boundaries (not fixed token counts) preserve entity context across chunks, which is especially important when entities span paragraph breaks. A sliding window of 256 tokens with 32-token overlap strikes a good balance for most domains.
2. Entity Extraction
Entity extraction is where most engineering effort concentrates. Two approaches dominate in 2026:
Traditional NER Pipelines
Fine-tuned NER models (SpaCy, GLiNER, or BERT-based) offer fast, cheap extraction for well-defined entity types:
import spacy
nlp = spacy.load("en_core_web_trf")
doc = nlp("Neo4j announced GraphRAG support at GraphSummit London.")
for ent in doc.ents:
print(f"{ent.text} -> {ent.label_}")
# Neo4j -> ORG
# GraphRAG -> PRODUCT
# GraphSummit London -> EVENT
Traditional NER excels at precision for fixed types (Person, Organisation, Location) but struggles with domain-specific entities — API names, CVE identifiers, product versions — without costly fine-tuning.
LLM-Based Extraction
LLMs can extract arbitrary entity types from any domain without fine-tuning:
import openai
def extract_entities(text: str, entity_types: list[str]) -> list[dict]:
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "system",
"content": f"Extract JSON entities: {entity_types}"
}, {
"role": "user",
"content": text
}]
)
return json.loads(response.choices[0].message.content)
The trade-off is cost and latency — LLM-based extraction is 10–100× more expensive per entity than traditional NER. The pragmatic approach is a hybrid pipeline: use traditional NER for high-volume standard entities and route only ambiguous or domain-specific cases to an LLM.
3. Relationship Inference
Once entities are extracted, you need edges between them. Three common strategies offer different trade-offs:
| Strategy | Approach | Precision | Recall | Cost |
|---|---|---|---|---|
| Co-occurrence | Entities in same sentence → edge | Low | High | Free |
| Dependency parsing | Subject-verb-object extraction | Medium | Medium | Low |
| LLM prompting | Extract relationships explicitly | High | High | High |
Co-occurrence is the simplest heuristic: if two entities appear in the same sentence, create an edge. This produces dense graphs with high recall but low precision — most co-occurring entities are related in the broadest sense but not in a semantically meaningful way.
Dependency parsing uses grammatical structure to extract subject-verb-object triples from each sentence. For "Neo4j acquired a startup," dependency parsing identifies Neo4j as the subject, acquired as the verb, and startup as the object, yielding (:Entity {name: "Neo4j"})-[:ACQUIRED]->(:Entity {name: "startup"}).
LLM-based extraction produces the highest-quality relationships but at the highest cost. A sensible middle ground: use dependency parsing as a first pass, then task an LLM with validating and refining only the extracted triples that fall below a confidence threshold.
4. Entity Resolution
Entity resolution — also called deduplication or record linkage — is the most underestimated component of KG construction. A graph built without deduplication quickly becomes unusable as the same real-world entity accrues multiple nodes with slight name variations.
Resolution techniques ranked by sophistication:
- Exact string match — Catches duplicates with identical names. Covers roughly 30% of real-world duplicates.
- Fuzzy string matching — Levenshtein or Jaro-Winkler distance for near-misses like "Google Inc." versus "Google Incorporated."
- Blocking plus similarity scoring — Group candidates by a blocking key (first three characters, date range) then score with TF-IDF or embedding cosine similarity.
- LLM-based resolution — Prompt the model with "Are these two entities the same?" and supporting evidence.
In production, a rule-based blocker followed by an embedding similarity scorer with a tunable threshold captures 90% or more of duplicates at acceptable latency.
5. Graph Population
With entities resolved and relationships inferred, you load the graph into Neo4j. Use periodic batching with MERGE to avoid duplicating nodes:
:auto USING PERIODIC COMMIT 500
LOAD CSV FROM 'file:///entities.csv' AS row
MERGE (e:Entity {id: row.id})
ON CREATE SET e.name = row.name,
e.type = row.type,
e.source = row.source
For relationships, batch via UNWIND:
UNWIND $relationships AS rel
MATCH (a:Entity {id: rel.source_id})
MATCH (b:Entity {id: rel.target_id})
MERGE (a)-[r:HAS_RELATIONSHIP {type: rel.type}]->(b)
SET r.confidence = rel.confidence,
r.evidence = rel.evidence
Always store a confidence score and evidence trace (source document ID, sentence) on each relationship. These enable downstream quality filtering and auditability — critical for the regulated use cases discussed in Knowledge Graphs Are the Antidote to AI Hallucination Liability.
6. Quality Assurance
A pipeline without QA metrics is one that silently degrades. Track these four signals:
- Entity precision and recall against a held-out gold set
- Relationship validity — sample 100 edges and manually label correct versus incorrect
- Graph density — unexpected density spikes indicate over-extraction
- Schema compliance — every node has required properties, every edge conforms to expected types
For ontology-aware validation, apply the principles from Ontology in Graph Databases: your construction pipeline should honour the node labels, property constraints, and relationship types defined by your ontology. When the pipeline produces a node that does not match the schema, it should either be transformed or flagged for review.
7. Pipeline Orchestration and Scheduling
Building the extraction logic is only half the work. The other half is running it reliably on a schedule, handling failures, and processing only what has changed.
Incremental vs. Full Rebuild
Most documents in an enterprise corpus are static — once ingested, they rarely change. A full rebuild on every run wastes tokens and time. Design your pipeline for incremental processing:
| Strategy | Trigger | Use Case |
|---|---|---|
| Timestamp-based | Compare lastModified against the last pipeline run | Files on S3, network drives |
| Event-driven | Webhook or SQS notification on upload | Real-time ingestion from document management systems |
| Watermark table | Track processed document IDs in a separate PipelineState node in Neo4j | General-purpose; survives restarts |
| Full rebuild | Reprocess every document | After ontology changes or extraction model upgrades |
A simple watermark approach stores pipeline state directly in the graph:
MERGE (p:PipelineState {name: "kg-construction"})
ON MATCH SET p.lastRunAt = datetime(), p.documentsProcessed = $count
ON CREATE SET p.lastRunAt = datetime(), p.documentsProcessed = $count
Orchestration Framework
A typical KG construction pipeline expressed as a Prefect or Airflow DAG has five tasks:
from prefect import flow, task
@task
def ingest_documents(source_path: str) -> list[dict]:
"""List and chunk documents modified since last run."""
...
@task
def extract_entities(chunks: list[dict]) -> list[dict]:
"""Run NER or LLM extraction on each chunk."""
...
@task
def resolve_entities(entities: list[dict]) -> list[dict]:
"""Deduplicate and merge entity references."""
...
@task
def write_graph(entities: list[dict], relations: list[dict]) -> None:
"""Batch-write to Neo4j with MERGE."""
...
@task
def run_quality_checks() -> dict:
"""Validate precision, recall, and schema compliance."""
...
@flow(log_prints=True)
def kg_pipeline(source_path: str = "/data/documents"):
chunks = ingest_documents(source_path)
entities = extract_entities(chunks)
resolved = resolve_entities(entities)
write_graph(entities, relations)
quality = run_quality_checks()
print(f"Quality report: {quality}")
Each task should be idempotent: running it twice produces the same result. Idempotency lets you retry failed stages without duplicating entities. The MERGE operations in the graph population stage already enforce this at the database level — the orchestration layer just needs to pass the same data twice.
Failure Handling
The most common failure modes in KG construction pipelines are API rate limits (LLM providers), malformed documents (corrupted PDFs), and transient database connection drops. Handle each with a circuit breaker pattern: retry up to three times with exponential backoff, then route the failed document to a dead-letter queue for manual inspection.
8. Cost and Performance Optimisation
LLM-based extraction dominates the pipeline budget. A pipeline processing 10,000 documents through GPT-4o can easily run up thousands of dollars in a single run. Three optimisation strategies keep costs under control.
Tiered Extraction
Route each chunk through a decision tree rather than sending everything to the most expensive model:
Chunk → Cheap NER (spaCy/GLiNER)
├─ Entities found with confidence > 0.9 → Accept directly
├─ Entities found, confidence 0.5–0.9 → Send to gpt-4o-mini for validation
└─ No entities found → Send to gpt-4o for extraction
This pattern reduces LLM calls by 60–80% in practice. The cheap NER model handles high-confidence extractions for standard entity types; the LLM only sees ambiguous cases and domain-specific concepts.
Caching
LLM calls are deterministic for the same input at temperature 0. Cache extraction results keyed by a hash of the input chunk text plus the extraction schema:
import hashlib
import diskcache as dc
cache = dc.Cache("/tmp/kg-extraction-cache")
def cached_extract(text: str, schema_hash: str) -> dict:
key = hashlib.sha256((text + schema_hash).encode()).hexdigest()
if key in cache:
return cache[key]
result = llm_extract(text) # expensive call
cache[key] = result
return result
When re-running the pipeline after a schema change, invalidate only entries with the old schema hash. This avoids re-extracting documents whose entities are unaffected by the change.
Batching
LLM providers charge per input and output token regardless of whether you send one chunk or ten, up to the context window limit. Batch multiple chunks into a single request with structured output requesting an array of entity sets:
response = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "system",
"content": "Extract entities from each chunk. Return a JSON array."
}, {
"role": "user",
"content": "\n---CHUNK BREAK---\n".join(chunks)
}],
response_format={"type": "json_object"}
)
Batching 5–10 chunks per request cuts API costs by 40–60% and improves throughput proportionally, with negligible quality degradation when chunks share a document context.
9. Pipeline Versioning and Schema Evolution
Knowledge graphs are living artefacts. Your ontology will change, extraction models will improve, and new document sources will appear. A versioning strategy prevents the graph from becoming an inconsistent mess.
Version-Locking
Tag every extracted entity and relationship with the pipeline version that produced it:
MERGE (e:Entity {id: $id})
SET e.pipelineVersion = $version,
e.extractedAt = datetime()
This enables three critical operations:
- Rollback: If a pipeline update degrades quality, revert to the previous version's output without rebuilding from scratch.
- A/B comparison: Run two pipeline versions in parallel on a held-out document set and compare precision and recall before promoting a new version.
- Stale data detection: Any entity with
pipelineVersionolder than N days can be flagged for re-extraction.
Schema Migration
When your ontology adds a new node label or relationship type, existing entities should be backfilled rather than re-extracted. Define migration scripts that transform the existing graph:
// Example: Split a generic :Entity label into typed labels
MATCH (e:Entity)
WHERE e.type = "Company"
CALL {
WITH e
SET e:Company
REMOVE e.type
} IN TRANSACTIONS OF 1000 ROWS;
Automate these migrations in your orchestration framework — they are database operations, not extraction tasks, and should run as a separate step after the extraction pipeline completes.
Which Approach Should You Choose?
| Factor | Traditional NLP | LLM-Based | Hybrid |
|---|---|---|---|
| Entity coverage | Limited to trained types | Unlimited | Both |
| Per-entity cost | Pennies per million | Cents per hundred | Tiered |
| Latency | Milliseconds | Seconds | Configurable |
| Precision | High (known types) | High (good prompts) | High |
| Recall | Low (misses domain entities) | High | High |
| Setup complexity | Fine-tuning pipeline | Prompt engineering | Both |
For most production KG construction workloads in 2026, the hybrid approach wins: fast traditional NER for high-volume standard entities, LLM-based extraction for domain-specific concepts and complex relationship inference, and a rule-based entity resolution layer that keeps the graph clean.
Putting It All Together
Building a knowledge graph from unstructured data is not a one-shot ETL job — it is an iterative process that improves as you add better extraction models, refine your ontology, and accumulate more training data for entity resolution. Start with a simple pipeline (co-occurrence plus exact match), measure quality, then layer in sophistication where the quality gap is widest.
The operational layers — orchestration, cost optimisation, versioning, and monitoring — are what separate a one-time extraction script from a production pipeline. Without them, your knowledge graph will drift as schemas evolve, costs spiral, and failures go undetected. Invest in them from day one, even if the initial pipeline handles only a few hundred documents.
For a deeper dive into the ontology design that your construction pipeline should target, see Ontology in Graph Databases. For a practical walkthrough using the neo4j-graphrag Python package, read Knowledge Graph Construction from Unstructured Data: A Practical Pipeline. And for how the finished graph powers AI applications, read Neo4j + AI: Building Intelligent Applications.
Further Reading
- Ontology in Graph Databases — Schema and ontology design principles your pipeline should target
- Knowledge Graph Construction from Unstructured Data: A Practical Pipeline — Hands-on implementation with the
neo4j-graphragpackage - Knowledge Graphs as a Context Layer for AI Agents — How to connect constructed graphs to agent workflows
- Neo4j + AI: Building Intelligent Applications — Using the finished graph for AI-powered applications
- Introduction to GraphRAG — Graph-augmented retrieval patterns for LLMs
- Cypher Query Optimisation — Performance-tuning the queries that read from your constructed graph