Sources¶
Concrete EntitySource wrappers over external targets too large,
or already indexed elsewhere, to enumerate as a list[Entity].
wn ¶
EntitySource wrapper for WordNet via the wn library.
Wraps wn -- a query-driven interface to
locally-installed wordnets (e.g. Open English WordNet) -- as an
EntitySource, so WSD/WSA can target a
full dictionary without materializing every synset as an Entity up
front. wn itself still requires a lexicon to have been downloaded once
(python -m wn download oewn:2021, or wn.download("oewn:2021")) --
this module only avoids loading its senses into memory ahead of time, not
the download step.
WnEntitySource ¶
WnEntitySource(
lexicon: str = "oewn:2021", lang: str | None = None
)
Bases: EntitySource
Queries a locally-installed wn lexicon as an
EntitySource.
See the module docstring for the one-time lexicon download wn
itself requires.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lexicon
|
str
|
A |
'oewn:2021'
|
lang
|
str | None
|
Restrict to a single BCP-47 language code (e.g. |
None
|
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/wn.py
86 87 88 89 90 91 92 93 | |
get ¶
get(entity_id: str) -> Entity | None
Look up a synset by its wn id, or None if it doesn't exist.
Source code in src/linkingtk/sources/wn.py
108 109 110 111 112 113 114 | |
search ¶
search(query: str, top_k: int = 10) -> list[Entity]
Return up to top_k synsets containing query as a lemma.
Ordering follows wn.synsets' own (the lexicon's stored sense
order -- for Open English WordNet, most-frequent-sense first).
Source code in src/linkingtk/sources/wn.py
95 96 97 98 99 100 101 102 103 104 105 106 | |
sensekey_to_synset_id ¶
sensekey_to_synset_id(
sense_key: str, lexicon: str = "omw-en:1.4"
) -> str | None
Resolve a Princeton WordNet sense key (e.g. "group%1:03:00::") to its synset id.
A sense key isn't a wn id by itself -- wn has no lookup-by-sense-key
API, so this instead lists every sense of the key's lemma in lexicon
and matches by each sense's own recorded identifier metadata (the
sense key wn was built from). Also tries sense_key's
adjective/adjective-satellite variant (its part-of-speech digit
swapped between 3 and 5) if the exact one doesn't match --
without it, every satellite-adjective instance in a real corpus
silently fails to resolve. Used by
UfsacDataset, whose corpora
are tagged with WordNet 3.0 sense keys rather than synset ids.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sense_key
|
str
|
A WordNet sense key, e.g. |
required |
lexicon
|
str
|
A |
'omw-en:1.4'
|
Returns:
| Type | Description |
|---|---|
str | None
|
The matching synset's |
str | None
|
carries that sense key. |
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/wn.py
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
synset_id_to_sensekey ¶
synset_id_to_sensekey(
synset_id: str, lemma: str, lexicon: str = "omw-en:1.4"
) -> str | None
The inverse of sensekey_to_synset_id: given a synset id and the lemma it was reached through, return that lemma's sense key on that synset.
A synset can lexicalize several lemmas, each with its own sense key on
that synset -- lemma picks which one, the same way UFSAC/SemEval's
own gold-standard sense keys are always relative to a specific
instance's lemma, not just its synset. Useful for converting this
package's predictions back to sense-key form to cross-check against an
external, sense-key-based scorer (e.g. the Raganato et al. WSD
evaluation framework's own Scorer.java).
Returns lexicon's own stored form as-is, unlike
sensekey_to_synset_id's adjective/adjective-satellite fallback --
there's no target key here to fall back towards. In practice this
is omw-en:1.4's own convention (always digit 5, adjective
satellite), which for adjectives can differ from what a specific
external gold file uses for that word (the official Raganato et al.
gold files use a genuine, per-word mix of both 3 and 5, not one
uniformly) -- a real, known source of mismatched-but-not-wrong
predictions when cross-checking adjective instances against an
external scorer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
synset_id
|
str
|
A |
required |
lemma
|
str
|
The lemma to resolve the sense key for. |
required |
lexicon
|
str
|
A |
'omw-en:1.4'
|
Returns:
| Type | Description |
|---|---|
str | None
|
The matching sense key, or |
str | None
|
|
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/wn.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
synset_id_to_wn30_offset ¶
synset_id_to_wn30_offset(synset_id: str) -> str
Convert an omw-en:1.4 synset id to its WordNet 3.0 byte-offset form.
E.g. "omw-en-02084071-n" -> "wn:02084071n". Pure string
manipulation, not a wn lookup -- confirmed directly that
omw-en:1.4's own synset ids already embed the PWN 3.0 offset
(wn.synsets("dog", pos="n", lexicon="omw-en:1.4")[0].id ==
"omw-en-02084071-n", matching EWISER's own checkpoint dictionary
entry "wn:02084071n"). Used to look up a candidate sense's index in
EwiserEncoder's
checkpoint-loaded output vocabulary.
This does not hold for oewn:* lexicons, whose offsets have
since diverged from PWN 3.0 (confirmed: oewn:2021's id for the same
synset is "oewn-01930264-v" for an unrelated example -- a different
number, not just a different prefix).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
synset_id
|
str
|
An |
required |
Returns:
| Type | Description |
|---|---|
str
|
The WordNet 3.0 offset form, e.g. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/linkingtk/sources/wn.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | |
synset_id_via_ili ¶
synset_id_via_ili(
synset_id: str, lexicon: str, target_lexicon: str
) -> str | None
Map synset_id (a synset id in lexicon) to its counterpart in
target_lexicon, via their shared Interlingual Index (ILI) entry.
Unlike wn30_offset_to_synset_id,
this works across any two lexicons with ILI links, not just ones that
happen to share omw-en:1.4's byte-offset-embedding id convention --
in particular it's what lets
EwiserLinker resolve
candidates from a WnEntitySource lexicon that differs from the one a
checkpoint's SenseVocabulary was built from (e.g. oewn:2025+
candidates against an omw-en:1.4 vocabulary): confirmed directly that
oewn:2025+'s "oewn-00319912-v" and omw-en:1.4's
"omw-en-00319111-v" are the same synset (different byte offsets
-- oewn's have diverged from PWN 3.0, see
synset_id_to_wn30_offset's own docstring -- but the same ILI entry,
"i23324"), so a plain id-string comparison would never find this
match, while this does (see #67).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
synset_id
|
str
|
A synset id in |
required |
lexicon
|
str
|
The |
required |
target_lexicon
|
str
|
The |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
The matching synset id in |
str | None
|
|
str | None
|
|
str | None
|
(any of which are real, unremarkable outcomes -- not every synset |
str | None
|
is cross-lexicon-linked). The first match is returned if more than |
str | None
|
one synset in |
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/wn.py
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | |
wn30_offset_to_synset_id ¶
wn30_offset_to_synset_id(
offset: str, lexicon: str = "omw-en:1.4"
) -> str | None
The inverse of
synset_id_to_wn30_offset:
given a WordNet 3.0 offset (e.g. "wn:02084071n"), return the
matching synset id in lexicon.
Unlike synset_id_to_wn30_offset, this validates existence via a real
wn lookup rather than pure string construction, since offset may
come from an external source (e.g. an EWISER checkpoint's own
dictionary file) that doesn't necessarily resolve in the installed
lexicon.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
offset
|
str
|
A WordNet 3.0 offset, e.g. |
required |
lexicon
|
str
|
A |
'omw-en:1.4'
|
Returns:
| Type | Description |
|---|---|
str | None
|
The matching synset id, or |
str | None
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/wn.py
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | |
wikipedia ¶
EntitySource wrapper for Wikipedia, via the public MediaWiki API.
Wraps the MediaWiki API as a query-driven EntitySource, so EL can target live Wikipedia directly -- without downloading or materializing a dump -- the same way WnEntitySource targets a local WordNet.
WikipediaEntitySource ¶
WikipediaEntitySource(
lang: str = "en", session: Session | None = None
)
Bases: EntitySource
Queries live Wikipedia, via the MediaWiki API, as an EntitySource.
Entity ids are Wikipedia page titles, kept exactly as Wikipedia has
them (including any parenthetical disambiguator, e.g.
"Paris (mythology)") so get can round-trip them. Labels, however,
have that disambiguator stripped (e.g. "Paris") with the topic
moved to the front of the description instead (e.g. "(mythology) ...")
-- see _split_title -- so that a bare-word mention can actually reach
a disambiguated page as a candidate through
ExactMatch's exact-label
blocking.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lang
|
str
|
The Wikipedia language edition to query (e.g. |
'en'
|
session
|
Session | None
|
A |
None
|
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/wikipedia.py
98 99 100 101 102 103 104 105 106 107 108 109 | |
get ¶
get(entity_id: str) -> Entity | None
Look up a Wikipedia page by title, or None if it doesn't exist.
Uses action=query&prop=extracts to fetch the page's intro
paragraph as plain text.
Source code in src/linkingtk/sources/wikipedia.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
search ¶
search(query: str, top_k: int = 10) -> list[Entity]
Full-text search Wikipedia for query, best match first.
Uses action=query&list=search -- each hit's snippet (an
HTML-highlighted excerpt around the matched terms) becomes the
returned entity's description, with the highlighting markup
stripped.
Source code in src/linkingtk/sources/wikipedia.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
wikidata ¶
EntitySource wrapper for Wikidata, via the public action API.
Wraps Wikidata's action API as a
query-driven EntitySource, the same way
WikipediaEntitySource
targets live Wikipedia. Live per-mention Wikidata queries don't scale well
(see linkingtk.sources.vector_index's module docstring) -- pass a
prebuilt VectorIndexEntitySource
as vector_index to search/get entirely locally instead. WikidataDumpEntities
in this module streams Entity objects straight from a downloaded Wikidata
dump to build one.
WikidataDumpEntities ¶
WikidataDumpEntities(
path: str | Path,
lang: str = "en",
limit: int | None = None,
progress: bool = True,
)
Streams Entity objects from an official Wikidata JSON dump.
Wikidata publishes its full dump
(https://www.wikidata.org/wiki/Wikidata:Database_download) as
latest-all.json.gz: a JSON array of entity objects, one compact
object per line (plus bracket-only first/last lines) -- written that
way specifically so it can be streamed line by line rather than parsed
as one huge array, which is exactly what this class does. A truncated
or filtered dump in the same line-delimited shape works too.
Generalizes (reads the dump directly, no intermediate file needed) the
dump-processing step of
wn-wd-entity-align's
own FAISS-index pipeline
(wikidata_faiss/extract_dump_labels.py, which instead expects an
already-filtered, already-sorted N-Triples export).
Re-iterable -- __iter__ reopens (re-requests the URL, or re-opens the
local file) and re-reads from the start every call -- so it can be
passed straight to
VectorIndexEntitySource.build
even with reduced_dim set (which needs two passes over its input); a
plain generator can't do that, since it's exhausted after one pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Path to the dump file, or an |
required |
lang
|
str
|
Which language's label/description/aliases to read -- see
|
'en'
|
limit
|
int | None
|
Stop after yielding this many entities, e.g. for a quick local test against a full multi-hundred-GB dump. |
None
|
progress
|
bool
|
Show a |
True
|
Source code in src/linkingtk/sources/wikidata.py
111 112 113 114 115 116 117 118 119 120 121 | |
WikidataEntitySource ¶
WikidataEntitySource(
lang: str = "en",
session: Session | None = None,
vector_index: VectorIndexEntitySource | None = None,
)
Bases: EntitySource
Queries live Wikidata, via its action API, as an EntitySource.
Entity ids are QIDs (e.g. "Q42"). A P31 ("instance of") claim,
when present, is surfaced into Entity.properties["instance_of"] as
space-joined QIDs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lang
|
str
|
The Wikidata language to request labels/descriptions in
(e.g. |
'en'
|
session
|
Session | None
|
A |
None
|
vector_index
|
VectorIndexEntitySource | None
|
An optional prebuilt local index (see
linkingtk.sources.vector_index) to search/get against
instead of the live API. |
None
|
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/wikidata.py
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | |
get ¶
get(entity_id: str) -> Entity | None
Look up a QID, or None if it doesn't resolve.
Tries vector_index first (if given); falls back to a live
action=wbgetentities call on a miss.
Source code in src/linkingtk/sources/wikidata.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | |
search ¶
search(query: str, top_k: int = 10) -> list[Entity]
Nearest entities to query, best match first.
Delegates to vector_index (no network) if one was given at
construction; otherwise uses action=wbsearchentities.
Source code in src/linkingtk/sources/wikidata.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
vector_index ¶
A local, FAISS-backed EntitySource -- generalized from a project-specific index.
Querying a huge external source (e.g. Wikidata) live, per mention, doesn't
scale: wn-wd-entity-align
hit exactly this and built a one-off FAISS label-similarity index over a bulk
Wikidata label dump to search entirely offline instead. This module
generalizes that approach into a reusable
EntitySource: build a
VectorIndexEntitySource
once, from any Iterable[Entity] (a Wikidata dump, a large WnEntitySource
export, anything), save() it, and search()/get() it later with no
network calls at all -- e.g. as
WikidataEntitySource's
vector_index argument.
Embedder ¶
Bases: Protocol
Structural type for a batch text encoder, e.g.
sentence_transformers.SentenceTransformer's own encode(list[str],
batch_size=...) -> np.ndarray (with its default convert_to_numpy=True)
matches this directly -- no wrapper class needed, the same way
Vectorizer matches
scikit-learn's fit_transform/transform interface.
batch_size must be accepted (even if a concrete Embedder ignores
it): VectorIndexEntitySource.build always passes it explicitly,
since SentenceTransformer.encode otherwise silently re-chunks
whatever's handed to it into its own default sub-batches of 32 for the
actual forward passes -- regardless of how many texts the call itself
received. That single silent default was confirmed (profiling a real
GPU) to cap throughput at ~7k texts/s no matter the caller's batch
size, vs. ~39k texts/s once a sane batch size (512+) is actually
threaded through -- a >5x slowdown, and the real reason a full
Wikidata-dump build was taking over two days (issue #68), not the low
CPU utilization that made it look like a parallelism problem.
VectorIndexEntitySource ¶
VectorIndexEntitySource(
index: Index,
embedder: Embedder,
ids_path: Path,
entities_db_path: Path,
vh: NDArray[floating[Any]] | None,
)
Bases: EntitySource
A local FAISS approximate-nearest-neighbor index over embedded entity text, queried as an EntitySource.
Build once via build
(optionally save/
load to
reuse across processes); search/get afterwards touch no network and
no external service.
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
Source code in src/linkingtk/sources/vector_index.py
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | |
build
classmethod
¶
build(
entities: Iterable[Entity],
embedder: Embedder,
path: Path,
field: Field = "label",
reduced_dim: int | None = 28,
sample_size: int = 100000,
sample_scan_limit: int | None = None,
batch_size: int = 4096,
encode_batch_size: int = 32,
) -> VectorIndexEntitySource
Build a fresh index over entities, persisted under path.
Streams entities rather than materializing it, so it scales to a
source too large to fit in memory at once -- e.g.
WikidataDumpEntities,
reading directly from a downloaded Wikidata dump.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entities
|
Iterable[Entity]
|
The entities to index, embedded on the text(s) |
required |
embedder
|
Embedder
|
A batch text encoder (see |
required |
path
|
Path
|
Directory to write the index bundle to (created if
missing) -- see |
required |
field
|
Field
|
Which entity text to embed: |
'label'
|
reduced_dim
|
int | None
|
If set (the default, 28, matching
|
28
|
sample_size
|
int
|
Max texts sampled (reservoir sampling, so not
biased toward |
100000
|
sample_scan_limit
|
int | None
|
If set, the SVD-fitting pass stops after
reading this many entities, reservoir-sampling
|
None
|
batch_size
|
int
|
Texts buffered per |
4096
|
encode_batch_size
|
int
|
The |
32
|
Raises:
| Type | Description |
|---|---|
OptionalDependencyError
|
If |
TypeError
|
If |
Source code in src/linkingtk/sources/vector_index.py
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 | |
get ¶
get(entity_id: str) -> Entity | None
Look up an indexed entity by id, or None if it isn't in the index.
Source code in src/linkingtk/sources/vector_index.py
473 474 475 476 477 478 | |
load
classmethod
¶
load(
path: Path, embedder: Embedder
) -> VectorIndexEntitySource
Reload an index bundle written by build/save from path.
embedder must be re-supplied (it isn't serialized) and should be
the same one -- or one producing embeddings in the same space --
used to build the index.
Source code in src/linkingtk/sources/vector_index.py
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 | |
save ¶
save(path: Path) -> None
Write this index's bundle to path (a directory, created if missing).
Writes index.faiss (the FAISS index itself), vh.bin (the SVD
projection matrix, only if one was used), ids.txt (+ a cached
ids.txt.offsets.npy, the entity id of each index row, one per line
in row order -- an id repeats once per label it was indexed under),
entities.db (a dbm store, entity id -> JSON), and meta.json.
Source code in src/linkingtk/sources/vector_index.py
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 | |
search ¶
search(query: str, top_k: int = 10) -> list[Entity]
Nearest entities to query by embedding cosine similarity, best match first.
Source code in src/linkingtk/sources/vector_index.py
433 434 435 | |
search_batch ¶
search_batch(
queries: list[str], top_k: int = 10
) -> list[list[Entity]]
Real batched search: one FAISS query for all of queries at once.
Several index rows can map to the same entity (one per label), so
rows are deduplicated by entity -- each ranked by its best-matching
row -- and the search is retried with a doubled row count until
every query has top_k distinct entities or the index is exhausted.
Source code in src/linkingtk/sources/vector_index.py
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 | |