<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ldes.tech/news/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ldes.tech/" rel="alternate" type="text/html" /><updated>2026-09-09T10:16:15+00:00</updated><id>https://ldes.tech/news/feed.xml</id><title type="html">LDES</title><subtitle>Linked Data Event Streams (LDES): an open specification for publishing append-only collections of immutable data on the Web, so consumers can replicate data and follow updates.</subtitle><entry><title type="html">LDES at SEMANTiCS 2026: a monitoring platform and a construction-data streaming demo</title><link href="https://ldes.tech/2026/09/09/semantics-2026-grmp-and-construction-demo.html" rel="alternate" type="text/html" title="LDES at SEMANTiCS 2026: a monitoring platform and a construction-data streaming demo" /><published>2026-09-09T00:00:00+00:00</published><updated>2026-09-09T00:00:00+00:00</updated><id>https://ldes.tech/2026/09/09/semantics-2026-grmp-and-construction-demo</id><content type="html" xml:base="https://ldes.tech/2026/09/09/semantics-2026-grmp-and-construction-demo.html"><![CDATA[<p>Two LDES-related contributions have been accepted for <a href="https://2026-eu.semantics.cc/">SEMANTiCS 2026</a>, the 21st International Conference on Semantic Systems: an industry-track talk about monitoring linked data resources, and a demo about streaming construction data that already comes with a great video.</p>

<h2 id="industry-track-grmp-a-platform-to-monitor-linked-data-resources">Industry track: GRMP, a platform to monitor linked data resources</h2>

<p><strong>GRMP: An open-source platform for automatic monitoring of linked data resources on the web</strong>, by Stijn Denis, Marc Portier, Cedric Decruw and Stijn Deknudt, has been accepted in the <a href="https://2026-eu.semantics.cc/page/accepted_industry.html">SEMANTiCS 2026 industry track</a>.</p>

<blockquote>
  <p>When publishing linked data-related resources through the web, it is strongly advised to actively monitor them and ensure they remain accessible, up-to-date and continue to provide data in the intended manner. Public registries and dashboards do exist, but they require active maintenance themselves lest they become outdated and provide incorrect information regarding the status of these resources. In order to support the management of their own LDES servers and SPARQL endpoints and monitor the status of external web resources they consume, the scientific institute VLIZ (Vlaams Instituut voor de Zee – Flanders Marine Institute) developed GRMP (Graph Resources Monitoring Platform) in cooperation with Flemish IT and software company Sirus. GRMP is an open-source and highly modular platform which allows for the automatic monitoring of linked data resources and web resources in general. GRMP is container-based and primarily consists of an orchestrator which manages and directs test suite containers based on configurations defined in one or more YAML files. Due to its modular nature, users can rapidly develop new test suites in any desired programming language, provided that they have been containerized and adhere to a few guidelines allowing for proper communication with the orchestrator. Additionally, the environment on top of which the system runs can be freely chosen, ranging from local deployments to integration with automation workflows like GitHub Actions. When running within GitHub Actions, additional features are available, such as automatic issue creation resulting from failed tests.</p>
</blockquote>

<p>This is directly relevant to everyone running an <a href="/examples/">LDES feed among our examples</a>: keeping a feed reachable, well-formed and up-to-date over time is exactly the kind of maintenance burden that motivated LDES’s append-only, cache-friendly design in the first place, and GRMP tackles the operational side of that promise. VLIZ, incidentally, is also the publisher of the <a href="/examples/">Marine Regions Gazetteer LDES</a>, one of the longest-running production feeds among our examples.</p>

<aside class="paper-citation" aria-label="Related research">
  <p class="paper-citation-heading">Read the paper and cite this work</p>
  <p class="paper-citation-context">For the research behind the Marine Regions feed mentioned above, read the original deployment paper.</p>
  <div typeof="schema:ScholarlyArticle" resource="https://ceur-ws.org/Vol-2969/paper8-s4biodiv.pdf">
    <p class="paper-citation-title"><cite property="schema:name"><a href="https://ceur-ws.org/Vol-2969/paper8-s4biodiv.pdf">Publishing the Marine Regions Gazetteer as a Linked Data Event Stream</a></cite></p>
    <p class="paper-citation-meta">S4BioDiv 2021, CEUR Workshop Proceedings, vol. 2969 &middot; 2021</p>
    <details class="paper-citation-details">
      <summary>Cite this work</summary>
      <p class="paper-citation-text">Lonneville, B., Delva, H., Portier, M., Van Maldeghem, L., Schepers, L., Bakeev, D., Vanhoorne, B., Tyberghein, L., and Colpaert, P. (2021). <cite>Publishing the Marine Regions Gazetteer as a Linked Data Event Stream</cite>. S4BioDiv 2021, CEUR Workshop Proceedings, vol. 2969. <a href="https://ceur-ws.org/Vol-2969/paper8-s4biodiv.pdf">https://ceur-ws.org/Vol-2969/paper8-s4biodiv.pdf</a></p>
      <p><a href="https://ceur-ws.org/Vol-2969/paper8-s4biodiv.pdf">Publication record</a></p>
    </details>
  </div>
  <a class="paper-citation-discover" href="/publications/">Discover other papers &rarr;</a>
</aside>

<h2 id="a-demo-worth-watching-streaming-construction-data-with-ldes">A demo worth watching: streaming construction data with LDES</h2>

<p>Also accepted at SEMANTiCS 2026 is a demo on <strong>Streaming Interoperable Construction Data with LDES</strong>, by Carlos Ramonell and Mathias Bonduel, which comes with a genuinely nice explainer video:</p>

<div class="iframe-container">
  <iframe src="https://www.youtube.com/embed/bX0UqZ7oGwI" title="Streaming Interoperable Construction Data with LDES" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</div>

<p>You can find the full entry, keywords and any accompanying materials on the <a href="https://2026-eu.semantics.cc/page/p%26d-detail?page=60">conference’s demo page</a>.</p>

<p>Both contributions are a good reminder that LDES keeps finding new domains — from marine biodiversity data to construction information management — united by the same simple idea: publish an append-only, cacheable stream once, and let every consumer replicate and stay in sync at their own pace.</p>

<p>See you at SEMANTiCS 2026!</p>]]></content><author><name>The ldes.tech editors</name></author><category term="LDES" /><category term="SEMANTiCS" /><summary type="html"><![CDATA[LDES at SEMANTiCS 2026: monitoring data feeds with GRMP and sharing construction data through event streams.]]></summary></entry><entry><title type="html">RDF Messages reaches its first draft</title><link href="https://ldes.tech/2026/06/21/rdf-messages-first-draft.html" rel="alternate" type="text/html" title="RDF Messages reaches its first draft" /><published>2026-06-21T00:00:00+00:00</published><updated>2026-06-21T00:00:00+00:00</updated><id>https://ldes.tech/2026/06/21/rdf-messages-first-draft</id><content type="html" xml:base="https://ldes.tech/2026/06/21/rdf-messages-first-draft.html"><![CDATA[<p><strong>RDF Messages reached a first draft on 21 June 2026.</strong> The <a href="https://w3c-cg.github.io/rsp/spec/messages">living document</a>, edited by Pieter Colpaert and Piotr Sowiński, describes how RDF data can be exchanged as distinct messages, streams of messages, and stored message logs.</p>

<p>An RDF message can contain several triples or quads that belong together and should be interpreted as one unit. Making those boundaries explicit helps software preserve the intended meaning as data moves between systems.</p>

<p>The work draws inspiration from Piotr Sowiński’s keynote at the LDES workshop during SEMIC 2025, where Jelly and LDES were discussed as technologies that could share a common foundation. <a href="https://pietercolpaert.be/conferences/2025/11/28/semic-trip-report.html">Pieter’s conference report</a> captures that conversation.</p>

<p>For LDES, this is a promising avenue for future work: a shared message model could help connect event streams with other RDF processing and transport tools. These are possibilities to explore, not requirements introduced into LDES by this draft.</p>

<p>Read the <a href="https://w3c-cg.github.io/rsp/spec/messages">RDF Messages draft</a> and join the discussion in the <a href="https://github.com/w3c-cg/rsp">RDF Stream Processing Community Group repository</a>.</p>]]></content><author><name>The ldes.tech editors</name></author><category term="RDF Messages" /><category term="LDES" /><summary type="html"><![CDATA[A first draft dated 21 June 2026 explores a common foundation for exchanging RDF messages, with promising possibilities for future LDES work.]]></summary></entry><entry><title type="html">LDES 1.0 launches at SEMIC 2025</title><link href="https://ldes.tech/2025/11/25/ldes-1-launches-at-semic.html" rel="alternate" type="text/html" title="LDES 1.0 launches at SEMIC 2025" /><published>2025-11-25T00:00:00+00:00</published><updated>2025-11-25T00:00:00+00:00</updated><id>https://ldes.tech/2025/11/25/ldes-1-launches-at-semic</id><content type="html" xml:base="https://ldes.tech/2025/11/25/ldes-1-launches-at-semic.html"><![CDATA[<p>LDES 1.0 launched at the LDES workshop during <strong>SEMIC 2025 on 25 November in Copenhagen</strong>. The session brought together specification work, working implementations, and plans for adoption across public administrations and cultural heritage.</p>

<p>Pieter Colpaert presented the updated specification after a year of development. The workshop also featured catalog harvesting, a cultural heritage server, water data, a .NET implementation, and RDF-Connect pipelines. Together, these contributions showed how a common publication interface can support different domains and software stacks.</p>

<p>Piotr Sowiński’s workshop keynote connected his work on Jelly with the emerging RDF Messages collaboration, opening a discussion about shared foundations for exchanging RDF streams.</p>

<p>For publishers and consumers, the next step is practical: read the <a href="https://w3id.org/ldes/specification">LDES specification</a>, explore the <a href="https://github.com/SEMICeu/LDES-implementation-reports">implementation reports</a>, and try an <a href="/examples/">example feed</a>.</p>

<p>Read <a href="https://pietercolpaert.be/conferences/2025/11/28/semic-trip-report.html">Pieter’s SEMIC 2025 report</a> for the workshop recap, or visit the <a href="https://interoperable-europe.ec.europa.eu/collection/semic-support-centre/semic-conference">SEMIC conference archive</a> for the event materials.</p>]]></content><author><name>The ldes.tech editors</name></author><category term="LDES" /><category term="SEMIC" /><summary type="html"><![CDATA[The SEMIC workshop in Copenhagen marks the launch of LDES 1.0 and brings together the people putting it into practice.]]></summary></entry><entry><title type="html">Incremental Knowledge Graph generation with YARRRML</title><link href="https://ldes.tech/2024/12/19/incrml-with-yarrrml.html" rel="alternate" type="text/html" title="Incremental Knowledge Graph generation with YARRRML" /><published>2024-12-19T00:00:00+00:00</published><updated>2024-12-19T00:00:00+00:00</updated><id>https://ldes.tech/2024/12/19/incrml-with-yarrrml</id><content type="html" xml:base="https://ldes.tech/2024/12/19/incrml-with-yarrrml.html"><![CDATA[<p>Knowledge graphs are the basis for data integration, power search engines and are the best fit to train AI models upon.
They are designed to be published on the web (public or private), so consumers can easily access and query these
knowledge graphs.
Knowledge graphs are typically constructed from various data sources spread across organisations, departments,
governments, the web…</p>

<p>Let’s introduce a simple example. Consider this table, representing who follows who on a social network:</p>

<table>
  <thead>
    <tr>
      <th>Person</th>
      <th>Follows</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bob</td>
      <td>Alice</td>
    </tr>
    <tr>
      <td>Jeff</td>
      <td>Bob</td>
    </tr>
    <tr>
      <td>Jeff</td>
      <td>Alice</td>
    </tr>
  </tbody>
</table>

<p>This can be represented by the following knowledge graph:</p>

<pre><code class="language-mermaid">flowchart LR
    bob[Bob]
    alice[Alice]
    jeff[Jeff]
    bob -- follows --&gt; alice
    jeff -- follows --&gt; bob
    jeff -- follows --&gt; alice
</code></pre>

<p>This graph can be broken down in simple statements describing objects and their relations, called <em>triples</em>. The example
consists of the following triples:</p>

<pre><code class="language-mermaid">flowchart LR
    bob[Bob]
    jeff[Jeff]
    jeff -- follows --&gt; bob
</code></pre>
<pre><code class="language-mermaid">flowchart LR
    bob[Bob]
    alice[Alice]
    bob -- follows --&gt; alice
</code></pre>
<pre><code class="language-mermaid">flowchart LR
    alice[Alice]
    jeff[Jeff]
    jeff -- follows --&gt; alice
</code></pre>

<h2 id="generating-a-knowledge-graph">Generating a Knowledge Graph</h2>

<p>Various specifications and software tools exist to build knowledge graphs.
A widely adopted specification is the RDF Mapping Language (<a href="https://rml.io/specs/rml/">RML</a>, <a href="https://rml.io">website</a>) 
which uses a declarative approach.
One can describe how sources can be accessed and how data records can be extracted.
Next, rules describe how these records can be turned into triples.
Finally, there are rules describing where the generated graph can be written to. This is called a target.</p>

<p>In RML the rules to convert the table to the knowledge graph look like:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">ex:</span><span class="w"> </span><span class="nl">&lt;http://example.com/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">rml:</span><span class="w"> </span><span class="nl">&lt;http://w3id.org/rml/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nl">&lt;http://example.com/base/TriplesMap1&gt;</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">rml:</span><span class="n">TriplesMap</span><span class="p">;</span><span class="w">

  </span><span class="nn">rml:</span><span class="n">logicalSource</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">rml:</span><span class="n">LogicalSource</span><span class="p">;</span><span class="w">
      </span><span class="nn">rml:</span><span class="n">referenceFormulation</span><span class="w"> </span><span class="nn">rml:</span><span class="n">CSV</span><span class="p">;</span><span class="w">
      </span><span class="nn">rml:</span><span class="n">source</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">rml:</span><span class="n">RelativePathSource</span><span class="p">;</span><span class="w">
          </span><span class="nn">rml:</span><span class="n">root</span><span class="w"> </span><span class="nn">rml:</span><span class="n">MappingDirectory</span><span class="p">;</span><span class="w">
          </span><span class="nn">rml:</span><span class="n">path</span><span class="w"> </span><span class="s">"data.csv"</span><span class="w">
        </span><span class="p">]</span><span class="w">
    </span><span class="p">];</span><span class="w">

  </span><span class="nn">rml:</span><span class="n">subjectMap</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="nn">rml:</span><span class="n">template</span><span class="w"> </span><span class="s">"http://example.com/{Person}"</span><span class="w">
    </span><span class="p">]</span><span class="w"> </span><span class="p">;</span><span class="w">

  </span><span class="nn">rml:</span><span class="n">predicateObjectMap</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="nn">rml:</span><span class="n">predicate</span><span class="w"> </span><span class="nn">ex:</span><span class="n">follows
</span><span class="w">      </span><span class="nn">rml:</span><span class="n">objectMap</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="nn">rml:</span><span class="n">template</span><span class="w"> </span><span class="s">"http://example.com/{Follows}"</span><span class="w">
        </span><span class="p">];</span><span class="w">
   </span><span class="p">].</span><span class="w">
</span></code></pre></div></div>
<p>Let’s not dive into detail too much, but what this mapping does is:</p>
<ul>
  <li>The <code class="language-plaintext highlighter-rouge">@prefix</code> statements map an abbreviation to a IRI.</li>
  <li>The <code class="language-plaintext highlighter-rouge">rml:logicalSource</code> part sais that the data comes from a file called <code class="language-plaintext highlighter-rouge">data.csv</code> (the table of the example)</li>
  <li>The <code class="language-plaintext highlighter-rouge">rml:subjectMap</code> assigns a unique IRI to a Person; this is required for knowledge graphs represented in
<a href="https://www.w3.org/TR/rdf-primer/">RDF</a></li>
  <li>The <code class="language-plaintext highlighter-rouge">rml:predicateObjectMap</code> makes a connection to another person in the <code class="language-plaintext highlighter-rouge">Follows</code> column, and assigns a label or
property <code class="language-plaintext highlighter-rouge">ex:follows</code> to the connection indicating the type of relation.</li>
</ul>

<p>Because this is a bit verbose, another language <a href="https://rml.io/yarrrml/spec/">YARRRML</a> was designed to make such 
mapping rules a bit more intuitive. The same result could be achieved with the following rules in YARRRML:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">prefixes</span><span class="pi">:</span>
  <span class="na">ex</span><span class="pi">:</span> <span class="s">http://example.com/</span>

<span class="na">mappings</span><span class="pi">:</span>
  <span class="na">followers</span><span class="pi">:</span>
    <span class="na">sources</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">data.csv~csv</span><span class="pi">]</span>
    <span class="na">subject</span><span class="pi">:</span> <span class="s">http://example.com/$(Person)</span>
    <span class="na">predicateobjects</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">[</span><span class="nv">ex</span><span class="pi">:</span><span class="nv">follows</span><span class="pi">,</span> <span class="nv">http</span><span class="pi">:</span><span class="nv">//example.com/$(Person)</span><span class="pi">]</span>
