Entity Linking and Knowledge Graph Construction › Building the Graph

Michael BrenndoerferMay 12, 20256 min read
Language AI HandbookAll chapters
Part 8 of 67Semantics and Information Extraction
Chapter 54Entity Linking and Knowledge Graph Construction › Building the Graph
Reading section 14 of 17 · Building the Graph · 6 minSection 14 of 17 · 6 min

Section 14 of 17 · 6 min read

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

Notes & comments

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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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]
CollapseExpand
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)
Out[35]:
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]
CollapseExpand
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!

Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy