Adding facts to a knowledge base

Keep provenance, conflicts, NIL records, and decisions from checks visible

systems
knowledge base population
entity linking
Learn how knowledge base population links mentions, adds typed facts, and records why the pipeline excludes some candidates.

Grace receives a week of short announcements about Riverton training programs. Some contain the same facts. Two give different deadlines for the same program. Another uses the name Riverton, which could mean two different places in the reference table.

Knowledge base population links document mentions to entities and adds typed facts to a knowledge base. Provenance records each fact’s sources and derivation. A knowledge base structures claims, sources, conflicts, and decisions from checks. It does not establish truth by itself.

A NIL record represents a mention with no matching entity in the reference table. A provisional ID preserves that mention without assuming an existing identity.

Note

The Riverton records in this lesson are fictional teaching examples about training programs, places, and organisations.

TipWhat you will learn

By the end of this lesson, you will be able to:

  • distinguish entity linking from fact insertion
  • declare single-valued and list-valued slots before merging
  • keep document dates, evidence text, and offsets for every fact
  • merge duplicate facts without losing provenance
  • keep conflicting dated values instead of overwriting them
  • route negated, hedged, ambiguous, NIL, and type-failed candidates to human checks.

Load the Riverton reference list

The existing knowledge base starts with entity IDs and aliases. The lesson reads local CSV files with fixed column types. It computes each file’s SHA-256 fingerprint, which identifies the file contents. It compares that fingerprint with the recorded value from file creation. A mismatch stops processing before the code adds any fact.

library(readr)
library(dplyr)
library(tibble)
library(tidyr)
library(purrr)
library(stringr)
library(digest)

entities <- read_csv(
  "data/riverton/riverton-entities.csv",
  na = character(),
  col_types = cols(
    entity_id = col_character(),
    canonical_name = col_character(),
    entity_type = col_character(),
    description = col_character()
  )
) |>
  select(entity_id, canonical_name, entity_type)

aliases <- read_csv(
  "data/riverton/riverton-aliases.csv",
  na = character(),
  col_types = cols(
    entity_id = col_character(),
    alias = col_character()
  )
) |>
  left_join(entities, by = "entity_id")

metadata <- read_csv(
  "data/riverton/riverton-reference-metadata.csv",
  na = character(),
  col_types = cols(
    artifact = col_character(),
    description = col_character(),
    source = col_character(),
    license = col_character(),
    created_on = col_character(),
    fingerprint = col_character()
  )
)

entity_hash <- digest(
  paste(read_lines("data/riverton/riverton-entities.csv"), collapse = "\n"),
  algo = "sha256",
  serialize = FALSE
)
alias_hash <- digest(
  paste(read_lines("data/riverton/riverton-aliases.csv"), collapse = "\n"),
  algo = "sha256",
  serialize = FALSE
)
Existing Riverton entity IDs used by the population step
Entity ID Canonical name Type
ORG-0001 Riverton Workforce Lab organisation
ORG-0002 Riverton Skills Centre organisation
ORG-0003 Marrow County Transit organisation
LOC-0001 Riverton place
LOC-0002 Riverton place
LOC-0003 Bellhaven place
CRD-0001 Forklift Operator Licence credential
CRD-0002 Data Support Certificate credential
Reference files compared with their recorded fingerprints before processing
Artifact Rows Computed SHA-256 Recorded SHA-256 Match
riverton-entities.csv 8 df260574f1f6088b51735a4778646ef22e5338186f74db321a96802d9033fe57 df260574f1f6088b51735a4778646ef22e5338186f74db321a96802d9033fe57 TRUE
riverton-aliases.csv 17 0a419b6a8a742ffab404b8c58c209eaa90b0247a0ee30dbbfc85a6e914a85c64 0a419b6a8a742ffab404b8c58c209eaa90b0247a0ee30dbbfc85a6e914a85c64 TRUE

The table prints names and types only. It omits the forklift credential’s description because this lesson concerns provenance. That description is not evidence for a job-board claim.

Declare slot rules before merging

A slot is a field with a specified value type. Some slots can contain a list. Others are single-valued for a named time period, so a second dated value becomes a conflict to examine. Cardinality specifies how many values a slot can contain.

