@_linked/fuseki

中文文档
3.9.2发布于 2026/10/28MITTypeScriptES Module构建来源已验证近 30 天下载 2331

A store for @_linked/core that talks SPARQL over HTTP to Apache Jena Fuseki, dataset admin helpers, and the Fuseki image every linked app runs: ghcr.io/linked-fw/fuseki, whose datasets infer RDFS classes.

原始使用文档
文档目录14 节
  1. What the image adds over stock Fuseki
  2. Fuseki store
  3. Reads from graphs
  4. The image
  5. Datasets infer classes
  6. The ds dataset
  7. Stored-only reads
  8. Graphs
  9. The graph registry
  10. Backups and deletion
  11. SERVICE is off
  12. Fuseki 6.2 notes
  13. Building the image locally
  14. Releasing the image

@_linked/fuseki

A store for @_linked/core that talks SPARQL over HTTP to Apache Jena Fuseki, dataset admin helpers, and the Fuseki image every linked app runs: ghcr.io/linked-fw/fuseki, whose datasets infer RDFS classes.

What the image adds over stock Fuseki

  1. Live class inference on every dataset. The dataset type lf:DatasetRDFSLive infers rdf:type through rdfs:subClassOf at query time, over TDB2. Nothing inferred is written to disk.
  2. A hierarchy that rebuilds itself. The class hierarchy is read from the stored rdfs:subClassOf triples in the default graph and rebuilt once, at the first read after a commit that changed one. Writes never rebuild (GSP writes included), so a burst of commits costs one rebuild, and every transaction sees a hierarchy that matches its own snapshot.
  3. Inference over every graph, named graphs included.
  4. Stored-only reads at /<ds>/stored: SPARQL queries and graph reads over the stored triples only, for backups, copies and counts. Writes there answer 405.
  5. New datasets infer by default. The admin form's template (POST $/datasets, dbType=tdb2) is baked in and declares an inferring dataset; a template guard checks it at start.
  6. SERVICE (federated queries) is off server-wide.
  7. Deleting a dataset removes its files. Stock Fuseki leaves a wrapped dataset's TDB2 files behind; the image removes them for datasets created through the admin API.
  8. Fuseki 6.2, with SHACL validation and the Lucene text index working over inferring datasets.
  9. A published image: ghcr.io/linked-fw/fuseki:<jena version>-<n>, multi-arch, immutable tags published only from main, a digest-pinned base, running as a non-root user, configured with the same environment variables as secoresearch/fuseki.

Details follow in The image and the sections after it.

Fuseki store

The FusekiStore sends SPARQL over HTTP to a Fuseki dataset. Configure it with environment variables:

  • FUSEKI_BASE_URL (required): e.g. http://localhost:3030, or http://fuseki:3030 in Docker. Trailing slashes are trimmed.
  • FUSEKI_DATASET (required): the dataset name (e.g. cn-main).
  • FUSEKI_DEFAULT_GRAPH (optional): a graph IRI to target; unset, factories fall back to process.env.DATA_ROOT. On the image's inferring datasets, rdfs:subClassOf triples written to a named graph are part of the class hierarchy only when the graph registry lists that graph (see Datasets infer classes).
  • FUSEKI_USER, FUSEKI_PASSWORD (optional): basic auth.

Selects go to /<ds>/sparql, inserts and updates to /<ds>/update, graph reads and writes to /<ds>/data.

Reads from graphs

@_linked/core reads named graphs with .from(graph | graph[]) on selects, counts and asks, and writes whole graphs with its graph API (OntologyGraph.create(...).withContent(...), NamedGraph.replaceContent, ...), which keeps each graph's record in the graph registry (see The graph registry). FusekiStore sets up both for Jena:

import {graphs} from '@_linked/core/ontologies/graphs';
import {OntologyGraph} from '@_linked/core/shapes/NamedGraph';

await Person.select((p) => p.name).from(graphs.allOntologies).exec(store); // every registered ontology graph
await Person.select((p) => p.name).from(OntologyGraph.iriFor('@_linked/schema')).exec(store); // one package graph
await Person.select((p) => p.name).from(graphs.union).exec(store); // every named graph
  • Portable names: graphs.default is read as Jena's urn:x-arq:DefaultGraph and graphs.union as urn:x-arq:UnionGraph (every named graph, without the default graph); jenaGraphName is the mapping. Either one works alone; together with other graphs core refuses the read for now (unsupported-query-kind). graphs.allOntologies is expanded by core to the graphs the registry lists.
  • Writes to a portable name, to the registry graph itself, or to any urn:x-arq: name are refused (graph-name-read-only) before a request is sent.
  • isolateLinkedTypeChecks is on: for a subject reached through a link, its inferred type check goes in a second GRAPH ?g block after a block that reads one of its stored triples, because Jena reorders the patterns inside one block and would otherwise run the type check in every graph of the set. It is a store setting, never per query.
  • The registry cache that expands graphs.allOntologies is kept per process under the dataset endpoint ({baseUrl}/{dataset}): every FusekiStore for one dataset shares it, its own graph writes update it, store.invalidateGraphRegistry() drops it, and an entry older than 30 seconds is re-read.
  • Image: the hierarchy of a registered ontology graph is read only by ghcr.io/linked-fw/fuseki:6.2.0-2 or later; on 6.2.0-1 classes declared in package graphs do not infer.

