Mova0.6 pre-release⌕
☰
home / docs / wordgraph — the word graph

wordgraph — the word graph

The Elephant's graph (docs/animals.md). A dense, bounded association graph over word ids: which subjects a verb takes, what is eaten, what a dog is, which adjectives go with a noun. It is built by passes over parsed text (Wikipedia tier 1, earlier parses in CoNLL-U), keeps only the strongest neighbours of every word under every relation, and is read in place from one file, so many threads of one process can share it.

It sits beside kg, not inside it. kg stores facts: one simple sentence becomes a fact vertex with its members, provenance per sentence, u64 heap ids, Ukrainian grammar. The word graph stores no sentence and no fact: it stores counted, scored word–word associations over the en dictionary's lemma numbers. In the terms of Worlds and knowledge it is an important-graph projection (operational, in RAM, prepared indexes), rebuilt from passes whose provenance it keeps; it is not the global graph's record of claims.

Model

Nodes. A node is (word, UPOS). A word is a lemma number of the built-in en dictionary (en::dict::lemma, append-only, so numbers are stable) or, for a lemma outside the dictionary, OOV_TAG | r with r the line of an append-only registry (oov-registry.tsv next to the graph). Only content words take part: NOUN, PROPN, VERB, ADJ, ADV. A lemma is lowercased unless it is a PROPN. An out-of-dictionary node is kept only if it occurs in at least min_oov (20) counted events.

Edges. Typed by relation, from the UD head to the dependent:

relationfromexample
nsubj, nsubj:pass, obj, iobjverb (or other predicate)nsubj(bark, dog), obj(eat, apple)
obl:<prep>, nmod:<prep>headobl:in(live, city), nmod:of(capital, country)
agent (obl:agent), poss (nmod:poss)headagent(elect, voter), poss(owner, dog)
amod, compound, advmod, xcomp, conjheadamod(dog, small)
isacopular subject → nominal predicateisa(dog, animal) from «a dog is an animal»
propcopular subject → adjectival predicateprop(snow, white)

A negated predicate gives no nsubj, obj, isa or prop. Conjuncts of a core argument inherit its relation, and a conjoined verb without a subject inherits the first verb's subject (one level of the enhanced-UD propagation). The rules are in src/extract.rs; its tests are small trees.

Scores. Over the merged counts c(h,r,d) with marginals c(h,r,·), c(·,r,d) and N_r: PMI = log2(c·N_r / (c(h,r,·)·c(·,r,d))) and LMI = c·PMI. A pair needs c ≥ min_count (3) and PMI > 0.

