Knowledge Graph Meetup · Solita

From Data Engineering
to Knowledge Engineering
in 1, 2, 3

A hands-on introduction for data engineers.
Veronika Heimsbakk  ·  Data Treehouse

About

Veronika Heimsbakk

Veronika Heimsbakk

Knowledge Graph Specialist at Data Treehouse

The company behind maplib

A Norwegian startup building high-performance knowledge graph tooling, entirely on open standards.

  • Founded on PhD research and industrial software experience.
  • Rust core, Python API. No Java. No vendor lock-in.
  • Standards: RDF, SPARQL, SHACL, stOTTR, OPC UA, CIM, DEXPI.

Industries: Oil & Gas, Electrical Grids, Energy, Rail, Public Sector, Defence, Aquaculture, Maritime, Finance.

Published benchmarks

Workloadvs.Speedup
SHACL validationJena, RDF4J, TopBraid4–22×
Time-series + graphOntop10–85×
SPARQL load & query11 frameworks (Trainmarks)leading

Peer-reviewed: IEEE Access, Expert Systems & Applications. Trainmarks: datatreehouse.github.io/trainmarks

Selected clients: AkerBP, Elvia, BKK, Bane NOR, DNV, DVLA (UK), EU Agency for Railways, Brønnøysundregistrene, Mattilsynet, Riksantikvaren, Nasjonalarkivet.

Today's promise

A small leap

1

Tools you already know

Python. DataFrames. Pipelines. Templates that look like SQL views.

2

One library: maplib

DataFrame in. Knowledge graph out. Polars-backed. Rust under the hood.

3

Hands-on

Build something tonight. Query, validate, reason on the same Model.

Part 1

Where you
already are

Two roles, similar instincts

What we do, side by side

Data engineer

  • Move data: ETL pipelines, SQL, Python, streams, orchestration.
  • Structure data: warehouses, lakehouses, schemas.
  • Keep it trustworthy: monitoring, SQL assertions, metadata, wikis.

Knowledge engineer

  • Capture human understanding: knowledge acquisition, concept mapping, structured interviews.
  • Formalise it: precise machine-readable structures using the RDF stack.
  • Make it usable for intelligent systems: reasoning, inference, decision support, explainability.
With LLMs in the mix: a data engineer makes sure they're grounded in correct, timely data. A knowledge engineer makes sure they operate within meaningful, explainable semantic boundaries.

What to keep in mind

Two things to know about knowledge graphs

Part 2

Think in
relationships

The same data, two shapes

A row becomes triples

As a DataFrame

shape: (1, 4)
┌───────┬───────────────┬─────────────┬──────┐
│ title │ book_iri      │ isbn        │ year │
╞═══════╪═══════════════╪═════════════╪══════╡
│ Dune  │ book:Dune     │ 97804411... │ 1965 │
└───────┴───────────────┴─────────────┴──────┘

One row. Four columns. Schema lives in your head.

As triples (Turtle)

book:Dune  rdfs:label         "Dune" .
book:Dune  :isbn               "9780441172719" .
book:Dune  :publicationYear    1965 .

Three statements. Subject, predicate, object. Schema lives in the data.

Same facts. Different geometry. Triples connect across sources by shared IRIs. No JOIN required.

The atom is the triple

Subject, predicate, object

  • Subject: what we're talking about. An IRI.
  • Predicate: which relationship. Also an IRI.
  • Object: the value. Another IRI, or a literal.

A knowledge graph is just a lot of these, sharing subjects and objects. Connections emerge from reuse, not from schema.

Part 3

1, 2, 3
with maplib

Step 1 of 3

Install the library

$ pip install maplib
  • Rust core, Python API. Native speed.
  • Polars bundled. DataFrames are first-class.
  • In-memory. No server, no daemon, no Docker.
from maplib import Model
import polars as pl

m = Model()      # empty graph
m.size()         # 0 triples

A Model is one in-memory knowledge graph. That is the whole entry point.

Step 2 of 3

Map your DataFrame to a graph

import polars as pl
from maplib import Model

ns = "http://data.treehouse.example/book/"
df = pl.read_csv("data/books.csv")

# build the subject IRI column
df = df.with_columns(
    (ns + pl.col("title")).alias("book_iri")
)

m = Model()
m.map_default(df, "book_iri")

m.size()   # N triples, ready to query

Four lines that matter:

  • Read the CSV into Polars. Normal stuff.
  • Add an iri column. Your subjects live there.
  • Model() creates the empty graph.
  • map_default turns every other column into a predicate.

Want more control over predicates and types? Write an stOTTR template and use m.map(...) instead.

Step 3 of 3

Query always returns a DataFrame

df = m.query("""
  PREFIX def: <urn:maplib_default:>
  SELECT ?title ?year
  WHERE {
    ?b def:title            ?title ;
       def:publicationYear  ?year .
    FILTER(?year >= 1980)
  }
""")

type(df)  # polars.DataFrame

SPARQL is the SQL of graphs.

  • SELECT returns a Polars DataFrame.
  • CONSTRUCT inserts new facts into the graph.
  • DELETE and INSERT mutate the graph.

Pattern-match across all your data in the Model. No JOINs to write. The predicates do that work.