</code></pre></div></div>
<p>A YARRRML processor takes these rules and generates the corresponding RML rules.</p>

<h2 id="all-about-change-ldes">All about change: LDES</h2>

<p>Now that we know how to generate a Knowledge Graph, let’s add a challenge. After a while Alice starts to follow Jeff.
The knowledge graph now looks like this:</p>

<pre><code class="language-mermaid">flowchart LR
    bob[Bob]
    alice[Alice]
    jeff[Jeff]
    bob -- follows --&gt; alice
    jeff -- follows --&gt; bob
    jeff -- follows --&gt; alice
    alice -- follows --&gt; jeff
</code></pre>

<p>A possible approach would be to just rebuild the whole knowledge
graph. No need to change the mapping rules.
And suppose we published this graph, we would have to notify the consumers that the graph changed. They would have to
download the whole graph to have the latest changes. This works well for small graphs, but is not very scalable.</p>

<p>Enter Linked Data Event Streams (<a href="https://w3id.org/ldes/specification">LDES</a>), a mechanism to publish changing
knowledge graphs on the web.
In short, LDES can be described as a collection of immutable data. 
New versions of the data can be added to the collection, forming a stream of updates.
This allows consumers of the data to keep up-to-date with minimal effort.</p>

<p>Furthermore, LDES allows <em>fragmentation</em> of the data making search more efficient by providing a
search tree view. Fragments that are unlikely to change can be  cached, e.g. using HTTP caching.</p>

<pre><code class="language-mermaid">flowchart TD
    index["/"]
    2024[2024]
    2025[2025]
    12[December]
    01[January]
    02[February]
    data_1[ ]
    data_2[ ]
    data_3[ ]
    data_4[ ]
    data_5[ ]
    data_6[ ]

    index --&gt; 2024
    index --&gt; 2025
    2024 --&gt; 12
    2025 --&gt; 01
    2025 --&gt; 02
    12 --&gt; data_1
    12 --&gt; data_2
    01 --&gt; data_3
    01 --&gt; data_4
    01 --&gt; data_5
    02 --&gt; data_6
</code></pre>

<p>This figure illustrates a LDES.
The blocks at the bottom represent data objects (called <em>members</em>).
In this case time-based hierarchical fragmentation is used.
The fragments representing a month point to its members.
Fragments can be linked (using the <a href="https://w3id.org/tree/specification">TREE</a> hypermedia controls).</p>

<h2 id="generating-a-changing-knowledge-graph">Generating a <em>changing</em> Knowledge Graph</h2>

<p>So now we have the tools to generate a knowledge graph, and we know how to publish a knowledge graph that changes.
But if the data changes, how to generate the changes <em>only</em> without rebuilding the whole graph?</p>

<p><a href="https://www.semantic-web-journal.net/content/incrml-incremental-knowledge-graph-construction-heterogeneous-data-sources">IncRML</a>
describes how this can be achieved using RML.
It introduces some functions that can, during the mapping process, detect changes in the data.
One can specify when a change in the data triggers the generation of new triples (thus updates of the knowledge graph).
Three types of changes are defined: <em>create</em>, <em>update</em> and <em>delete</em>.
As a bonus, a target is added that can write the changes as a LDES.
It requires a certain effort to get it right: every type of change (for the same data) needs a new set of rules, with
the appropriate function arguments configured.</p>

<p>To lower this effort, we recently introduced a <a href="https://rml.io/yarrrml/spec/incrml/">YARRRML extension</a> that lowers
the complexity by generating the appropriate RML mappings and functions.</p>

<p>Back to the example. We started with a csv file having three rows:</p>

<table>
  <thead>
    <tr>
      <th>Person</th>
      <th>Follows</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bob</td>
      <td>Alice</td>
    </tr>
    <tr>
      <td>Jeff</td>
      <td>Bob</td>
    </tr>
    <tr>
      <td>Jeff</td>
      <td>Alice</td>
    </tr>
  </tbody>
</table>

<p>Alice decides to follow Jeff, so the file gets updated:</p>

<table>
  <thead>
    <tr>
      <th>Person</th>
      <th>Follows</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bob</td>
      <td>Alice</td>
    </tr>
    <tr>
      <td>Jeff</td>
      <td>Bob</td>
    </tr>
    <tr>
      <td>Jeff</td>
      <td>Alice</td>
    </tr>
    <tr>
      <td>Alice</td>
      <td>Jeff</td>
    </tr>
  </tbody>
</table>

<p>We somehow know that the data source is updated, but we are not notified about <em>what</em> exactly changed.
The only way to know is to read the file again, then we see a new record is created.
This is called an <em>implicit</em> change.</p>

<p>Another possibility is that only the changes are advertised, which is called <em>explicit</em> changes.
For example, a Kafka topic could stream only updates in JSON format:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"change"</span><span class="p">:</span><span class="w"> </span><span class="s2">"create"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Person"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Alice"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Follows"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Jeff"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>But let’s take the implicit change as an example. We can specify this in our YARRRML mappings like this:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">prefixes</span><span class="pi">:</span>
  <span class="na">ex</span><span class="pi">:</span> <span class="s">http://example.com/</span>

<span class="na">mappings</span><span class="pi">:</span>
  <span class="na">followers</span><span class="pi">:</span>
    <span class="na">sources</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">data.csv~csv</span><span class="pi">]</span>
    <span class="na">subject</span><span class="pi">:</span> <span class="s">http://example.com/$(Person)</span>
    <span class="na">predicateobjects</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">[</span><span class="nv">ex</span><span class="pi">:</span><span class="nv">follows</span><span class="pi">,</span> <span class="nv">http</span><span class="pi">:</span><span class="nv">//example.com/$(Person)</span><span class="pi">]</span>
    <span class="na">changeDetection</span><span class="pi">:</span>
      <span class="na">create</span><span class="pi">:</span>
        <span class="na">explicit</span><span class="pi">:</span> <span class="kc">false</span>
</code></pre></div></div>
<p>When the first version of the data is mapped to a knowledge graph, the three triples displayed at the top
of this post are generated. When the second version is mapped, only one triple gets generated:</p>

<pre><code class="language-mermaid">flowchart LR
    alice[Alice]
    jeff[Jeff]
    alice -- follows --&gt; jeff
</code></pre>
<p>If we add an LDES target, everything will be mapped as a stream of changes:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">prefixes</span><span class="pi">:</span>
  <span class="na">ex</span><span class="pi">:</span> <span class="s">http://example.com/</span>

