Skip to content

Port GeoNames RDF to LDE #782

Description

@ddeboer

geonames-rdf converts the GeoNames data dumps to a ~13.5M-feature geonames.nt with a set of shell scripts (download.sh, map.sh, test.sh) driving the SPARQL Anything CLI. We want to port it to LDE. #511 is the first piece: @lde/sparql-anything with a SparqlAnythingConverter.

This issue tracks what has to change before the migration can start, split into musts (the port cannot run correctly or finish without them) and shoulds (worth fixing, but a migration could ship around them).

Status

Every must and should is done. @lde/sparql-anything is at 0.1.0.

Four of them landed differently from the description below, and two follow-ups came out of the work; see the status comment and the one after it.

Still open: the Scope beyond #511 section – chunking, the ontology step, publication, and keeping test.sh's golden-file diff.

What map.sh actually does

It makes four different SPARQL Anything invocations, not one:

# invocation query --load {SOURCE}
1 admin codes admin-codes.rq – (paths inline in the query)
2 places chunks places.rq admin-codes.ttl yes
3 alternate-names chunks alternate-names.rq yes
4 ontology ontology.rq ontology.rdf (as the input)

SparqlAnythingConverter as first proposed expressed only #2.

Musts

  • 1. Parallelism. convert() is a sequential for loop with an await inside. map.sh runs an xargs -P worker pool sized from nproc, capped by the cgroup memory limit (or /proc/meminfo on a host), budgeting ~3 GB per worker. On allCountries that is ~14 places chunks plus ~4 alternate-names chunks; running them one at a time multiplies the conversion, which takes ~17 min of the ~40 min workflow (measured, see below).

    Needs a concurrency option; the memory-aware default is generic enough to belong in LDE rather than in the consumer. This is the one that decides whether the port is viable – everything else is correctness. Do it after 6+7, so the pool is not reworked when its unit of work changes shape.

  • 2. JVM heap control, and a CLI escape hatch. map.sh passes -Xmx2g per worker precisely because SPARQL Anything materialises a chunk’s whole result graph before writing it. The converter hardcodes java -jar … with no -Xmx, so each JVM takes a quarter of host RAM by default – which over-subscribes the moment concurrency lands. Also needs a passthrough for flags the converter does not model, such as -ad on SPARQL Anything 1.2-dev for #624/#625. In review: feat(sparql-anything): cap the JVM heap, and pass through CLI arguments #789 (javaOptions, cliArgs).

  • 3. A generic, optional load. adminCodesFile was required, single, and named after a GeoNames concept; it could not express invocation test: collect coverage #3 (no --load) or feat: add dataset-registry-client #4 (--load is the input). Now load?: string. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)

  • 4. Guard against an empty chunk output. SPARQL Anything exits 0 when it cannot read or parse an input: it logs, writes an empty --output and stops, which is how we once shipped a geonames.nt missing every ontology triple while the run stayed green. assertNonEmpty now fails the conversion, and an empty chunkPaths is rejected outright. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)

  • 5. convert() resolving before the output was flushed. pipeline(…, {end: false}) resolves when the source ends; output.end() ran un-awaited in finally, so a caller could read a truncated file. Now await finished(output), plus a newline between concatenated files so two triples can never share a line. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)

Shoulds

  • 6+7. One call over jobs, not one converter per query. Originally two separate points – a single worker pool, and returning the chunk outputs; 8’s workDir change merged them and made them more pressing rather than less.

    map.sh deliberately feeds both chunk sets through a single pool: an alternate-names chunk is roughly a quarter of the work of a places chunk (one triple per row, no admin-codes join), so the long jobs are listed first and the short ones fill the tail. One SparqlAnythingConverter per query means two sequential drains, which loses that packing.

    And the port has to get places + alternate names + ontology.nt into one file. convert() returns void, and now that the per-chunk outputs live in a run directory deleted in finally, the caller can no longer concatenate them itself – which it could when they sat next to the inputs.

    Both want the same change: convert() takes jobs of {chunk, query, load} in a single call, or gains an explicit append. Settle this before 1.

  • 8. Chunk outputs beside the inputs, never cleaned. Outputs and generated queries now go to a fresh per-run directory under a caller-supplied workDir, removed in finally, and are referred to by runner-relative paths so the same command works on the host and under DockerTaskRunner. (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511)

  • 9. Shell interpolation, and the wait() race. Interpolated values go through shellQuote (feat(sparql-anything): add SparqlAnythingConverter for chunked non-RDF to RDF conversion #511). wait() attached its close listener only after run() returned, so a process that failed fast could close first and wait() would never settle – invisible while the converter was sequential, fatal under a pool. Fixed in @lde/task-runner-native (fix(task-runner-native): settle wait() for a process that already closed #787).

Scope beyond the converter

Conversion is roughly a third of the migration. Still unhomed:

What should not move into LDE, because it is GeoNames domain policy rather than plumbing: the awk synthesis of the adm1/adm2 foreign keys, the filtering of alternate names belonging to out-of-scope features, and the pinned ontology version.

Finally, test.sh is a golden-file test that runs the real map.sh over fixtures and diffs the result against test/expected/geonames.nt. Whatever replaces it has to keep that end-to-end diff: reviewing that file line by line is the actual test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions