Linking named entities

Link mentions to stable identifiers

entity enrichment
entity linking
workforce research
Learn how alias tables generate entity candidates and why resolution and NIL handling remain separate decisions.

A notice says forklift certification, while a reference list says Forklift Operator Licence. The coordinator needs to know whether those names refer to the same credential before counting credentials.

Named entity linking connects a mention in text to a stable identifier in a reference list. The identifier matters because spellings can change while the record stays the same.

Note

The Riverton organisations, places, and credentials here are invented for the lesson.

TipWhat you will learn

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

  • explain why an entity identifier matters.
  • join mention strings to an alias table.
  • count mentions with zero, one, or several candidates.
  • show how one credential can have multiple aliases.
  • state the limit of alias matching.

Load entities and aliases

Three CSV files provide the reference list, aliases, and sentence examples. The chunk compares the alias-file fingerprint, a value used to detect text changes, with its expected value. dplyr and tibble create joins and tables.

An entity is an item in the reference list, such as an organisation, place, or credential. The canonical name is the reference list’s main name for that entity. An alias is an alternative name that may appear in text.

library(readr)
library(dplyr)
library(tibble)
library(digest)

sentences <- read_csv(
  "data/workforce/workforce_sentences.csv",
  na = character(),
  col_types = cols(
    sentence_id = col_character(),
    document_id = col_character(),
    source_line = col_character(),
    text = col_character(),
    reference_label = col_character(),
    uncertainty = col_character(),
    annotator_id = col_character(),
    rationale = col_character(),
    codebook_version = col_character(),
    codebook_hash = col_character(),
    derived = col_character(),
    transformation = col_character()
  )
)

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()
  )
)

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

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()
  )
)

alias_hash <- digest(
  paste(read_lines("data/riverton/riverton-aliases.csv"), collapse = "\n"),
  algo = "sha256",
  serialize = FALSE
)
expected_alias_hash <- metadata |>
  filter(artifact == "riverton-aliases.csv") |>
  pull(fingerprint)
Invented Riverton entity reference list
Entity ID Canonical name Type Description
ORG-0001 Riverton Workforce Lab organisation Fictional research group that labels workforce text
ORG-0002 Riverton Skills Centre organisation Fictional training provider in Riverton
ORG-0003 Marrow County Transit organisation Fictional public transport operator
LOC-0001 Riverton place Fictional city in Marrow County, Calder
LOC-0002 Riverton place Fictional town in Tidewater, unrelated to LOC-0001
LOC-0003 Bellhaven place Fictional city in Marrow County
CRD-0001 Forklift Operator Licence credential Fictional certificate named in the job board
CRD-0002 Data Support Certificate credential Fictional certificate named in the training flyer

The alias table deliberately contains two rows for Riverton to create an ambiguous case. It also contains the exact strings forklift certification and Forklift Operator Licence. A singleton candidate means that only one record matches. The singleton results below reflect this small teaching table, not general linking accuracy.

Riverton Skills Centre is an organisation in this entity list. In the geocoding lesson, the same name is a building. The difference is a normal entity-linking problem: one name can point to an institution, a site, or both.

Prepare mention candidates

Two mention candidates come from the Riverton sentences. Three constructed checks cover credential matches and missing matches.

real_mentions <- bind_rows(
  sentences |>
    filter(sentence_id == "s023") |>
    transmute(mention_id = sentence_id, source = "Riverton text", text, mention = "Riverton"),
  sentences |>
    filter(sentence_id == "s024") |>
    transmute(mention_id = sentence_id, source = "Riverton text", text, mention = "DATA SUPPORT CERTIFICATE")
)

mention_candidates <- bind_rows(
  real_mentions,
  tibble(
    mention_id = c("link-03", "link-04", "link-05"),
    source = "constructed check",
    text = c(
      "Applicants ask about forklift certification.",
      "The posting names a Forklift Operator Licence.",
      "Customer service badge"
    ),
    mention = c(
      "forklift certification",
      "Forklift Operator Licence",
      "Customer service badge"
    )
  )
)
Mention candidates for alias matching
Mention ID Source Text Mention
s023 Riverton text RIVERTON SKILLS OPEN HOUSE Riverton
s024 Riverton text DATA SUPPORT CERTIFICATE DATA SUPPORT CERTIFICATE
link-03 constructed check Applicants ask about forklift certification. forklift certification
link-04 constructed check The posting names a Forklift Operator Licence. Forklift Operator Licence
link-05 constructed check Customer service badge Customer service badge

A mention candidate is a span of text the team wants to link. This lesson starts with hand-selected mentions so the linking step remains visible.

Generate candidates from normalized aliases

Names in real datasets can use different capitalization. The code converts mentions and aliases to lowercase before the join. The join generates candidates but does not prove a link. Each mention can have zero, one, or several candidates.

A separate resolution policy decides whether to accept a candidate or return NIL, meaning no accepted reference entity.