slot_schema <- tribble(
  ~slot, ~subject_type, ~object_type, ~cardinality, ~merge_rule,
  "offers", "organisation", "credential", "list-valued",
  "One fact per subject-object pair. Additional evidence adds provenance.",
  "requires", "organisation", "credential", "list-valued",
  "One fact per subject-object pair. Negated or hedged statements do not become facts.",
  "application_deadline", "organisation", "date literal", "single-valued",
  "Keep each dated value. Mark the conflict. Let a person resolve the conflict."
)
Slot cardinality and merge rules declared before population
Slot Subject type Object type Cardinality Merge rule
offers organisation credential list-valued One fact per subject-object pair. Additional evidence adds provenance.
requires organisation credential list-valued One fact per subject-object pair. Negated or hedged statements do not become facts.
application_deadline organisation date literal single-valued Keep each dated value. Mark the conflict. Let a person resolve the conflict.

The slot schema defines duplicates and conflicts. Without those rules, a pipeline might replace a deadline and omit the value recorded on 4 September. It might also incorrectly reject two offerings as a conflict.

Prepare dated announcements

The documents below are constructed announcements. They include one duplicate, one single-valued conflict, one negated statement, and one hedged statement that expresses uncertainty. They also include an ambiguous Riverton mention, one NIL organisation, one undeclared slot, and one visible type-check failure.

extractor_version <- "riverton-kbp-rules-1.0"

documents <- tribble(
  ~document_id, ~document_date, ~text,
  "A001", as.Date("2026-09-01"), "Riverton Skills Centre offers the Data Support Certificate.",
  "A002", as.Date("2026-09-03"), "The Skills Centre offers DATA SUPPORT CERTIFICATE.",
  "A003", as.Date("2026-09-04"), "Riverton Skills Centre application deadline is October 15.",
  "A004", as.Date("2026-09-06"), "Riverton Skills Centre application deadline is November 1.",
  "A005", as.Date("2026-09-07"), "Marrow County Transit does not require the Forklift Operator Licence.",
  "A006", as.Date("2026-09-08"), "Riverton will host the Data Support Certificate.",
  "A007", as.Date("2026-09-09"), "Riverton Transit offers forklift certification.",
  "A008", as.Date("2026-09-10"), "Bellhaven offers the Data Support Certificate.",
  "A009", as.Date("2026-09-11"), "Riverton Skills Centre may offer the Forklift Operator Licence.",
  "A010", as.Date("2026-09-12"), "Riverton Skills Centre promotes the Data Support Certificate."
)