Part 4

What you get
for free

You're not the first

Reuse ontologies you didn't write

Already standardised, already maintained:

  • SKOS  terms, definitions, hierarchies
  • DCAT  datasets and data catalogs
  • BIBO  bibliographic resources
  • Schema.org  Person, CreativeWork, Movie...
  • FIBO  finance  ·  SNOMED-CT  health
  • CPSV  public services  ·  PROV  provenance
  • CIDOC-CRM  cultural heritage
  • gist  upper ontology, domain-neutral
# Load the ontology into the Model
m.read("ontology.ttl")
# Book rdfs:subClassOf bibo:Book
# Author rdfs:subClassOf schema:Person

# Type your book IRIs as :Book
m.update("""
  PREFIX : <http://data.treehouse.example/>
  PREFIX def: <urn:maplib_default:>
  INSERT { ?s a :Book }
  WHERE  { ?s def:title ?title }
""")

Your data is now connected to a knowledge layer that someone else maintains.

Reusability and interoperability. 💚 Well-defined and curated knowledge has already been modelled in finance, health, culture, and provenance.

The JOIN that wasn't

One graph, four datasets, zero JOINs

# authors, series, books, adaptations -> one Model
m.map_default(df_authors,     "author_iri")
m.map_default(df_series,      "series_iri")
m.map_default(df_books,       "book_iri")
m.map_default(df_adaptations, "adaptation_iri")

df = m.query("""
  PREFIX : <http://data.treehouse.example/>
  SELECT ?author (COUNT(?adaptation) AS ?n)
  WHERE {
    ?adaptation :basedOn ?book .
    ?author     :wrote   ?book .
  } GROUP BY ?author
""")
# Tolkien 4 · Adams 2 · Herbert 2 · ...

Four datasets joined themselves through shared IRIs.

And there's more:

  • Property paths: ?b :nextInSeries+ ?later traverses a series to any depth. One line. No recursive CTE.
  • Federation: SERVICE <http://dbpedia.org/sparql> { ... } reaches into a remote endpoint as if it were local.

Validate the graph

SHACL: declarative constraints

# sh.ttl: every Book must have a title,
# a 13-digit ISBN, a publication year,
# and belong to a Series.
m.read("ttl/sh.ttl")

report = m.validate()

report.results()

Schema, but declarative.

  • Cardinality, datatype, allowed values, regex, value range.
  • Constraints follow the data: same graph, same language.
  • The report is a structured object. results() returns them as a DataFrame.

Note: validate is licensed for commercial use. Free for academic, personal, and evaluation use.

Reason over the graph

Derive new facts, two ways

Datalog rules with m.infer

# If an author wrote a book, then the
# book was written by that author.
[?book, :writtenBy, ?author] :-
    [?author, :wrote, ?book] .

m.infer(rules)

Rules-based. Inverse properties, transitive closures, role propagation. Inferred triples are first-class.

Recursive SPARQL CONSTRUCT with m.infer

m.infer(sparql="""
  CONSTRUCT { ?b a :ModernEraBook }
  WHERE {
    ?b a :Book ;
       :publicationYear ?year .
    FILTER(?year >= 1980)
  }""")

Pattern, filter, materialize, repeat until fixpoint. Each round can match facts the previous round produced.

Note: infer is licensed for commercial use. Free for academic, personal, and evaluation use.

One more thing, built in

chrontext: time-series, virtualized

Your graph models the books, the authors, the series.

Daily sales live in a time-series store. Millions of rows. You don't want them in your graph.

Don't copy them. Reference them.

  • Backends: PostgreSQL, DuckDB, BigQuery, OPC UA.
  • One SPARQL query joins structure with measurements.
  • Built into maplib. No extra dependency.
# Link a time-series source to the graph
m.add_virtualization("config.yaml")

df = m.query("""
  SELECT ?author (SUM(?copies) AS ?total)
  WHERE {
    ?author :wrote ?book .
    ?book :salesSeries ?ts .
    ?ts :reading [ :date ?d ;
                   :copiesSold ?copies ] .
    FILTER(?d >= "2026-01-01"^^xsd:date)
  } GROUP BY ?author
""")
Benchmarked 10 to 85× faster than Ontop for federated time-series queries. Published in Expert Systems & Applications.

Not every problem is a graph

When knowledge graphs earn their keep

Heterogeneous sources

Three systems, three schemas, one concept of customer. The graph is where they finally agree, through shared IRIs, not ETL.

Evolving semantics

The business definition of active subscription changes quarterly. Templates and rules are easier to edit than star schemas.

AI agents need ground truth

LLMs are great at language, bad at facts. A knowledge graph gives an agent deterministic, queryable, auditable answers.

Compliance & interop

Open standards: RDF, OWL, SHACL, SPARQL. Portable across vendors. Auditable by design. Lineage you can query.

If your hardest problem is "fast scans over one big table", stay in Polars. If it is "what does this mean, across everything we have", reach for the graph.

Kiitos

Try it tonight

$ pip install maplib
  • One DataFrame. One IRI column. Three lines of Python.
  • Then: SPARQL, SHACL, Datalog. All on the same Model.
  • No server. No new infrastructure. Just a library.

Where to go next:

1 / 22