Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Composites, one layer at a time

A composite is a resource computed from other resources. The graph in Giving the data names was one already: a template, filled from the context and the week. This chapter builds taller ones, a layer at a time, and each layer is a name the next one reads. Then it asks the kernel what hangs from what, and what a composite costs when one of the things it reads changes every minute.

The Run cells use this page’s kernel and the recorded weeks, so their answers are the ones printed beside them. The shell steps that write files are replayed against ikigai-cli 0.2.6 by the book’s checks. The live feed’s steps are not, because the feed moves: they are what the REPL printed on 2026-10-29, and they say so.

1. Filter: a question whose answer is a graph

The questions in the last chapter answered rows. A layer that another layer reads has to answer something that can be read the same way the graph was: a graph. SPARQL’s CONSTRUCT does that. This one keeps the earthquakes of magnitude 5.5 and up, with the facts the next layers need:

PREFIX qk: <https://ikigai-rs.dev/ns/quakes#> CONSTRUCT { ?quake a qk:Earthquake ; qk:mag ?mag ; qk:magType ?magType ; qk:place ?place ; qk:time ?time } WHERE { ?quake a qk:Earthquake ; qk:mag ?mag ; qk:magType ?magType ; qk:place ?place ; qk:time ?time FILTER(?mag >= 5.5) }
[computed]

FILTER(?mag >= 5.5) is the filter; everything else copies statements across. Because mag is typed as a number by the context, >= compares numbers, not text.

This page holds the question’s answer at a name of its own, urn:quakes:strong: the question read through the kernel and asked of urn:quakes:graph, whenever someone reads the name. So the next layer can ask its questions of it, exactly as the last chapter asked them of the graph. The count question from that chapter, unchanged, now counts the strong ones:

quakes
12
[2 computed]
cached

Twelve of the week’s 178, and the filtered graph is now cached under its name. The count question did not change: a qk:Earthquake matched because the filter’s CONSTRUCT kept each earthquake’s type.

In this page the questions are fixed, because they are compiled into it. At a shell each one is a .rq file you write and edit, and section 6 writes one.

2. Sort: biggest first

The second layer reads the first by name. ORDER BY DESC(?mag) puts the biggest first, and ?place after it breaks ties, so the order is the same every time:

quake,mag,place
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m0xl,7.5,"2024 Noto Peninsula, Japan Earthquake"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m0xm,6.2,"8 km SW of Anamizu, Japan"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m0xj,5.8,"42 km NE of Anamizu, Japan"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m1ld,5.7,"125 km W of Houma, Tonga"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m0ya,5.6,"54 km NE of Anamizu, Japan"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m0xt,5.6,"7 km N of Anamizu, Japan"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m2cv,5.6,Balleny Islands region
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m26y,5.6,Carlsberg Ridge
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m0y9,5.5,"56 km NE of Anamizu, Japan"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m1cg,5.5,"68 km WNW of Vinchina, Argentina"
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m2d7,5.5,Balleny Islands region
https://earthquake.usgs.gov/earthquakes/eventpage/us6000m27f,5.5,southern East Pacific Rise
[2 computed]

Seven of the twelve are the Noto Peninsula earthquake and its aftershocks, near Anamizu.

3. Group and count: by magnitude type

The last chapter’s by-magtype question, asked of the strong layer instead of the whole week:

magType,quakes
mb,4
mwb,1
mww,7
[2 computed]

Over the whole week, mb (body-wave magnitude) measured 143 of 178. Among the strong ones, mww (moment magnitude, the measure USGS prefers for large earthquakes) measured most. The question is one resource, read by two layers; nothing was copied.

4. A join: the week after

USGS’s service gives any window you ask for. This page holds a second one, recorded the same way as the first: the week after, 2024-01-08 to 2024-01-15, 105 earthquakes, at urn:file:quakes-2024-01-08.geojson, and filled through the same context at urn:quakes:graph:2024-01-08.

urn:sparql:select loads every graph it is given, so a question can read the strong layer and the second week together. This one asks, for each region where the first week had a strong earthquake, how many earthquakes the second week had there. The region is the end of USGS’s place, after its last comma, and the two weeks are told apart by time: 1704672000000 is 2024-01-08T00:00Z in USGS’s milliseconds.

region,strongest,weekAfter
Japan,7.5,6
Tonga,5.7,9
Balleny Islands region,5.6,0
Carlsberg Ridge,5.6,0
Argentina,5.5,2
southern East Pacific Rise,5.5,0
[2 computed]

