Skip to main content
Extraction reads your documents and pulls out entities and relationships. The two lists every remember() takes, entity_types and relationship_types, advise khora which entities and relationships to extract. This flat list can work for simple content, but says nothing about how to describe a type, when two mentions are the same entity, or what to infer. ExpertiseConfig is the next level up: a complete, reusable domain ontology that tells Khora not only which types to extract but how to describe them, how to recognise the same entity across sources, what new edges to infer, how confident to be, and what prompt to extract with.
The sections below build a reusable ExpertiseConfig (call it ontology) and pass it via expertise= on the same call. The next section shows the whole define-then-use in one block.

What an ontology bundles

One ExpertiseConfig carries all of this, most of it optional, with sensible defaults:

A minimal ontology in Python

The three building blocks (ExpertiseConfig, EntityTypeConfig, RelationshipTypeConfig) are importable straight from khora:
entity_types and relationship_types are required on every remember() even when you pass expertise=. The expertise object doesn’t replace them. Pass ontology.get_entity_type_names() / get_relationship_type_names() so the lists stay in sync with the ontology. (The field is name=, not type=.)

Entity types: attributes, identifiers, aliases

An EntityTypeConfig is more than a label. Three fields shape extraction and matching:
  • attributes: {required: [...], optional: [...]}, a soft schema that nudges the LLM to pull the fields you care about.
  • identifiers: the attributes that identify the same entity across documents (e.g. email for a person, repo_url for a service). These drive deduplication.
  • aliases: alternative type labels, so a model that emits COMPANY still lands on your ORGANIZATION type.

Relationship types

source_types / target_types constrain which entity types an edge may connect (["*"] means any). bidirectional: true marks symmetric edges like COLLABORATES_WITH.
A relationship type’s source_types / target_types must reference entity type names that exist in the same ontology (or *). An edge that points at an undeclared type is dropped.

Cross-source entity unification (correlation rules)

Correlation rules merge the same real-world entity seen through different sources, the canonical “the Slack @ada and the Gmail ada@acme.com are one person” problem. A rule matches on match_fields (or a regex pattern), scoped to entity_types:
Tie a rule to stable identifiers (email, url, id) at high confidence (0.85–0.95). Match on names only at lower confidence (0.7–0.8), since names collide.

Inferring new edges (inference rules)

Inference rules derive relationships that were never written down, from ones that were. Each rule is a when → then: a list of conditions to match, and the edge to create. The matcher walks chains of relationships and supports three linking shapes: then.source / then.target pick which matched entities form the new edge, by ordinal (first.source, second.target, …):
Inference only runs when an ontology with inference_rules is loaded and expansion.relationship_inference is on with a non-none inference_mode. A plain remember() with no expertise does no inference. The inferrer logs “No expertise or inference rules configured, skipping inference” and creates nothing. Keep 2–4 rules per ontology. Broad rules can explode the edge count.

Confidence thresholds

The ontology carries its own thresholds, and the extractor drops anything below them, a per-ontology precision knob:

Expansion behavior

expansion controls the optional expansion phase of ingestion:

Authoring in YAML and loading it

For anything beyond a couple of types, YAML is the natural home. It keeps the whole ontology in one reviewable file. Load it with ExpertiseLoader:
A complete ontology pulling the pieces together:
Ingesting one document (“Ada (ada@acme.com) and Bob are engineers on the Payments team. The Payments team owns the billing-api service.”) with this ontology extracts PERSON/TEAM/SERVICE entities and the MEMBER_OF / OWNS edges, then infers Ada COLLABORATES_WITH Bob and Ada/Bob CONTRIBUTES_TO billing-api.

Composing ontologies with extends

Ontologies are versioned and inherit. A config can extends one or more parents. The loader resolves the chain and merges: entity/relationship types add-or-override by name, rules combine (later wins on a name clash), prompts/confidence/expansion are overridden by the child:
System prompts are Jinja2 templates rendered against the ontology, so a child can wrap its parent’s prompt with {{ parent_prompt }} and iterate the merged {{ entity_types }}. Loading hiring above yields the types PERSON, ORGANIZATION, CANDIDATE and a prompt that embeds the parent’s text plus the live type list.

The built-in type hierarchy

Inference and relationship rules match through a built-in subtype map, so a rule written for a general type also fires on specific ones: EMPLOYEE and EXTERNAL_PERSON satisfy a rule expecting PERSON. COMPANY / DEPARTMENT / TEAM satisfy ORGANIZATION, and CALL satisfies EVENT. You can write rules against broad types and still match a richly-typed graph.

Engine notes

VectorCypher supports ontology-driven typed extraction via the entity_types / relationship_types kwargs, and runs expansion, so it honours correlation_rules, inference_rules, and the expansion block.

Ingestion

Where the ontology plugs in: the three-phase write path.

Workloads example

A runnable ExpertiseConfig in the resume-search walkthrough.

API reference

ExpertiseConfig and the ontology dataclasses.

Retrieval

The read path that queries what extraction produced.