<span class="na">mappings</span><span class="pi">:</span>
  <span class="na">followers</span><span class="pi">:</span>
    <span class="na">sources</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">data.csv~csv</span><span class="pi">]</span>
    <span class="na">subject</span><span class="pi">:</span> 
      <span class="pi">-</span> <span class="na">value</span><span class="pi">:</span> <span class="s">http://example.com/$(Person)</span>
        <span class="na">targets</span><span class="pi">:</span>
          <span class="na">access</span><span class="pi">:</span> <span class="s">out-ldes.ttl</span>
          <span class="na">type</span><span class="pi">:</span> <span class="s">localfile</span>
          <span class="na">serialization</span><span class="pi">:</span> <span class="s">turtle</span>
          <span class="na">ldes</span><span class="pi">:</span>
            <span class="na">id</span><span class="pi">:</span> <span class="s">https://my-ldes.org/social-network-ldes</span>
    <span class="na">predicateobjects</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="pi">[</span><span class="nv">ex</span><span class="pi">:</span><span class="nv">follows</span><span class="pi">,</span> <span class="nv">http</span><span class="pi">:</span><span class="nv">//example.com/$(Person)</span><span class="pi">]</span>
    <span class="na">changeDetection</span><span class="pi">:</span>
      <span class="na">create</span><span class="pi">:</span>
        <span class="na">explicit</span><span class="pi">:</span> <span class="kc">false</span>
</code></pre></div></div>
<p>When mapping the first version of the data with an LDES target, the knowledge graph looks like:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">:</span><span class="n">Bob</span><span class="w"> </span><span class="nn">:</span><span class="n">follows</span><span class="w"> </span><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="nn">:</span><span class="n">Jeff</span><span class="w"> </span><span class="nn">:</span><span class="n">follows</span><span class="w"> </span><span class="nn">:</span><span class="n">Bob,
</span><span class="w">               </span><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">my-ldes:</span><span class="n">social-network-ldes
</span><span class="w">  </span><span class="k">a</span><span class="w"> </span><span class="nn">ldes:</span><span class="n">EventStream</span><span class="p">;</span><span class="w">
  </span><span class="nn">tree:</span><span class="n">member</span><span class="w"> </span><span class="nn">:</span><span class="n">Bob,
</span><span class="w">              </span><span class="nn">:</span><span class="n">Jeff</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>The second run with the updated data generates following triples:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="nn">:</span><span class="n">follows</span><span class="w"> </span><span class="nn">:</span><span class="n">Jeff</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">my-ldes:</span><span class="n">social-network-ldes
</span><span class="w">  </span><span class="k">a</span><span class="w"> </span><span class="nn">ldes:</span><span class="n">EventStream</span><span class="p">;</span><span class="w">
  </span><span class="nn">tree:</span><span class="n">member</span><span class="w"> </span><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>When merged together the complete knowledge graph is formed:</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">:</span><span class="n">Bob</span><span class="w"> </span><span class="nn">:</span><span class="n">follows</span><span class="w"> </span><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="nn">:</span><span class="n">Jeff</span><span class="w"> </span><span class="nn">:</span><span class="n">follows</span><span class="w"> </span><span class="nn">:</span><span class="n">Bob,
</span><span class="w">               </span><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="nn">:</span><span class="n">follows</span><span class="w"> </span><span class="nn">:</span><span class="n">Jeff</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">my-ldes:</span><span class="n">social-network-ldes
</span><span class="w">  </span><span class="k">a</span><span class="w"> </span><span class="nn">ldes:</span><span class="n">EventStream</span><span class="p">;</span><span class="w">
  </span><span class="nn">tree:</span><span class="n">member</span><span class="w"> </span><span class="nn">:</span><span class="n">Bob,
</span><span class="w">              </span><span class="nn">:</span><span class="n">Jeff,
</span><span class="w">              </span><span class="nn">:</span><span class="n">Alice</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>This is only a simple example. A few more elaborate examples are available on
<a href="https://github.com/ghsnd/incrml-generation-in-yarrrml-examples">GitHub</a>.
The corresponding RML examples can be found
<a href="https://github.com/RMLio/rmlmapper-java/tree/master/src/test/resources/rml-ldes">here</a>.</p>]]></content><author><name>Gerald Haesendonck</name></author><category term="LDES" /><summary type="html"><![CDATA[Generate updates to a knowledge graph with YARRRML and publish the changes as a Linked Data Event Stream.]]></summary></entry><entry><title type="html">Growing an LDES: more data, faster updates, more consumers</title><link href="https://ldes.tech/2024/08/06/scalability.html" rel="alternate" type="text/html" title="Growing an LDES: more data, faster updates, more consumers" /><published>2024-08-06T00:00:00+00:00</published><updated>2026-09-09T00:00:00+00:00</updated><id>https://ldes.tech/2024/08/06/scalability</id><content type="html" xml:base="https://ldes.tech/2024/08/06/scalability.html"><![CDATA[<p>A museum collection grows over decades. A sensor produces observations every minute. A public dataset suddenly attracts thousands of consumers. Each puts a different kind of pressure on a data publisher.</p>

<p>“Does LDES scale?” becomes more useful when we ask what is growing and what consumers need. The answer guides decisions about pages, caching, retention, and processing capacity.</p>

<h2 id="shared-source">A shared source, independent views</h2>

<p>LDES lets a publisher expose data and its changes through a common interface. Consumers can copy the available members, follow updates, and maintain their own databases, search indexes, or APIs. Query work can then happen on those consumer views.</p>

<p>A typical publication splits the stream into linked pages. A time-based view might lead from years to months and then to pages of members. A new consumer traverses the available history; a returning consumer follows the part relevant to its next updates. The <a href="https://w3id.org/ldes/specification">LDES specification</a> defines the contract between those publishers and consumers.</p>

<p>That design gives us several ways to manage growth. Each comes with a tradeoff.</p>

<div class="article-table">

  <table>
    <thead>
      <tr>
        <th>What is growing?</th>
        <th>What helps?</th>
        <th>What to watch</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Stored history</td>
        <td>Compression, page sizing, retention</td>
        <td>How much history a new or returning client can recover</td>
      </tr>
      <tr>
        <td>Update rate</td>
        <td>Processing capacity, sensible polling</td>
        <td>End-to-end delay and a growing backlog</td>
      </tr>
      <tr>
        <td>Consumer numbers</td>
        <td>Shared HTTP caches and reusable pages</td>
        <td>Requests reaching the origin server</td>
      </tr>
      <tr>
        <td>Dataset numbers</td>
        <td>Shared vocabularies and application profiles</td>
        <td>Mapping and validation work per source</td>
      </tr>
    </tbody>
  </table>

</div>

<h2 id="more-data">More data: make history affordable</h2>

<p>Start with the bytes you actually transfer. HTTP compression can reduce the cost of textual RDF, but the saving depends on the data and format. Measure compressed transfer size alongside parsing time: smaller responses do not automatically mean faster processing.</p>

<p>Page size is another balancing act. Small pages can make following recent changes efficient, but require more requests during a full copy. Large pages can reduce request overhead while making clients download more data than they need. Try both a first-time harvest and an incremental update when choosing a size.</p>

<p>Retention is a separate decision. A sensor publisher might keep recent observations available and publish daily aggregates for longer-term analysis. A collection publisher might retain only a subset of record versions in one view. An archive can preserve more history for consumers that need it.</p>

<p>Document that choice. A client that returns after a long pause can only catch up from a view if the necessary data is still available. If it has missed the retention window, it may need to rebuild its view from an appropriate snapshot or another source. Aggregates also answer different questions from raw observations; they should be described as such.</p>

<h2 id="faster-updates">Faster updates: keep the backlog under control</h2>

<p>There are three different measurements here: how quickly members arrive, how quickly a consumer processes them, and how long a change takes to reach an application.</p>

<p>A consumer needs enough sustained throughput to keep up, with spare capacity to recover after interruptions. Count the whole pipeline: downloading, parsing RDF, extracting members, applying transformations, and writing to the destination. A fast parser does not compensate for a slow database write.</p>

<p>For example, suppose a source publishes 100 members per second and a consumer can process 150. After an hour offline, it has 360,000 members to catch up on. While new members keep arriving, only 50 members per second of capacity remains for that backlog. Under those simplified, constant-rate assumptions, catching up takes another two hours.</p>

<p>Polling affects freshness too. A two-minute polling interval alone cannot promise a two-minute end-to-end delay: publication, caching, network time, and downstream processing all contribute. Choose a polling strategy that respects the publisher’s HTTP cache information and leaves room for processing. Conditional requests can avoid retransmitting unchanged responses. See the <a href="https://www.rfc-editor.org/rfc/rfc9111">HTTP caching specification</a>.</p>

<p>A throughput figure from one implementation is a benchmark for that setup, not a limit imposed by LDES. Record the dataset, hardware, page layout, network conditions, and destination alongside any number you publish.</p>

<h2 id="more-consumers">More consumers: reuse the same responses</h2>

<p>If many clients need the same pages, a shared HTTP cache or CDN can serve them without every request reaching the publisher. Consumers still maintain and query their own views; the publisher concentrates on making a reusable source available.</p>

<p>Distinguish immutable members from immutable pages. A page may acquire new members or links even though existing members never change. Only give a response a long cache lifetime when its contents support that promise.</p>

<p>For a finished page, the HTTP <code class="language-plaintext highlighter-rouge">immutable</code> directive tells a cache that the response will not change during its freshness lifetime. It does not mean “never request this URL again.” Mutable pages need cache lifetimes that fit the desired update delay. <a href="https://www.rfc-editor.org/rfc/rfc8246">RFC 8246</a> explains that distinction.</p>

<p>Check the cache hit rate and the requests that still reach your origin. Authentication, personalised responses, and frequently changing pages can reduce the opportunity for shared caching. More subscribers will still use bandwidth somewhere; caching helps distribute that work.</p>

<h2 id="more-datasets">More datasets: reduce integration work</h2>

<p>A common stream interface makes fetching data reusable. Understanding the data remains part of the job.</p>

<p>Shared vocabularies and application profiles let a consumer recognise familiar structures across publishers. SHACL shapes can help express and validate those expectations. A catalog harvester built around a shared profile has less source-specific work to do than one built around every publisher’s individual response format.</p>

<p>The <a href="https://github.com/SEMICeu/LDES-implementation-reports">SEMIC implementation reports</a> show this approach for catalog metadata and cultural heritage. They are useful starting points for deciding what your publishers and consumers should agree on.</p>