candidate_plan <- tribble(
  ~candidate_id, ~document_id, ~slot, ~subject_mention, ~object_mention, ~status, ~expected_action,
  "CAND-001", "A001", "offers", "Riverton Skills Centre", "Data Support Certificate", "asserted", "add fact",
  "CAND-002", "A002", "offers", "Skills Centre", "DATA SUPPORT CERTIFICATE", "asserted", "merge duplicate",
  "CAND-003", "A003", "application_deadline", "Riverton Skills Centre", "October 15", "asserted", "add fact",
  "CAND-004", "A004", "application_deadline", "Riverton Skills Centre", "November 1", "asserted", "flag conflict",
  "CAND-005", "A005", "requires", "Marrow County Transit", "Forklift Operator Licence", "negated", "reject negated",
  "CAND-006", "A006", "offers", "Riverton", "Data Support Certificate", "asserted", "hold ambiguous",
  "CAND-007", "A007", "offers", "Riverton Transit", "forklift certification", "asserted", "hold NIL",
  "CAND-008", "A008", "offers", "Bellhaven", "Data Support Certificate", "asserted", "reject type",
  "CAND-009", "A009", "offers", "Riverton Skills Centre", "Forklift Operator Licence", "hedged", "hold hedged",
  "CAND-010", "A010", "promotes", "Riverton Skills Centre", "Data Support Certificate", "asserted", "reject undeclared slot"
)
Constructed dated announcements for knowledge base population
Document ID Document date Evidence text
A001 2026-09-01 Riverton Skills Centre offers the Data Support Certificate.
A002 2026-09-03 The Skills Centre offers DATA SUPPORT CERTIFICATE.
A003 2026-09-04 Riverton Skills Centre application deadline is October 15.
A004 2026-09-06 Riverton Skills Centre application deadline is November 1.
A005 2026-09-07 Marrow County Transit does not require the Forklift Operator Licence.
A006 2026-09-08 Riverton will host the Data Support Certificate.
A007 2026-09-09 Riverton Transit offers forklift certification.
A008 2026-09-10 Bellhaven offers the Data Support Certificate.
A009 2026-09-11 Riverton Skills Centre may offer the Forklift Operator Licence.
A010 2026-09-12 Riverton Skills Centre promotes the Data Support Certificate.
Same-author expected actions written before pipeline execution
Candidate ID Document ID Slot Subject mention Object mention Status Expected action
CAND-001 A001 offers Riverton Skills Centre Data Support Certificate asserted add fact
CAND-002 A002 offers Skills Centre DATA SUPPORT CERTIFICATE asserted merge duplicate
CAND-003 A003 application_deadline Riverton Skills Centre October 15 asserted add fact
CAND-004 A004 application_deadline Riverton Skills Centre November 1 asserted flag conflict
CAND-005 A005 requires Marrow County Transit Forklift Operator Licence negated reject negated
CAND-006 A006 offers Riverton Data Support Certificate asserted hold ambiguous
CAND-007 A007 offers Riverton Transit forklift certification asserted hold NIL
CAND-008 A008 offers Bellhaven Data Support Certificate asserted reject type
CAND-009 A009 offers Riverton Skills Centre Forklift Operator Licence hedged hold hedged
CAND-010 A010 promotes Riverton Skills Centre Data Support Certificate asserted reject undeclared slot

These expected actions are not a benchmark. The author wrote both the examples and the teaching pipeline’s checklist. The author also supplied candidates manually, in place of output from an extractor such as lesson 69’s relation extractor. The code links their mentions, examines types and status, and applies population rules.

Apply slot, status, and type decisions

The action table is the evaluation for this lesson. It shows whether the pipeline made the expected decision for each candidate. It is not precision and recall.

Show the slot and type checks
schema_checks <- candidates |>
  left_join(slot_schema, by = "slot") |>
  mutate(
    declared_slot = !is.na(subject_type.y),
    subject_type_ok = declared_slot & subject_type.x == subject_type.y,
    object_type_ok = case_when(
      !declared_slot ~ FALSE,
      object_type.y == "date literal" ~ str_detect(object_mention, "^[A-Z][a-z]+ [0-9]+$"),
      TRUE ~ object_type.x == object_type.y
    ),
    type_check = subject_type_ok & object_type_ok,
    fact_key = paste(slot, subject_id, object_value, sep = "|")
  )

first_seen <- schema_checks |>
  filter(status == "asserted", subject_link_status == "linked", type_check) |>
  arrange(document_date, candidate_id) |>
  distinct(fact_key, .keep_all = TRUE) |>
  transmute(first_candidate_id = candidate_id, fact_key)

single_value_seen <- schema_checks |>
  filter(status == "asserted", subject_link_status == "linked", type_check) |>
  filter(cardinality == "single-valued") |>
  count(slot, subject_id, name = "single_value_count")

first_single_seen <- schema_checks |>
  filter(status == "asserted", subject_link_status == "linked", type_check) |>
  filter(cardinality == "single-valued") |>
  arrange(document_date, candidate_id) |>
  group_by(slot, subject_id) |>
  slice_head(n = 1) |>
  ungroup() |>
  transmute(slot, subject_id, first_single_candidate_id = candidate_id)

