Skip to content

Commit 2733433

Browse files
namedgraphclaude
andcommitted
Make committed TriG base-relative (base applied at load time)
Committed datasets/current/**/*.trig now use base-relative IRIs with no @base (e.g. <persons/84854/#this>) instead of absolute https://linkeddata.lt/… URIs. The base is supplied at load/parse/validate time, so one committed dataset serves any deployment base (localhost dev, linkeddata.lt prod) with no regeneration. RDF stores only absolute IRIs, so the data loaded into Fuseki is identical; only the git serialization changes. The mappings still mint absolute IRIs in-model — a new etl/lib/relativize.sh rewrites arq/rdflib output to relative on write (riot streaming writer + strip @base), leaving external URIs (Wikidata, Commons, lrs.lt, schema.org, EU tables, mailto/tel) absolute. - write: relativize.sh; graphify.sh pipes arq through it; reconcile/photos become base-aware via rdflib publicID and are relativized on write - read: validate.sh/shacl.sh/run.sh take a base, resolving relative→absolute - load: tdb-loader resolves against BASE_URI (.env) via riotcmd.riot before tdb2.tdbloader; make load passes -e BASE_URI - CI (shacl-validation.yml) + docs (CLAUDE.md, README, URI-SCHEME, config.mk) Verified with Jena 5.6.0: round-trip (riot --base=$B --output=nquads) of every relativized file is byte-identical to the prior absolute triples; validation, cross-graph queries, and load-time rebase to both bases pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1c45a3d commit 2733433

36 files changed

Lines changed: 93729 additions & 100320 deletions

.github/workflows/shacl-validation.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ on:
1818

1919
env:
2020
JENA_VERSION: 6.1.0
21+
# committed TriG is base-relative; resolve against the prod base for validation
22+
BASE: https://linkeddata.lt/
2123

2224
jobs:
2325
validate:
@@ -58,10 +60,10 @@ jobs:
5860
# overlay files (owl:sameAs/foaf:depiction onto other files' graphs):
5961
# graph invariants intentionally absent, so skip validate.sh
6062
alignments.trig|photos.trig)
61-
etl/lib/shacl.sh "etl/shapes/$domain.ttl" "$f" \
63+
etl/lib/shacl.sh "etl/shapes/$domain.ttl" "$f" "$BASE" \
6264
|| { rc=1; echo "::error file=$f::SHACL validation failed"; echo "- FAIL: $f" >> "$GITHUB_STEP_SUMMARY"; } ;;
6365
*)
64-
etl/lib/validate.sh "$f" \
66+
etl/lib/validate.sh "$f" "$BASE" \
6567
|| { rc=1; echo "::error file=$f::validation failed"; echo "- FAIL: $f" >> "$GITHUB_STEP_SUMMARY"; } ;;
6668
esac
6769
echo "::endgroup::"

CLAUDE.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ cd etl && make # everything: taxonomies → admin-units → seim
1010
make -C etl/<domain> all # one domain (fetch is always fresh — FORCE prerequisite)
1111
make -C etl/seimas photos # opt-in: scrape official portraits from lrs.lt
1212
make BASE=https://linkeddata.lt/ # prod base URI (default https://localhost:4443/, see etl/config.mk);
13-
# committed datasets/current/ are generated with the prod base
13+
# committed datasets/current/ are base-RELATIVE (no @base) — identical
14+
# for any BASE; the base is (re)applied at load/parse/validate time
1415

1516
etl/queries/run.sh <q.rq> # SPARQL over ALL datasets loaded in-memory (~1M quads, -Xmx4g)
1617
python3 etl/queries/render-examples.py # regenerate etl/queries/EXAMPLES.md result tables
1718

18-
uv run --project etl/tools ltlod-reconcile <admin-units|persons> --input … --output …
19+
uv run --project etl/tools ltlod-reconcile <admin-units|persons> [--base …] --input … --output …
1920

