Skip to content

REST server mode

create_app wraps any BaseLinker as a REST service, mainly by exposing BaseLinker.link as POST /link/{linker_name}. The deployer constructs whatever linker(s)/EntitySource(s) they need in Python -- however they need to (model paths, device, checkpoints, API keys, ...) -- and registers them by name; there's no generic "build a linker from JSON config" layer, since the ~25 linkers in this library have very different constructors.

"""Offer a linker as a REST service, wrapping BaseLinker.link.

Run with: `uv run python examples/rest_server_example.py`, then e.g.:

    curl -X POST localhost:8000/link/exact-match \\
      -F 'request={"dataset1":[{"id":"s1","labels":["cat"]}],
                    "dataset2_entities":[{"id":"t1","labels":["cat"]},
                                          {"id":"t2","labels":["dog"]}]}'

`app` is also importable directly for production-style deployment, e.g.
`uvicorn examples.rest_server_example:app`.
"""

from linkingtk.algorithms.base import DEFAULT_BLOCKING, BaseLinker
from linkingtk.core.result import AlignmentResult
from linkingtk.server import create_app


class ExactMatchLinker(BaseLinker):
    """Turns ExactMatch's candidate pairs directly into links, for demo purposes."""

    def link(self, dataset1, dataset2, graph=None, blocking=DEFAULT_BLOCKING):
        pairs = blocking.candidate_pairs(dataset1, dataset2)
        return [AlignmentResult(source_id=a.id, target_id=b.id) for a, b in pairs]


# A pre-registered EntitySource can be referenced by name too, instead of
# sending dataset2 inline -- see docs/examples/rest_server.md.
app = create_app(linkers={"exact-match": ExactMatchLinker()})


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=8000)

Run with:

uv run python examples/rest_server_example.py

or, for production-style deployment where uvicorn manages the process:

uvicorn examples.rest_server_example:app

Requires the server extra: uv sync --extra server (or pip install "linkingtk[server]").

Endpoints

  • GET /health -- liveness check.
  • GET /linkers -- names of the registered linkers.
  • GET /sources -- names of the registered entity sources.
  • POST /link/{linker_name} -- runs link(). A multipart/form-data request with:
    • request (required): a JSON object with dataset1 (a list of entities) and exactly one of dataset2_entities (a list of entities, sent inline) or dataset2_source (the name of a pre-registered EntitySource, passed straight through to link() so search-based blocking still works instead of materializing every candidate).
    • graph (optional): an uploaded file with the supporting graph.
    • graph_format (optional, default "triples"): "triples" for a JSON list of [subject, predicate, object] triples, "rdf" for any RDF serialization rdflib can parse (Turtle, N-Triples, RDF/XML, JSON-LD, ...), or "graphml" / "gexf" / "edgelist" for the matching NetworkX file format.

Response: a JSON list of {"source_id", "target_id", "score", "alternatives"} objects, mirroring AlignmentResult.

Targeting an EntitySource by name

Pass a sources dict to create_app alongside linkers to let requests reference a live source (Wikidata, a local vector index, ...) instead of sending dataset2 inline:

app = create_app(
    linkers={"exact-match": ExactMatchLinker()},
    sources={"wikidata": WikidataEntitySource()},
)
curl -X POST localhost:8000/link/exact-match \
  -F 'request={"dataset1":[{"id":"s1","labels":["cat"]}],"dataset2_source":"wikidata"}'

Uploading a graph

curl -X POST localhost:8000/link/my-linker \
  -F 'request={"dataset1":[...],"dataset2_entities":[...]}' \
  -F 'graph_format=rdf' \
  -F 'graph=@my_graph.ttl'

Blocking-strategy selection isn't exposed over REST yet -- each linker always runs with its own default blocking strategy.