The dataset helpers (@_linked/fuseki/utils/datasets, also exported from the package root): datasetExists, createDataset (dbType: 'tdb2' creates an inferring dataset on the image), ensureDatasetExists, ensureDatasetsExist, clearDataset, clearDatasets, and storedEndpoint (below).

The image

ghcr.io/linked-fw/fuseki:6.2.0-2 (tags <jena version>-<n>, immutable) is secoresearch/fuseki:6.2.0 plus:

  • the module linked-fuseki-module.jar in /javalibs/ (the image's classpath directory): the dataset type lf:DatasetRDFSLive, the /stored endpoint, SERVICE switched off, the template guard and the delete filter;
  • /fuseki-base/templates/config-tdb2, the template POST $/datasets uses, declaring an inferring dataset;
  • an inferring ds dataset (/fuseki-base/configuration/assembler.ttl).

Everything else is the base image's: the entrypoint and its environment (ADMIN_PASSWORD, ENABLE_DATA_WRITE, ENABLE_UPDATE, ENABLE_SHACL, QUERY_TIMEOUT, USE_SYSTEM_CA_CERTS), FUSEKI_BASE=/fuseki-base, uid 9008 (group 0), port 3030.

docker run -d --name fuseki -p 3030:3030 -e ADMIN_PASSWORD=secret ghcr.io/linked-fw/fuseki:6.2.0-2
services:
  fuseki:
    image: ghcr.io/linked-fw/fuseki:6.2.0-2
    ports: ["3030:3030"]
    environment:
      ADMIN_PASSWORD: ${FUSEKI_ADMIN_PASSWORD}
    volumes:
      - fuseki_databases:/fuseki-base/databases
volumes:
  fuseki_databases:

Datasets infer classes

A dataset created through the admin form (POST $/datasets with dbName and dbType=tdb2) infers RDFS classes: with ex:A rdfs:subClassOf ex:B stored, every ex:A instance is also an ex:B instance to every query. dbType=mem datasets (config-mem) do not infer.

  • Classes only. The hierarchy is the transitive closure of the stored rdfs:subClassOf triples of the default graph and of every graph the graph registry lists (see The graph registry). rdfs:subPropertyOf, rdfs:domain and rdfs:range infer nothing, even when the dataset holds them. A subClassOf triple in any other named graph (a draft, an import) is not part of the hierarchy.
  • Every graph is inferred: the default graph, each named graph and the union graph. Inferred triples stay in the graph whose data caused them.
  • Nothing is materialized. Jena's query-time RDFS view (org.apache.jena.rdfs) answers inferred triples while it reads; TDB2 holds only what was written, and its transactions are unchanged. A delete of an inferred triple is a no-op (the store does not hold it).
  • The hierarchy follows the data, lazily. A commit that may change it only marks the hierarchy stale: any write to the registry graph; a write or delete of an rdfs:subClassOf triple (or a wildcard delete, a clear, a graph added or dropped) on the default graph or on a listed graph. The first transaction that reads afterwards rebuilds it once (readers starting meanwhile wait); write-only transactions, Graph Store Protocol POST, PUT and DELETE on /<ds>/data included, never rebuild, so a burst of hundreds of ontology commits costs one rebuild (~20 ms for 800 classes, under 10 ms for schema.org's 952 axioms). A transaction keeps the hierarchy it first read with until it ends, and never reads with one older than its own snapshot (a transaction that waited for the writer lock, or began during another's commit, builds its own). It may read with a newer one: when another transaction rebuilt after it began, an axiom deleted since is no longer inferred while the deleted triple is still visible to it. A writer sees its own subClassOf change from its next transaction; within the same transaction it usually reads with the hierarchy from before its change (so one update request that inserts a subClassOf and then runs DELETE WHERE { ?x a ex:Super } matches without it), and with its change included only when the shared hierarchy was already stale at its first read.
  • ADD, COPY and MOVE store inferred triples. SPARQL Update's ADD, COPY and MOVE … TO DEFAULT (or to any graph) read the source graph through the inferred view, so the target receives the source's inferred triples as stored ones. To copy only what is stored, read the source from /<ds>/stored and write that.
  • Every write goes through the dataset. Only writes through the lf:DatasetRDFSLive dataset (its service's endpoints) mark the hierarchy stale. A second service or assembler over the same TDB2 location, or a tool writing to the database files while the server runs, changes the data but not the hierarchy until the next hierarchy change through the dataset or a restart.
  • Opening a dataset (server start) builds its hierarchy once; the log says [linked-fuseki] <location>: hierarchy built (<n> subClassOf, <k> graphs, <ms> ms) (<k>: the listed graphs, the default graph not counted), and each rebuild hierarchy rebuilt (…, generation <g>).

A static dataset declares the type in its assembler:

PREFIX ja:   <http://jena.hpl.hp.com/2005/11/Assembler#>
PREFIX tdb2: <http://jena.apache.org/2016/tdb#>
PREFIX lf:   <https://linked.cm/ont/fuseki#>

:ds  a lf:DatasetRDFSLive ; ja:dataset :tdb .
:tdb a tdb2:DatasetTDB2 ; tdb2:location "/fuseki-base/databases/<name>" .

A Lucene index wraps it as any dataset: :text a text:TextDataset ; text:dataset :ds ; text:index …. Over a wrapped dataset jena-text commits the Lucene index after TDB2's commit, not inside it (as over Jena's own ja:DatasetRDFS): a crash between the two can leave the index behind the data; rebuild it with jena.textindexer ($TEXTINDEXER in the image).

The template is kept in place. Fuseki copies its stock templates into /fuseki-base/templates only when they are missing, so a run area persisted from an older image would create plain datasets. At start the module rewrites templates/config-tdb2 when it differs from its own, with a WARN line. Mount databases (/fuseki-base/databases) rather than the whole run area.

The ds dataset

The image keeps the base image's ds service: sparql and a read-only data always; update (ENABLE_UPDATE=true), read-write data (ENABLE_DATA_WRITE=true) and shacl (ENABLE_SHACL=true) on request; stored always. Its dataset is lf:DatasetRDFSLive over TDB2 at /fuseki-base/databases/ds (no union default graph) with the base image's Lucene index at /fuseki-base/databases/lucene. It is for tests and experiments; nothing should rely on data in it. A consumer that mounts its own /fuseki-base/configuration replaces it.

Stored-only reads

Every inferring dataset has /<ds>/stored, one endpoint over the stored triples only:

RequestAnswer
GET /<ds>/stored with Accept: application/n-quadsthe whole stored dataset, every graph
GET /<ds>/stored?default, ?graph=<iri>one stored graph
?query=…, POST application/sparql-query, a form query=a SPARQL query over the stored triples
any write405
import { storedEndpoint } from '@_linked/fuseki';
storedEndpoint('http://localhost:3030', 'cn-main'); // http://localhost:3030/cn-main/stored

Use it for whatever must not see inferred triples: backups, dataset copies (forks), quad counts. GET /<ds> (the dataset URL) and GET /<ds>/data return inferred triples too, and a copy made from them would store the inference.

Graphs

The default graph is the stored default graph: no dataset sets tdb2:unionDefaultGraph (the RDFS view does not see it, and where it applies it hides the stored default graph from reads while writes still land there). Named graphs persist in TDB2 and are inferred like the default graph; the union graph is <urn:x-arq:UnionGraph> (it does not include the default graph; never write to it).

The graph registry

Every dataset may hold a registry of its named graphs, the graph <https://linked.cm/pkg/core/graph/registry> (the same IRI in every dataset), described with SPARQL Service Description plus @_linked/core's terms. @_linked/core writes it (each package's ontology graph {package IRI}/graph/ontology and its record in one update); the module only reads it. A record lists its graph as a hierarchy source with core:feedsHierarchy true:

# in GRAPH <https://linked.cm/pkg/core/graph/registry>
<https://linked.cm/pkg/schema/graph/ontology> a core:OntologyGraph ;
  sd:name <https://linked.cm/pkg/schema/graph/ontology> ;
  core:package "@_linked/schema" ; core:version "1.7.0" ;
  core:contentHash "3f9c…" ; core:feedsHierarchy true ;
  dcterms:modified "2026-10-30T10:12:00Z"^^xsd:dateTime ;
  dcterms:source "data/schema.ttl", "data/schema-added.ttl" .

(core: is https://linked.cm/ont/core/, sd: http://www.w3.org/ns/sparql-service-description#.)

  • The hierarchy is built from the default graph plus every graph ?g with ?r core:feedsHierarchy true ; sd:name ?g in the registry, read as stored (no inference; the flag is an xsd:boolean, so "true" as a plain string does not count). Drafts and imports are recorded with false and never feed it.
  • A graph written before its record feeds once the record is written. Dropping the record (or the graph) removes its classes at the next read.
  • The IRIs are fixed in the module: no dataset configuration names them, and datasets created for 6.2.0-1 need no change.
  • On 6.2.0-1 the hierarchy is the default graph's only: a @_linked/core that moves the hierarchy into package graphs needs 6.2.0-2 or later.

Backups and deletion

  • Backups stream GET /<ds>/stored (Accept: application/n-quads), and restore by posting the N-Quads to a new dataset's URL. POST $/backup/<ds> dumps the service's dataset, inferred triples included, and a restore of that dump would store them: never use it for an inferring dataset.
  • DELETE $/datasets/<ds> removes the dataset's files under /fuseki-base/databases/<ds> for a dataset created through the admin API (one with its own configuration/<ds>.ttl). Fuseki skips that for a wrapped dataset; the module completes it under Fuseki's own rule: after a successful delete only, that directory only, never a symbolic link. A dataset re-created under the same name starts empty. A dataset declared in a shared configuration file (ds, or datasets in your own assembler.ttl) is only unregistered and keeps its files, as on plain Fuseki.
  • POST $/compact/<ds> works as on plain TDB2.

SERVICE is off

The module sets arq:httpServiceAllowed false for the whole server: a query with SERVICE <url> answers 422. Do not set arq:httpServiceAllowed true in a dataset's ja:context. SPARQL Update's LOAD <url> still fetches http: and file: IRIs: keep update endpoints behind authentication or off the public network.

/stored reads below every wrapper of the live dataset. Do not put an access-control dataset (jena-fuseki-access) above an inferring dataset whose service also has stored: /stored would bypass it.

Fuseki 6.2 notes

  • /<ds>/upload answers 405 ("No Fuseki dispatch") on Fuseki 6.2, in this image as in the base image: Fuseki's upload handler comes with its UI module. Post files to /<ds>/data (Graph Store Protocol) or N-Quads to /<ds> instead.

Building the image locally

docker build -t linked-fuseki:dev server     # from this repository's root
server/scripts/smoke.sh linked-fuseki:dev    # port 3233 (SMOKE_PORT)

or in compose: build: { context: <path to this repository>/server } with image: linked-fuseki:dev. The module alone: mvn -B -f server verify (Java 21, Maven; Jena 6.2.0 is a provided dependency). server/probe/probe.sh runs the measurements of report 001 on a built image; server/bench/ is the 1M-triple benchmark.

Releasing the image

The image has its own version, server/IMAGE_TAG (<jena>-<n>, kept equal to the version in server/pom.xml; the workflows check it). The Image workflow publishes ghcr.io/linked-fw/fuseki:<IMAGE_TAG> for linux/amd64 and linux/arm64 when a push to main touches server/, or on manual dispatch; an existing tag is never overwritten, so a change to server/ is published by bumping IMAGE_TAG. The Server workflow builds, tests and smoke-tests on every push that touches server/. The npm package (changesets, publish.yml) does not include server/.

The first publish creates the GHCR package private: set it to public once (package settings → visibility) and link it to this repository if the label did not. Keep it linked to linked-fw/fuseki with Actions access: the workflow reads existing tags through the Packages API, which answers 404 both for a missing package and for one the workflow may not see, and a 404 reads as "not published yet".

版本与发布记录

@_linked/fuseki 共发布 28 个版本,首次发布于 2026/04/23;过去 12 个月发布了 28 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
3.9.2latest2026/10/2810 天前
3.9.12026/10/2513 天前
3.9.02026/10/2414 天前
3.8.02026/10/2414 天前
3.7.02026/10/2415 天前
3.6.02026/10/2216 天前

关于 @_linked/fuseki 的常见问题

@_linked/fuseki 是什么?

A store for @_linked/core that talks SPARQL over HTTP to Apache Jena Fuseki, dataset admin helpers, and the Fuseki image every linked app runs: ghcr.io/linked-fw/fuseki, whose datasets infer RDFS classes.

如何安装 @_linked/fuseki?

在项目目录运行 npm install @_linked/fuseki,或使用 pnpm add / yarn add / bun add。

@_linked/fuseki 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@_linked/fuseki 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@_linked/fuseki 可以商用吗?

@_linked/fuseki 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@_linked/fuseki 的下载量有多少?

根据 npm 官方下载统计,@_linked/fuseki 在 2026/10/05 – 2026/11/03 这 30 天内被下载 2,331 次,日均约 78 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@_linked/fuseki 还在维护吗?

最新版本 3.9.2 发布于 2026/10/28(10 天前);至今共发布 28 个版本,其中过去 12 个月发布了 28 个;首次发布于 2026/04/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。