Skip to content

Publish the graph as Parquet with a generated dataset card - #50

Merged
tamnd merged 1 commit into
mainfrom
m21-parquet-dataset
Aug 7, 2026
Merged

Publish the graph as Parquet with a generated dataset card#50
tamnd merged 1 commit into
mainfrom
m21-parquet-dataset

Conversation

@tamnd

@tamnd tamnd commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Closes #49.

The graph has been published on Hugging Face as a tarball of Neo4j import CSVs.
That is the right artifact for loading it into Neo4j and the wrong one for everything else.
The hub renders it as a download link, the viewer has nothing to show, and anybody deciding whether to spend half a gigabyte has to spend it first.

This adds luatdo export parquet, which converts the CSV export into one Parquet table per file and writes the dataset card next to them.

Why convert the CSVs rather than project the store again

A second projection would be a second list of node types, a second set of column names and a second place to forget a layer.
This repository has shipped a graph with a missing layer three times, and each time nothing failed, because nothing asked.
The CSV headers already say what every column is, because neo4j-admin requires them to, so a node type added to the export appears in the Parquet without anybody editing anything.
Same reasoning as running the export's own import.sh rather than reproducing its file list.

What the conversion does

Every header directive is mapped: :ID, :LABEL, :START_ID, :END_ID, :TYPE, :int, :float, :boolean, :string[].
The colon names become ordinary ones, so start_id joins to id without a reader having to know how neo4j-admin spells either.
An unknown directive is refused rather than written as text, because a column of numbers in quotes with nothing to explain it is worse than a failed export.

An empty CSV field becomes null and not the empty string.
A missing deadline and a deadline of nothing are different claims, and every tool that reads Parquet can tell null from "" while none of them can tell it from "" in a CSV.

Arrays split on the delimiter the export writes, multiline provision text stays whole, and tables shard at a million rows.
The shard names carry the total, which is not known until the last row is read, so files are written under a temporary name and renamed at the end.

A table with no rows is still published.
A layer that came out empty and a layer that was never exported are different facts, and the file list is the only place that difference shows.

Why the card is generated

A hand written list of tables goes stale the first time somebody adds a node type.
Here it goes worse than silently: a config naming a table with no files does not degrade, it stops the whole dataset page from loading.

Generating it also settled two things the previous card had wrong.
It said seventeen node labels and the data carries thirteen, so the card now counts them off the rows.
It said the concept mention layer was populated, and that table has no rows at all, along with concept relations, instance links, concept merges, act participants and conflicts.
The card now says so.
The row counts sit right above the paragraph, so the two had to agree.

Verification

Run over the real export on this machine.

Tables 33, being 14 node and 19 relationship
Rows 17,294,357
Files 47
Size 623MB of Parquet from 3.4GB of CSV

The node and relationship totals are 8,175,346 and 9,119,011, which match the loaded database exactly.
Reading every shard back with pyarrow gives the same 17,294,357 rows.
A duckdb join from cites to documents across shards returns the expected citation counts, and deadline_value and confidence come back as BIGINT and DOUBLE rather than as text.

Tests

At the value level rather than the file level, because every way this can be wrong still produces a valid Parquet file.
A column written under the wrong index is the one that does not: parquet.Group is a map and NewSchema orders fields by name, so a row assembled in header order crashes the process on the first column whose type does not match.
That is not a hypothetical, it is what the first two attempts did, and the test that covers it asserts values by column name.

The rest cover null against zero, arrays against pipe separated strings, multiline fields, sharding and the rename that follows it, the empty table, both refusals, and that the card's globs match the file names the shard writer produced.

Not in this change

The tarball stays exactly as it is.
luatdo neo4j install depends on it and its checksum is pinned, so the two artifacts live side by side and nobody has to download anything again.

The graph is published as a tarball of Neo4j import CSVs, which is the
right artifact for loading it into Neo4j and the wrong one for anything
else. The hub shows it as a download link, the viewer has nothing to
render, and nobody can look at the data before committing to half a
gigabyte.

This adds luatdo export parquet, which converts the CSV export into one
Parquet table per file and writes the dataset card next to them. The
conversion is driven by the header row rather than by a second projection
of the store, for the same reason import.sh is generated rather than
listed: a second projection would be a second list of node types and a
second place to forget a layer, which has happened three times already.

Every neo4j header directive is mapped. The colon names become ordinary
ones, so start_id joins to id without anybody having to know how
neo4j-admin spells either. Empty fields become null rather than the empty
string, arrays become lists, and the multiline provision text stays whole.
Large tables shard at a million rows and the shard names carry the total,
which is why they are written under a temporary name and renamed at the
end.

The card is generated because a hand written list of tables goes stale the
first time somebody adds a node type, and here it goes worse than
silently: a config naming a table with no files stops the whole dataset
page from loading rather than degrading. Generating it also settled a
question the old card got wrong. It claimed seventeen node labels and the
data has thirteen, and it claimed the concept mention layer was populated
when that table has no rows at all. The card now counts labels off the
rows and says plainly that the concept layer is empty.

Verified over the real export: 33 tables, 17,294,357 rows, 623MB of
Parquet from 3.4GB of CSV, and the node and relationship totals match the
loaded database exactly at 8,175,346 and 9,119,011. Joins across shards in
duckdb return the expected counts and the numeric columns come back as
BIGINT and DOUBLE rather than text.
@tamnd
tamnd merged commit 9bebe5e into main Aug 7, 2026
5 checks passed
@tamnd
tamnd deleted the m21-parquet-dataset branch August 7, 2026 06:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M21: Publish the graph as Parquet with a generated dataset card

1 participant