Skip to content

Fully-offline Entity Linking via a local vector index

Live per-mention queries against a huge external source (Wikidata, Wikipedia, ...) don't scale -- the live Wikidata example is fine for a handful of mentions, but not for linking a whole corpus. This example instead builds a VectorIndexEntitySource once, from a small in-memory batch of entities (standing in for a bulk-downloaded label dump -- the same role wn-wd-entity-align's FAISS index plays over a full Wikidata export), save()s it to disk, load()s it back (as a separate process would), and wires the reloaded index into WikidataEntitySource(vector_index=...) -- so search/get never touch the network at all.

A real large-scale build follows the exact same build/save/load API, just streamed from many more entities -- e.g. WikidataDumpEntities, which reads a downloaded Wikidata dump directly, in place of _ITEMS in the script below. See Entity Sources for that variant.

Requires the vector-index optional dependency (faiss-cpu + sentence-transformers; the latter downloads a small model the first time it runs):

uv pip install linkingtk[vector-index]
"""Fully-offline Entity Linking via a local vector index.

Live per-mention queries against a huge external source (Wikidata,
Wikipedia, ...) don't scale -- `wikidata_el.py`'s live
[`WikidataEntitySource`](../reference/sources.md) is fine for a handful of
mentions, but not for linking a whole corpus. This example instead builds a
[`VectorIndexEntitySource`](../reference/sources.md) once, from a small
in-memory batch of entities (standing in for a bulk-downloaded label dump --
the same role `wn-wd-entity-align`'s FAISS index plays over a full Wikidata
export, https://github.com/jmccrae/wn-wd-entity-align), `save()`s it to
disk, `load()`s it back (as a separate process would), and wires the
reloaded index into `WikidataEntitySource(vector_index=...)` -- so `search`/
`get` never touch the network at all.

A real large-scale build follows the exact same `build`/`save`/`load` API,
just streamed from many more entities -- e.g.
[`WikidataDumpEntities`](../reference/sources.md), which reads a downloaded
Wikidata dump directly, in place of `_ITEMS` below.

Requires the `vector-index` optional dependency (`faiss-cpu` +
`sentence-transformers`; the latter downloads a small model the first time
it runs):

    uv pip install linkingtk[vector-index]

Run with: `uv run python examples/vector_index_el.py`
"""

import tempfile
from pathlib import Path

from sentence_transformers import SentenceTransformer

from linkingtk.algorithms.string_similarity import StringSimilarityLinker
from linkingtk.blocking import ExactMatch
from linkingtk.core import Entity
from linkingtk.sources import VectorIndexEntitySource, WikidataEntitySource

_ITEMS = [
    Entity(id="Q90", labels=["Paris"], description="capital and most populous city of France"),
    Entity(id="Q830149", labels=["Paris"], description="city in Lamar County, Texas, USA"),
    Entity(id="Q18331346", labels=["Paris"], description="a family name"),
    Entity(id="Q162121", labels=["Paris"], description="genus of flowering plants"),
]


def main() -> None:
    embedder = SentenceTransformer("all-MiniLM-L6-v2")

    with tempfile.TemporaryDirectory() as tmp_dir:
        index_path = Path(tmp_dir) / "wikidata_paris_index"
        VectorIndexEntitySource.build(_ITEMS, embedder, index_path, reduced_dim=8)

        # Reload as a separate process would -- no network, no re-embedding.
        index = VectorIndexEntitySource.load(index_path, embedder)
        items = WikidataEntitySource(vector_index=index)

        mentions = [
            Entity(
                id="m1",
                labels=["Paris"],
                context="capital and most populous city of France",
            ),
        ]
        linker = StringSimilarityLinker(
            source_field="context", target_field="description", metric="word_overlap"
        )
        results = linker.link(mentions, items, blocking=ExactMatch())
        for result in results:
            print(f"{result.source_id} -> {result.target_id} (score={result.score})")
            print(f"  alternatives: {result.alternatives}")

        target = items.get(results[0].target_id)
        assert target is not None
        print(f"  description: {target.description}")


if __name__ == "__main__":
    main()

Run with:

uv run python examples/vector_index_el.py
m1 -> Q90 (score=5.0)
  alternatives: ['Q830149', 'Q162121', 'Q18331346']
  description: capital and most populous city of France