<h2 id="what-to-measure">What to measure before you grow</h2>

<p>Run three realistic scenarios: a consumer starting from nothing, a consumer following current updates, and a consumer returning after an outage. Measure transferred bytes, request counts, processing throughput, destination write time, and the delay until a member becomes useful in the application.</p>

<p>Then increase the pressure you expect to face: more history, a burst of updates, more clients, or another dataset. Watch where the work accumulates and adjust that part of the system.</p>

<p>LDES gives publishers and consumers a shared basis for this work. Good scaling comes from matching that design to the data, the available infrastructure, and the freshness consumers actually need.</p>]]></content><author><name>Pieter Colpaert</name></author><category term="LDES" /><summary type="html"><![CDATA[Scale an LDES by understanding four different pressures and choosing where to spend storage, bandwidth, and processing time.]]></summary></entry><entry><title type="html">LDES for interoperability within and across dataspaces</title><link href="https://ldes.tech/2024/07/22/iop-dataspaces.html" rel="alternate" type="text/html" title="LDES for interoperability within and across dataspaces" /><published>2024-07-22T00:00:00+00:00</published><updated>2024-07-22T00:00:00+00:00</updated><id>https://ldes.tech/2024/07/22/iop-dataspaces</id><content type="html" xml:base="https://ldes.tech/2024/07/22/iop-dataspaces.html"><![CDATA[<p>Linked Data Event Streams (LDES) is a relatively new protocol designed for exchanging interoperable datasets <em>within</em> and <em>across</em> dataspaces.
A dataspace is an ecosystem of actors—such as providers, consumers, and intermediary participants—that facilitates data flows to achieve specific goals.</p>

<p>For instance, in Flanders, a <a href="https://www.interregnorthsea.eu/sites/default/files/2024-06/Mobility%20Data%20Space%20Flanders.pdf">mobility dataspace on traffic counts</a> was established to address the fragmented nature of traffic measurements.
The story goes that one organization that needed access to traffic counts on a road segment decided to re-count traffic on a segment that was already counted by another organization.
Setting up the data sharing would have been more tedious than installing rather expensive hardware all over again.
By creating an ecosystem where stakeholders agree on data models, discovery methods, usage control policies, and data exchange APIs, the need for redundant data collection is eliminated.</p>

<p>This concept is applicable not only to fast data but also to slow-moving data, such as base registries or personal data sharing (which is outside the scope of this post).
Examples include the federation of data portals toward <a href="https://data.europa.eu">the European data portal</a>, the creation of a <a href="https://julianrojas.org/papers/iswc2021-in-use/">pan-European railway infrastructure database</a> (Rojas, 2021), creating an overview of all art-works in European museums with <a href="https://www.europeana.eu/en">Europeana</a>.</p>

<p>Each dataspace can decide independently how to achieve interoperability and query answering.
However, with LDES, we provide a specific approach.
As depicted in the figure below, we utilize an architectural style known as <em>Event Sourcing</em> (see <a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing">Microsoft’s</a> and <a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/service-per-team.html">Amazon’s</a> descriptions about it).
This involves describing changes at the source and creating query-able views tailored to consumer needs and usage control policies.
For example, an ANPR camera can write raw number plate data to an event source, which a trusted intermediary participant transforms and republishes according to the agreed-upon data model of the traffic measurements dataspace as aggregated and anonymized traffic counts.</p>

<p><img src="https://docs.google.com/drawings/d/e/2PACX-1vT94isiCM5-M69i7D96-vNFTgBScZ9bsr24vw04fO0Z2a5O3R3qtqxgmuoarMLReuGRlVfBYw-UPpw3/pub?w=981&amp;h=291" alt="A simple pipeline" /></p>

<p>Again, the story can be adapted for any other kind of dataset.
For example, for data portals, we have created the <a href="https://semiceu.github.io/LDES-DCAT-AP-feeds/">DCAT-AP Feeds specification</a> that specifies how a data portal can change, so that aggregators like <em>data.europa.eu</em> can build their derived views incrementally knowing what types of changes it can expect.</p>

<p>For each type of dataset there are always intermediary participants that would like to aggregate datasets, transform them into a model that can be used into a certain dataspace, or provide specialized APIs on top of them.
Intermediary participants can self-regulate such services based on demand: i.e., if an autocompletion API would not be used anymore, it can be brought offline, still allowing others in the ecosystem to take over certain functionalities.
The LDES protocol therefore is focused on enabling intermediary participants, and thus lowering the burden on dataset providers as they do not need to focus on offering all these services on their own.</p>

<p>This requires the participants themselves, at the minimum, to be able to replicate and always stay in-sync with the source of the data.
This is why we designed the simplest LDES views to be chronological search trees.
A client can decide what time extent it wants to replicate, and can then on each synchronization job always skip to the latest data.</p>

<p>When you look at an LDES page, such as <a href="https://era.ilabt.imec.be/rinf/ldes">the one of the European Railway Infrastructure dataset</a>, you would not recognize this to be a “streaming” interface.
The streaming aspects will only come visible from the moment you use an LDES client.
There are LDES client implementations in <a href="https://informatievlaanderen.github.io/VSDS-Linked-Data-Interactions/ldio/ldio-inputs/ldio-ldes-client">Java</a> and in <a href="https://github.com/rdf-connect/ldes-client">TypeScript</a>.
Clients will start from the beginning of the stream and incrementally, while time passes, emit all new updates it can find by following links, polling the page, or even subscribing to a pubsub channel.
Those updates can then be pipelined using tooling such as <a href="https://informatievlaanderen.github.io/VSDS-Linked-Data-Interactions/ldi-nifi/index">Apache Nifi</a>, <a href="https://informatievlaanderen.github.io/VSDS-Linked-Data-Interactions/ldio/index">LDIO</a>, <a href="https://github.com/rdf-connect/">RDF-Connect</a> or <a href="https://github.com/rdf-connect/piveau-consus-importing-ldes">Piveau Consus</a>.</p>

<p>The event stream itself is thus being described in the page: it is defined as an append-only log of objects described using a set of RDF triples.
Those clients will frequently poll the page based on the calculated expiration based on their caching headers.
Based on <a href="https://brechtvdv.github.io/Article-Live-Open-Data-Interfaces/">our earlier research</a> (Van de Vyvere, 2020), polling remains more powerful and cost-efficient for this use case than push-based protocols such as Websockets or Server-Sent Events (SSE) until you want all subscribers to be informed about a change with a latency that is less than ~10 seconds.
In that case, publishers can offer a push-based channel on top of their polling interface that also publishes the history of the stream, although this has not yet been needed in use cases we handled thusfar.</p>

<p>Interoperability and queryability <em>within</em> one dataspace are thus ensured by transforming a source dataset into the right data model, and making the right services available through intermediary participants.
However, in reality, a dataset is going to be useful for multiple dataspaces, requiring interoperability with multiple ecosystems.
If more dataspaces rely on Linked Data and Event Streaming, interoperability <em>across</em> thematic dataspaces would become possible: a data provider could offer the LDES in multiple data models, or intermediary participants can derive multiple views from the same source.</p>

<p>That’s how Linked Data Event Streams (LDES) enables the exchange of interoperable datasets within and across dataspaces by applying event sourcing on Web APIs.
Publishing an LDES yourself is light-weight by design to facilitate adoption.
The responsibility of knowing where you are in the stream is fully up to the subscribers, which can be done with the open-source LDES clients.</p>

<aside class="paper-citation" aria-label="Related research">
  <p class="paper-citation-heading">Read the paper and cite this work</p>
  <p class="paper-citation-context">Related research: the VSDS in-use paper documents how this approach is applied in Flanders.</p>
  <div typeof="schema:ScholarlyArticle" resource="https://doi.org/10.1007/978-3-031-77847-6_16">
    <p class="paper-citation-title"><cite property="schema:name"><a href="https://pietercolpaert.be/papers/preprint-iswc2024-vanlancker-vsds.pdf">Semantic and technically interoperable data exchange in the Flanders Smart Data Space</a></cite></p>
    <p class="paper-citation-meta">The Semantic Web – ISWC 2024, Part III (in-use track; proceedings published in 2025) &middot; 2025</p>
    <details class="paper-citation-details">
      <summary>Cite this work</summary>
      <p class="paper-citation-text">Van Lancker, D., Logghe, S., Rojas, J., De Craene, A., Vanlishout, Z., and Colpaert, P. (2025). <cite>Semantic and technically interoperable data exchange in the Flanders Smart Data Space</cite>. The Semantic Web – ISWC 2024, Part III (in-use track; proceedings published in 2025), pp. 289–303. <a href="https://doi.org/10.1007/978-3-031-77847-6_16">https://doi.org/10.1007/978-3-031-77847-6_16</a></p>
      <p><a href="https://biblio.ugent.be/publication/01JEZJ1JJBBHX7X2RXF1VB24GC">Publication record</a></p>
    </details>
  </div>
  <a class="paper-citation-discover" href="/publications/">Discover other papers &rarr;</a>
