Sources: the data model¶
sources/ holds what cartolex was given or collected. Six tables describe it,
in sources/tables/, as Parquet files. A slot’s raw material (the CSV someone
imported, a folder of documents, the records a service returned) stays in
sources/<slot>/, so the tables can always be rebuilt from it.
texts ──< text_parts
│
└──< authorships >── people ──< affiliations >── organisations ──┐
(person_id) (org_id) <─┘ parents
Columns marked nullable are optional: a file written before one existed reads it as empty.
Every table is sorted by its key, and every key is a stable string: an id, once
given, never changes and is never reused. Ids are opaque: those cartolex gives
start with t, p or o, and an importer may keep its own. The ids a source
uses (DOI, ORCID, OpenAlex, ROR, HAL, arXiv…) are kept in their own columns.
Dates are kept everywhere: every text has its year, every affiliation its years, every row its retrieval time. Changes over time are computed from them.
texts.parquet: one row per text¶
A text is one document: an article, a preprint, a report, a thesis. A work written by five people is one text with five authorships.
column |
type |
meaning |
|---|---|---|
|
string |
the key |
|
string |
the slot the text came through |
|
int64 |
its order inside the slot; with the slot order, it fixes the order a person’s texts are read in |
|
int32, nullable |
publication year |
|
string, nullable |
a finer date, ISO 8601, when known |
|
string |
|
|
string |
the display title |
|
string, nullable |
lower case, without the |
|
map<string, string> |
other identifiers by scheme: |
|
string, nullable |
the |
|
int32 |
the number of authors on the text, including those outside the project |
|
string |
the finder that brought it in: |
|
timestamp (UTC) |
when the record was read |
text_parts.parquet: what a text says¶
A text has parts, each from a provider: a title, an abstract per language, a full text. Keeping them apart lets a project choose what feeds the lexicon (titles and abstracts by default, the whole document for a folder or a corpus slot), weigh the parts, and fill a missing abstract from another provider.
column |
type |
meaning |
|---|---|---|
|
string |
the text |
|
string |
|
|
string |
the language detected or declared, |
|
string |
where the words came from: |
|
string |
|
|
large string |
the cleaned text |
|
timestamp (UTC) |
when it was read |
The key is (text_id, part, language, provider). Which provider wins when
several give the same part is a parameter (sources.provider_priority), so
the choice can change without collecting again.
Private parts. body and full parts are full texts: collected on
request from open-access services, or a user’s own documents. They never
leave the project: a shared output (a site, a map bundle, an export, a share)
carries titles and abstracts at most. The rule is on the part, whatever its
provider (PRIVATE_PARTS and shareable_parts() in
cartolex.project.tables); the privacy page and the share step follow it.
Reading order. A text is read as its chosen parts in the order title,
abstract, body, each part in every language it has, separated by a blank line
and ending with a newline; full stands alone. People are read in person_id
order, and each person’s texts in slot order, then position. A preprint
whose published version is in the tables (its version_of) is not read: the
published version is, so that a work counts once. Likewise, texts of a slot
with the same normalised title (at least 25 characters), years at most one
apart and an author in common are one work: only the version of record
(article, review, chapter, conference version, then other types, a preprint
last; else the text with the most words in its parts) is read, by the authors
of every copy.
people.parquet: one row per person record¶
column |
type |
meaning |
|---|---|---|
|
string |
the key |
|
string |
as the source gives them |
|
string, nullable |
|
|
map<string, list |
other identifiers by scheme; a person may have several OpenAlex records; an idHAL confirmed as a record in |
|
string |
how the person entered: |
|
map<string, string> |
the extra columns of an imported list, kept as text; each becomes a filter, and a person attribute of the engine’s index |
|
list<struct<last_name, first_name, source>> |
every other name form a source gives for this person (marital name, name added by an institution, one half of a double surname, a transliteration); matching tries all of them |
|
timestamp (UTC) |
Aliases. Two rows are proposed as one person when they share a strong
identifier (ORCID, an OpenAlex record, an institutional email). Without one,
they are proposed when the first names match, one last name contains or extends
the other, and they share an affiliation or co-authors, with publication years
that follow on rather than overlap. A proposal is listed for review and never
applied on its own: only merged_into in people.csv joins the rows, and the
name forms of both then go to aliases.
Who is mapped, who only feeds the lexicon and who is projected is a decision,
not a source: see people.csv in Decisions.
organisations.parquet: teams, labs, universities, panels¶
column |
type |
meaning |
|---|---|---|
|
string |
the key |
|
string |
|
|
string, nullable |
|
|
string, nullable |
the project level it belongs to (see |
|
list |
the |
|
map<string, string> |
|
|
string, nullable |
ISO 3166-1 alpha-2 |
|
struct<lat: double, lon: double>, nullable |
|
|
string |
|
|
timestamp (UTC) |
affiliations.parquet: who belonged where, when¶
column |
type |
meaning |
|---|---|---|
|
string |
|
|
string |
|
|
int32, nullable |
inclusive; open when unknown |
|
string |
|
Which organisation a text counts for¶
A text counts once for an organisation, however many of its members wrote it. For each author, the organisations are, in this order:
the organisations stated on the text for that author (
authorships.orgs);otherwise, the author’s affiliations whose years contain the text’s year;
otherwise, the author’s current affiliations, and the text is flagged.
Higher levels follow the parents links.
A slot’s own folder: the raw runs¶
The tables are rebuilt from what each slot keeps in sources/<slot>/raw/, so
the raw runs and the id registry are part of the format.
sources/<slot>/raw/
ids.parquet the id registry (cartolex-ids/2)
<kind>/<run id>.jsonl.gz one run (cartolex-raw/1)
sources/merges.json every merge of texts across finders (cartolex-merges/1)
A run is one job’s material, written whole or not at all and never
changed afterwards. It is a gzip-compressed UTF-8 JSON-lines file
(.jsonl.gz; the header and the records may be separate gzip members, read as
one stream); a run written by an earlier version is a plain .jsonl file,
read the same way. The first line is the header, an object with format (cartolex-raw/1), kind (the folder’s
name), run_id and what the job was asked (its window of years, its people);
each further line is one record, an object. Run ids are
<UTC time to the microsecond>Z-<6 hex> (20260928T101200123456Z-3f2a1c); a
slot’s runs of one kind are read in the order of their ids, and a new run’s id
always sorts after the earlier ones. A file whose header is not
cartolex-raw/1 is refused when the tables are rebuilt.
kind |
the job |
records |
rows built |
|---|---|---|---|
|
a list imported |
one per row (never an e-mail address): names, identifiers, filter columns, organisations with levels and parents |
people ( |
|
a folder of documents |
one per file read: its path in the folder, digest, person, title, year, language, text |
texts ( |
|
a corpus imported |
one per index row: names, unit, attributes, file, year, type, language, text |
people, units as organisations, texts with a |
|
people taken from institutions |
the units (institution records), then one per person: records, names, ORCID, units with years |
people ( |
|
a round of collaborators |
one per collaborator: round, record, name, the people they wrote with, path, joint works (with the institutions stated), fit |
people ( |
|
a harvest |
author records and works as received, each with the person it was collected for |
texts, title and abstract parts, authorships, organisations, affiliations ( |
|
a harvest |
the registry’s works and records, per person |
affiliations ( |
|
the archive, the journal platform |
deposits and articles as received, with the people found (by idHAL, by ORCID) and structures |
texts, parts per language, authorships, organisations, affiliations |
|
text providers |
the parts each provider found for a text, and the links it stated |
parts; a preprint’s |
|
proposals |
candidates with their evidence |
none: they wait for a decision |
|
any finder |
one per person whose collection failed: finder, service, status, cause, time |
none: the coverage report reads them |
A harvest names its people in its header; a person’s latest harvest replaces
their earlier ones. A long harvest writes several runs, one per chunk of people,
each naming its own; a paused one keeps its checkpoint (the people done, the
options to resume with) in sources/<slot>/raw/checkpoints/harvest-<key>.json.gz,
removed once the harvest completes. Other kinds add up, the first record of a text, part or
authorship winning and later ones only filling what it lacks, except a text
provider’s part, which replaces the same service’s part as a finder. A reader
that meets a kind it does not know skips it and says so.
The id registry, ids.parquet, is a Parquet table of three string columns,
table (texts, people or organisations), key and id, one row per key,
sorted by table then key; its metadata holds format (cartolex-ids/2) and
next, the next number of each table as JSON ({"organisations": 4, "people": 13, "texts": 57}).
table |
key |
id |
|---|---|---|
|
|
|
|
|
|
|
|
|
Each row maps a natural key a slot has met (a DOI, a service record, an
imported row) to the id it was given. An id is given once and never again: text
keys are looked up in their slot’s registry, people and organisations in every
slot’s, and a new number comes after every number any registry gave. A
registry of the first format, ids.json (cartolex-ids/1: a JSON object with
next and keys, a mapping of key to id per table), is read and replaced by
ids.parquet at the next rebuild; a version of cartolex that knows only the
first format cannot open a project rebuilt since.
The merge log, sources/merges.json (cartolex-merges/1), is rebuilt with
the tables: merges (each with slot, the text kept, the texts merged,
the rule and its evidence), versions (a preprint and its published
text), refused (two texts, the rule and the reason), conflicts (a
field of a text whose finders disagree: the value kept and where it came
from, the other and where it came from) and fills (parts one text took
from another).
Rebuilding reads a harvest from a digest of each run kept in
cache/sources/ (see Collection internals): a cache, which gives the same
tables as the runs and can be deleted.
slot kind |
|
|---|---|
|
nothing else: the documents’ text is in the runs, never their path on your computer |
|
nothing else: the index’s rows and texts are in the runs |
Merged texts. A work found by several finders is one text: the tables are
rebuilt with texts merged by DOI, by a shared identifier, then by title and
year for the same person, each field taken from the highest-priority finder
that has it. sources/merges.json (cartolex-merges/1), rebuilt with the
tables, lists every merge with its rule and evidence, the preprints linked to
their published version, the matches refused and why, and the values finders
gave differently (see docs/dev/collection.md).
Overlays and bases use the same layout under their own root, and
sources/bases/<id>/ holds a copy of each base’s map bundle.