Entity Linking and Knowledge Graph Construction › Building the Graph
Language AI HandbookAll chapters Entity Linking and Knowledge Graph Construction › Building the GraphSection view · Sections in this chapter14 / 17 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
We now start from hand-written relation-extraction candidates with linked arguments. The toy KB omits two objects, so we explicitly label those arguments NIL and assign document-scoped local nodes. This is a construction policy for this fixture; a failed link in general would first require separating uncertainty from confirmed KB absence.
PythonIn [30]Expand
from hashlib import sha256
SOURCE_TEXTS = {
"kg1": "Mercury orbits the Sun.",
"kg2": "Hubble and Mercury appear together in this passage.",
"kg3": "Amman is the capital of Jordan.",
"kg4": "Apple designs the iPhone.",
"kg5": "Michael Jordan played for the Chicago Bulls.",
"kg6": "Mercury was used in thermometers.",
}
FACTS = [
{
"sid": "s1",
"doc": "kg1",
"subject": "mercury_planet",
"relation": "orbits",
"object": "sun",
"object_nil": True,
"confidence": 0.92,
},
{
"sid": "s2",
"doc": "kg2",
"subject": "hubble",
"relation": "co_mentioned_with",
"object": "mercury_planet",
"object_nil": False,
"confidence": 0.88,
},
{
"sid": "s3",
"doc": "kg3",
"subject": "amman",
"relation": "is_capital_of",
"object": "jordan_country",
"object_nil": False,
"confidence": 0.95,
},
{
"sid": "s4",
"doc": "kg4",
"subject": "apple_inc",
"relation": "designs",
"object": "iphone",
"object_nil": False,
"confidence": 0.90,
},
{
"sid": "s5",
"doc": "kg5",
"subject": "jordan_person",
"relation": "member_of",
"object": "chicago_bulls",
"object_nil": False,
"confidence": 0.71,
},
{
"sid": "s6",
"doc": "kg6",
"subject": "mercury_element",
"relation": "used_in",
"object": "thermometer",
"object_nil": True,
"confidence": 0.64,
},
]
for fact in FACTS:
source_text = SOURCE_TEXTS[fact["doc"]]
fact["span"] = (0, len(source_text))
fact["source_sha256"] = sha256(source_text.encode("utf-8")).hexdigest()
fact["extractor_version"] = "hand-written-fixture-v1"The confidence values are illustrative scores. The second source sentence puts the names Hubble and Mercury together. The candidate supplies the planet ID as a linking hypothesis. The sentence leaves Mercury’s identity open and makes no claim that Hubble observed it. Each half-open span covers its entire retained source sentence using Python string indices, which count Unicode code points. The hash identifies the UTF-8 encoding of that sentence, and the extractor field records that these are hand-written fixtures. An extraction timestamp would be a separate field in a production record. Retaining SOURCE_TEXTS makes the offsets auditable; a hash alone would not recover the text.
The following store uses ordinary Python collections. It implements a small assertion catalog and source grouping; it does not implement RDF, SPARQL, OWL inference, or a production graph database. The earlier RDF example describes an interoperable representation to which a real system could export these records.
PythonIn [31]Expand
statement_store = []
nodes = set(ENTITIES)
by_source = defaultdict(list)Each assertion has its own statement ID. Repeated triples can therefore retain separate source records and confidence values instead of overwriting one another. For reproducible inspection, interpret offsets against the retained source version.
PythonIn [32]Expand
for fact in FACTS:
object_id = (
f"local:{fact['doc']}:{fact['object']}"
if fact["object_nil"]
else fact["object"]
)
nodes.add(object_id)
statement = {**fact, "object": object_id, "source": fact["doc"]}
statement_store.append(statement)
by_source[fact["doc"]].append(fact["sid"])ConsoleOut [33]Expand
statements: 6 source groups: 6 nodes: 13
The statement store contains one record per candidate assertion, plus a separate index of statement IDs by source. The object_nil flag triggers local node creation. For records with that flag, the same raw label in the same document reuses the same node ID. That keying rule leaves cross-document resolution open and does not verify that repeated labels refer to the same entity.
Now filter the records by their illustrative confidence scores. This is a review policy, not a truth test. We keep the lower-scoring records in the store and select a subset for display.
PythonIn [34]Expand
provenance_rows = [
{
key: statement[key]
for key in ["subject", "relation", "object", "confidence", "source"]
}
for statement in statement_store
if statement["confidence"] >= 0.85
]
provenance_rows.sort(key=lambda row: row["confidence"], reverse=True)
provenance_df = pd.DataFrame(provenance_rows)| subject | relation | object | confidence | source | |
|---|---|---|---|---|---|
| 0 | amman | is_capital_of | jordan_country | 0.95 | kg3 |
| 1 | mercury_planet | orbits | local:kg1:sun | 0.92 | kg1 |
| 2 | apple_inc | designs | iphone | 0.90 | kg4 |
| 3 | hubble | co_mentioned_with | mercury_planet | 0.88 | kg2 |
The filter selects four of the six records. The membership candidate at 0.71 and the thermometer candidate at 0.64 remain available for review. Their source sentences describe past relationships. These two records carry no validity dates, so the catalog leaves their present validity open. A deployment would choose its threshold from labeled validation data and the costs of false accepted facts and missed facts; the numerical value 0.85 has no calibrated meaning here.
The next example filters a separate set of records by their recorded year bounds. The championship records mark event years; the membership records use coarse inclusive year windows. Event time and the validity of an ongoing relation are different meanings, even when both are encoded using lower and upper bounds. A stored championship win does not become historically false after its event year. The time_role field keeps that distinction visible in this fixture.
PythonIn [36]Expand
TEMPORAL_FACTS = [
{
"subject": "chicago_bulls",
"relation": "has_championship",
"object": "1991_title",
"from_year": 1991,
"through_year": 1991,
"time_role": "event_year",
"rank": "normal",
},
{
"subject": "chicago_bulls",
"relation": "has_championship",
"object": "1996_title",
"from_year": 1996,
"through_year": 1996,
"time_role": "event_year",
"rank": "normal",
},
{
"subject": "jordan_person",
"relation": "member_of",
"object": "chicago_bulls",
"from_year": 1984,
"through_year": 1993,
"time_role": "membership_years",
"rank": "preferred",
},
{
"subject": "jordan_person",
"relation": "member_of",
"object": "chicago_bulls",
"from_year": 1995,
"through_year": 1998,
"time_role": "membership_years",
"rank": "preferred",
},
]
def records_for_year(year):
return [
f for f in TEMPORAL_FACTS if f["from_year"] <= year <= f["through_year"]
]
for year in [1990, 1991, 1994, 1996, 1997]:
active = records_for_year(year)
print(
f"{year}: "
+ (
", ".join(
f"{f['subject']} {f['relation']} {f['object']}" for f in active
)
or "no matching records"
)
)records_for_year selects records whose inclusive bounds contain the requested year. It returns the 1991 championship event for 1991 and the 1996 event for 1996, together with the membership records whose windows cover those years. It returns no records for 1994. This illustrates record selection, not a complete historical roster or a truth test. Membership-year windows do not specify day-level membership dates. Applications needing that precision must retain the applicable dates and an explicit policy for unknown bounds.
This assertion store does not resolve new nodes across sources: sun and thermometer each occur as an unknown object in one candidate. A larger store could retrieve candidate duplicate records, compare their evidence, and decide whether to merge them or map them to an updated KB. Record the evidence and version of a merge decision so that it can be inspected or reversed later.
Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!