</aside>]]></content><author><name>Pieter Colpaert</name></author><category term="LDES" /><summary type="html"><![CDATA[How Linked Data Event Streams help participants exchange and reuse changing data within and across dataspaces.]]></summary></entry><entry><title type="html">How to read a Linked Data Event Stream</title><link href="https://ldes.tech/2023/01/23/how-to-read-an-ldes.html" rel="alternate" type="text/html" title="How to read a Linked Data Event Stream" /><published>2023-01-23T00:00:00+00:00</published><updated>2023-01-23T00:00:00+00:00</updated><id>https://ldes.tech/2023/01/23/how-to-read-an-ldes</id><content type="html" xml:base="https://ldes.tech/2023/01/23/how-to-read-an-ldes.html"><![CDATA[<p>A <a href="https://w3id.org/ldes/specification">Linked Data Event Stream (LDES)</a> is a collection of immutable events, representing changes that occur in a certain dataset/data collection.
An event, also referred to as a member, is represented as a group of triples within the <a href="https://www.w3.org/TR/rdf11-concepts/">Representation Description Framework (RDF)</a>.
Each one has a URI as a subject, which makes sure it is unique.</p>

<p>The publishing of an LDES allows data consumers to always stay in sync with a data source. 
You can watch the video fragment below to learn more about the ecosystem that the event stream provides.</p>
<div class="iframe-container">
  <iframe src="https://player.vimeo.com/video/523711762?h=f9ed9c8ae2" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen=""></iframe>
</div>

<p>The specification describes the structure of the LDES itself. With that information, you, as a human, can read the event stream. However, what if you do not want to read it yourself, but instead want one of your applications to ingest the events in the Event Stream?
The question is then: “How does an application consume events of a Linked Data Event Stream”.</p>

<p>In the following sections of this blog, I will first elaborate on how to read one with some simple example code. 
Next, I will provide some tools that are available to ingest any kind of LDES.</p>

<p>All code examples and toolings are written in Javascript/Typescript, more specifically <a href="https://nodejs.org/en/">Node.js</a>.</p>

<h2 id="custom-code">Custom code</h2>

<p>In this section, I’ll provide some code that can read an LDES.</p>

<h3 id="local">Local</h3>

<p>Below is a simple LDES which contains two members. Each member has a title, a description and the time at its creation.
In the triple <code class="language-plaintext highlighter-rouge">example:EventStream a ldes:Evenstream .</code>, we see that the URI <code class="language-plaintext highlighter-rouge">http://www.example.org/EventStream</code>  is of type <code class="language-plaintext highlighter-rouge">ldes:EventStream</code>.</p>

<p>From this <strong>URI</strong>, the members can be reached due to the <code class="language-plaintext highlighter-rouge">tree:member</code> property.</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">example:</span><span class="w"> </span><span class="nl">&lt;http://www.example.org/&gt;</span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">ldes:</span><span class="w"> </span><span class="nl">&lt;http://w3id.org/ldes#&gt;</span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">tree:</span><span class="w"> </span><span class="nl">&lt;https://w3id.org/tree#&gt;</span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">dct:</span><span class="w"> </span><span class="nl">&lt;http://purl.org/dc/terms/&gt;</span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">xsd:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2001/XMLSchema#&gt;</span><span class="p">.</span><span class="w">

</span><span class="nn">example:</span><span class="n">EventStream</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">ldes:</span><span class="n">Evenstream</span><span class="p">;</span><span class="w">
	</span><span class="nn">tree:</span><span class="n">member</span><span class="w"> </span><span class="nn">example:</span><span class="n">member1,</span><span class="w"> </span><span class="nn">example:</span><span class="n">member2</span><span class="p">.</span><span class="w">

</span><span class="nn">example:</span><span class="n">member1</span><span class="w"> </span><span class="nn">dct:</span><span class="n">title</span><span class="w"> </span><span class="s">"The first member"</span><span class="p">;</span><span class="w">
	</span><span class="nn">dct:</span><span class="n">description</span><span class="w"> </span><span class="s">"The first member of the Linked Data Event Stream"</span><span class="p">;</span><span class="w">
	</span><span class="nn">dct:</span><span class="n">created</span><span class="w"> </span><span class="s">"2023-01-02T10:00:00Z"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">dateTime</span><span class="p">.</span><span class="w">
	
</span><span class="nn">example:</span><span class="n">member2</span><span class="w"> </span><span class="nn">dct:</span><span class="n">title</span><span class="w"> </span><span class="s">"The second member"</span><span class="p">;</span><span class="w">
	</span><span class="nn">dct:</span><span class="n">description</span><span class="w"> </span><span class="s">"The second member of the Linked Data Event Stream"</span><span class="p">;</span><span class="w">
	</span><span class="nn">dct:</span><span class="n">created</span><span class="w"> </span><span class="s">"2023-01-02T11:00:00Z"</span><span class="p">^^</span><span class="nn">xsd:</span><span class="n">dateTime</span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>Let’s extract the events from this LDES now in code.
First, download the following npm packages, which allow you to load this example LDES in memory as a <a href="https://github.com/rdfjs/N3.js/">N3 Store</a>.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Some packages to install</span>
npm i rdf-store-stream rdf-parse streamify-string
</code></pre></div></div>

<p>Using the above libraries, the following piece of code parses the LDES into a Store.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// imports to read the LDES in an N3 Store</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">storeStream</span> <span class="p">}</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">rdf-store-stream</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">rdfParser</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">rdf-parse</span><span class="dl">"</span><span class="p">).</span><span class="k">default</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">streamifyString</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">streamify-string</span><span class="dl">'</span><span class="p">);</span>

<span class="c1">// the LDES example as a string</span>
<span class="kd">const</span> <span class="nx">ldesText</span> <span class="o">=</span> <span class="s2">`@prefix example: &lt;http://www.example.org/&gt;.
@prefix ldes: &lt;http://w3id.org/ldes#&gt;.
@prefix tree: &lt;https://w3id.org/tree#&gt;.
@prefix dct: &lt;http://purl.org/dc/terms/&gt;.
@prefix xsd: &lt;http://www.w3.org/2001/XMLSchema#&gt;.

example:EventStream a ldes:Evenstream;
	tree:member example:member1, example:member2.

example:member1 dct:title "The first member";
	dct:description "The first member of the Linked Data Event Stream";
	dct:created "2023-01-02T10:00:00Z"^^xsd:dateTime.
	
example:member2 dct:title "The second member";
	dct:description "The second member of the Linked Data Event Stream";
	dct:created "2023-01-02T11:00:00Z"^^xsd:dateTime.`</span><span class="p">;</span>