candidate_actions <- schema_checks |>
  left_join(first_seen, by = "fact_key") |>
  left_join(single_value_seen, by = c("slot", "subject_id")) |>
  left_join(first_single_seen, by = c("slot", "subject_id")) |>
  mutate(
    single_value_count = coalesce(single_value_count, 0L),
    actual_action = case_when(
      status == "negated" ~ "reject negated",
      status == "hedged" ~ "hold hedged",
      subject_link_status == "ambiguous" ~ "hold ambiguous",
      subject_link_status == "NIL" ~ "hold NIL",
      !declared_slot ~ "reject undeclared slot",
      object_link_status == "NIL" | is.na(type_check) ~ "hold NIL",
      !type_check ~ "reject type",
      candidate_id != first_candidate_id ~ "merge duplicate",
      cardinality == "single-valued" &
        single_value_count > 1L &
        candidate_id != first_single_candidate_id ~ "flag conflict",
      TRUE ~ "add fact"
    ),
    action_matches = actual_action == expected_action,
    action_reason = case_when(
      status == "negated" ~ "negated statements never become facts",
      status == "hedged" ~ "hedged statements require human checks",
      subject_link_status == "ambiguous" ~ "alias maps to more than one ID",
      subject_link_status == "NIL" ~ "new name receives a provisional ID for human checks",
      !declared_slot ~ "slot is not declared in the schema",
      object_link_status == "NIL" | is.na(type_check) ~ "object mention is not linked",
      !type_check ~ "slot type check failed",
      candidate_id != first_candidate_id ~ "same fact already exists. Keep provenance.",
      cardinality == "single-valued" &
        single_value_count > 1L &
        candidate_id != first_single_candidate_id ~ "single-valued slot has dated conflict",
      TRUE ~ "asserted, linked, and type-compatible"
    )
  )
Expected-versus-actual action table for candidate facts
Candidate ID Expected action Pipeline action Match Reason
CAND-001 add fact add fact TRUE asserted, linked, and type-compatible
CAND-002 merge duplicate merge duplicate TRUE same fact already exists. Keep provenance.
CAND-003 add fact add fact TRUE asserted, linked, and type-compatible
CAND-004 flag conflict flag conflict TRUE single-valued slot has dated conflict
CAND-005 reject negated reject negated TRUE negated statements never become facts
CAND-006 hold ambiguous hold ambiguous TRUE alias maps to more than one ID
CAND-007 hold NIL hold NIL TRUE new name receives a provisional ID for human checks
CAND-008 reject type reject type TRUE slot type check failed
CAND-009 hold hedged hold hedged TRUE hedged statements require human checks
CAND-010 reject undeclared slot reject undeclared slot TRUE slot is not declared in the schema

The type failure is visible: Bellhaven is a place ID, not an organisation. The pipeline rejects the negated requirement row. It neither stores a negative fact nor deletes an existing fact.

Build facts and provenance

The final fact table keeps one fact for the duplicate offer and two dated deadline values for the conflict. Provenance stays in a separate table so one fact can point to several source records.

accepted_candidates <- candidate_actions |>
  filter(actual_action %in% c("add fact", "merge duplicate", "flag conflict"))

fact_keys <- accepted_candidates |>
  distinct(slot, subject_id, object_value, fact_key) |>
  arrange(slot, subject_id, object_value) |>
  mutate(fact_id = sprintf("FACT-%03d", row_number()))

populated_facts <- accepted_candidates |>
  inner_join(fact_keys, by = c("slot", "subject_id", "object_value", "fact_key")) |>
  group_by(fact_id, slot, subject_id, object_value) |>
  summarise(
    provenance_records = n(),
    conflict_flag = first(cardinality) == "single-valued" &
      n_distinct(candidate_actions$object_value[
        candidate_actions$slot == first(slot) &
          candidate_actions$subject_id == first(subject_id) &
          candidate_actions$actual_action %in% c("add fact", "flag conflict")
      ]) > 1L,
    .groups = "drop"
  ) |>
  arrange(fact_id)

fact_provenance <- accepted_candidates |>
  inner_join(fact_keys, by = c("slot", "subject_id", "object_value", "fact_key")) |>
  transmute(
    fact_id,
    candidate_id,
    document_id,
    document_date,
    evidence_text = text,
    subject_start,
    subject_end,
    object_start,
    object_end,
    candidate_source = "hand-supplied candidate",
    pipeline_rules_version = extractor_version
  ) |>
  arrange(fact_id, document_id)

review_queue <- candidate_actions |>
  filter(!actual_action %in% c("add fact", "merge duplicate", "flag conflict")) |>
  transmute(
    candidate_id,
    document_id,
    subject_mention,
    object_mention,
    provisional_or_candidate_id = subject_id,
    review_reason = action_reason
  )