2021
make sef # compile the client-side XSLT override (files/client.xsl) to a
2122
# Saxon-JS SEF (files/client.xsl.sef.json, gitignored). Run once
@@ -33,8 +34,8 @@ make install # set up the dataspace via LDH CLI: make it publi
3334
# or `printf '\n\n\n\n' | make install`); enter another Base URL
3435
# + owner cert to install onto any LDH instance
3536
make load # bulk-load datasets/current/*/*.trig into fuseki-end-user TDB2;
36-
# regenerate with `make -C etl` first (committed data has prod base);
37-
# ends with `make public` (anonymous read, LDH make-public.sh equivalent)
37+
# resolves the base-relative TriG against BASE_URI (.env) via riot
38+
# before tdb2.tdbloader; ends with `make public` (anonymous read)
3839
make down / make drop # stop stack / wipe LDH runtime state (never datasets/current/)
3940
```
4041

@@ -53,13 +54,17 @@ Every domain runs the same four stages (shared scripts in `etl/lib/`):
5354
2. **normalize** — CSV → CSV2RDF identity transform (docker), XML → XSLT 1.0
5455
(`xsltproc`), both emit source-shaped RDF with `<{base}#column>` properties.
5556
3. **graphify** — LDH-style quad `CONSTRUCT { GRAPH ?graph {…} }` mapping
56-
(`mappings/*.rq`, `$base`-parameterized) executed by `arq` → TriG. The `.rq`
57-
files are reusable verbatim as LinkedDataHub CSV imports.
57+
(`mappings/*.rq`, `$base`-parameterized) executed by `arq`; the CONSTRUCT mints
58+
ABSOLUTE IRIs in-model, then `etl/lib/relativize.sh` rewrites the output to
59+
base-RELATIVE IRIs with no `@base` → TriG. The `.rq` files are reusable verbatim
60+
as LinkedDataHub CSV imports. (`ltlod-reconcile`/photos take `--base` as the
61+
rdflib publicID and are relativized the same way; see the relative-TriG gotcha.)
5862
4. **validate**`riot --validate` + every graph must have `dct:title` and
5963
`foaf:primaryTopic` on the graph URI (see `etl/lib/validate.sh`) + SHACL
6064
shapes per entity type (`etl/shapes/<domain>.ttl`, auto-selected by output
6165
dir, executed via `etl/lib/shacl.sh`; also run in CI on committed datasets
62-
by `.github/workflows/shacl-validation.yml`).
66+
by `.github/workflows/shacl-validation.yml`). Both `validate.sh` and `shacl.sh`
67+
take a base to resolve the relative IRIs (validate.sh pre-resolves to N-Quads).
6368

6469
Post-ETL: `ltlod-reconcile` matches entities to Wikidata (closed candidate sets
6570
via WDQS, exact label + parent disambiguation) and writes `owl:sameAs` + images
@@ -166,9 +171,21 @@ merge on load). Unmatched entities go to `cache/unmatched*.csv`, never force-mat
166171
- **`make load` bypasses LDH's HTTP API**: it runs `tdb2.tdbloader` directly against
167172
the end-user TDB2 store via the one-off `tdb-loader` compose service (the
168173
`atomgraph/fuseki` image bundles the full Jena CLI inside the fuseki-server jar).
174+
Because the committed TriG is base-relative, the service first resolves it against
175+
`BASE_URI` (from `.env`, passed as `-e BASE_URI=…`) with `riotcmd.riot --base=…
176+
--output=nquads`, then loads the N-Quads (tdb2.tdbloader has no `--base`).
169177
Load is append-only — clean rebuild: `make down && rm -rf fuseki/end-user &&
170178
make up && make load`. It stops fuseki-end-user first and removes the stale
171179
`tdb.lock` (lock PIDs are container-relative), then restarts the Varnish caches.
180+
- **Committed TriG is base-RELATIVE with no `@base`** (base-agnostic: one dataset
181+
serves any deployment base). RDF stores only absolute IRIs, so the base must be
182+
reapplied on EVERY read — always pass `--base` to `riot`/`arq`/`shacl`
183+
(`riot --base=$BASE …`) or relative IRIs resolve to `file://…` garbage. The write
184+
path (`etl/lib/relativize.sh`) uses riot's STREAMING trig writer (`--output`, which
185+
relativizes against a prepended `@base`; `--formatted` does NOT) and strips the base
186+
header. Only base-internal IRIs relativize; external URIs (Wikidata, Commons, lrs.lt,
187+
EU tables, schema.org, mailto/tel) stay absolute. Round-trip proof:
188+
`riot --base=$BASE --output=nquads file.trig | sort` is base-independent.
172189
- **Fuseki ports are never published to the host** — query via
173190
`https://localhost:4443/sparql` or from inside the network:
174191
`docker compose exec linkeddatahub curl http://varnish-end-user/ds/`.

Makefile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ install:
106106
./app/install.sh "$$BASE_URL" "$$CERT_PATH" "$$PASSWORD"; \
107107
fi
108108