<span class="c1">// parse the LDES RDF into an N3 Store </span>
<span class="kd">const</span> <span class="nx">textStream</span> <span class="o">=</span> <span class="nf">streamifyString</span><span class="p">(</span><span class="nx">ldesText</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">quadStream</span> <span class="o">=</span> <span class="nx">rdfParser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">textStream</span><span class="p">,</span> <span class="p">{</span> <span class="na">contentType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/turtle</span><span class="dl">'</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">ldesStore</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">storeStream</span><span class="p">(</span><span class="nx">quadStream</span><span class="p">);</span>
</code></pre></div></div>

<p>Okay great! Now let us read the members of the LDES.</p>

<p>When the LDES URI is not known, we can query the RDF graph to look for the <code class="language-plaintext highlighter-rouge">ldes:EventStream</code> class.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">LDESSubjects</span> <span class="o">=</span> <span class="nx">ldesStore</span><span class="p">.</span><span class="nf">getSubjects</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://www.w3.org/1999/02/22-rdf-syntax-ns#type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">https://w3id.org/ldes#EventStream</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>However, we know the URI already, so we can use that to extract all the metadata of the LDES itself.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// extract the LDES metadata</span>
<span class="kd">const</span> <span class="nx">metadataQuads</span> <span class="o">=</span> <span class="nx">ldesStore</span><span class="p">.</span><span class="nf">getQuads</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://www.example.org/EventStream</span><span class="dl">"</span><span class="p">);</span>

<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">quad</span> <span class="k">of</span> <span class="nx">metadataQuads</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">quad</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// extract the members of the LDES</span>
<span class="kd">const</span> <span class="nx">memberSubjects</span> <span class="o">=</span> <span class="nx">ldesStore</span><span class="p">.</span><span class="nf">getObjects</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://www.example.org/EventStream</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">https://w3id.org/tree#member</span><span class="dl">"</span><span class="p">)</span>
<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">membersubject</span> <span class="k">of</span> <span class="nx">memberSubjects</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">memberQuads</span> <span class="o">=</span> <span class="nx">ldesStore</span><span class="p">.</span><span class="nf">getQuads</span><span class="p">(</span><span class="nx">membersubject</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">membersubject</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">memberQuads</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="online">Online</h3>

<p>In this part, we are going to work with an LDES which is hosted on the web. 
With this part, I want to show that the above custom code to retrieve all members in the LDES will work as long as it is <strong>valid RDF</strong> and adheres to the structure of an LDES and that it <strong>does not matter</strong> where it is hosted.</p>

<p>Let’s continue with our example.</p>

<!-- , to not make it more complex than it has to be. -->

<p>This means the first thing that has to be done is to host this LDES on an HTTP server.</p>

<p><strong>Install an HTTP server</strong></p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i http-server
</code></pre></div></div>

<p><strong>Set up the HTTP server</strong></p>

<p>To set up the server, execute the following following command:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx http-server
</code></pre></div></div>

<p>The above command starts an HTTP server and hosts all the files from the directory where the command has been run at URL <a href="http://localhost:8080/"><code class="language-plaintext highlighter-rouge">http://localhost:8080/</code></a>.</p>

<p>Saving the example LDES as <code class="language-plaintext highlighter-rouge">example-LDES.ttl</code> and storing it in that directory results in our own hosted LDES at URL <code class="language-plaintext highlighter-rouge">http://localhost:8080/example-LDES.ttl</code>.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// send an HTTP fetch request to the hosted example LDES URL</span>
<span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://localhost:8080/example-LDES.ttl</span><span class="dl">"</span><span class="p">)</span>

<span class="c1">// convert its body to string</span>
<span class="kd">const</span> <span class="nx">ldesText</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">text</span><span class="p">()</span>

<span class="c1">// parse the LDES RDF into an N3 Store </span>
<span class="kd">const</span> <span class="nx">textStream</span> <span class="o">=</span> <span class="nf">streamifyString</span><span class="p">(</span><span class="nx">ldesText</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">quadStream</span> <span class="o">=</span> <span class="nx">rdfParser</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">textStream</span><span class="p">,</span> <span class="p">{</span> <span class="na">contentType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">text/turtle</span><span class="dl">'</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">ldesStore</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">storeStream</span><span class="p">(</span><span class="nx">quadStream</span><span class="p">);</span>
</code></pre></div></div>

<p>And at this point, we can interact with the LDES in the same way that was explained in the first part.</p>

<h3 id="limitations">Limitations</h3>

<p>When the LDES is hosted as one page, the custom code will work fine to consume all the events in the event stream.
However, when the LDES is fragmented over multiple pages (using TREE hypermedia), the simple approach is not sufficient anymore.</p>

<p>I could explain how to extract a <code class="language-plaintext highlighter-rouge">tree:view</code> from the LDES and how to dereference the relations to retrieve all the members.
Though, I would not want to reinvent the wheel as there are already libraries that offer this kind of functionality: the <strong><a href="https://github.com/treecg/event-stream-client">LDES client</a></strong>.</p>

<h2 id="ldes-client">LDES client</h2>

<p>The LDES client is a prototype (developed by Brecht Van de Vyvere) and is used to retrieve all the members of an LDES by passing an URL.
This library parses the metadata of the LDES and (when present) dereferences all the relations of the view. Thus it abstracts away the technicalities of the LDES structure (and the TREE hypermedia) so that end users can focus on consuming the members in a streaming way.</p>

<p>Let’s use it!
First, we have to install the package <a href="https://github.com/treecg/event-stream-client">@treecg/actor-init-ldes-client</a></p>

<pre><code class="language-ttl">npm i @treecg/actor-init-ldes-client
</code></pre>

<p>After installation, we can use the code below to retrieve the members of an LDES. 
The LDES client only deals with LDESs on the web. So for simplicity’s sake, I re-used the LDES we hosted ourselves.</p>

<p>We start by creating an engine. Then we invoke the <code class="language-plaintext highlighter-rouge">createReadStream</code> function with an <strong>url</strong> and several options. Here we ask the engine to disable synchronization and as a representation, we want Quads, which actually will give us members following the <a href="https://github.com/TREEcg/types/blob/main/lib/Member.ts"><code class="language-plaintext highlighter-rouge">Member</code></a> Interface.
Disabling synchronization means that once all relations are followed and all members are read, the engine is done and closes. When enabled, the engine acts a service and would (depending on the <code class="language-plaintext highlighter-rouge">pollingInterval</code>) fetch certain pages to look for new members in the LDES.</p>

<p>It is important to note that this will generate a <strong>stream</strong> of <strong>members</strong>. To consume any members of the LDES, we add a <strong>listener</strong> (<code class="language-plaintext highlighter-rouge">data</code>) so we can do something (in the example below just logging) with a member.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">newEngine</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@treecg/actor-init-ldes-client</span><span class="dl">'</span><span class="p">).</span><span class="nx">newEngine</span><span class="p">;</span>

<span class="kd">let</span> <span class="nx">url</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">http://localhost:8080/example-LDES.ttl</span><span class="dl">"</span>
<span class="kd">const</span> <span class="nx">LDESClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">newEngine</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">eventstream</span> <span class="o">=</span> <span class="nx">LDESClient</span><span class="p">.</span><span class="nf">createReadStream</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span><span class="na">disableSynchronization</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">representation</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Quads</span><span class="dl">"</span><span class="p">});</span>

<span class="nx">eventstream</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">data</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">member</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">member</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">member</span><span class="p">.</span><span class="nx">quads</span><span class="p">);</span>
<span class="p">})</span>

<span class="nx">eventstream</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">metadata</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">metadata</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">metadata</span><span class="p">)</span>
<span class="p">})</span>

<span class="nx">eventstream</span><span class="p">.</span><span class="nf">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">end</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">No more data!</span><span class="dl">"</span><span class="p">);</span>
</code></pre></div></div>

<p>Furthermore, we also log the metadata. In the custom code, we’ve received an array of Quads.
With the LDES client now, we see that it contains the url of the entry point of the engine and an object that contains <code class="language-plaintext highlighter-rouge">treeMetadata</code> with collections, nodes and relations.</p>

<p>This object is generated by the function <code class="language-plaintext highlighter-rouge">extractMetadata</code> <a href="https://www.npmjs.com/package/@treecg/tree-metadata-extraction">@treecg/tree-metadata-extraction</a> package.</p>

<h3 id="tree-metadata-extractor">TREE metadata extractor</h3>

<p>The TREE metadata extractor does exactly what its name stands for: extracting TREE hypermedia (and by extension LDES) metadata from RDF data.</p>

<p>It is very simple to use, the only thing we have to pass to its only method, <code class="language-plaintext highlighter-rouge">extractMetadata</code>, is an array of <a href="https://rdf.js.org/data-model-spec/#quad-interface">Quads</a>.</p>

<p>The resulting output is an object which contains a Map of <code class="language-plaintext highlighter-rouge">collection</code>, <code class="language-plaintext highlighter-rouge">node</code> and <code class="language-plaintext highlighter-rouge">relation</code>.</p>

<p>Let’s use this tool to read all the members from the non-fragmented example LDES.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm i @treecg/tree-metadata-extraction
</code></pre></div></div>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">extractMetadata</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">@treecg/tree-metadata-extraction</span><span class="dl">'</span><span class="p">).</span><span class="nx">extractMetadata</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">metadata</span> <span class="o">=</span>  <span class="k">await</span> <span class="nf">extractMetadata</span><span class="p">(</span><span class="nx">ldesStore</span><span class="p">.</span><span class="nf">getQuads</span><span class="p">());</span>