Keep only the important. Per (head, relation) the top k (32) dependents by LMI are kept (forward lists), and per (dependent, relation) the top k heads (reverse lists). Why LMI for the choice: raw PMI ranks rare pairs first (a pair seen 3 times between two rare words beats «dog barks»); G² (Dunning 1993) ranks by evidence, and its observed-cell term is 2·ln2·LMI, so LMI ranks like G² on the frequent pairs that matter for hints at the cost of one multiplication (the local-MI weighting of Evert 2005 and Baroni & Lenci 2010's Distributional Memory, which is the same typed-tuple design). PMI is stored as the edge weight, because it is scale-free: plausibility queries compare it across words of different frequency. Bound: edges ≤ 2·k·(number of (node, relation) groups).

Quantized weights. PMI in 1/16 bit (u8), count as log2 in 1/8 steps (u8). A forward or reverse edge costs 6 bytes (target u32, PMI u8, count u8) plus its share of the group table.

Passes and build

A pass reads one source into parts: one part per input unit (a tier-1 shard of 500 articles, or a CoNLL-U file), each a zstd stream of sorted unique (key, count) records with its own sorted OOV table, plus a sidecar with counts and the part's SHA-256. meta.json keeps the provenance: source, inputs, the parse models seen in the records, the dictionary fingerprint, the date and, for a control, the shuffle seed. A pass resumes: a part whose files exist is kept.

The build merges all parts of all passes with a streaming k-way merge (the monotone OOV rank map keeps the merged stream sorted without re-sorting), twice: first for the marginals, then for the candidates. Then it selects the top k both ways, assigns registry ids to the kept OOV words and writes the file. Merging is commutative: the same set of passes, in any order, with the same parameters and registry, gives the same bytes.

Passes are additive. Two passes over the same text count it twice; that is the caller's choice (and recorded).

Exceptions: the list of the wrong and the white list

Parse errors that the counts repeat are fixed by hand, through two lists in this crate: seeds/wrong.tsv (the list of the wrong) and seeds/right.tsv (the white list). Columns: head lemma, relation, dependent lemma, kind, note, source phrase. Both are hints, never bans, so a joke or a figurative reading can still use a listed edge.

kindeffect
compound_reading (wrong)where the phrase stands as consecutive words ending at an end of the edge («eating disorder», «hot dog», «swimming pool»), the extractor does not count the edge and counts it as suppressed (suppressed:<phrase> in the part); elsewhere the edge is counted and penalised
parse_error, figurative (wrong)counted; queries return the edge with its plausibility × 0.25 and its note
correct (white)a listed pair without an edge answers 4 bits

A graph carries the text of the lists it was built with, and answers with them (Graph::judged); the writer hint (plausibility_isa, rank), the role score and the absurdity check use the judged value, and plausibility keeps the counted one. The seed holds the two operator examples, 22 gerund compounds that the 50 k graph shows as verb + object (swimming pool, living room, operating system, running mate …), one parse error («dogs are prone to …») and one figurative line («time is money»); wordgraph lists check --graph G shows every entry against a graph and wordgraph lists add wrong|right FILE … appends a checked line (then rebuild the crate and the graph). The gerund compounds are one systematic parser error (a VBG modifier read as a verb with an object); the list names its frequent cases, the parser should learn the pattern.

Role swaps: the nonsense matrix

For every clause with two or more arguments in different roles (nsubj, nsubj:pass, obj, iobj, obl:<prep>, agent), the pass also counts each pair of arguments with their roles traded: «the dog chases the cat» adds ~nsubj(chase, cat) and ~obj(chase, dog). Transpositions give the same per-(word, role) totals as all permutations, up to a constant per clause size. The swaps are relation codes of their own (SWAP | code, printed ~nsubj), counted in the same parts; no sentence is kept. Swaps are defined on dependency roles, not on positions, so the same code serves languages with other word order; the part records its language.

The build compares each swap count s(h, ~r, d) with the real count c(h, r, d) of the same pair and keeps the swap edge when s ≥ min_count and contrast = log2((s+1)/(c+1)) ≥ 1 bit, ranked by s·contrast: swap edges are kept only where they do not coincide with real use. Graph::role_score(h, r, d) is the judged plausibility minus that contrast, minus half of how much better the part-of-speech swap reads, for nsubj/obj («the bark dogs» against «the dog barks»: the dependent as the verb, the head as the noun; looked up in the real lists, nothing extra stored). Like the hand lists, it is a hint: a joke can still say «the bark dogs».

Parse scorer. score::tree_score sums, over a candidate tree's typed pairs, the role score for core relations and the judged plausibility for the others (prepositional attachment, modifiers). wordgraph rerank GRAPH MODEL DEV adds λ · tree_score to the parser's score of its k best trees; λ is chosen on one half of the sentences (by parity) and applied to the other, so no number is chosen where it is reported. The test set is not used.

Graph format 2

The file keeps, per list, the number of candidates it had before pruning and the LMI of its last kept edge (sections 22–25), and the hand lists (26–27). Graph::absence(h, r, d) says why a pair has no edge: NotCandidate (certain: a list that kept all its candidates cannot miss a candidate; so seen fewer than min_count times, with PMI ≤ 0, with an end outside the kept vocabulary, or never), MaybePruned { below_lmi } (both lists were full), Unknown (a format-1 file). Format-1 files still open; all earlier methods are unchanged.

The whole of Wikipedia

peta/tools/wikigraph (internal) is the Elephant's pass over the whole English Wikipedia dump: streams read in place through wikicorpus's reader, articles of namespace 0 that are neither redirects nor disambiguation pages, cleaned as the Wikipedia school reads them, parsed with en.bin in memory, extracted and counted. Only count parts and their provenance are written: no text, tree, CoNLL-U or cache. A unit grows in chunks of 64 streams until it holds 20 M events or 1 000 streams, so the memory stays bounded on the dump's oldest, longest articles; a part is named by its stream range (dump-AAAAAA-BBBBBB) and the pass resumes at the first range not done. A STOP file stops it between chunks. Tier 1 is counted again here with the current model and one cleaning, and the whole-Wikipedia graph is built from this pass alone, so no article is counted twice. The shuffled control of a finished pass is made from its parts (wordgraph control), without parsing again. Before the build, wordgraph compact tamps the parts into fewer, larger ones (each open part holds a zstd window during the merge); the build over the compacted pass gives the same lists.

Pass v5 (current). wikigraph v5 reads the articles in page-view order (a year of user views from wikicorpus's graph/articles.tsv, most viewed first, ties by page id, disambiguation pages out; 6 786 209 articles), so every checkpoint covers the most viewed articles first. Each article is read from the dump by stream offset and annotated as wikicorpus annotates (text, sentences, en trees, frames, pragmatics). A unit writes its count part (events from the record's own CoNLL-U) and its treestore files (trees/rank-A-B.w.mts, two writer threads, level 19 with the context coder, store model trained on 250 articles of the pass's own output); the store files are complete before the part's done mark. Past the first million ranks, windows of a million ranks are read in stream order, so one stream decode serves about four articles; inside a window the view order is not kept. A budget guard stops the pass when the store reaches 250 GB or the disk's free space falls under 300 GB. Earlier stream-order passes (v3, v4) were stopped and are not part of the graph: no article is counted twice. runs/post-pass.sh (outside the repo) builds and measures a graph from the parts at any time; a watcher runs it at 10, 30, 50, 70 and 90 % of the order and, tamped, at the end.

Prospective predictions for v5 (2026-11-06, before its first checkpoint). 230–360 M sentences; 240–390 CPU-hours of annotation (about 3.4 times the trees-only pass: frames and pragmatics); with 10 annotation threads on a box at load about 12, 1.1–1.8 days; store about 25 GB; count parts about 8 GB; peak memory under 3 GB.

The Elephant (docs/animals.md) builds the graph, spins sentences' roles into the absurdity graph (the role swaps), and tamps the knowledge and absurdity graphs.

File and API

One file, sections as flat little-endian arrays, 8-aligned (layout table in src/graph.rs): nodes sorted by (word, UPOS); relation codes; forward and reverse CSR (node → relation groups → ranked edges); OOV words (by id and by string); provenance JSON. No pointer, no deserialization: Graph::open maps the file (memmap2), checks the section table and the CSR offsets once (O(N + G)), and every query reads the bytes in place. Graph is Clone + Send + Sync (an Arc over the map), so threads of one process — the Owls, later — share one copy; reads need no lock.

let g = wordgraph::Graph::open(path)?;
let bark = g.node("bark", Some(UPos::VERB)).unwrap();
let subj = wordgraph::rel::parse("nsubj").unwrap();
g.edges(bark, subj, Dir::Fwd);                 // typical subjects, best first
g.plausibility(bark, subj, dog);               // PMI bits, 0 = no evidence
g.plausibility_isa(bark, subj, poodle);        // with the isa back-off: (score, Via)
g.rank(bark, subj, &[dog, tree, bank]);        // the writer's hint: candidates for a missing slot

Commands

cargo build --release -p wordgraph
G=$MOVA_DATA/graph
wordgraph pass --out $G/passes/wiki-t1-00-10 --name wiki-t1-00-10 --wiki $MOVA_DATA/corpus/wikipedia-en/tier1/shards --shards 0..10 --jobs 6
wordgraph pass --out $G/passes/ctl-shuffle --name ctl-shuffle --wiki … --shards 0..10 --shuffle 1   # negative control
wordgraph pass --out $G/passes/school --name school --conllu /path/to/all.conllu
wordgraph build --out $G/wiki-5k.wg $G/passes/wiki-t1-00-10
wordgraph stats $G/wiki-5k.wg
wordgraph query $G/wiki-5k.wg subjects bark      # also: objects, isa, modifiers, does, show, near, edge, hint
wordgraph query $G/wiki-5k.wg hint bark nsubj dog,tree,bank
wordgraph absurdity $G/wiki-5k.wg global/data/absurdity-roles.tsv
wordgraph rerank $G/wiki-all.wg $MOVA_DATA/en/models/en.bin $EN_UD_DIR/UD_English-EWT/en_ewt-ud-dev.conllu --k 8
wordgraph control --in $G/passes/wiki-all-v3 --out $G/passes/wiki-all-v3-shuffled --name wiki-all-v3-shuffled --seed 1
wordgraph lists check --graph $G/wiki-all.wg

Heavy runs go through tools/jobs (jobs run --mem-gb 4 …).

Checks

Prospective predictions (written 2026-11-06, before the first real run)

Prospective predictions for the whole of Wikipedia (written 2026-11-06, before its first build)

Measurements (2026-11-06)

Source: Wikipedia tier 1 (enwiki-20260901, trees by en.bin:31a93401fe7135b1), shards 0–9 (5 000 articles) and 0–99 (50 000), extraction rules version 2, k = 32, k_core = 128, min_count = 3, min_oov = 20. A shared box (load 10–20 on 20 cores), 6 threads, every run under tools/jobs with a 4 GB limit.

5 000 articles50 000 articlespredicted (5 k / 50 k)
sentences1 952 51712 087 0292.2 M / —
events18.0 M103.1 M25 M / —
pass: wall, peak RAM6 s, 0.59 GB35 s, 0.57 GB30–120 s / 5–15 min, < 1.5 GB
pass on disk53 MB337 MB—
merged distinct triples—35.1 M—
candidates (c ≥ 3, PMI > 0)—4.44 M—
build: wall, peak RAM11 s, 0.23 GB58 s, 1.25 GB< 2 min, < 2 GB / < 4 GB
nodes (outside the dictionary)46 184 (19 825 words)135 115 (86 699 words)100 k / 300 k
forward + reverse edges0.59 M + 0.66 M2.07 M + 2.83 M2–4 M / 8–15 M
file10.0 MB38.0 MB20–50 MB / 80–200 MB

The predictions overestimated everything: fewer events per token than assumed (only content–content arcs count), and the LMI/PMI/min-count filter keeps 4.4 M of 35 M triples. A query (open the map, look up, print) takes 7 ms of wall time. 50 k nodes by UPOS: PROPN 73 231, NOUN 40 211, ADJ 12 153, VERB 7 520, ADV 2 000; 259 relation codes.

Determinism. Two builds of the 50 k pass: byte-identical (SHA-256 20bf205c…). Parts of shards 0–2 rebuilt in a fresh directory with 2 and with 3 threads: byte-identical to the 6-thread pass.

Samples (50 k). Subjects of «elect»: Assembly, voter, member, council, constituency, Congress, Parliament, assembly, parliament, ward. Subjects of «bark»: dog (PMI 10.3, n ≈ 10), Samantha. «parse»: no node (the verb is not in 50 000 encyclopedic articles with a subject seen 3 times). Objects of «eat»: food, meat, disorder (a parse error: «eating disorder»), fruit, meal, fish, flesh, egg. Is-a of «dog»: animal, species, mammal, sausage («hot dog»), pet, threat, breed. Verbs with subject «dog»: go, have, eat, bark, bite. Hint «bark nsubj tree,dog,bank,river» → dog (direct), the rest without evidence; «eat obj stone,apple,idea,bread» → bread, apple (direct), stone 0.75 through isa material, idea none.

Absurdity matrix (global/data/absurdity-roles.tsv, real-world scale, 243 346 verb–noun pairs; covered = both words are nodes). AUC: normal (0) against absurd (3–4), ties half. Evidence: share of pairs with an edge.

graphcolumncoveredAUCAUC with isa back-offSpearman ρevidence at score 0 / 4
5 ksubject228 0680.5330.5520.1567.4 % / 0.52 %
50 ksubject235 7500.5660.6150.21816.1 % / 1.89 %
50 k shuffled controlsubject230 5990.5210.5190.08711.9 % / 6.41 %
5 kobject228 0680.5810.6000.22517.3 % / 0.85 %
50 kobject235 7500.6460.6840.29332.7 % / 3.13 %
50 k shuffled controlobject230 5990.5440.5630.12222.7 % / 10.4 %

The direction is right and grows with data: an edge is 8.5 times (subject) and 10.5 times (object) likelier for a normal pair than for an absurd one, against 1.9 and 2.2 in the control. The control is not at 0.5: it keeps every marginal, so frequent words still meet by chance, and normal pairs are made of more frequent words. It is the frequency-only baseline; the association signal is the difference. The AUC stays low because most normal pairs have no edge at all (84 % of the subject ones): absence is weak evidence. The prediction (AUC 0.65–0.80 for subjects) missed. k_core = 128 was chosen after seeing this table at k = 32 (subject AUC 0.537 / 0.581 with back-off, object 0.592 / 0.639, extraction rules version 1), so the numbers above are not independent of that choice.

A pass over the Wikipedia school treebank (all.conllu, 127 096 sentences, 1.08 M events, 0.9 s) shows the CoNLL-U source; its articles overlap tier 1, so it is not merged into the graphs above.

Absence (2026-11-06, 50 k graph rebuilt in format 2 from the same passes). Among normal subject pairs (score 0) without an edge, 33 315 were not candidates at all and 6 510 possibly pruned; for objects 20 381 and 5 714. The missing evidence is mostly missing counts, not the bound k.

k-best reranking (2026-11-06, 50 k graph, rules v2 without swaps). EWT dev 2.18 (2 001 sentences, 25 148 words), en.bin 2483516a, k = 8, λ grid 0–100, chosen on the other fold: λ = 10 in both folds. LAS 0.7974 → 0.8003 (fold 0) and 0.8120 → 0.8143 (fold 1); overall 0.8047 → 0.8073 (+0.26 points); 98 sentences change; the 8-best oracle is 0.8536. Two runs give identical output.

French

A pass, a part and a graph have a language (rel::Lang: en, fr); the relations and the role swaps are the same, defined on dependency roles (UD, nivre-2020, demarneffe-2021). Per language:

The French pass. peta/tools/wikigraph-fr (internal) reads the French Wikipedia dump (frwiki-20261001) in the in-link order of its article graph (wikicorpus scan --lang fr, wikicorpus graph: 2 635 316 ranked articles; there is no French page-view file, so the score is the inbound links alone), cleans the frequent French templates, splits sentences and tokens in GSD's conventions, parses with Mova's own parser trained on UD French GSD (fr.bin), gives lemmas and UPOS from the GSD lexicon (Mova has no French morphology: lemma by (form, tag), by form, else an edit script by tag and suffix as in Morfette), and writes count parts rank-A-B (language fr) and treestore files of the records (text, sentence rows, CoNLL-U). Measured 2026-11-06 on UD French GSD dev:

value
parser, LAS (own tags / gold tags; French subtypes read as their base, as en parse-eval reads them)87.09 / 89.78
parser, LAS with the French subtypes (obl:mod, aux:tense, expl:pass … count as errors)78.80
UAS (own tags)91.12
tokenizer (from # text), word F197.87 (P 98.35, R 97.39)
lemma accuracy on gold tokens (known forms / unseen forms / content words)97.68 (98.42 / 87.91 / 96.16)
UPOS accuracy (known / unseen)97.03 (97.56 / 90.03)

On 1 000 articles evenly spaced over the order: 27 688 sentences, 214 632 events, 5.8 MB of store (from 50.3 MB of JSON); on the top 1 000 articles: 330 110 sentences, 3.67 M events, 88 MB of store; about 3.4–3.5 CPU-ms per sentence including the store, 0.6–1.25 GB. Projection for the whole pass: about 73 M sentences, 0.57 G events, a store of 15–20 GB, parts under 3 GB, about 70 CPU-hours (12 h with six free cores, about 2.3 days at the 1.3 cores a nice-15 job got on the box at load 30–40). A 300-article range with 6 and 3 threads: part and store files byte-identical. A graph of the top 1 000 articles: «guerre» is modified by mondial, second, civil, froid; «roi» is the subject of mourir, acheter, accorder, construire; «Paris» is a ville, a capitale.

Tamping papers

tamp/ (elephant-tamp) is the Elephant's tamping step over papers: statements that many papers say alike are collapsed into shared approach nodes (WordNet synonyms confirmed by this graph, anchor salience from Graph::freq), and each paper keeps its important statements with references to the nodes.

Whole-dump passes (2026-11-06). Trees-only, in stream order (v3/v4, 8 threads): 6 000–7 000 sentences/s; the dump's first streams hold its oldest, longest articles (70 articles and 14 000 sentences a stream against about 27 and 900 on average), so fixed 1 000-stream units passed 4 GB (stopped by jobs) and units are bounded by 20 M events. Checkpoint at 9 536 streams (v3: 438 k articles, 37.8 M sentences, 391 M events; build 128 s, 1.07 GB, 119 MB): subject AUC 0.584 (isa back-off 0.639, role score 0.623), object AUC 0.672 (0.707); normal subject pairs with an edge 21 % (50 k: 16 %); absent normal subject pairs that were not even candidates 63 % (50 k: 84 %). v3 stopped (plural compound phrases), v4 stopped at 31 392 streams for v5. v5 sample (250 view-ordered articles): 0.196 s per article on one thread in the tail (0.030 s the random stream read), 1.55 s in the top 50 000; store 3.6 KB and 18.2 KB per article. First v5 unit (ranks 0–6 144): 1.75 M sentences, 20.8 M events, 180 MB of store, 4 746 s on a box at load 30–60 (the pass at nice 15 got 3–5 cores), peak 2.1 GB. Its graph alone: EWT dev LAS 0.8047 → 0.8066.

Limits

Sources (French pass)

Sources

Cited in the code as «(author-year)»; library cards under the research library's papers/:

short idcardwhere it is used
dunning-1993dunning-1993-accurate-methods-statistics-surprise-coincidenceG² and why LMI ranks like it (compile.rs)
levy-2015levy-2015-improving-distributional-similarity-lessons-learnedPMI's rare-pair bias, minimum counts, context smoothing left out (compile.rs)
pado-2007pado-2007-dependency-based-construction-semantic-spacetyped dependency co-occurrence as the space (compile.rs)
turney-2010turney-2010-frequency-meaning-vector-space-modelsthe family of count-based word spaces (compile.rs)
hearst-1992hearst-1992-automatic-acquisition-hyponyms-large-textisa from text patterns; the copula is ours (extract.rs)
nivre-2017nivre-2017-universal-dependenciesthe relation inventory (extract.rs)
mikolov-2013mikolov-2013-distributed-representations-words-phrases-theirnegatives by corruption, the idea behind the role swaps (extract.rs)
resnik-1993, resnik-1997resnik-1993-semantic-classes-syntactic-ambiguity, resnik-1997-selectional-preference-sense-disambiguationclass-based back-off of argument plausibility (graph.rs)
collins-2005collins-2005-discriminative-reranking-natural-language-parsingreranking the parser's k best (score.rs)
zeman-2018zeman-2018-conll-2018-shared-task-multilingualLAS (score.rs)

Works without a card (rows in docs/usage.tsv): Church & Hanks 1990 (PMI), Evert 2005 and Baroni & Lenci 2010 (local MI, Distributional Memory), Schuster & Manning 2016 (enhanced UD), Smith & Eisner 2005 (contrastive estimation), Knuth 1997 (Fisher–Yates), Steele et al. 2014 (splitmix64), Hinnant 2013 (civil-from-days), Mann & Whitney 1947 (AUC as U), Spearman 1904, O'Neil et al. 1996 (LSM-tree compaction, the tamping).

Further sources

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/wordgraph/