Populated facts after duplicate and conflict handling
Fact ID Slot Subject ID Object value Provenance records Conflict flagged
FACT-001 application_deadline ORG-0002 November 1 1 TRUE
FACT-002 application_deadline ORG-0002 October 15 1 TRUE
FACT-003 offers ORG-0002 CRD-0002 2 FALSE
Provenance records for every populated fact
Fact ID Candidate ID Document ID Document date Evidence text Subject start Subject end Object start Object end Candidate source Pipeline rules version
FACT-001 CAND-004 A004 2026-09-06 Riverton Skills Centre application deadline is November 1. 1 22 48 57 hand-supplied candidate riverton-kbp-rules-1.0
FACT-002 CAND-003 A003 2026-09-04 Riverton Skills Centre application deadline is October 15. 1 22 48 57 hand-supplied candidate riverton-kbp-rules-1.0
FACT-003 CAND-001 A001 2026-09-01 Riverton Skills Centre offers the Data Support Certificate. 1 22 35 58 hand-supplied candidate riverton-kbp-rules-1.0
FACT-003 CAND-002 A002 2026-09-03 The Skills Centre offers DATA SUPPORT CERTIFICATE. 5 17 26 49 hand-supplied candidate riverton-kbp-rules-1.0
Candidates excluded from the fact table for human checks
Candidate ID Document ID Subject mention Object mention Provisional or candidate ID Reason for human checks
CAND-005 A005 Marrow County Transit Forklift Operator Licence ORG-0003 negated statements never become facts
CAND-006 A006 Riverton Data Support Certificate LOC-0001; LOC-0002 alias maps to more than one ID
CAND-007 A007 Riverton Transit forklift certification NIL-0001 new name receives a provisional ID for human checks
CAND-008 A008 Bellhaven Data Support Certificate LOC-0003 slot type check failed
CAND-009 A009 Riverton Skills Centre Forklift Operator Licence ORG-0002 hedged statements require human checks
CAND-010 A010 Riverton Skills Centre Data Support Certificate ORG-0002 slot is not declared in the schema

One offer fact has two provenance records. Both deadline facts remain and require human checks. A later document might contain an update, an error, or information about a different session. After applying the check rules, the pipeline adds 3 populated facts. The table records the conflict but does not resolve it.

Rendering creates the FACT- display IDs. A real knowledge base keeps fact IDs stable after publication.

Prepare the graph facts used in the visualization lesson

Lesson 81 redraws a small graph from the same IDs. The table below is a compact edge view of the populated facts. The two Riverton place IDs stay separate because entity IDs, not labels, are the keys.

graph_facts <- populated_facts |>
  left_join(
    fact_provenance |>
      group_by(fact_id) |>
      summarise(document_ids = paste(document_id, collapse = "; "), .groups = "drop"),
    by = "fact_id"
  ) |>
  transmute(
    subject_id,
    relation = slot,
    object_value,
    document_ids,
    source = "populated fact"
  )
Fact edges rebuilt in the graph visualization lesson
Subject ID Relation Object value Document IDs Source
ORG-0002 application_deadline November 1 A004 populated fact
ORG-0002 application_deadline October 15 A003 populated fact
ORG-0002 offers CRD-0002 A001; A002 populated fact

These graph edges are still KB records, not proof about the real world. The source column names the constructed population step. Document IDs refer to the provenance table above. Both Riverton place IDs remain in the entity table. Their shared label is a naming note for the visualization lesson, not a factual relation.

What the knowledge base does not know

A missing edge does not establish that a relation is false. Under this open-world approach, the KB records only claims accepted under its current policy. Records may be incomplete or outdated. A later document may contradict them. Provenance helps readers examine claims, but it does not certify truth.

What to remember

  • Knowledge base population combines linking and slot filling.
  • Declare slot cardinality before merging facts.
  • Duplicate facts can share one fact ID with several provenance records.
  • Single-valued conflicts keep both dated values until a human examines them.
  • Negated and hedged statements do not become facts.
  • Riverton stays ambiguous, and a NIL organisation receives a provisional ID.

Grace’s resulting knowledge base contains fewer automatic facts than candidate statements. The population step records additions, candidates requiring human checks, and the reasons for each decision.

Sources