library(stringr)
normalized_mentions <- mention_candidates |>
  mutate(normalized_mention = str_to_lower(mention))

normalized_aliases <- aliases |>
  mutate(normalized_alias = str_to_lower(alias)) |>
  distinct(entity_id, normalized_alias)

linked_rows <- normalized_mentions |>
  left_join(
    normalized_aliases,
    by = join_by(normalized_mention == normalized_alias),
    relationship = "many-to-many"
  ) |>
  left_join(entities, by = join_by(entity_id))

link_status <- linked_rows |>
  summarise(
    candidate_ids = paste(unique(na.omit(entity_id)), collapse = ", "),
    candidates = n_distinct(na.omit(entity_id)),
    .by = c(mention_id, source, text, mention)
  ) |>
  mutate(
    candidate_ids = if_else(candidate_ids == "", "none", candidate_ids),
    outcome = case_when(
      candidates == 0L ~ "no candidate",
      candidates == 1L ~ "one candidate",
      TRUE ~ "multiple candidates"
    )
  )

resolved_links <- link_status |>
  mutate(
    resolved_entity_id = if_else(
      candidates == 1L,
      candidate_ids,
      NA_character_
    ),
    resolution = case_when(
      candidates == 0L ~ "NIL",
      candidates == 1L ~ "accepted singleton candidate",
      TRUE ~ "needs disambiguation"
    )
  )

outcome_counts <- tibble(
  outcome = c("one candidate", "multiple candidates", "no candidate")
) |>
  left_join(
    link_status |>
      count(outcome, name = "mentions"),
    by = join_by(outcome)
  ) |>
  mutate(mentions = coalesce(mentions, 0L))
Alias matching outcomes
Mention ID Source Text Mention Candidate IDs Candidates Candidate outcome
s023 Riverton text RIVERTON SKILLS OPEN HOUSE Riverton LOC-0001, LOC-0002 2 multiple candidates
s024 Riverton text DATA SUPPORT CERTIFICATE DATA SUPPORT CERTIFICATE CRD-0002 1 one candidate
link-03 constructed check Applicants ask about forklift certification. forklift certification CRD-0001 1 one candidate
link-04 constructed check The posting names a Forklift Operator Licence. Forklift Operator Licence CRD-0001 1 one candidate
link-05 constructed check Customer service badge Customer service badge none 0 no candidate
Candidate-generation outcomes
Outcome Mentions
one candidate 3
multiple candidates 1
no candidate 1

The relationship argument tells dplyr that one mention may match several alias rows. DATA SUPPORT CERTIFICATE produces one candidate. This teaching policy accepts singleton candidates, but a production linker would still apply a score or NIL threshold. Riverton maps to two place records, so the alias alone cannot choose one. Customer service badge has no candidate.

The 3/1/1 counts describe only candidate generation for these five mentions.

Count by identifier

Two different aliases can point to the same credential identifier. That is the reason the identifier matters.

credential_mentions <- resolved_links |>
  filter(resolution == "accepted singleton candidate") |>
  rename(entity_id = resolved_entity_id) |>
  left_join(entities, by = join_by(entity_id)) |>
  filter(entity_type == "credential") |>
  count(entity_id, canonical_name, name = "mentions") |>
  arrange(entity_id)
Credential mentions counted by stable identifier
Entity ID Canonical name Mentions
CRD-0001 Forklift Operator Licence 2
CRD-0002 Data Support Certificate 1

The two forklift aliases count under CRD-0001. Without that identifier, the team would have to decide later whether the two spellings named the same credential.

Real-world linking pipelines

The normalized join here shows candidate generation from an alias table. Production entity linking systems add a separate resolution step:

  • Unicode and case normalization policy: State the accent and capitalization rules before the join. NFC/NFD normalization specifies consistent Unicode representations for characters such as accented letters. Apply the same rules to mentions and aliases. Do not report linking rates from an unnormalized exact-string join.
  • Candidate generation and ranking: An ambiguous alias, such as Riverton, can match several entities. Production systems rank those candidates with the surrounding sentence context.
  • NIL handling: A matching alias can refer to a different entity outside the knowledge base, the reference collection of entities. The system needs a NIL result rather than an incorrect link.
  • KB versioning: An entity’s canonical name or status may change. Linking results must record the specific knowledge-base (KB) version.
  • Alias and candidate-recall checks: Evaluation must measure coverage of common misspellings in the alias table, called alias recall. It must also measure whether candidate generation retains the correct entity, called candidate recall.

What to remember

  • Entity linking connects a mention to a stable identifier.
  • An alias table can link different spellings to one record.
  • Count ambiguous cases caused by duplicate aliases.
  • Alias matching finds only names that the table contains.

The two forklift spellings count as one credential in these examples. The table does not identify the intended Riverton. The badge name remains unlinked because the alias file does not contain that name.

Sources