Six more in Japan, after the aftershocks of the first week; nine in Tonga; none on the ridges and the Balleny Islands. The OPTIONAL is the join: the second week’s earthquakes are matched to the first week’s strong ones on the region they share, and GROUP BY ?region counts them. The question is urn:quakes:question:week-after, a file like the others, in crates/quakes/queries/.

5. What hangs from what

Every layer read its sources through the kernel, so every answer hangs from the threads of what it read. The context is under everything:

dependents of urn:file:quakes.context.jsonld  (cut 0 times)
  10 live cached entries hang from it — a cut makes them stale, and the next read of each recomputes it
  urn:file:quakes.context.jsonld  application/ld+json           918 B  1 thread    root
  urn:json:query                  application/json              746 B  2 threads   root
  urn:quakes:graph                application/ld+json        123.6 KB  5 threads   root
  urn:quakes:graph:2024-01-08     application/ld+json         73.9 KB  5 threads   root
  urn:quakes:strong               text/turtle                  4.5 KB  8 threads   root
  urn:sparql:construct            text/turtle                  4.5 KB  6 threads   root
  urn:sparql:select               text/csv                       12 B  10 threads  root
  urn:sparql:select               text/csv                       36 B  10 threads  root
  urn:sparql:select               text/csv                      158 B  13 threads  root
  urn:sparql:select               text/csv                     1.1 KB  10 threads  root
[uncacheable]

Both graphs, the strong layer, the CONSTRUCT behind it, and each answer above. The join is the one with 13 threads: it read both weeks.

A question is a thread too. Ask what hangs from the filter’s:

dependents of urn:quakes:question:strong  (cut 0 times)
  6 live cached entries hang from it — a cut makes them stale, and the next read of each recomputes it
  urn:quakes:question:strong  application/sparql-query      275 B  1 thread    root
  urn:quakes:strong           text/turtle                  4.5 KB  8 threads   root
  urn:sparql:select           text/csv                       12 B  10 threads  root
  urn:sparql:select           text/csv                       36 B  10 threads  root
  urn:sparql:select           text/csv                      158 B  13 threads  root
  urn:sparql:select           text/csv                     1.1 KB  10 threads  root
[uncacheable]

The filter’s answer and everything read from it: the count, the sort, the grouping and the join. The graphs do not hang from it, and neither does the CONSTRUCT (the layer handed it the question’s text, not its name). Cut the question’s thread, the way a change to the .rq file would, and ask the grouping again:

cut urn:quakes:question:strong
[uncacheable]
not cached
cached
magType,quakes
mb,4
mwb,1
mww,7
[1 cached · 1 computed]

The strong layer was stale and was computed again; the graph under it was still cached, because the question is not one of its sources. The grouping’s own question was cached, and its answer recomputed, from the recomputed layer. A cut reaches exactly what hangs from the thread.

6. At a shell: your own notes

The REPL has none of this page’s urn:quakes: names, so at a shell each layer is a file. In the REPL you started over ~/quakes, download the week after beside the first:

cd ~/quakes
curl -sO https://raw.githubusercontent.com/ikigai-rs/ikigai-tutorial/main/crates/quakes/quakes-2024-01-08.geojson

Fill both weeks as JSON-LD, as the last chapter did for one (if you still have graph.txt and quakes.jsonld from it, the first two lines are done already):

ikigai> sink urn:file:graph.txt {"@context": $r{urn:json:query?src=urn:file:quakes.context.jsonld&q=.[]}, "@graph": [$r{urn:file:quakes-2024-01-01.geojson}]}
wrote 125 bytes to graph.txt
[uncacheable]
ikigai> source urn:iki:fn:compose src=urn:file:graph.txt | sink urn:file:quakes.jsonld
wrote 126539 bytes to quakes.jsonld
[1 computed · 1 uncacheable]
ikigai> sink urn:file:graph-2024-01-08.txt {"@context": $r{urn:json:query?src=urn:file:quakes.context.jsonld&q=.[]}, "@graph": [$r{urn:file:quakes-2024-01-08.geojson}]}
wrote 125 bytes to graph-2024-01-08.txt
[uncacheable]
ikigai> source urn:iki:fn:compose src=urn:file:graph-2024-01-08.txt | sink urn:file:quakes-2024-01-08.jsonld
wrote 75659 bytes to quakes-2024-01-08.jsonld
[1 computed · 1 uncacheable]

The notes are yours: a file for each earthquake you have something to say about, named after USGS’s id for it, under notes/. Write two, and the one that stands in for a note not written yet:

ikigai> sink urn:file:notes/us6000m0xl.txt Felt in Kanazawa. The tsunami warning came first.
wrote 49 bytes to notes/us6000m0xl.txt
[uncacheable]
ikigai> sink urn:file:notes/us6000m2jp.txt Mindanao again.
wrote 15 bytes to notes/us6000m2jp.txt
[uncacheable]
ikigai> sink urn:file:notes/none.txt no note yet
wrote 11 bytes to notes/none.txt
[uncacheable]

Now the earthquakes the notes are about: the ones of magnitude 6 and up in either week, with each one’s id. A question with both graphs reads both weeks; its answer goes to a file, because this page gives an answer with arguments no name of its own (the REPL’s bind can, from ikigai-cli 0.2.6: named requests, ledger #2447, which this part has not taken up yet), and urn:json:query turns the CSV into the JSON list the next step reads:

ikigai> sink urn:file:queries/notable.rq PREFIX qk: <https://ikigai-rs.dev/ns/quakes#> SELECT ?id ?mag ?place WHERE { ?quake qk:mag ?mag ; qk:place ?place FILTER(?mag >= 6) BIND(STRAFTER(STR(?quake), "eventpage/") AS ?id) } ORDER BY DESC(?mag)
wrote 202 bytes to queries/notable.rq
[uncacheable]
ikigai> source urn:file:queries/notable.rq | urn:sparql:select graph="urn:file:quakes.jsonld urn:file:quakes-2024-01-08.jsonld" as=text/csv | sink urn:file:notable.csv
wrote 216 bytes to notable.csv
[2 computed · 1 uncacheable]
ikigai> source urn:json:query src=urn:file:notable.csv q=. | sink urn:file:notable.json
wrote 299 bytes to notable.json
[1 computed · 1 uncacheable]

The join with your notes is a template for one row, filled once for each earthquake by urn:iki:fn:each. {id} is the row’s id, so urn:file:notes/{id}.txt is that earthquake’s note, and || names what to splice when there is none:

ikigai> sink urn:file:row.txt <li>M$h{{mag}} $h{{place}}: $h{urn:file:notes/{id}.txt || urn:file:notes/none.txt}</li>
wrote 87 bytes to row.txt
[uncacheable]
ikigai> source urn:iki:fn:each src=urn:file:notable.json template=urn:file:row.txt
<li>M7.5 2024 Noto Peninsula, Japan Earthquake: Felt in Kanazawa. The tsunami warning came first.</li><li>M6.7 93 km SE of Sarangani, Philippines: Mindanao again.</li><li>M6.4 45 km SSW of Jurm, Afghanistan: no note yet</li><li>M6.2 8 km SW of Anamizu, Japan: no note yet</li>
[computed]

One answer from three kinds of source: two recorded weeks of USGS’s, read as a graph; a template of yours; and your notes, one file each. (The rows come out on one line, as HTML list items: a line typed at the prompt cannot hold a line break, so the template cannot end with one.)

Ask what hangs from a note, and from a note that does not exist yet:

ikigai> source urn:kernel:dependents thread=urn:file:notes/us6000m0xl.txt
dependents of urn:file:notes/us6000m0xl.txt  (cut 2 times)
  2 live cached entries hang from it — a cut makes them stale, and the next read of each recomputes it
  urn:file:notes/us6000m0xl.txt  text/plain                     49 B  1 thread    root
  urn:iki:fn:each                text/plain                    276 B  8 threads   root

[uncacheable]
ikigai> source urn:kernel:dependents thread=urn:file:notes/us6000m31m.txt
dependents of urn:file:notes/us6000m31m.txt  (cut 0 times)
  1 live cached entry hangs from it — a cut makes it stale, and the next read of each recomputes it
  urn:iki:fn:each  text/plain                    276 B  8 threads   root

[uncacheable]

The list hangs from the note it spliced (its eight threads are the four notes it looked for, the fallback, the template, the JSON and its own name), and also from the note it looked for and did not find: the Jurm earthquake’s, us6000m31m. Write that note, and the list is stale; the CSV and the JSON under it are not, because no note is one of their sources:

ikigai> sink urn:file:notes/us6000m31m.txt East of Herat.
wrote 14 bytes to notes/us6000m31m.txt
[uncacheable]
ikigai> cache urn:iki:fn:each src=urn:file:notable.json template=urn:file:row.txt
not cached
ikigai> cache urn:json:query src=urn:file:notable.csv q=.
cached
ikigai> source urn:iki:fn:each src=urn:file:notable.json template=urn:file:row.txt
<li>M7.5 2024 Noto Peninsula, Japan Earthquake: Felt in Kanazawa. The tsunami warning came first.</li><li>M6.7 93 km SE of Sarangani, Philippines: Mindanao again.</li><li>M6.4 45 km SSW of Jurm, Afghanistan: East of Herat.</li><li>M6.2 8 km SW of Anamizu, Japan: no note yet</li>
[computed]

The files in the middle are where this differs from the page. notable.json is a copy: it does not hang from the weeks, and it changes only when you write it again. In the page, urn:quakes:strong is a name, not a copy, and a cut of what it read reaches it.

7. What a composite costs

A composite is cached for as long as everything it read is. The recorded weeks and your notes change only when someone writes them. The live feed from Somebody else’s resource is good for 60 seconds, because USGS says so. Put the feed into a composite and the composite is good for 60 seconds too.

These steps reach the network, so the book’s checks cannot replay them. They are what ikigai-cli 0.2.5 printed on 2026-10-29. Make the list a page fragment of its own, then join the live feed into it. A template can splice the feed by its URL, through urn:http:get; these steps count the answer’s lines with urn:text:wc rather than print 130 KB:

sink urn:file:report.txt <ul>$r{urn:iki:fn:each?src=urn:file:notable.json&template=urn:file:row.txt}</ul>
sink urn:file:report.txt <ul>$r{urn:iki:fn:each?src=urn:file:notable.json&template=urn:file:row.txt}</ul> $r{urn:http:get?url=https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson}
source urn:iki:fn:compose src=urn:file:report.txt | urn:text:wc
cache urn:iki:fn:compose src=urn:file:report.txt
wrote 80 bytes to report.txt
[uncacheable]
wrote 176 bytes to report.txt
[uncacheable]
196
[2 computed]
cached

A minute later:

cache urn:iki:fn:compose src=urn:file:report.txt
cache urn:iki:fn:each src=urn:file:notable.json template=urn:file:row.txt
source urn:kernel:uncached
not cached
cached
uncached (why a computed read was not cached; the last 64, most recent first)
  urn:json:from-csv      ×1  policy  [root]

[uncacheable]

The report expired with the feed. The list inside it did not: it is a name of its own, and it read no feed. Everything that reads the report now expires every minute too, however expensive it is to compute.

urn:kernel:uncached says nothing about it: the one line there is from the CSV conversion at the shell step before. It logs computations that were never stored, and the report was stored; it was stored for a minute. Nothing in the kernel’s readouts says how long an entry has left, so the way to see the lifetime is the one above: ask cache again after it.

The fix is to join volatile data where it costs nothing to recompute: into a resource that is cheap, or already uncacheable, never into the expensive cached one. Put the report back as it was, and splice it by name into a small template of its own that adds the feed:

sink urn:file:report.txt <ul>$r{urn:iki:fn:each?src=urn:file:notable.json&template=urn:file:row.txt}</ul>
sink urn:file:live.txt $r{urn:iki:fn:compose?src=urn:file:report.txt} $r{urn:http:get?url=https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_week.geojson}
source urn:iki:fn:compose src=urn:file:live.txt | urn:text:wc
source urn:iki:fn:compose src=urn:file:report.txt
wrote 80 bytes to report.txt
[uncacheable]
wrote 142 bytes to live.txt
[uncacheable]
196
[2 computed]
<ul><li>M7.5 2024 Noto Peninsula, Japan Earthquake: Felt in Kanazawa. The tsunami warning came first.</li><li>M6.7 93 km SE of Sarangani, Philippines: Mindanao again.</li><li>M6.4 45 km SSW of Jurm, Afghanistan: East of Herat.</li><li>M6.2 8 km SW of Anamizu, Japan: no note yet</li></ul>
[cached]

The report was cached already: filling live.txt read it by name, and that read kept it. A minute later:

cache urn:iki:fn:compose src=urn:file:report.txt
cache urn:iki:fn:compose src=urn:file:live.txt
source urn:iki:fn:compose src=urn:file:live.txt | sink urn:file:page.html
cached
not cached
wrote 137620 bytes to page.html
[1 computed · 1 uncacheable]

Only the small template expired. The report it splices was still cached, so the next read computed one splice and one fetch, not the list. The write at the end is uncacheable anyway, which is the last place volatile data can go: a write happens every time it is asked for.

What each layer costs is what it reads. A layer that reads only recorded data and your files lasts until one of them is written; one that reads the feed lasts a minute; and a layer that reads another by name pays only for its own work when it is recomputed. Later in this part, a page built from them is served over HTTP.

Next, the copies in the middle get refreshed by a file of REPL lines instead of by hand, and a pipeline becomes a plan you can question before it runs: Process scripts are files.