Querying the Semantic Web with SPARQL
The Semantic Web is one of the most ambitious ideas in the history of computing: a vision of the World Wide Web where data is not just readable by humans, but understandable by machines. Tim Berners-Lee originally proposed it in 2001 as an extension of the existing web, and while the full vision has not been realized in the way he imagined, parts of it have become remarkably successful. Two of the largest publicly accessible knowledge graphs in the world, DBPedia and Wikidata, expose billions of structured facts through a query language called SPARQL, and anyone can query them for free.
In this chapter, we will build a desktop SPARQL client using Tauri v2, Vite, and vanilla TypeScript. The application lets you select an endpoint (DBPedia or Wikidata), write a SPARQL query in a text area, and see the results rendered as a formatted table. Along the way, I will introduce you to the core Semantic Web concepts (RDF triples, knowledge graphs, and the SPARQL query language) and show you how surprisingly little code is needed to tap into these massive knowledge bases from a TypeScript application.
This project lives in the semantic-web-app directory and is directly inspired by the knowledge representation examples in my earlier TypeScript AI book. If you have worked through those examples, you will recognize the same patterns here, now wrapped in a native desktop UI.
The Semantic Web in Brief
Before we look at code, let me give you a short tour of the ideas that make this application possible.
RDF: The Data Model
The foundation of the Semantic Web is RDF (Resource Description Framework). RDF represents all knowledge as triples: statements of the form subject–predicate–object. For example:
1 <http://dbpedia.org/resource/Berlin> <http://dbpedia.org/ontology/country> <http://dbpedia.org/resource/Germany>
This triple says “Berlin’s country is Germany.” Every element is identified by a URI that is a globally unique identifier, like a web URL. This is what makes RDF powerful: any two datasets that refer to the same URI are automatically linked. You do not need a foreign key or a join table; the shared URI is the link.
A collection of RDF triples forms a knowledge graph, a directed graph where subjects and objects are nodes, and predicates are the labeled edges connecting them. Knowledge graphs are a natural fit for representing the kind of interconnected, heterogeneous knowledge that the real world contains.
SPARQL: The Query Language
SPARQL (pronounced “sparkle”) is the query language for RDF data. If RDF is the Semantic Web’s equivalent of relational tables, SPARQL is its SQL. A SPARQL query uses triple patterns with variables (prefixed with ?) to match against the knowledge graph:
1 SELECT ?city ?population WHERE {
2 ?city <http://dbpedia.org/ontology/type> <http://dbpedia.org/resource/City> .
3 ?city <http://dbpedia.org/property/populationEst> ?population .
4 }
This query finds all entities typed as a City that have a population estimate, and returns them. The SPARQL engine matches these patterns against the triples in the database and returns all variable bindings that satisfy the constraints.
Knowledge Graphs and AI
Knowledge graphs are a cornerstone of knowledge representation in AI. While modern large language models store knowledge implicitly in neural network weights, knowledge graphs store it explicitly as structured, queryable facts. This makes them valuable for applications where you need precise, verifiable answers, not probabilistic guesses. Many AI systems use knowledge graphs alongside neural models: the graph provides factual grounding, while the model provides natural language understanding.
DBPedia and Wikidata are two of the most important open knowledge graphs. DBPedia extracts structured data from Wikipedia infoboxes, those summary tables you see in the sidebar of Wikipedia articles. Wikidata is a collaboratively edited knowledge base maintained by the Wikimedia Foundation, and it serves as the structured data backbone for Wikipedia itself. Both expose SPARQL endpoints that anyone can query over HTTP.
DBPedia vs. Wikidata
Our application supports both endpoints, and it is worth understanding how they differ:
DBPedia extracts its data automatically from Wikipedia. Its URIs look like http://dbpedia.org/resource/Berlin, which map directly to Wikipedia article titles. DBPedia uses familiar RDF Schema predicates like rdfs:label and its own ontology namespace (dbo:, dbp:). Queries tend to be straightforward because the property names are human-readable. The downside is that DBPedia’s data can be inconsistent, it inherits whatever quirks exist in Wikipedia’s infoboxes, and its SPARQL endpoint can sometimes be slow or unavailable.
Wikidata is a first-class, curated knowledge base. Its URIs use opaque identifiers like wd:Q64 (which happens to be Berlin) and wdt:P1082 (which means “population”). This makes the queries harder to read at first glance, but the data quality is generally higher, and the query service is fast and reliable. Wikidata also provides a powerful SERVICE wikibase:label clause that automatically resolves those opaque identifiers into human-readable labels, which is extremely convenient.
Project Structure
Here is the layout of the semantic-web-app project:
1 semantic-web-app/
2 ├── index.html ← App shell: form with endpoint selector,
3 │ query textarea, and results area
4 ├── package.json ← Tauri v2 + Vite dependencies
5 ├── vite.config.ts ← Vite dev server on port 1420 for Tauri
6 ├── tsconfig.json
7 ├── src/
8 │ ├── main.ts ← SPARQL client: query execution, result
9 │ │ formatting, URI shortening (190 lines)
10 │ └── styles.css ← Light/dark theme with table styling
11 │ (219 lines)
12 └── src-tauri/
13 ├── tauri.conf.json ← Tauri app config: window size, CSP,
14 │ build commands
15 ├── Cargo.toml
16 └── src/
17 └── lib.rs ← Minimal Rust backend (Tauri shell)
The architecture is simple by design. All the SPARQL logic lives in the frontend TypeScript: there are no Rust commands involved in querying. The Tauri shell provides the native window, and Vite handles the dev server and build pipeline. The fetch API makes HTTP requests directly to the SPARQL endpoints.
Let me note something about the Tauri configuration that matters for this application: the security.csp field is set to null in tauri.conf.json. This disables the Content Security Policy, which would otherwise block the cross-origin fetch requests to dbpedia.org and wikidata.org. In a production application, you would want to set a specific CSP that allows only those two origins. For a demo, disabling it keeps things simple.
The SPARQL Client Implementation
Let us walk through the TypeScript code in src/main.ts. This file is 190 lines and contains everything: type definitions, endpoint configuration, the SPARQL query function, result formatting, and DOM event handling.
Type Definitions
We start with TypeScript interfaces that model the SPARQL JSON results format. This is a standard format defined by the W3C, every SPARQL endpoint returns results in the same shape:
1 type SparqlEndpoint = "dbpedia" | "wikidata";
2
3 interface SparqlValue {
4 value: string;
5 type: string;
6 datatype?: string;
7 "xml:lang"?: string;
8 }
9
10 interface SparqlBinding {
11 [variable: string]: SparqlValue | undefined;
12 }
13
14 interface SparqlResults {
15 head: {
16 vars: string[];
17 };
18 results: {
19 bindings: SparqlBinding[];
20 };
21 }
The SparqlEndpoint type is a union of the two endpoint names we support. The SparqlValue interface represents a single value in a result row: it has a value string, a type (which is "uri", "literal", or "typed-literal"), and optional datatype and xml:lang fields for typed and language-tagged literals. The SparqlBinding interface is an index signature mapping variable names to their values, this models one row of results. Finally, SparqlResults wraps the whole response: head.vars lists the variable names (the column headers), and results.bindings is the array of rows.
I want to call your attention to the undefined in SparqlBinding. A binding can be missing for a given variable if that variable was matched in an OPTIONAL clause and no value was found. Our code needs to handle this gracefully, and you will see how formatValue does that shortly.
Endpoint Configuration
Next, we define the SPARQL endpoints and their default queries:
1 const ENDPOINTS: Record<SparqlEndpoint, { url: string; userAgent: string }> = {
2 dbpedia: {
3 url: "https://dbpedia.org/sparql",
4 userAgent: "SemanticWebApp/1.0 (DBPedia demo)",
5 },
6 wikidata: {
7 url: "https://query.wikidata.org/sparql",
8 userAgent: "SemanticWebApp/1.0 (Wikidata demo)",
9 },
10 };
Each endpoint has a URL and a custom User-Agent string. The User-Agent is important: both DBPedia and Wikidata require a meaningful User-Agent header. If you send the default browser User-Agent or an empty string, the endpoint may reject your request or rate-limit you aggressively. Setting a descriptive User-Agent is good practice and good citizenship when querying public SPARQL endpoints.
Default Queries
The application ships with two default queries that demonstrate the capabilities of each endpoint. Let us look at them in detail.
DBPedia: Cities by Population
1 const DEFAULT_QUERIES: Record<SparqlEndpoint, string> = {
2 dbpedia: `SELECT ?city_uri ?dbpedia_label ?population ?country_label WHERE {
3 ?city_uri <http://dbpedia.org/ontology/type> <http://dbpedia.org/resource/City> .
4 ?city_uri <http://dbpedia.org/property/populationEst> ?population .
5 ?city_uri <http://www.w3.org/2000/01/rdf-schema#label> ?dbpedia_label
6 FILTER (lang(?dbpedia_label) = 'en') .
7 OPTIONAL {
8 ?city_uri <http://dbpedia.org/ontology/country> ?country .
9 ?country <http://www.w3.org/2000/01/rdf-schema#label> ?country_label
10 FILTER (lang(?country_label) = 'en') .
11 }
12 } ORDER BY DESC(?population) LIMIT 10`,
This query demonstrates several SPARQL features:
- Triple patterns: The first line matches entities whose
dbo:typeisdbr:City. The second matches entities that have adbp:populationEstproperty. - FILTER: The
FILTER (lang(?dbpedia_label) = 'en')clause restricts results to English-language labels. Without this, you would get labels in every language available in DBPedia, e.g., German, French, Japanese, and so on. - OPTIONAL: The country lookup is wrapped in an
OPTIONALblock. Not every city in DBPedia has adbo:countrylink, so withoutOPTIONALthose cities would be excluded entirely. With it, they appear in the results with an empty country column. - ORDER BY DESC and LIMIT: We sort by population descending and take only the top 10.
Wikidata: Albert Einstein Facts
1 wikidata: `SELECT ?personLabel ?birthPlaceLabel ?birthDate ?occupationLabel WHERE {
2 ?person wdt:P31 wd:Q5 .
3 ?person rdfs:label "Albert Einstein"@en .
4 OPTIONAL { ?person wdt:P19 ?birthPlace . }
5 OPTIONAL { ?person wdt:P569 ?birthDate . }
6 OPTIONAL { ?person wdt:P106 ?occupation . }
7 SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . }
8 } LIMIT 10`,
9 };
This query shows Wikidata’s style. The identifiers are opaque: wdt:P31 means “instance of,” wd:Q5 means “human,” wdt:P19 is “place of birth,” wdt:P569 is “date of birth,” and wdt:P106 is “occupation.” You learn these by browsing Wikidata or using its search interface.
The SERVICE wikibase:label clause is a Wikidata-specific extension. It is a federated query that calls a label-resolution service. When you include it, Wikidata automatically creates *Label versions of your variables (e.g., ?personLabel from ?person) that contain the human-readable English labels. Without this clause, you would see results like wd:Q64 instead of “Berlin.” The SERVICE keyword is part of the SPARQL standard for federated queries: queries that reach out to remote SPARQL endpoints during execution.
Notice that the Wikidata query returns multiple rows even though we are asking about a single person. This is because Einstein had multiple occupations (physicist, philosopher, professor, etc.), and each one produces a separate row in the results. This is a natural consequence of the triple-based data model: there are multiple wdt:P106 triples for Einstein, each pointing to a different occupation entity.
The Query Function
The core of the application is a single async function that sends a SPARQL query to an endpoint and returns the parsed JSON results:
1 async function sparqlQuery(
2 endpoint: SparqlEndpoint,
3 query: string
4 ): Promise<SparqlResults> {
5 const config = ENDPOINTS[endpoint];
6 const resp = await fetch(
7 `${config.url}?query=${encodeURIComponent(query)}`,
8 {
9 headers: {
10 Accept: "application/sparql-results+json",
11 "User-Agent": config.userAgent,
12 },
13 }
14 );
15 if (!resp.ok) {
16 throw new Error(
17 `SPARQL query failed: ${resp.status} ${resp.statusText}`
18 );
19 }
20 return resp.json();
21 }
This is pleasantly simple. The SPARQL protocol specifies that you can send a query as a URL parameter named query, URL-encoded, via an HTTP GET request. The Accept: application/sparql-results+json header tells the endpoint to return results in JSON format rather than XML or CSV. The function URL-encodes the query, sends it with the appropriate headers, checks for HTTP errors, and parses the JSON response.
I want to highlight that we are using the browser’s built-in fetch API here. No SPARQL library, no RDF toolkit, just a plain HTTP request. SPARQL endpoints are designed to be accessed this way, and the JSON results format is simple enough that we can work with it directly using TypeScript interfaces. This is one of the beautiful things about the Semantic Web: the protocol is just HTTP, and the data format is just JSON.
URI Shortening
Raw URIs in SPARQL results are verbose. The function shortenUri converts them into the compact prefix notation that SPARQL users are familiar with:
1 function shortenUri(value: string): string {
2 if (value.startsWith("http://dbpedia.org/resource/")) {
3 return `dbr:${value.slice(28)}`;
4 }
5 if (value.startsWith("https://dbpedia.org/resource/")) {
6 return `dbr:${value.slice(29)}`;
7 }
8 if (value.startsWith("http://dbpedia.org/ontology/")) {
9 return `dbo:${value.slice(28)}`;
10 }
11 if (value.startsWith("http://dbpedia.org/property/")) {
12 return `dbp:${value.slice(28)}`;
13 }
14 if (value.startsWith("http://www.wikidata.org/entity/")) {
15 return `wd:${value.slice(31)}`;
16 }
17 if (value.startsWith("http://www.wikidata.org/prop/direct/")) {
18 return `wdt:${value.slice(36)}`;
19 }
20 return value;
21 }
This function maps common URI prefixes to their standard abbreviations: dbr: for DBPedia resources, dbo: for DBPedia ontology terms, dbp: for DBPedia properties, wd: for Wikidata entities, and wdt: for Wikidata direct properties. So instead of displaying http://dbpedia.org/resource/Berlin in the results table, we display dbr:Berlin that is much more readable.
Notice that we handle both http:// and https:// for DBPedia resources. DBPedia has historically used http:// URIs, but some responses now include https:// variants. This kind of real-world inconsistency is common when working with Semantic Web data, and handling it gracefully is part of building a robust client.
Value Formatting
The formatValue function handles the different types of values that appear in SPARQL results:
1 function formatValue(binding: SparqlValue | undefined): string {
2 if (!binding) return "";
3
4 if (binding.type === "uri") {
5 return shortenUri(binding.value);
6 }
7
8 if (binding.type === "literal" || binding.type === "typed-literal") {
9 const datatype = binding.datatype;
10 if (datatype === "http://www.w3.org/2001/XMLSchema#integer") {
11 return parseInt(binding.value, 10).toLocaleString();
12 }
13 if (
14 datatype === "http://www.w3.org/2001/XMLSchema#decimal" ||
15 datatype === "http://www.w3.org/2001/XMLSchema#float" ||
16 datatype === "http://www.w3.org/2001/XMLSchema#double"
17 ) {
18 return parseFloat(binding.value).toLocaleString();
19 }
20 if (
21 datatype === "http://www.w3.org/2001/XMLSchema#dateTime" ||
22 datatype === "http://www.w3.org/2001/XMLSchema#date"
23 ) {
24 return binding.value.slice(0, 10);
25 }
26 }
27
28 return binding.value;
29 }
There are several cases to handle:
- Undefined bindings: If the binding is
undefined(from anOPTIONALclause that did not match), we return an empty string. - URIs: Passed through
shortenUrifor compact display. - Integers: Parsed and formatted with locale-aware thousand separators. A population of
3645000becomes"3,645,000". - Floating-point numbers: Similarly formatted with
toLocaleString(). - Dates and datetimes: Truncated to the first 10 characters (the
YYYY-MM-DDportion). SPARQL date values often include time and timezone information that clutters the display. - Plain literals: Returned as-is. This covers strings like labels and descriptions.
The datatypes are XML Schema datatypes, which is the standard that RDF uses for typed literals. You will see these URI-based datatype identifiers in any SPARQL results that include numbers or dates.
Rendering Results as HTML
The renderResults function builds an HTML table from the SPARQL JSON response:
1 function renderResults(results: SparqlResults): string {
2 const { vars } = results.head;
3 const { bindings } = results.results;
4 if (bindings.length === 0) {
5 return '<p class="info">No results found.</p>';
6 }
7
8 let html = '<table class="results-table"><thead><tr>';
9 for (const v of vars) {
10 html += `<th>${v}</th>`;
11 }
12 html += "</tr></thead><tbody>";
13 for (const row of bindings) {
14 html += "<tr>";
15 for (const v of vars) {
16 html += `<td>${formatValue(row[v])}</td>`;
17 }
18 html += "</tr>";
19 }
20 html += "</tbody></table>";
21 return html;
22 }
The column headers come from results.head.vars, the list of variable names from the SELECT clause. Each row in results.results.bindings is an object mapping variable names to their SparqlValue. The function iterates over the variables for each row, calls formatValue to produce a display string, and builds the table HTML.
This approach is generic: it works with any SPARQL SELECT query, regardless of what variables are projected. You do not need to know the shape of the results at compile time. The table adapts automatically to whatever columns the query returns.
Running a Query
The runQuery function ties the UI to the SPARQL client:
1 let endpointSelect: HTMLSelectElement | null;
2 let queryInput: HTMLTextAreaElement | null;
3 let resultsEl: HTMLElement | null;
4
5 async function runQuery() {
6 if (!endpointSelect || !queryInput || !resultsEl) return;
7
8 const endpoint = endpointSelect.value as SparqlEndpoint;
9 const query = queryInput.value.trim();
10
11 resultsEl.innerHTML = '<p class="info">Loading...</p>';
12
13 try {
14 const data = await sparqlQuery(endpoint, query);
15 const count = data.results.bindings.length;
16 resultsEl.innerHTML =
17 `<p class="info">${count} result${count === 1 ? "" : "s"} ` +
18 `from ${endpoint}</p>`;
19 resultsEl.innerHTML += renderResults(data);
20 } catch (err) {
21 const message =
22 err instanceof Error ? err.message : "Unknown error";
23 resultsEl.innerHTML =
24 `<p class="error">Error: ${message}</p>`;
25 if (endpoint === "dbpedia") {
26 resultsEl.innerHTML +=
27 '<p class="note">Note: DBPedia\'s SPARQL endpoint may be ' +
28 "temporarily unavailable or require a different network " +
29 "configuration.</p>";
30 }
31 }
32 }
The function reads the selected endpoint and the query text from the form, shows a loading indicator, calls sparqlQuery, and either renders the results or displays an error message. The error handling includes a special note for DBPedia, which is historically less reliable than Wikidata’s query service. This is a practical consideration that saves users from confusion when DBPedia is having one of its occasional outages.
DOM Initialization
Finally, the DOMContentLoaded event handler wires everything up:
1 window.addEventListener("DOMContentLoaded", () => {
2 endpointSelect = document.querySelector("#endpoint-select");
3 queryInput = document.querySelector("#query-input");
4 resultsEl = document.querySelector("#results");
5
6 document
7 .querySelector("#query-form")
8 ?.addEventListener("submit", (e) => {
9 e.preventDefault();
10 runQuery();
11 });
12
13 endpointSelect?.addEventListener("change", () => {
14 if (queryInput && endpointSelect) {
15 queryInput.value =
16 DEFAULT_QUERIES[endpointSelect.value as SparqlEndpoint];
17 }
18 });
19
20 if (queryInput && endpointSelect) {
21 queryInput.value =
22 DEFAULT_QUERIES[endpointSelect.value as SparqlEndpoint];
23 }
24 });
There are three things happening here:
- Form submission: The submit handler calls
preventDefault()to stop the browser from navigating, then callsrunQuery(). - Endpoint switching: When the user changes the endpoint dropdown, the textarea is automatically populated with the default query for that endpoint. This gives users a working example to start from.
- Initial population: The textarea is populated with the default query for whichever endpoint is initially selected (Wikidata, since it has the
selectedattribute in the HTML).
The HTML Shell
1 The HTML file is minimal. t isjust a form and a results container:
1 <!doctype html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <link rel="stylesheet" href="/src/styles.css" />
6 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7 <title>Semantic Web SPARQL Client</title>
8 <script type="module" src="/src/main.ts" defer></script>
9 </head>
10 <body>
11 <main class="container">
12 <h1>Semantic Web SPARQL Client</h1>
13 <p>Query DBPedia or Wikidata using SPARQL.</p>
14 <form class="column" id="query-form">
15 <label for="endpoint-select">Endpoint</label>
16 <select id="endpoint-select">
17 <option value="dbpedia">DBPedia</option>
18 <option value="wikidata" selected>Wikidata</option>
19 </select>
20 <label for="query-input">SPARQL Query</label>
21 <textarea
22 id="query-input"
23 rows="14"
24 placeholder="Enter a SPARQL SELECT query..."
25 ></textarea>
26 <button type="submit">Run Query</button>
27 </form>
28 <div id="results"></div>
29 </main>
30 </body>
31 </html>
The <select> dropdown lists the two endpoints. The <textarea> with 14 rows gives enough room to display multi-line SPARQL queries comfortably. The #results div is where the rendered table (or error messages) will be injected. Vite handles the TypeScript compilation and module loading using the <script type="module"> tag that references the .ts file directly, and Vite transforms it during development.
Running the Example
To run the semantic web application:
1 cd semantic-web-app
2 npm install
3 npm run tauri dev
The Tauri dev server will start Vite on port 1420 and open a native desktop window. You will see the SPARQL query form pre-populated with the Wikidata query for Albert Einstein.
Click Run Query to execute the default Wikidata query. You should see results similar to:
1 10 results from wikidata
2
3 personLabel birthPlaceLabel birthDate occupationLabel
4 ─────────────────────────────────────────────────────────────────
5 Albert Einstein Ulm 1879-03-14 physicist
6 Albert Einstein Ulm 1879-03-14 university teacher
7 Albert Einstein Ulm 1879-03-14 patent examiner
8 Albert Einstein Ulm 1879-03-14 philosopher
9 Albert Einstein Ulm 1879-03-14 inventor
10 Albert Einstein Ulm 1879-03-14 writer
11 Albert Einstein Ulm 1879-03-14 teacher
12 Albert Einstein Ulm 1879-03-14 diplomat
13 Albert Einstein Ulm 1879-03-14 theoretical physicist
14 Albert Einstein Ulm 1879-03-14 philosopher of science
Each row shares the same birth place and birth date, but lists a different occupation. This is the triple-based data model at work: Einstein has many wdt:P106 (occupation) triples in Wikidata, and each one produces a separate result row.
Now switch the endpoint dropdown to DBPedia and click Run Query. The textarea will auto-populate with the cities-by-population query. After a moment (DBPedia can be slower), you should see the world’s most populous cities:
1 10 results from dbpedia
2
3 city_uri dbpedia_label population country_label
4 ──────────────────────────────────────────────────────────────────
5 dbr:Shanghai Shanghai 24,870,895 dbr:China
6 dbr:Beijing Beijing 21,893,095 dbr:China
7 dbr:Delhi Delhi 18,980,000 India
8 dbr:Lagos Lagos 16,060,303 Nigeria
9 dbr:Istanbul Istanbul 15,840,900 Turkey
10 dbr:Tokyo Tokyo 14,094,034 Japan
11 dbr:Mumbai Mumbai 12,478,447 India
12 dbr:Moscow Moscow 12,380,664 Russia
13 dbr:São_Paulo São Paulo 12,252,023 Brazil
14 dbr:Dhaka Dhaka 10,278,882 Bangladesh
Notice how the URI shortening makes the city_uri column readable (dbr:Shanghai instead of http://dbpedia.org/resource/Shanghai), and how the population numbers are formatted with thousand separators. The country labels come from the OPTIONAL clause, e.g., if a city did not have a dbo:country link, that column would be empty rather than the entire row being excluded.
You can also write your own SPARQL queries. Here are a few to try:
Find programming languages and their designers (Wikidata):
1 SELECT ?langLabel ?designerLabel ?yearLabel WHERE {
2 ?lang wdt:P31 wd:Q9143 .
3 ?lang wdt:P287 ?designer .
4 OPTIONAL { ?lang wdt:P571 ?year . }
5 SERVICE wikibase:label { bd:serviceParam wikibase:language "en" . }
6 } LIMIT 20
Find universities in a country (DBPedia):
1 SELECT ?uni ?name WHERE {
2 ?uni a <http://dbpedia.org/ontology/University> .
3 ?uni <http://dbpedia.org/ontology/country> <http://dbpedia.org/resource/Canada> .
4 ?uni <http://www.w3.org/2000/01/rdf-schema#label> ?name
5 FILTER (lang(?name) = 'en') .
6 } LIMIT 15
Wrap Up
In about 190 lines of TypeScript, we have built a fully functional SPARQL client that can query two of the world’s largest knowledge graphs. The implementation is deliberately minimal with no third-party SPARQL libraries, no complex state management, just the fetch API and some careful type definitions.
The key ideas to take away from this chapter are:
- RDF triples are the universal data model of the Semantic Web. Everything is a subject–predicate–object statement, identified by URIs.
- SPARQL is a pattern-matching query language. You describe the shape of the triples you want, and the engine finds all matches.
- Public SPARQL endpoints like DBPedia and Wikidata are free to query. A polite User-Agent header and URL-encoded GET request are all you need.
- The JSON results format is standardized by the W3C, so the same TypeScript interfaces work with any SPARQL endpoint.
- OPTIONAL clauses prevent missing data from excluding entire rows that is an important pattern when querying real-world knowledge graphs where data is often incomplete.
- URI shortening transforms verbose identifiers into the compact prefix notation that makes SPARQL results human-readable.
Knowledge graphs complement neural AI models beautifully. Where a language model might hallucinate a fact, a knowledge graph returns only what has been explicitly asserted and can be traced back to its source. As you build AI applications, consider using SPARQL queries against knowledge graphs as a way to ground your systems in verifiable, structured knowledge. The combination of a desktop Tauri shell and live SPARQL queries gives you a practical tool for exploring these massive knowledge bases interactively.