<span class="kd">const</span> <span class="nx">collection</span> <span class="o">=</span> <span class="nx">metadata</span><span class="p">.</span><span class="nx">collections</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">http://www.example.org/EventStream</span><span class="dl">"</span><span class="p">);</span>
<span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">member</span> <span class="k">of</span> <span class="nx">collection</span><span class="p">.</span><span class="nx">member</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">memberID</span> <span class="o">=</span> <span class="nx">member</span><span class="p">[</span><span class="dl">'</span><span class="s1">@id</span><span class="dl">'</span><span class="p">];</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">ldesStore</span><span class="p">.</span><span class="nf">getQuads</span><span class="p">(</span><span class="nx">memberID</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The extractor creates a Map for all collections, nodes and relations it can find in the input quads.
In our input, there are no relations and nodes and only one collection.
So we can get this <code class="language-plaintext highlighter-rouge">collection</code> out of the metadata to retrieve all the member identifiers.
And finally, we use the member identifiers to fetch the member triples from the RDF data.</p>]]></content><author><name>Wout Slabbinck</name></author><category term="LDES" /><summary type="html"><![CDATA[Follow the links, read the members, and keep track of updates: a practical introduction to consuming an LDES.]]></summary></entry><entry><title type="html">Escaping the false dichotomy of API vs. data dump with Linked Data Event Streams</title><link href="https://ldes.tech/2021/09/03/escaping-false-dichotomy.html" rel="alternate" type="text/html" title="Escaping the false dichotomy of API vs. data dump with Linked Data Event Streams" /><published>2021-09-03T00:00:00+00:00</published><updated>2021-09-03T00:00:00+00:00</updated><id>https://ldes.tech/2021/09/03/escaping-false-dichotomy</id><content type="html" xml:base="https://ldes.tech/2021/09/03/escaping-false-dichotomy.html"><![CDATA[<p>Data publishers often face a familiar but painful choice: should they provide a full data dump or a querying API?
Each option comes with a hidden cost.
The data dump leads to replication hell — multiple copies scattered across consumers, each potentially outdated.
A querying API leads to maintenance hell — constant effort to keep endpoints up-to-date, scalable, and reliable.
With Linked Data Event Streams (LDES), we provide a way out.</p>

<p>Data publishers are too often asked to pick between two unsatisfying options.
The data dump looks harmless: publish a full export and let consumers do their thing.
In practice it creates a <strong>replication hell</strong>: multiple uncoordinated copies drift out of sync; consumers juggle deltas and snapshots; provenance cannot be traced; history gets lost in overwrites; and every “fresh” download quietly rebuilds the same indexes in a hundred places.</p>

<p>Take for example the address registry in Flanders for which <a href="https://basisregisters.vlaanderen.be/producten/grar#downloadbestandgrar">data dumps are available</a>.
Probably every municipality in Flanders takes a copy of this file for use cases such as autocompleting the street names for the forms they use everywhere.
When developers make such integrations, synchronization is an afterthought: this dataset doesn’t change that often anyway, right?
Think again: there are minor changes every day, with new addresses coming into play and old ones becoming “historized”.
When in <a href="https://en.wikipedia.org/wiki/Fusion_of_the_Belgian_municipalities">2016, 2019 and 2025, cities in Belgium decided to fuse</a>, a lot of street names also needed to be renamed to avoid duplicate names.
Instead of <a href="https://interoperable-europe.ec.europa.eu/collection/semic-support-centre/base-registries">this base registry</a> being updated from the source, services started making the changes they needed manually in their local copies, leading to a replication hell.</p>

<p>On the other side sits the querying API.
It promises precision — ask only for what you need — while quietly enrolling the publisher in <strong>maintenance hell</strong>.
New use case? New endpoint.
New query language that became popular? Again yet another API to be provided.
The provider becomes an involuntary platform operator, while consumers still only have a limited processing possibility of the dataset as they can only query the API in the ways the data provider managed to set up.
Each endpoint that has been set up comes with its own maintenance cost.
After a while, when priorities shift or budgets shrink, it will have become impossible to turn off any of the existing APIs as there may still be an application that relies on it.
The budget that once was used for innovation and creating a better public service, is now being used for maintaining legacy APIs.</p>

<p>Take again the example of the address registry in Flanders for which, next to data dumps, <a href="https://web.archive.org/web/20250823085609/https://vlaamseoverheid.atlassian.net/wiki/spaces/AGB/pages/6099766021/Raadpleegdiensten">also a plethora of API products are available</a>.
Specific functionalities that were once brought online need to remain maintained: there may always be that one service that is still relying on this API.
Certainly for address registries, multiple functionalities of the dataset are expected, such as: finding the geolocation of an (or multiple) address(es), a geospatial interface to visualize the data, a historic view of what addresses existed in the past until today, an autocompletion interface for streetnames, municipalities and addresses, a SPARQL, GQL and GraphQL API for graph-based access, a specific service that allows calculating what addresses will be impacted by a road closure, etc.
It doesn’t matter how many APIs you have: it will never be sufficient — there’s always going to be that other person who needs a functionality that does not yet exist.</p>

<p>There are, however, other paths. Let’s start from the idea of taking full copies, i.e. “dumps”, but let’s change the intent and the name. Let’s call it a stream. This way, it sets the expectation that developers of consumption pipelines will code for history and future: they will re-interpret what happened and, with exactly the same code, stay in sync with what will happen.
This is the mindset behind <a href="https://ldes.tech">Linked Data Event Streams (LDES)</a>.
The ambition level is to introduce semantic interoperability through Linked Data, and combine this idea with how developers interact with streams.
LDES publishes the dataset as an append-only sequence of immutable members with stable identifiers, so any party can replicate once and then follow updates.
Hence the straightforward name: the combination of having this ambition towards interoperability, and the ambition to always keep every copy up to date, becomes Linked Data + Event Streams: LDES in short.</p>

<p><img src="https://tree.linkeddatafragments.org/img/logo-ldes.svg" alt="The LDES logo" /></p>

<p>This shift unlocks governance opportunities.
With an authoritative event source online, the publisher can decide which higher-level interfaces to keep maintaining, and which to let the ecosystem carry.
A SPARQL endpoint, an OGC API, or a GraphQL service may be useful today and optional tomorrow.
If a GraphQL API stops aligning with the publisher’s priorities, the publisher can bring it offline, while the consumer that still needs it can spin up their own GraphQL server that replicates and synchronises from the event source, preserving functionality without forcing the publisher to keep every interface alive forever.
If you maintain a base registry or any dataset that changes over time, start by publishing the LDES at the event source. Everything else can — and should — derive from there.</p>

<aside class="paper-citation" aria-label="Related research">
  <p class="paper-citation-heading">Read the paper and cite this work</p>
  <p class="paper-citation-context">This paper presents the base-registry architecture behind the argument in this post.</p>
  <div typeof="schema:ScholarlyArticle" resource="https://doi.org/10.1007/978-3-030-74296-6_3">
    <p class="paper-citation-title"><cite property="schema:name"><a href="https://github.com/ddvlanck/Publishing-Base-Registries-As-LDES/raw/master/Linked-Data-Event-Streams.pdf">Publishing base registries as Linked Data Event Streams</a></cite></p>
    <p class="paper-citation-meta">Proceedings of the 21st International Conference on Web Engineering (ICWE 2021) &middot; 2021</p>
    <details class="paper-citation-details">
      <summary>Cite this work</summary>
      <p class="paper-citation-text">Van Lancker, D., Colpaert, P., Delva, H., Van de Vyvere, B., Rojas Meléndez, J., Dedecker, R., Michiels, P., Buyle, R., De Craene, A., and Verborgh, R. (2021). <cite>Publishing base registries as Linked Data Event Streams</cite>. Proceedings of the 21st International Conference on Web Engineering (ICWE 2021), pp. 28–36. <a href="https://doi.org/10.1007/978-3-030-74296-6_3">https://doi.org/10.1007/978-3-030-74296-6_3</a></p>
      <p><a href="https://ruben.verborgh.org/publications/vanlancker_icwe_2021/">Publication record</a></p>
    </details>
  </div>
  <a class="paper-citation-discover" href="/publications/">Discover other papers &rarr;</a>
</aside>

<p>For the technical details, see the LDES specification at <a href="https://w3id.org/ldes/specification">https://w3id.org/ldes/specification</a>.
Various implementations of clients and servers are available. In order to interact with the community, visit <a href="https://ldes.tech">https://ldes.tech</a>.</p>

<h3 id="the-talk-at-endorse-2021">The talk at ENDORSE 2021</h3>

<p>At ENDORSE 2021 there was a talk in which I explain the contents of this blog post:</p>

<div class="iframe-container">
  <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/89UVTahjCvo?start=1096" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</div>

<h3 id="ps">P.S.</h3>

<p>This post was planned to be published in 2021, but remained in draft until 2025. It was only in 2025 that there was time to finalize it, keeping the initial planned publication date.</p>]]></content><author><name>Pieter Colpaert</name></author><category term="LDES" /><summary type="html"><![CDATA[Data dumps drift out of date, while custom query APIs take work to maintain. Event streams let consumers build and update their own views.]]></summary></entry></feed>