109-
# Bulk-load datasets/current/*/*.trig into the end-user TDB2 store. APPEND-ONLY:
110-
# clean rebuild = `make down && rm -rf fuseki/end-user && make up && make load`.
109+
# Bulk-load datasets/current/*/*.trig into the end-user TDB2 store. The committed
110+
# TriG is base-relative; the loader resolves it against BASE_URI (from .env) so
111+
# the same files load at whatever base this deployment uses — no per-base regen.
112+
# APPEND-ONLY: clean rebuild = `make down && rm -rf fuseki/end-user && make up && make load`.
111113
load:
112114
@ls datasets/current/*/*.trig >/dev/null 2>&1 || \
113115
{ echo "ERROR: no TriG files under datasets/current/ — run 'make -C etl' first."; exit 1; }
@@ -119,7 +121,7 @@ load:
119121
done
120122
docker compose stop fuseki-end-user
121123
rm -f fuseki/end-user/DB2/tdb.lock
122-
docker compose run --rm tdb-loader
124+
docker compose run --rm -e BASE_URI="$(BASE_URI)" tdb-loader
123125
docker compose up -d fuseki-end-user
124126
docker compose restart varnish-end-user varnish-frontend
125127
$(MAKE) public

README.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ todėl grafas saugo ne tik dabartinę būseną, bet ir **istoriją**: pasikeitus
3838
narystė gauna pabaigos datą, o nauja — pradžios.
3939

4040
Kiekvienas objektas gyvena savo **named graph'e** (dokumente), kurio URI sutampa su dokumento
41-
adresu. Štai visas Birštono savivaldybės dokumentas ([TriG](https://www.w3.org/TR/trig/) sintakse):
41+
adresu. Štai visas Birštono savivaldybės dokumentas ([TriG](https://www.w3.org/TR/trig/) sintakse;
42+
čia parodyta išskleista absoliuti forma — užfiksuoti failai naudoja santykines URI, pvz.
43+
`<admin-units/12/#this>`, o bazė pritaikoma krovimo metu):
4244

4345
```turtle
4446
<https://linkeddata.lt/admin-units/12/> {
@@ -153,18 +155,20 @@ make up # sugeneruoja slaptažodžius + serverio sertifikatą ir paleidžia
153155
make install # suteikia viešą skaitymo prieigą, sukuria konteinerių dokumentus ir vardų erdvės
154156
# ontologiją per LDH CLI (reikia ../LinkedDataHub);
155157
# interaktyvus: Enter×4 = lokali aplinka, kitas Base URL = bet kuri LDH instancija
156-
make -C etl # perkuria rinkinius su numatytąja baze https://localhost:4443/
157158
make load # užkrauna datasets/current/*/*.trig tiesiai į triplestore
159+
# (nereikia perkurti — rinkiniai bazei neutralūs; bazė pritaikoma krovimo metu)
158160
```
159161

160162
Po `make up` LDH pasiekiamas adresu **<https://localhost:4443/>** (savo pasirašytas
161163
sertifikatas — naršyklė įspės; pirmas paleidimas trunka ~1–2 min.). Administravimo
162164
aplinka — <https://admin.localhost:4443/>.
163165

164166
**Svarbu dėl bazinės URI:** repozitorijoje užfiksuoti `datasets/current/` failai
165-
sugeneruoti su produkcine baze `https://linkeddata.lt/`, tad prieš `make load` rinkinius
166-
reikia perkurti su numatytąja lokalia baze (`make -C etl`) — kitaip dokumentų URI
167-
nesutaps su LDH adresu ir jie nebus pasiekiami.
167+
naudoja **santykines URI** (be `@base`), tad yra neutralūs bazei — tas pats rinkinys
168+
tinka bet kuriam diegimui. `make load` išsprendžia santykines URI pagal `.env` bazę
169+
(`BASE_URI`, lokaliai `https://localhost:4443/`) su `riot` prieš `tdb2.tdbloader`, tad
170+
dokumentų URI visada sutampa su LDH adresu. Perkurti prieš krovimą nebūtina —
171+
`make -C etl` reikia tik norint atsinaujinti duomenis iš gyvų šaltinių.
168172

169173
Duomenų struktūra kuriama dviem lygiais:
170174

@@ -180,8 +184,9 @@ Duomenų struktūra kuriama dviem lygiais:
180184
repozitorijos (`../LinkedDataHub`, keičiama per `make install LDH_HOME=…`).
181185
- **Duomenys** (`make load`): ETL rinkiniai — vien `dh:Item` dokumentai su
182186
`sioc:has_container` nuorodomis į karkasą — rašomi **tiesiogiai į
183-
`fuseki-end-user` TDB2 saugyklą** (`tdb2.tdbloader` per vienkartinį
184-
`tdb-loader` konteinerį), ne po vieną dokumentą per HTTP: ~1 mln. ketvertų
187+
`fuseki-end-user` TDB2 saugyklą** (santykinės URI pirma išsprendžiamos pagal
188+
`.env` bazę su `riot`, tada `tdb2.tdbloader` per vienkartinį `tdb-loader`
189+
konteinerį), ne po vieną dokumentą per HTTP: ~1 mln. ketvertų
185190
užsikrauna per kelias minutes. Pabaigoje suteikiama vieša skaitymo prieiga
186191
(`make public` — LDH CLI `make-public.sh` atitikmuo, vykdomas tiesiogiai per
187192
`fuseki-admin` konteinerių tinkle).
@@ -275,8 +280,9 @@ Daugiau klausimų, į kuriuos duomenys jau atsako (visi su užklausomis ir pilna
275280

276281
**Publikavimas.** Lokalus LinkedDataHub diegimas jau yra — žr.
277282
[Publikavimas su LinkedDataHub](#publikavimas-su-linkeddatahub). Lieka produkcinis
278-
diegimas `https://linkeddata.lt/` adresu (rinkiniai jau generuojami su `$base`
279-
parametrizuotomis URI, tad tereikia `make -C etl BASE=https://linkeddata.lt/`).
283+
diegimas `https://linkeddata.lt/` adresu (rinkiniai bazei neutralūs — santykinės URI —
284+
tad tuos pačius `datasets/current/` failus užtenka užkrauti su produkcine `.env` baze;
285+
perkurti nereikia).
280286

281287
**Nauji rinkiniai** (integracijos taškai jau paruošti — žr. „kaip pridėti“ žemiau):
282288

0 commit comments

Comments
 (0)